Skip to content

Commit

Permalink
feat: Use ruff for linting and formatting instead of black, flake8 an…
Browse files Browse the repository at this point in the history
…d pylint.

A large number of ruff tests are enabled and result in various code quality improvements.

This also adds a new `create_temp_node` fixture.

fixes: #15, #16
  • Loading branch information
captainhammy committed Dec 25, 2023
1 parent f012610 commit 0e7ca27
Show file tree
Hide file tree
Showing 24 changed files with 429 additions and 172 deletions.
10 changes: 5 additions & 5 deletions docs/fixtures/hip_file.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ load_module_test_hip_file
-------------------------

The ``load_module_test_hip_file`` fixture will load a test hip file with the same name as the running module. It
supports .hip, .hiplc, and .hipnc type files (in that order). The hip file must be under a **data/** directory which is
supports .hip, .hiplc, and .hipnc file types (in that order). The hip file must be under a **data/** directory which is
a sibling of the test file. For this package, looking at the tests for the fixtures, we can see that we have a matching
hip file for ``test_nodes.py`` (``test_nodes.hiplc``).

Expand All @@ -36,19 +36,19 @@ hip file for ``test_nodes.py`` (``test_nodes.hiplc``).
The fixture will also clear the hip file after the tests are completed.

As this is a **module** level fixture, to use it ensure you've added the following at the top of the test file:
As this is a **module** level fixture, to use it, ensure you've added the following at the top of the test file:

.. code-block:: python
pytestmark = pytest.mark.usefixtures("load_module_test_hip_file")
In the event the fixture cannot find a matching file, the raised ``RuntimeError`` will contain a list of all the paths
which were tried:
In the event the fixture cannot find a matching file, the raised ``NoModuleTestFileError`` will contain a list of all
the paths which were tried:

.. code-block:: python
RuntimeError: Could not find a valid test hip: {test_file_dir}/data/{test_file_name}{.hip,.hiplc,.hipnc}
NoModuleTestFileError: Could not find a valid test hip: {test_file_dir}/data/{test_file_name}{.hip,.hiplc,.hipnc}
set_test_frame
Expand Down
75 changes: 65 additions & 10 deletions docs/fixtures/nodes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,57 @@
Nodes
=====

create_temp_node
----------------

The ``create_temp_node`` fixture can be used to create temporary testing nodes and not have to worry about
destroying them after the test is over. Additionally, it can be called multiple times to create many temporary nodes.

It supports a limited number of parameters to affect the node creation:

.. code-block:: python
def _create(
parent: hou.Node,
node_type_name: str,
node_name: str | None = None,
*, run_init_scripts: bool = True
) -> hou.Node:
"""Function to create a test node that will be destroyed on cleanup.
Args:
parent: The parent to create the test node under.
node_type_name: The node type to create.
node_name: Optional node name.
run_init_scripts: Whether to run the node initialization scripts.
Return:
The created test node.
"""
In the following example we create some temp nodes to test with.

.. code-block:: python
def test_some_func(create_temp_node):
node1 = create_temp_node(hou.node("/obj"), "geo", "TEST_NODE1", run_init_scripts=False)
node2 = create_temp_node(hou.node("/obj"), "null", "TEST_NODE2")
... # do testing
After the test function is executed, the created nodes will both be destroyed. If the test code itself
destroys a node, the ``hou.ObjectWasDeleted`` exception will be suppressed.


Existing Test Node Fixtures
---------------------------

There are a number of convenience functions which can be used to automatically find test related nodes in the current
hip file based on the test name data.

The underlying tooling will inspect the ``pytest.FixtureRequest`` object and construct a number of acceptable
node names/paths for the specific test and try to return one of those. If a matching node cannot be found a
``RuntimeError`` is raised and the test will fail.
``NoTestNodeError`` is raised and the test will fail.

The node search order is as follows:
- Node matching the exact test name
Expand Down Expand Up @@ -45,25 +90,36 @@ the possible nodes found (and their order of precedence) is as follows:
- /obj/TestMyFunc
- /obj/testmyfunc

In the event that no valid nodes could be provided, the raised ``RuntimeError`` will contain a list of all paths which
In the event that no valid nodes could be provided, the raised ``NoTestNodeError`` will contain a list of all paths which
were tried:

.. code-block:: python
RuntimeError: Could not find any matching test nodes: /obj/test_none_args, /obj/TestMyFunc_none_args, /obj/testmyfunc_none_args, /obj/TestMyFunc/none_args, /obj/testmyfunc/none_args, /obj/TestMyFunc, /obj/testmyfunc
NoTestNodeError: Could not find any matching test nodes: /obj/test_none_args, /obj/TestMyFunc_none_args, /obj/testmyfunc_none_args, /obj/TestMyFunc/none_args, /obj/testmyfunc/none_args, /obj/TestMyFunc, /obj/testmyfunc
obj_test_node
-------------
Basic Context Fixtures
^^^^^^^^^^^^^^^^^^^^^^

The ``obj_test_node`` fixture will attempt to find a test node under **/obj** (as detailed above in examples).
\*_test_node
''''''''''''

