]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
adapt pytest plugin to support pytest v7
authorFederico Caselli <cfederico87@gmail.com>
Sat, 27 Nov 2021 08:53:29 +0000 (09:53 +0100)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 29 Nov 2021 21:13:15 +0000 (16:13 -0500)
Implemented support for the test suite to run correctly under Pytest 7.
Previously, only Pytest 6.x was supported for Python 3, however the version
was not pinned on the upper bound in tox.ini. Pytest is not pinned in
tox.ini to be lower than version 8 so that SQLAlchemy versions released
with the current codebase will be able to be tested under tox without
changes to the environment.   Much thanks to the Pytest developers for
their help with this issue.

Change-Id: I3b12166199be2b913ee16e78b3ebbff415654396

doc/build/changelog/unreleased_14/pytest7.rst [new file with mode: 0644]
lib/sqlalchemy/testing/asyncio.py
lib/sqlalchemy/testing/plugin/bootstrap.py
lib/sqlalchemy/testing/plugin/plugin_base.py
lib/sqlalchemy/testing/plugin/pytestplugin.py
test/base/test_except.py
test/conftest.py
tox.ini

diff --git a/doc/build/changelog/unreleased_14/pytest7.rst b/doc/build/changelog/unreleased_14/pytest7.rst
new file mode 100644 (file)
index 0000000..4397626
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: bug, tests
+
+    Implemented support for the test suite to run correctly under Pytest 7.
+    Previously, only Pytest 6.x was supported for Python 3, however the version
+    was not pinned on the upper bound in tox.ini. Pytest is not pinned in
+    tox.ini to be lower than version 8 so that SQLAlchemy versions released
+    with the current codebase will be able to be tested under tox without
+    changes to the environment.   Much thanks to the Pytest developers for
+    their help with this issue.
+
index 877d1eb94bf1164060c85ab7697c816c2201052f..b964ac57cebf9fbe46132d19b992f2a1d0b2d4a5 100644 (file)
@@ -63,7 +63,6 @@ def _maybe_async_provisioning(fn, *args, **kwargs):
 
     """
     if not ENABLE_ASYNCIO:
-
         return fn(*args, **kwargs)
 
     if config.any_async:
index 1220561e868530c14d5c13eb33b777d32f1a5897..e4f6058e108b17076fa388d0ccf8bc59af3ab1b9 100644 (file)
@@ -12,11 +12,10 @@ of the same test environment and standard suites available to
 SQLAlchemy/Alembic themselves without the need to ship/install a separate
 package outside of SQLAlchemy.
 
-NOTE:  copied/adapted from SQLAlchemy main for backwards compatibility;
-this should be removable when Alembic targets SQLAlchemy 1.0.0.
 
 """
 
+import importlib.util
 import os
 import sys
 
@@ -27,14 +26,12 @@ to_bootstrap = locals()["to_bootstrap"]
 
 def load_file_as_module(name):
     path = os.path.join(os.path.dirname(bootstrap_file), "%s.py" % name)
-    if sys.version_info >= (3, 3):
-        from importlib import machinery
 
-        mod = machinery.SourceFileLoader(name, path).load_module()
-    else:
-        import imp
-
-        mod = imp.load_source(name, path)
+    spec = importlib.util.spec_from_file_location(name, path)
+    assert spec is not None
+    assert spec.loader is not None
+    mod = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(mod)
     return mod
 
 
