Repository: pyvista/pytest-pyvista (language: python)

Issue:
Release 0.1.9
Hi @pyvista/maintainers, pinging @germa89 for visibility.

I was wondering if I could make a release for the plugin, since we need to use the new flag introduced in #82 

If that's okay, I will proceed. 


Discussion / hints:

Fix patch:
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 <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"]


Test patch:
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*")