Repository: businho/pytest-ruff (language: python)

Issue:
AttributeError: module 'pytest' has no attribute 'StashKey'
Is it a `pytest` version incompatibility issue?
```
/usr/local/lib/python3.10/site-packages/_pytest/config/__init__.py:318: PluggyTeardownRaisedWarning: A plugin raised an exception during an old-style hookwrapper teardown.
Plugin: helpconfig, Hook: pytest_cmdline_parse
AttributeError: module 'pytest' has no attribute 'StashKey'
For more information see https://pluggy.readthedocs.io/en/stable/api_reference.html#pluggy.PluggyTeardownRaisedWarning
```

python: 3.10
pytest: 6.2.5
pytest-ruff: 0.2.1


Discussion / hints:
The `pytest.stash` exists only in `pytest>=7`.

I'll check if it is possible to implement something similar to `pytest<7`, but I suggest you to update your pytest.
TIL the stash feature was called store and was private.

https://github.com/pytest-dev/pytest/pull/8920


Fix patch:
diff --git a/pyproject.toml b/pyproject.toml
index 0712e1d..f3dd12b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -14,11 +14,11 @@ requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
 build-backend = "poetry_dynamic_versioning.backend"
 
 [tool.poetry.dependencies]
+pytest = ">=5"
 python = "^3.8"
 ruff = ">=0.0.242"
 
 [tool.poetry.group.dev.dependencies]
-pytest = "^7.2.0"
 pytest-mock = "^3.10.0"
 pytest-cov = "^4.1.0"
 
diff --git a/tox.ini b/tox.ini
index f16d762..fcbc6d6 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,14 +1,22 @@
 [tox]
 isolated_build = true
-envlist = py{38,39,310,311,312},lint
+envlist = py{38,39,310,311,312}-pytest{5,6,7,8},lint
 
 [testenv]
 allowlist_externals = poetry
 commands =
     poetry install --no-root --with dev
+    pytest5: pip install "pytest<6"
+    pytest6: pip install "pytest>=6,<7"
+    pytest7: pip install "pytest>=7,<8"
+    pytest8: pip install "pytest>=8,<9"
     # Disable ruff plugin to generate better coverage results
     poetry run pytest -p no:ruff -vvv --cov --cov-append --cov-report term --cov-report xml {posargs}
 
+# pytest 5 does not work on python>=3.10
+[testenv:py{310,311,312}-pytest{5}]
+ignore_outcome = true
+
 [testenv:lint]
 description = lint source code
 deps =


Test patch:
diff --git a/pytest_ruff.py b/pytest_ruff/__init__.py
similarity index 79%
rename from pytest_ruff.py
rename to pytest_ruff/__init__.py
index ffa8456..dea76a7 100644
--- a/pytest_ruff.py
+++ b/pytest_ruff/__init__.py
@@ -1,13 +1,12 @@
 from subprocess import Popen, PIPE
 
-# Python<=3.8 don't support typing with builtin dict.
-from typing import Dict
-
 import pytest
+
 from ruff.__main__ import find_ruff_bin
 
+from pytest_ruff._pytest_compat import get_stash, make_path_kwargs, set_stash
+
 HISTKEY = "ruff/mtimes"
-_MTIMES_STASH_KEY = pytest.StashKey[Dict[str, float]]()
 
 
 def pytest_addoption(parser):
@@ -24,18 +23,18 @@ def pytest_configure(config):
     if not config.option.ruff or not hasattr(config, "cache"):
         return
 
-    config.stash[_MTIMES_STASH_KEY] = config.cache.get(HISTKEY, {})
+    set_stash(config, config.cache.get(HISTKEY, {}))
 
 
-def pytest_collect_file(file_path, path, parent):
+def pytest_collect_file(path, parent, fspath=None):
     config = parent.config
     if not config.option.ruff:
         return
 
-    if file_path.suffix != ".py":
+    if path.ext != ".py":
         return
 
-    return RuffFile.from_parent(parent, path=file_path)
+    return RuffFile.from_parent(parent, **make_path_kwargs(path))
 
 
 def pytest_sessionfinish(session, exitstatus):
@@ -48,7 +47,7 @@ def pytest_sessionfinish(session, exitstatus):
     # It works fine if pytest-xdist is not being used.
     if not hasattr(config, "workerinput"):
         cache = config.cache.get(HISTKEY, {})
-        cache.update(config.stash[_MTIMES_STASH_KEY])
+        cache.update(get_stash(config))
         config.cache.set(HISTKEY, cache)
 
 
