diff --git a/docs/changelog.md b/docs/changelog.md index b82d2fc1..491edbec 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,10 @@ # Changelog -### 3.8.3 (in progress) - TBD +### 3.8.3 - Support for `pytest` version 8 - - tbd + - Fixed compliance with pytest 8. Fixed [#330](https://github.com/smarie/python-pytest-cases/issues/330). PR + [#335](https://github.com/smarie/python-pytest-cases/pull/335) by [smarie](https://github.com/smarie) and + [larsoner](https://github.com/larsoner). ### 3.8.2 - bugfixes and project improvements diff --git a/src/pytest_cases/common_pytest.py b/src/pytest_cases/common_pytest.py index a0826666..99c6014e 100644 --- a/src/pytest_cases/common_pytest.py +++ b/src/pytest_cases/common_pytest.py @@ -30,7 +30,8 @@ from .common_others import get_function_host from .common_pytest_marks import make_marked_parameter_value, get_param_argnames_as_list, \ get_pytest_parametrize_marks, get_pytest_usefixture_marks, PYTEST3_OR_GREATER, PYTEST6_OR_GREATER, \ - PYTEST38_OR_GREATER, PYTEST34_OR_GREATER, PYTEST33_OR_GREATER, PYTEST32_OR_GREATER, PYTEST71_OR_GREATER + PYTEST38_OR_GREATER, PYTEST34_OR_GREATER, PYTEST33_OR_GREATER, PYTEST32_OR_GREATER, PYTEST71_OR_GREATER, \ + PYTEST8_OR_GREATER from .common_pytest_lazy_values import is_lazy_value, is_lazy @@ -554,6 +555,14 @@ def set_callspec_arg_scope_to_function(callspec, arg_name): callspec._arg2scopenum[arg_name] = get_pytest_function_scopeval() # noqa +def in_callspec_explicit_args( + callspec, # type: CallSpec2 + name # type: str +): # type: (...) -> bool + """Return True if name is explicitly used in callspec args""" + return (name in callspec.params) or (not PYTEST8_OR_GREATER and name in callspec.funcargs) + + if PYTEST71_OR_GREATER: from _pytest.python import IdMaker # noqa @@ -653,14 +662,27 @@ def getfuncargnames(function, cls=None): return arg_names +class FakeSession(object): + __slots__ = ('_fixturemanager',) + + def __init__(self): + self._fixturemanager = None + + class MiniFuncDef(object): - __slots__ = ('nodeid',) + __slots__ = ('nodeid', 'session') def __init__(self, nodeid): self.nodeid = nodeid + if PYTEST8_OR_GREATER: + self.session = FakeSession() class MiniMetafunc(Metafunc): + """ + A class to know what pytest *would* do for a given function in terms of callspec. + It is used in function `case_to_argvalues` + """ # noinspection PyMissingConstructor def __init__(self, func): from .plugin import PYTEST_CONFIG # late import to ensure config has been loaded by now @@ -685,12 +707,18 @@ def __init__(self, func): self.fixturenames_not_in_sig = [f for f in get_pytest_usefixture_marks(func) if f not in self.fixturenames] if self.fixturenames_not_in_sig: self.fixturenames = tuple(self.fixturenames_not_in_sig + list(self.fixturenames)) + + if PYTEST8_OR_GREATER: + # dummy + self._arg2fixturedefs = dict() # type: dict[str, Sequence["FixtureDef[Any]"]] + # get parametrization marks self.pmarks = get_pytest_parametrize_marks(self.function) if self.is_parametrized: self.update_callspecs() # preserve order - self.required_fixtures = tuple(f for f in self.fixturenames if f not in self._calls[0].funcargs) + ref_names = self._calls[0].params if PYTEST8_OR_GREATER else self._calls[0].funcargs + self.required_fixtures = tuple(f for f in self.fixturenames if f not in ref_names) else: self.required_fixtures = self.fixturenames @@ -773,8 +801,7 @@ def get_callspecs(func): Returns a list of pytest CallSpec objects corresponding to calls that should be made for this parametrized function. This mini-helper assumes no complex things (scope='function', indirect=False, no fixtures, no custom configuration) - :param func: - :return: + Note that this function is currently only used in tests. """ meta = MiniMetafunc(func) # meta.update_callspecs() diff --git a/src/pytest_cases/common_pytest_marks.py b/src/pytest_cases/common_pytest_marks.py index 3dff960f..d4211514 100644 --- a/src/pytest_cases/common_pytest_marks.py +++ b/src/pytest_cases/common_pytest_marks.py @@ -43,6 +43,7 @@ PYTEST6_OR_GREATER = PYTEST_VERSION >= Version('6.0.0') PYTEST7_OR_GREATER = PYTEST_VERSION >= Version('7.0.0') PYTEST71_OR_GREATER = PYTEST_VERSION >= Version('7.1.0') +PYTEST8_OR_GREATER = PYTEST_VERSION >= Version('8.0.0') def get_param_argnames_as_list(argnames): diff --git a/src/pytest_cases/plugin.py b/src/pytest_cases/plugin.py index 457b8da4..00d3b985 100644 --- a/src/pytest_cases/plugin.py +++ b/src/pytest_cases/plugin.py @@ -28,9 +28,9 @@ from .common_mini_six import string_types from .common_pytest_lazy_values import get_lazy_args -from .common_pytest_marks import PYTEST35_OR_GREATER, PYTEST46_OR_GREATER, PYTEST37_OR_GREATER, PYTEST7_OR_GREATER +from .common_pytest_marks import PYTEST35_OR_GREATER, PYTEST46_OR_GREATER, PYTEST37_OR_GREATER, PYTEST7_OR_GREATER, PYTEST8_OR_GREATER from .common_pytest import get_pytest_nodeid, get_pytest_function_scopeval, is_function_node, get_param_names, \ - get_param_argnames_as_list, has_function_scope, set_callspec_arg_scope_to_function + get_param_argnames_as_list, has_function_scope, set_callspec_arg_scope_to_function, in_callspec_explicit_args from .fixture_core1_unions import NOT_USED, USED, is_fixture_union_params, UnionFixtureAlternative @@ -41,7 +41,8 @@ from .case_parametrizer_new import get_current_cases -_DEBUG = False +_DEBUG = True +"""Note: this is a manual flag to turn when developing (do not forget to also call pytest with -s)""" # @pytest.hookimpl(hookwrapper=True, tryfirst=True) @@ -753,7 +754,7 @@ def remove_all(self, values): self._update_fixture_defs() -def getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()): +def _getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()): """ Replaces pytest's getfixtureclosure method to handle unions. """ @@ -764,7 +765,10 @@ def getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()): # new argument "ignore_args" in 4.6+ kwargs['ignore_args'] = ignore_args - if PYTEST37_OR_GREATER: + if PYTEST8_OR_GREATER: + # two outputs and sig change + ref_fixturenames, ref_arg2fixturedefs = fm.__class__.getfixtureclosure(fm, parentnode, fixturenames, **kwargs) + elif PYTEST37_OR_GREATER: # three outputs initial_names, ref_fixturenames, ref_arg2fixturedefs = \ fm.__class__.getfixtureclosure(fm, fixturenames, parentnode, **kwargs) @@ -781,12 +785,19 @@ def getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()): assert set(super_closure) == set(ref_fixturenames) assert dict(arg2fixturedefs) == ref_arg2fixturedefs - if PYTEST37_OR_GREATER: + if PYTEST37_OR_GREATER and not PYTEST8_OR_GREATER: return _init_fixnames, super_closure, arg2fixturedefs else: return super_closure, arg2fixturedefs +if PYTEST8_OR_GREATER: + def getfixtureclosure(fm, parentnode, initialnames, ignore_args): + return _getfixtureclosure(fm, fixturenames=initialnames, parentnode=parentnode, ignore_args=ignore_args) +else: + getfixtureclosure = _getfixtureclosure + + def create_super_closure(fm, parentnode, fixturenames, @@ -835,6 +846,11 @@ def _merge(new_items, into_list): # we cannot sort yet - merge the fixture names into the _init_fixnames _merge(fixturenames, _init_fixnames) + # Bugfix GH#330 in progress... + # TODO analyze why in the test "fixture_union_0simplest + # the first node contains second, and the second contains first + # or TODO check the test for get_callspecs, it is maybe simpler + # Finally create the closure fixture_defs_mgr = FixtureDefsCache(fm, parentnode) closure_tree = FixtureClosureNode(fixture_defs_mgr=fixture_defs_mgr) @@ -1035,7 +1051,8 @@ def create_call_list_from_pending_parametrizations(self): if _DEBUG: print("\n".join(["%s[%s]: funcargs=%s, params=%s" % (get_pytest_nodeid(self.metafunc), - c.id, c.funcargs, c.params) + c.id, c.params if PYTEST8_OR_GREATER else c.funcargs, + c.params) for c in calls]) + "\n") # clean EMPTY_ID set by @parametrize when there is at least a MultiParamsAlternative @@ -1107,7 +1124,7 @@ def _cleanup_calls_list(metafunc, # A/ set to "not used" all parametrized fixtures that were not used in some branches for fixture, p_to_apply in pending_dct.items(): - if fixture not in c.params and fixture not in c.funcargs: + if not in_callspec_explicit_args(c, fixture): # parametrize with a single "not used" value and discard the id if isinstance(p_to_apply, UnionParamz): c_with_dummy = _parametrize_calls(metafunc, [c], p_to_apply.union_fixture_name, [NOT_USED], @@ -1132,7 +1149,7 @@ def _cleanup_calls_list(metafunc, # For this we use a dirty hack: we add a parameter with they name in the callspec, it seems to be propagated # in the `request`. TODO is there a better way? for fixture_name in _not_always_used_func_scoped: - if fixture_name not in c.params and fixture_name not in c.funcargs: + if not in_callspec_explicit_args(c, fixture_name): if not n.requires(fixture_name): # explicitly add it as discarded by creating a parameter value for it. c.params[fixture_name] = NOT_USED diff --git a/tests/cases/issues/test_issue_126.py b/tests/cases/issues/test_issue_126.py index 269d49e3..ce1b7265 100644 --- a/tests/cases/issues/test_issue_126.py +++ b/tests/cases/issues/test_issue_126.py @@ -2,12 +2,18 @@ # + All contributors to # # License: 3-clause BSD, +from packaging.version import Version + import pytest from pytest_cases.common_pytest_marks import PYTEST3_OR_GREATER from pytest_cases import parametrize_with_cases +PYTEST_VERSION = Version(pytest.__version__) +PYTEST8_OR_GREATER = PYTEST_VERSION >= Version('8.0.0') + + @pytest.fixture() def dependent_fixture(): return 0 @@ -66,7 +72,23 @@ def test_synthesis(module_results_dct): for host in (test_functionality, test_functionality_again, TestNested.test_functionality_again2): assert markers_dict[host] == (set(), set()) - if PYTEST3_OR_GREATER: + if PYTEST8_OR_GREATER: + # in version 8 they added a smart suffix in case last char of id is already a numeric + assert list(module_results_dct) == [ + 'test_functionality[_requirement_1_0]', + 'test_functionality[_requirement_2_0]', + 'test_functionality[_requirement_1_1]', + 'test_functionality[_requirement_2_1]', + 'test_functionality_again[_requirement_1_0]', # <- note: same fixtures than previously + 'test_functionality_again[_requirement_2_0]', # idem + 'test_functionality_again[_requirement_1_1]', # idem + 'test_functionality_again[_requirement_2_1]', # idem + 'test_functionality_again2[_requirement_1_0]', # idem + 'test_functionality_again2[_requirement_2_0]', # idem + 'test_functionality_again2[_requirement_1_1]', # idem + 'test_functionality_again2[_requirement_2_1]' # idem + ] + elif PYTEST3_OR_GREATER: assert list(module_results_dct) == [ 'test_functionality[_requirement_10]', 'test_functionality[_requirement_20]', diff --git a/tests/pytest_extension/parametrize_plus/test_getcallspecs.py b/tests/pytest_extension/parametrize_plus/test_getcallspecs.py index 47cd41ad..466402d7 100644 --- a/tests/pytest_extension/parametrize_plus/test_getcallspecs.py +++ b/tests/pytest_extension/parametrize_plus/test_getcallspecs.py @@ -2,6 +2,8 @@ # + All contributors to # # License: 3-clause BSD, +from packaging.version import Version + import pytest from pytest_cases import parametrize @@ -9,6 +11,10 @@ from pytest_cases.common_pytest_marks import has_pytest_param +PYTEST_VERSION = Version(pytest.__version__) +PYTEST8_OR_GREATER = PYTEST_VERSION >= Version('8.0.0') + + if not has_pytest_param: @pytest.mark.parametrize('new_style', [False, True]) def test_getcallspecs(new_style): @@ -48,10 +54,18 @@ def test_foo(a): calls = get_callspecs(test_foo) assert len(calls) == 2 - assert calls[0].funcargs == dict(a=1) + if PYTEST8_OR_GREATER: + # funcargs disappears in version 8 + assert calls[0].params == dict(a=1) + else: + assert calls[0].funcargs == dict(a=1) assert calls[0].id == 'a=1' if new_style else 'oh' assert calls[0].marks == [] - assert calls[1].funcargs == dict(a='12') + if PYTEST8_OR_GREATER: + # funcargs disappears in version 8 + assert calls[1].params == dict(a='12') + else: + assert calls[1].funcargs == dict(a='12') assert calls[1].id == 'a=12' if new_style else 'hey' assert calls[1].marks[0].name == 'skip'