index d79931b91ea7e6976290304ab8839ec94aeabe3b..7bc88a14b79c238712043f976220f25c168aa56f 100644 (file)
@@ -86,7 +86,7 @@ def setup_options(make_option):
     make_option(
         "--dbdriver",
         action="append",
-        type="string",
+        type=str,
         dest="dbdriver",
         help="Additional database drivers to include in tests.  "
         "These are linked to the existing database URLs by the "
index ba774b118dac5f57fb7d18efa6d3907e52cc90e7..7caa50438c1f3c0aa36689a2200747691da04075 100644 (file)
@@ -197,27 +197,34 @@ def pytest_collection_modifyitems(session, config, items):
     items[:] = [
         item
         for item in items
-        if isinstance(item.parent, pytest.Instance)
-        and not item.parent.parent.name.startswith("_")
+        if item.getparent(pytest.Class) is not None
+        and not item.getparent(pytest.Class).name.startswith("_")
     ]
 
-    test_classes = set(item.parent for item in items)
+    test_classes = set(item.getparent(pytest.Class) for item in items)
+
+    def collect(element):
+        for inst_or_fn in element.collect():
+            if isinstance(inst_or_fn, pytest.Collector):
+                yield from collect(inst_or_fn)
+            else:
+                yield inst_or_fn
 
     def setup_test_classes():
         for test_class in test_classes:
             for sub_cls in plugin_base.generate_sub_tests(
-                test_class.cls, test_class.parent.module
+                test_class.cls, test_class.module
             ):
                 if sub_cls is not test_class.cls:
                     per_cls_dict = rebuilt_items[test_class.cls]
 
-                    # support pytest 5.4.0 and above pytest.Class.from_parent
-                    ctor = getattr(pytest.Class, "from_parent", pytest.Class)
-                    for inst in ctor(
-                        name=sub_cls.__name__, parent=test_class.parent.parent
-                    ).collect():
-                        for t in inst.collect():
-                            per_cls_dict[t.name].append(t)
+                    module = test_class.getparent(pytest.Module)
+                    for fn in collect(
+                        pytest.Class.from_parent(
+                            name=sub_cls.__name__, parent=module
+                        )
+                    ):
+                        per_cls_dict[fn.name].append(fn)
 
     # class requirements will sometimes need to access the DB to check
     # capabilities, so need to do this for async
@@ -225,8 +232,9 @@ def pytest_collection_modifyitems(session, config, items):
 
     newitems = []
     for item in items:
-        if item.parent.cls in rebuilt_items:
-            newitems.extend(rebuilt_items[item.parent.cls][item.name])
+        cls_ = item.cls
+        if cls_ in rebuilt_items:
+            newitems.extend(rebuilt_items[cls_][item.name])
         else:
             newitems.append(item)
 
@@ -235,8 +243,8 @@ def pytest_collection_modifyitems(session, config, items):
     items[:] = sorted(
         newitems,
         key=lambda item: (
-            item.parent.parent.parent.name,
-            item.parent.parent.name,
+            item.getparent(pytest.Module).name,
+            item.getparent(pytest.Class).name,
             item.name,
         ),
     )
@@ -249,14 +257,15 @@ def pytest_pycollect_makeitem(collector, name, obj):
         if config.any_async:
             obj = _apply_maybe_async(obj)
 
-        ctor = getattr(pytest.Class, "from_parent", pytest.Class)
         return [
-            ctor(name=parametrize_cls.__name__, parent=collector)
+            pytest.Class.from_parent(
+                name=parametrize_cls.__name__, parent=collector
+            )
             for parametrize_cls in _parametrize_cls(collector.module, obj)
         ]
     elif (
         inspect.isfunction(obj)
-        and isinstance(collector, pytest.Instance)
+        and collector.cls is not None
         and plugin_base.want_method(collector.cls, obj)
     ):
         # None means, fall back to default logic, which includes
@@ -345,9 +354,6 @@ _current_class = None
 def pytest_runtest_setup(item):
     from sqlalchemy.testing import asyncio
 
-    if not isinstance(item, pytest.Function):
-        return
-
     # pytest_runtest_setup runs *before* pytest fixtures with scope="class".
     # plugin_base.start_test_class_outside_fixtures may opt to raise SkipTest
     # for the whole class and has to run things that are across all current
@@ -356,48 +362,66 @@ def pytest_runtest_setup(item):
 
     global _current_class
 
-    if _current_class is None:
+    if isinstance(item, pytest.Function) and _current_class is None:
         asyncio._maybe_async_provisioning(
             plugin_base.start_test_class_outside_fixtures,
-            item.parent.parent.cls,
+            item.cls,
         )
-        _current_class = item.parent.parent
+        _current_class = item.getparent(pytest.Class)
 
-        def finalize():
-            global _current_class, _current_report
-            _current_class = None
 
-            try:
-                asyncio._maybe_async_provisioning(
-                    plugin_base.stop_test_class_outside_fixtures,
-                    item.parent.parent.cls,
-                )
-            except Exception as e:
-                # in case of an exception during teardown attach the original
-                # error to the exception message, otherwise it will get lost
-                if _current_report.failed:
-                    if not e.args:
-                        e.args = (
-                            "__Original test failure__:\n"
-                            + _current_report.longreprtext,
-                        )
-                    elif e.args[-1] and isinstance(e.args[-1], str):
-                        args = list(e.args)
-                        args[-1] += (
-                            "\n__Original test failure__:\n"
-                            + _current_report.longreprtext
-                        )
-                        e.args = tuple(args)
-                    else:
-                        e.args += (
-                            "__Original test failure__",
-                            _current_report.longreprtext,
-                        )
-                raise
-            finally:
-                _current_report = None
+@pytest.hookimpl(hookwrapper=True)
+def pytest_runtest_teardown(item, nextitem):
+    # runs inside of pytest function fixture scope
+    # after test function runs
+
+    from sqlalchemy.testing import asyncio
 
-        item.parent.parent.addfinalizer(finalize)
+    asyncio._maybe_async(plugin_base.after_test, item)
+
+    yield
+    # this is now after all the fixture teardown have run, the class can be
+    # finalized. Since pytest v7 this finalizer can no longer be added in
+    # pytest_runtest_setup since the class has not yet been setup at that
+    # time.
+    # See https://github.com/pytest-dev/pytest/issues/9343
+    global _current_class, _current_report
+
+    if _current_class is not None and (
+        # last test or a new class
+        nextitem is None
+        or nextitem.getparent(pytest.Class) is not _current_class
+    ):
+        _current_class = None
+
+        try:
+            asyncio._maybe_async_provisioning(
+                plugin_base.stop_test_class_outside_fixtures, item.cls
+            )
+        except Exception as e:
+            # in case of an exception during teardown attach the original
+            # error to the exception message, otherwise it will get lost
+            if _current_report.failed:
+                if not e.args:
+                    e.args = (
+                        "__Original test failure__:\n"
+                        + _current_report.longreprtext,
+                    )
+                elif e.args[-1] and isinstance(e.args[-1], str):
+                    args = list(e.args)
+                    args[-1] += (
+                        "\n__Original test failure__:\n"
+                        + _current_report.longreprtext
+                    )
+                    e.args = tuple(args)
+                else:
+                    e.args += (
+                        "__Original test failure__",
+                        _current_report.longreprtext,
+                    )
+            raise
+        finally:
+            _current_report = None
 
 
 def pytest_runtest_call(item):
@@ -409,8 +433,8 @@ def pytest_runtest_call(item):
     asyncio._maybe_async(
         plugin_base.before_test,
         item,
-        item.parent.module.__name__,
-        item.parent.cls,
+        item.module.__name__,
+        item.cls,
         item.name,
     )
 
@@ -424,15 +448,6 @@ def pytest_runtest_logreport(report):
         _current_report = report
 
 
-def pytest_runtest_teardown(item, nextitem):
-    # runs inside of pytest function fixture scope
-    # after test function runs
-
-    from sqlalchemy.testing import asyncio
-
-    asyncio._maybe_async(plugin_base.after_test, item)
-
-
 @pytest.fixture(scope="class")
 def setup_class_methods(request):
     from sqlalchemy.testing import asyncio
index 6e9a3c5df3b725b4103700421effa5fe5ae7ee57..0bde988b79c094c6281dfdde95b0abe8a428ceac 100644 (file)
@@ -533,7 +533,6 @@ class PickleException(fixtures.TestBase):
         for cls_list, callable_list in ALL_EXC:
             unroll.extend(product(cls_list, callable_list))
 
-        print(unroll)
         return combinations_list(unroll)
 
     @make_combinations()
index 6f08a7c0dd624dc2a8a1481cab89ffa7853e4a87..921a4aadc55063aec7d655cff8518944ed80746d 100755 (executable)
@@ -46,4 +46,4 @@ with open(bootstrap_file) as f:
     code = compile(f.read(), "bootstrap.py", "exec")
     to_bootstrap = "pytest"
     exec(code, globals(), locals())
-    from pytestplugin import *  # noqa
+    from sqla_pytestplugin import *  # noqa
diff --git a/tox.ini b/tox.ini
index 9f6ccb0c65f735af17f291e187d284747e3c90da..5ac5ef1a286743f22b6d7031cb247404e3a39275 100644 (file)
--- a/tox.ini
+++ b/tox.ini
@@ -17,7 +17,7 @@ usedevelop=
 
 deps=
      pytest>=4.6.11,<5.0; python_version < '3'
-     pytest>=6.2; python_version >= '3'
+     pytest>=6.2,<8; python_version >= '3'
      pytest-xdist
      mock; python_version < '3.3'