@@ -93,16 +92,17 @@ def __init__(self, *k, **kwargs):
         self.add_marker("ruff")
 
     def setup(self):
-        ruffmtimes = self.config.stash.get(_MTIMES_STASH_KEY, {})
+        ruffmtimes = get_stash(self.config)
         self._ruffmtime = self.fspath.mtime()
-        old = ruffmtimes.get(str(self.fspath))
-        if old == self._ruffmtime:
-            pytest.skip("file previously passed ruff checks")
+        if ruffmtimes:
+            old = ruffmtimes.get(str(self.fspath))
+            if old == self._ruffmtime:
+                pytest.skip("file previously passed ruff checks")
 
     def runtest(self):
         self.handler(path=self.fspath)
 
-        ruffmtimes = self.config.stash.get(_MTIMES_STASH_KEY, None)
+        ruffmtimes = get_stash(self.config)
         if ruffmtimes:
             ruffmtimes[str(self.fspath)] = self._ruffmtime
 
diff --git a/pytest_ruff/_pytest_compat.py b/pytest_ruff/_pytest_compat.py
new file mode 100644
index 0000000..2d2db7d
--- /dev/null
+++ b/pytest_ruff/_pytest_compat.py
@@ -0,0 +1,46 @@
+from pathlib import Path
+
+# Python<=3.8 don't support typing with builtin dict.
+from typing import Dict
+
+import pytest
+
+try:
+    from pytest import Stash, StashKey
+except ImportError:
+    import _pytest.store
+
+    Stash = _pytest.store.Store
+    StashKey = _pytest.store.StoreKey
+
+PYTEST_VER = tuple(int(x) for x in pytest.__version__.split(".")[:2])
+_MTIMES_STASH_KEY = StashKey[Dict[str, float]]()
+
+
+def make_path_kwargs(p):
+    """
+    Make keyword arguments passing either path or fspath, depending on pytest version.
+
+    In pytest 7.0, the `fspath` argument to Nodes has been deprecated, so we pass `path`
+    instead.
+    """
+    return dict(path=Path(p)) if PYTEST_VER >= (7, 0) else dict(fspath=p)
+
+
+def get_stash_object(config):
+    try:
+        stash = config.stash
+    except AttributeError:
+        stash = config._store
+    return stash
+
+
+def get_stash(config):
+    missing = object()
+    stash = get_stash_object(config).get(_MTIMES_STASH_KEY, default=missing)
+    assert stash is not missing
+    return stash
+
+
+def set_stash(config, value):
+    get_stash_object(config)[_MTIMES_STASH_KEY] = value
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index bf0bc10..7775a22 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -4,28 +4,28 @@
 import pytest
 
 import pytest_ruff
+from pytest_ruff._pytest_compat import Stash, get_stash
 
 
 def test_configure(mocker):
     config = mocker.Mock(
         cache={pytest_ruff.HISTKEY: mocker.sentinel.cache},
         option=mocker.Mock(ruff=True),
-        stash=pytest.Stash(),
+        stash=Stash(),
     )
     pytest_ruff.pytest_configure(config)
-    assert config.stash[pytest_ruff._MTIMES_STASH_KEY] == mocker.sentinel.cache
+    assert get_stash(config) == mocker.sentinel.cache
 
 
 def test_configure_without_ruff(mocker):
     config = mocker.Mock(
         option=mocker.Mock(ruff=False),
-        stash=pytest.Stash(),
         # Mocking to `not hasattr(config, "cache")`.
         spec=["addinivalue_line", "option", "stash"],
     )
+    set_stash_mock = mocker.patch("pytest_ruff.set_stash", spec=True)
     pytest_ruff.pytest_configure(config)
-    with pytest.raises(KeyError):
-        config.stash[pytest_ruff._MTIMES_STASH_KEY]
+    set_stash_mock.assert_not_called()
 
 
 def test_check_file():
@@ -61,6 +61,7 @@ def test_pytest_ruff_format():
         stdout=subprocess.PIPE,
         stderr=subprocess.PIPE,
     ).communicate()
+    assert err.decode() == ""
     assert "File would be reformatted" in out.decode("utf-8")
 
 
@@ -76,4 +77,5 @@ def test_pytest_ruff_noformat():
         stdout=subprocess.PIPE,
         stderr=subprocess.PIPE,
     ).communicate()
+    assert err.decode() == ""
     assert "File would be reformatted" not in out.decode("utf-8")