diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 480abee..b0033fc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,14 +4,14 @@ updates: directory: "/" # Location of package manifests insecure-external-code-execution: allow schedule: - interval: "daily" + interval: "monthly" labels: - "maintenance" - "dependencies" - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "daily" + interval: "monthly" labels: - "maintenance" - - "dependencies" \ No newline at end of file + - "dependencies" diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index d6d4e15..6f6cc58 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -22,7 +22,7 @@ jobs: name: Build Documentation runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python 3.11 uses: actions/setup-python@v4 @@ -59,8 +59,6 @@ jobs: matrix: include: - - python-version: '3.7' - vtk-version: '9.0.3' - python-version: '3.8' vtk-version: '9.0.3' - python-version: '3.9' @@ -70,7 +68,7 @@ jobs: - python-version: '3.11' vtk-version: 'latest' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -87,6 +85,10 @@ jobs: if: ${{ matrix.vtk-version != 'latest' }} run: pip install vtk==${{ matrix.vtk-version }} + - name: Limit NumPy for VTK 9.0.3 + if: ${{ matrix.vtk-version == '9.0.3' }} + run: pip install 'numpy<1.24' + - uses: awalsh128/cache-apt-pkgs-action@v1.1.3 with: packages: libgl1-mesa-glx xvfb python-tk @@ -113,7 +115,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 @@ -147,7 +149,7 @@ jobs: name: Downstream tests runs-on: ubuntu-20.04 # matching pyvista steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-python@v4 with: python-version: '3.11' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 94be9fb..aab84d6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,26 +1,28 @@ +ci: + autofix_prs: false + autoupdate_schedule: monthly + repos: - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 23.9.1 hooks: - id: black - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.12.0 hooks: - id: isort - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 6.1.0 hooks: - id: flake8 additional_dependencies: [ - "flake8-black==0.3.3", - "flake8-isort==4.2.0", - "flake8-quotes==3.3.1", + "flake8-quotes==3.3.2", ] - repo: https://github.com/codespell-project/codespell - rev: v2.2.2 + rev: v2.2.5 hooks: - id: codespell args: [ @@ -29,7 +31,7 @@ repos: ] - repo: https://github.com/pycqa/pydocstyle - rev: 6.1.1 + rev: 6.3.0 hooks: - id: pydocstyle additional_dependencies: [toml==0.10.2] @@ -37,18 +39,18 @@ repos: exclude: ^pytest-pyvista/ext/ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.982 + rev: v1.5.1 hooks: - id: mypy exclude: ^(doc/|tests/|examples/|pytest-pyvista/ext/|examples_flask/) additional_dependencies: [ - "mypy-extensions==0.4.3", + "mypy-extensions==1.0.0", "toml==0.10.2", "typing-extensions==4.1.1", ] - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.4.0 hooks: - id: check-merge-conflict - id: debug-statements @@ -58,6 +60,6 @@ repos: # this validates our github workflow files - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.18.4 + rev: 0.26.3 hooks: - id: check-github-workflows diff --git a/LICENSE b/LICENSE index 7cdac63..68e8075 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2022 The PyVista Developers +Copyright (c) 2022-2023 The PyVista Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index cd6dbe8..21f11b8 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,15 @@ ============== pytest-pyvista ============== -.. image:: https://img.shields.io/pypi/v/pytest-pyvista.svg +.. image:: https://img.shields.io/pypi/v/pytest-pyvista.svg?color=orange&label=pypi&logo=python&logoColor=white :target: https://pypi.org/project/pytest-pyvista :alt: PyPI version -.. image:: https://img.shields.io/pypi/pyversions/pytest-pyvista.svg +.. image:: https://img.shields.io/conda/vn/conda-forge/pytest-pyvista?color=orange&label=conda-forge&logo=conda-forge&logoColor=white + :target: https://anaconda.org/conda-forge/pytest-pyvista + :alt: conda-forge version + +.. image:: https://img.shields.io/pypi/pyversions/pytest-pyvista.svg?color=orange&logo=python&label=python&logoColor=white :target: https://pypi.org/project/pytest-pyvista :alt: Python versions @@ -34,7 +38,7 @@ fail. Requirements ------------ -You must have a Python version greater than 3.7, as well as PyVista installed +You must have a Python version >= 3.8, as well as PyVista installed in your environment. pyvista version >=0.37.0 and vtk version >=9.0.0 required. @@ -45,6 +49,9 @@ You can install "pytest-pyvista" via `pip`_ from `PyPI`_:: $ pip install pytest-pyvista +Alternatively, you can also install via `conda`_ or `mamba`_ from `conda-forge`_:: + + $ mamba install -c conda-forge pytest-pyvista Usage ----- @@ -104,6 +111,8 @@ These are the flags you can use when calling ``pytest`` in the command line: * ``--image_cache_dir `` sets the image cache dir. This will override any configuration, see below. +* ``--reset_only_failed`` reset the image cache of the failed tests only. + Test specific flags ------------------- These are attributes of `verify_image_cache`. You can set them as ``True`` if needed @@ -164,3 +173,6 @@ description. .. _`tox`: https://tox.readthedocs.io/en/latest/ .. _`pip`: https://pypi.org/project/pip/ .. _`PyPI`: https://pypi.org/project +.. _`conda`: https://github.com/conda/conda +.. _`mamba`: https://github.com/mamba-org/mamba +.. _`conda-forge`: https://anaconda.org/conda-forge/pytest-pyvista diff --git a/pyproject.toml b/pyproject.toml index 37439b5..88c39e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ classifiers=[ "Framework :: Pytest", "Intended Audience :: Developers", "Topic :: Software Development :: Testing", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -22,7 +21,7 @@ classifiers=[ "License :: OSI Approved :: MIT License", ] dependencies=["pytest>=3.5.0"] -python_requires=">=3.7" +python_requires=">=3.8" [project.urls] Home = "https://github.com/pyvista/pytest-pyvista" @@ -31,15 +30,16 @@ Home = "https://github.com/pyvista/pytest-pyvista" [project.optional-dependencies] tests = [ "codecov<2.2.0", - "coverage==7.2.2", + "coverage==7.3.1", "pytest>=3.5.0", - "pytest-cov==4.0.0", - "numpy<1.24", + "pytest-cov==4.1.0", + "numpy<1.26", ] docs = [ - "pydata-sphinx-theme==0.13.1", - "sphinx_copybutton==0.5.1", - "sphinx-notfound-page==0.8.3", + "pydata-sphinx-theme==0.13.3", + "sphinx==7.2.6", + "sphinx_copybutton==0.5.2", + "sphinx-notfound-page==1.0.0", ] [project.entry-points."pytest11"] diff --git a/pytest_pyvista/__init__.py b/pytest_pyvista/__init__.py index a748244..ddb7ce6 100644 --- a/pytest_pyvista/__init__.py +++ b/pytest_pyvista/__init__.py @@ -1,5 +1,6 @@ """Pytest-pyvista package""" -__version__ = "0.1.8" +__version__ = "0.1.9" + from .pytest_pyvista import VerifyImageCache diff --git a/pytest_pyvista/pytest_pyvista.py b/pytest_pyvista/pytest_pyvista.py index e88865b..8805251 100644 --- a/pytest_pyvista/pytest_pyvista.py +++ b/pytest_pyvista/pytest_pyvista.py @@ -8,6 +8,18 @@ import pyvista +class RegressionError(RuntimeError): + """Error when regression does not meet the criteria""" + + pass + + +class RegressionFileNotFound(FileNotFoundError): + """Error when regression file is not found""" + + pass + + def pytest_addoption(parser): """Adds new flag options to the pyvista plugin.""" @@ -45,6 +57,11 @@ def pytest_addoption(parser): default="image_cache_dir", help="Path to the image cache folder.", ) + group.addoption( + "--reset_only_failed", + action="store_true", + help="Reset only the failed images in the PyVista cache.", + ) class VerifyImageCache: @@ -103,6 +120,7 @@ class VerifyImageCache: ignore_image_cache = False fail_extra_image_cache = False add_missing_images = False + reset_only_failed = False def __init__( self, @@ -183,7 +201,9 @@ def __call__(self, plotter): # cached image name. We remove the first 5 characters of the function name # "test_" to get the name for the image. - image_filename = os.path.join(self.cache_dir, test_name[5:] + ".png") + image_name = test_name[5:] + ".png" + image_filename = os.path.join(self.cache_dir, image_name) + if ( not os.path.isfile(image_filename) and self.fail_extra_image_cache @@ -191,13 +211,15 @@ def __call__(self, plotter): ): # Make sure this doesn't get called again if this plotter doesn't close properly plotter._before_close_callback = None - raise RuntimeError(f"{image_filename} does not exist in image cache") + raise RegressionFileNotFound( + f"{image_filename} does not exist in image cache" + ) if ( self.add_missing_images and not os.path.isfile(image_filename) or self.reset_image_cache - ): + ) and not self.reset_only_failed: plotter.screenshot(image_filename) if self.generated_image_dir is not None: @@ -205,15 +227,24 @@ def __call__(self, plotter): self.generated_image_dir, test_name[5:] + ".png" ) plotter.screenshot(gen_image_filename) + error = pyvista.compare_images(image_filename, plotter) if error > allowed_error: - # Make sure this doesn't get called again if this plotter doesn't close properly - plotter._before_close_callback = None - raise RuntimeError( - f"{test_name} Exceeded image regression error of " - f"{allowed_error} with an image error equal to: {error}" - ) + if self.reset_only_failed: + warnings.warn( + f"{test_name} Exceeded image regression error of " + f"{allowed_error} with an image error equal to: {error}" + f"\nThis image will be reset in the cache." + ) + plotter.screenshot(image_filename) + else: + # Make sure this doesn't get called again if this plotter doesn't close properly + plotter._before_close_callback = None + raise RegressionError( + f"{test_name} Exceeded image regression error of " + f"{allowed_error} with an image error equal to: {error}" + ) if error > allowed_warning: warnings.warn( f"{test_name} Exceeded image regression warning of " @@ -233,6 +264,7 @@ def verify_image_cache(request, pytestconfig): "fail_extra_image_cache" ) VerifyImageCache.add_missing_images = pytestconfig.getoption("add_missing_images") + VerifyImageCache.reset_only_failed = pytestconfig.getoption("reset_only_failed") cache_dir = pytestconfig.getoption("image_cache_dir") if cache_dir is None: diff --git a/tests/test_pyvista.py b/tests/test_pyvista.py index 8d1fdb1..f0a0f0a 100644 --- a/tests/test_pyvista.py +++ b/tests/test_pyvista.py @@ -75,6 +75,8 @@ def test_imcache(verify_image_cache): result = testdir.runpytest("--fail_extra_image_cache") result.stdout.fnmatch_lines("*[Ff]ailed*") result.stdout.fnmatch_lines("*Exceeded image regression error*") + result.stdout.fnmatch_lines("*pytest_pyvista.pytest_pyvista.RegressionError:*") + result.stdout.fnmatch_lines("*Exceeded image regression error of*") def test_skip(testdir): @@ -281,3 +283,47 @@ def test_imcache(cleanup_tester, verify_image_cache): result = testdir.runpytest("--fail_extra_image_cache") result.stdout.fnmatch_lines("*[Pp]assed*") + + +def test_reset_only_failed(testdir): + """Test usage of the `reset_only_failed` flag.""" + filename = make_cached_images(testdir.tmpdir) + filename_original = make_cached_images(testdir.tmpdir, name="original.png") + assert filecmp.cmp(filename, filename_original, shallow=False) + + testdir.makepyfile( + """ + import pyvista as pv + pv.OFF_SCREEN = True + def test_imcache(verify_image_cache): + sphere = pv.Box() + plotter = pv.Plotter() + plotter.add_mesh(sphere, color="blue") + plotter.show() + """ + ) + + result = testdir.runpytest("--reset_only_failed") + result.stdout.fnmatch_lines("*[Pp]assed*") + result.stdout.fnmatch_lines("*This image will be reset in the cache.") + # file was overwritten + assert not filecmp.cmp(filename, filename_original, shallow=False) + + +def test_file_not_found(testdir): + """Test RegressionFileNotFound is correctly raised.""" + testdir.makepyfile( + """ + import pyvista as pv + pv.OFF_SCREEN = True + def test_imcache_num2(verify_image_cache): + sphere = pv.Box() + plotter = pv.Plotter() + plotter.add_mesh(sphere, color="blue") + plotter.show() + """ + ) + + result = testdir.runpytest("--fail_extra_image_cache") + result.stdout.fnmatch_lines("*RegressionFileNotFound*") + result.stdout.fnmatch_lines("*does not exist in image cache*")