diff options
-rw-r--r-- | doc/build/changelog/unreleased_14/pytest7.rst | 11 | ||||
-rw-r--r-- | lib/sqlalchemy/testing/asyncio.py | 1 | ||||
-rw-r--r-- | lib/sqlalchemy/testing/plugin/bootstrap.py | 15 | ||||
-rw-r--r-- | lib/sqlalchemy/testing/plugin/plugin_base.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/testing/plugin/pytestplugin.py | 151 | ||||
-rw-r--r-- | test/base/test_except.py | 1 | ||||
-rwxr-xr-x | test/conftest.py | 2 | ||||
-rw-r--r-- | tox.ini | 2 |
8 files changed, 103 insertions, 82 deletions
diff --git a/doc/build/changelog/unreleased_14/pytest7.rst b/doc/build/changelog/unreleased_14/pytest7.rst new file mode 100644 index 000000000..439762626 --- /dev/null +++ b/doc/build/changelog/unreleased_14/pytest7.rst @@ -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. + diff --git a/lib/sqlalchemy/testing/asyncio.py b/lib/sqlalchemy/testing/asyncio.py index 877d1eb94..b964ac57c 100644 --- a/lib/sqlalchemy/testing/asyncio.py +++ b/lib/sqlalchemy/testing/asyncio.py @@ -63,7 +63,6 @@ def _maybe_async_provisioning(fn, *args, **kwargs): """ if not ENABLE_ASYNCIO: - return fn(*args, **kwargs) if config.any_async: diff --git a/lib/sqlalchemy/testing/plugin/bootstrap.py b/lib/sqlalchemy/testing/plugin/bootstrap.py index 1220561e8..e4f6058e1 100644 --- a/lib/sqlalchemy/testing/plugin/bootstrap.py +++ b/lib/sqlalchemy/testing/plugin/bootstrap.py @@ -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 diff --git a/lib/sqlalchemy/testing/plugin/plugin_base.py b/lib/sqlalchemy/testing/plugin/plugin_base.py index d79931b91..7bc88a14b 100644 --- a/lib/sqlalchemy/testing/plugin/plugin_base.py +++ b/lib/sqlalchemy/testing/plugin/plugin_base.py @@ -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 " diff --git a/lib/sqlalchemy/testing/plugin/pytestplugin.py b/lib/sqlalchemy/testing/plugin/pytestplugin.py index ba774b118..7caa50438 100644 --- a/lib/sqlalchemy/testing/plugin/pytestplugin.py +++ b/lib/sqlalchemy/testing/plugin/pytestplugin.py @@ -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 diff --git a/test/base/test_except.py b/test/base/test_except.py index 6e9a3c5df..0bde988b7 100644 --- a/test/base/test_except.py +++ b/test/base/test_except.py @@ -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() diff --git a/test/conftest.py b/test/conftest.py index 6f08a7c0d..921a4aadc 100755 --- a/test/conftest.py +++ b/test/conftest.py @@ -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 @@ -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' |