The ``*_test_node`` fixtures will attempt to find a test node under their specific contexts (as detailed above for **obj_test_node**)

The following contexts are currently provided:
- /obj (obj_test_node)
- /out (out_test_node)


Object Specific Fixtures
^^^^^^^^^^^^^^^^^^^^^^^^

obj_test_geo
------------
''''''''''''

The ``obj_test_geo`` fixture will attempt to return the geometry of the display node of the found Object test node.

If the found test node (using ``obj_test_node``) does not contain **SOP** nodes a ``RuntimeError`` is raised.
If the found test node (using ``obj_test_node``) does not contain **SOP** nodes a ``TestNodeDoesNotContainSOPsError`` is raised.

The returned ``hou.Geometry`` object is **read only**.

Expand All @@ -74,9 +130,8 @@ The returned ``hou.Geometry`` object is **read only**.
assert obj_test_geo.isReadOnly()
obj_test_geo_copy
-----------------
'''''''''''''''''

The ``obj_test_geo_copy`` fixture is the same as ``obj_test_geo`` however the found geometry is copied/merged into a
new ``hou.Geometry`` instance and is not **read only**.
Expand Down
2 changes: 2 additions & 0 deletions docs/fixtures/shelf_tools.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ that would be passed to the shelf tool by Houdini.

In order for this fixture to work, the shelf tool must already be loaded in the Houdini session.

If a tool of the name cannot be found, a ``MissingToolError`` is raised.

Consider the following simple tool definition where the execution sets a value in the kwargs dict:

.. code-block:: xml
Expand Down
7 changes: 4 additions & 3 deletions docs/fixtures/ui.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,11 @@ The temporary ``hou.ui`` object is removed after the test is completed.
set_ui_available
----------------

The ``set_ui_available`` fixtures forces the ``hou.isUIAvailable()`` function to return True.
The ``set_ui_available`` fixture forces the ``hou.isUIAvailable()`` function to return True. It does **NOT**
handle any mocking of *hou.ui*, however.

.. code-block:: python
def test_ui_available(set_ui_available):
@pytest.mark.usefixtures("set_ui_available")
def test_ui_available():
assert hou.isUIAvailable()
5 changes: 3 additions & 2 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
sphinx-rtd-theme==1.3.0
sphinx-copybutton==0.5.2
sphinx
sphinx-copybutton
sphinx-rtd-theme
63 changes: 58 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
requires = ["setuptools>=42", "setuptools_scm[toml]>=6.2", "wheel"]
build-backend = "setuptools.build_meta"

[tool.pytest.ini_options]
# Disable sugar as it interferes with pytester output parsing.
addopts = "-p no:sugar --cov --cov-report=html --cov-report=xml --color=yes"

[tool.coverage]

[tool.coverage.run]
Expand All @@ -10,11 +14,10 @@
"pytest_houdini",
]
omit = [
"*tests*",
"*/plugin.py",
"*/fixtures/exceptions.py"
]
disable_warnings = [
"module-not-imported",
"module-not-measured",
]
[tool.coverage.report]
Expand Down Expand Up @@ -64,6 +67,56 @@
module = "hou.*"
ignore_missing_imports = true

[tool.pytest.ini_options]
# Disable sugar as it interferes with pytester output parsing.
addopts = "-p no:sugar --cov --cov-report=html --cov-report=xml --color=yes"
[tool.ruff]
line-length = 120

[tool.ruff.lint]
extend-select = [
"E", # pycodestyle
"W", # pycodestyle
"UP", # pyupgrade
"D", # pydocstyle
"F", # Pyflakes
"PL", # Pylint
"RSE", # flake8-raise
"B", # flake8-bugbear
"PT", # flake8-pytest-style
"C90", # mccabe
"TRY", # tryceratops
"FLY", # flynt
"PERF", # Perflint
"LOG", # flake8-logging
"BLE", # flake8-blind-except
"A", # flake8-builtins
"C4", # flake8-comprehensions
"RET", # flake8-return
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"PTH", # flake8-use-pathlib
"RUF", # Ruff specific
"FBT", # flake8-boolean-trap
]
ignore = [
"D104", # Missing docstring in public module
"D105", # Missing docstring in magic method
"D107", # Missing docstring in __init__
"PT004", # Fixtures not returning anything not starting with _
]

[tool.ruff.lint.per-file-ignores]
"plugin.py" = [
"F401", # Module level import not at top of file
]
"tests/*.py" = [
"PLR2004", # Magic value in comparison
"PLR6301", # 'no-self-use' for tests
]

[tool.ruff.lint.flake8-pytest-style]
fixture-parentheses = false # Match actual pytest recommendation with no parentheses

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.lint.pylint]
max-args = 6
12 changes: 6 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
URL = "https://github.com/captainhammy/pytest-houdini"
AUTHOR = "Graham Thompson"
AUTHOR_EMAIL = "captainhammy@gmail.com"
REQUIRES_PYTHON = ">=3.7.0"
REQUIRES_PYTHON = ">=3.9.0"

this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text()
Expand All @@ -30,13 +30,13 @@
packages=find_packages(where="src", exclude=("tests",)),
install_requires=[
"pytest",
"pytest-datadir",
"pytest-mock",
],
extras_require={
"test": [
"pytest",
"coverage",
"pytest-cov",
"pytest-datadir",
"pytest-mock",
"tox",
]
},
Expand All @@ -50,10 +50,10 @@
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Testing",
],
project_urls={"Documentation": "https://pytest-houdini.readthedocs.io/"},
Expand Down
60 changes: 60 additions & 0 deletions src/pytest_houdini/fixtures/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Exceptions raised by pytest-houdini."""

# Future
from __future__ import annotations

# Standard Library
from typing import TYPE_CHECKING

if TYPE_CHECKING:
import hou


# Exceptions


class MissingToolError(Exception):
"""Exception raised when a matching tool cannot be found.
Args:
tool_name: The missing tool name.
"""

def __init__(self, tool_name: str) -> None:
super().__init__(f"Could not find tool: {tool_name}")


class NoModuleTestFileError(Exception):
"""Exception raised when a module test file cannot be found.
Args:
test_file: The test file stem (no extension.)
extensions: The extensions which were checked.
"""

def __init__(self, test_file: str, extensions: tuple[str, ...]) -> None:
msg = f"Could not find a valid test hip: {test_file}{{{','.join(extensions)}}}"

super().__init__(msg)


class NoTestNodeError(Exception):
"""Exception raised when no test node could be found.
Args:
searched_paths: The test node paths which were searched.
"""

def __init__(self, searched_paths: list[str]) -> None:
super().__init__(f"Could not find any matching test nodes: {', '.join(searched_paths)}")


class TestNodeDoesNotContainSOPsError(Exception):
"""Exception raised when a test node does not contain SOP nodes.
Args:
node: The node which does not contain SOP nodes.
"""

def __init__(self, node: hou.OpNode) -> None:
super().__init__(f"{node.path()} does not contain SOP nodes.")
Loading

0 comments on commit 0e7ca27

Please sign in to comment.