]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
use @classmethod per pytest guidance
authorMike Bayer <mike_mp@zzzcomputing.com>
Mon, 22 Jun 2026 15:36:02 +0000 (11:36 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 22 Jun 2026 22:57:55 +0000 (18:57 -0400)
Fixed class-scoped pytest fixtures that were defined as instance methods
using ``self``, which is deprecated as of pytest 9.1 and will be removed in
pytest 10. Fixtures are now decorated with a compatibility ``@classmethod``
decorator and use ``cls`` as the first parameter.

In the 2.0 branch, the approach is considerably more complicated
as python 3.7, 3.8, 3.9 doesn't work with this pattern; since the
warning appears in pytest 9.1 and not 9.0, which itself is only
python 3.10+, we use a conditional classmethod wrapper.

in the 2.1 branch, we can just use straight `@classmethod`.

Fixes: #13392
Change-Id: I866de54e22569c9d55eb97dce165670994ec4a57
(cherry picked from commit cd7eeb34d3741c94b2f9476aec7f3386132a71d5)

doc/build/changelog/unreleased_20/13392.rst [new file with mode: 0644]
lib/sqlalchemy/testing/__init__.py
lib/sqlalchemy/testing/config.py
lib/sqlalchemy/testing/fixtures/mypy.py
lib/sqlalchemy/testing/fixtures/orm.py
lib/sqlalchemy/testing/fixtures/sql.py
lib/sqlalchemy/testing/plugin/plugin_base.py
lib/sqlalchemy/testing/plugin/pytestplugin.py
test/aaa_profiling/test_misc.py
test/ext/test_mutable.py

diff --git a/doc/build/changelog/unreleased_20/13392.rst b/doc/build/changelog/unreleased_20/13392.rst
new file mode 100644 (file)
index 0000000..cd9089b
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: bug, tests
+    :tickets: 13392
+
+    Fixed class-scoped pytest fixtures that were defined as instance methods
+    using ``self``, which is deprecated as of pytest 9.1 and will be removed in
+    pytest 10. Fixtures are now decorated with a compatibility ``@classmethod``
+    decorator and use ``cls`` as the first parameter.
index 41ddcedaff178c29b2333cc6350ec219de038918..388ec8a72489f641e64be2b9976d79c76ea1eed6 100644 (file)
@@ -52,6 +52,7 @@ from .config import combinations
 from .config import combinations_list
 from .config import db
 from .config import fixture
+from .config import fixture_classmethod
 from .config import requirements as requires
 from .config import skip_test
 from .config import Variation
index 09ed89bfbc4a199f44fa39187bcc180514f6d288..c723911d157dbc879735826ff6f15cb841e55a42 100644 (file)
@@ -80,6 +80,9 @@ else:
         def async_test(self, fn):
             return fn
 
+        def fixture_classmethod(self, fn):
+            return classmethod(fn)
+
     # default fixture functions; these are replaced by plugin_base when
     # pytest runs
     _fixture_functions = _NullFixtureFunctions()
@@ -432,3 +435,7 @@ def skip_test(msg):
 
 def async_test(fn):
     return _fixture_functions.async_test(fn)
+
+
+def fixture_classmethod(fn):
+    return _fixture_functions.fixture_classmethod(fn)
index baac539afb3a60646c8e9d57ec4c71ca0d48ec7d..299e2157a768ba3c631abcde83ab95109d973b4b 100644 (file)
@@ -31,10 +31,12 @@ class MypyTest(TestBase):
         yield from self._cachedir()
 
     @config.fixture(scope="class")
-    def cachedir(self):
-        yield from self._cachedir()
+    @config.fixture_classmethod
+    def cachedir(cls):
+        yield from cls._cachedir()
 
-    def _cachedir(self):
+    @config.fixture_classmethod
+    def _cachedir(cls):
         # as of mypy 0.971 i think we need to keep mypy_path empty
         mypy_path = ""
 
index 65693b719c6210ae17d45288625cce8ad35fe166..05050a8e4b2a5db61803fac30dd395ef57d4a35c 100644 (file)
@@ -40,8 +40,8 @@ class MappedTest(ORMTest, TablesTest, assertions.AssertsExecutionResults):
     classes: Any = None
 
     @config.fixture(autouse=True, scope="class")
-    def _setup_tables_test_class(self):
-        cls = self.__class__
+    @config.fixture_classmethod
+    def _setup_tables_test_class(cls):
         cls._init_class()
 
         if cls.classes is None:
index 18ac4f554bc10130fd8a29fa121862d0a4f72cb7..6f2087815c8ec140629be7d5510ee5482dc5cd3b 100644 (file)
@@ -53,8 +53,8 @@ class TablesTest(TestBase):
     sequences = None
 
     @config.fixture(autouse=True, scope="class")
-    def _setup_tables_test_class(self):
-        cls = self.__class__
+    @config.fixture_classmethod
+    def _setup_tables_test_class(cls):
         cls._init_class()
 
         cls._setup_once_tables()
index c4cabb33ebc2bd0e3eb3d909584559784404fc2b..1cab431f6af1214064e915f838b2fbbf19e6ac0a 100644 (file)
@@ -819,6 +819,10 @@ class FixtureFunctions(abc.ABC):
     def add_to_marker(self):
         raise NotImplementedError()
 
+    @abc.abstractmethod
+    def fixture_classmethod(self, fn) -> Any:
+        raise NotImplementedError()
+
 
 _fixture_fn_class = None
 
index c430e76c1053eebcd8b8c63d921d90a352453ffe..56ba20c3299498df46bce338333d7c2192cd06c5 100644 (file)
@@ -11,6 +11,7 @@ from __future__ import annotations
 import argparse
 import collections
 from functools import update_wrapper
+from functools import wraps
 import inspect
 import itertools
 import operator
@@ -679,6 +680,28 @@ def %(name)s%(grouped_args)s:
 
 
 class PytestFixtureFunctions(plugin_base.FixtureFunctions):
+
+    def fixture_classmethod(self, fn):
+        """a conditional `@classmethod` decorator that we use only on py3.10
+        on forward, for compatibility with pytest 9.1+."""
+
+        if pytest.version_tuple >= (9, 1):
+            return classmethod(fn)
+        else:
+            if inspect.isgeneratorfunction(fn):
+
+                @wraps(fn)
+                def wrap(self, *args, **kw):
+                    yield from fn(self.__class__, *args, **kw)
+
+            else:
+
+                @wraps(fn)
+                def wrap(self, *args, **kw):
+                    return fn(self.__class__, *args, **kw)
+
+            return wrap
+
     def skip_test_exception(self, *arg, **kw):
         return pytest.skip.Exception(*arg, **kw)
 
@@ -862,10 +885,17 @@ class PytestFixtureFunctions(plugin_base.FixtureFunctions):
         # now apply wrappers to the function, including fixture itself
 
         def wrap(fn):
+            is_classmethod = isinstance(fn, classmethod)
+            if is_classmethod:
+                fn = fn.__func__
+
             if config.any_async:
                 fn = asyncio._maybe_async_wrapper(fn)
             # other wrappers may be added here
 
+            if is_classmethod:
+                fn = classmethod(fn)
+
             # now apply FixtureFunctionMarker
             fn = fixture(fn)
 
index c51648c677c22bdadb8ac7f1179f8ceff2120a81..60fb348daedb346a5a9d4c7be45db293bceb3b75 100644 (file)
@@ -56,7 +56,8 @@ class CacheKeyTest(fixtures.TestBase):
     __requires__ = ("cpython", "python_profiling_backend")
 
     @testing.fixture(scope="class")
-    def mapping_fixture(self):
+    @testing.fixture_classmethod
+    def mapping_fixture(cls):
         # note in order to work nicely with "fixture" we are emerging
         # a whole new model of setup/teardown, since pytest "fixture"
         # sort of purposely works badly with setup/teardown
index c72f182f8fc5ccc0a33b262417d06f077915a1a0..5ac05d1e598038ebdefd014f0d6baa34fd29cc9f 100644 (file)
@@ -1078,7 +1078,8 @@ class _MutableSetTestBase(_MutableSetTestFixture):
 
 class _MutableNoHashFixture:
     @testing.fixture(autouse=True, scope="class")
-    def set_class(self):
+    @testing.fixture_classmethod
+    def set_class(cls):
         global Foo
 
         _replace_foo = Foo