Skip to content

Commit

Permalink
Fixed compliance with pytest 8. Fixed #330
Browse files Browse the repository at this point in the history
  • Loading branch information
Sylvain MARIE committed Mar 8, 2024
1 parent c2a517b commit 9d2a902
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 52 deletions.
6 changes: 4 additions & 2 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
8 changes: 8 additions & 0 deletions src/pytest_cases/common_pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -555,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

Expand Down
86 changes: 39 additions & 47 deletions src/pytest_cases/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
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, 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

Expand Down Expand Up @@ -754,56 +754,48 @@ def remove_all(self, values):
self._update_fixture_defs()


if PYTEST8_OR_GREATER:
def getfixtureclosure(fm, parentnode, initialnames, ignore_args):
"""
Replaces pytest's getfixtureclosure method to handle unions.
"""
# (1) first retrieve the normal pytest output for comparison
ref_fixturenames, ref_arg2fixturedefs = fm.__class__.getfixtureclosure(fm, parentnode, initialnames, ignore_args)

# (2) now let's do it by ourselves to support fixture unions
_init_fixnames, super_closure, arg2fixturedefs = create_super_closure(fm, parentnode, ref_fixturenames, ignore_args)
def _getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()):
"""
Replaces pytest's getfixtureclosure method to handle unions.
"""

# Compare with the previous behaviour TODO remove when in 'production' ?
# NOTE different order happens all the time because of our "prepend" strategy in the closure building
# which makes much more sense/intuition than pytest default
assert set(super_closure) == set(ref_fixturenames)
assert dict(arg2fixturedefs) == ref_arg2fixturedefs
return super_closure, arg2fixturedefs
else:
def getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()):
"""
Replaces pytest's getfixtureclosure method to handle unions.
"""
# (1) first retrieve the normal pytest output for comparison
kwargs = dict()
if PYTEST46_OR_GREATER:
# new argument "ignore_args" in 4.6+
kwargs['ignore_args'] = ignore_args

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)
else:
# two outputs
ref_fixturenames, ref_arg2fixturedefs = fm.__class__.getfixtureclosure(fm, fixturenames, parentnode)

# (1) first retrieve the normal pytest output for comparison
kwargs = dict()
if PYTEST46_OR_GREATER:
# new argument "ignore_args" in 4.6+
kwargs['ignore_args'] = ignore_args
# (2) now let's do it by ourselves to support fixture unions
_init_fixnames, super_closure, arg2fixturedefs = create_super_closure(fm, parentnode, fixturenames, ignore_args)

if PYTEST37_OR_GREATER:
# three outputs
initial_names, ref_fixturenames, ref_arg2fixturedefs = \
fm.__class__.getfixtureclosure(fm, fixturenames, parentnode, **kwargs)
else:
# two outputs
ref_fixturenames, ref_arg2fixturedefs = fm.__class__.getfixtureclosure(fm, fixturenames, parentnode)
# Compare with the previous behaviour TODO remove when in 'production' ?
# NOTE different order happens all the time because of our "prepend" strategy in the closure building
# which makes much more sense/intuition than pytest default
assert set(super_closure) == set(ref_fixturenames)
assert dict(arg2fixturedefs) == ref_arg2fixturedefs

# (2) now let's do it by ourselves to support fixture unions
_init_fixnames, super_closure, arg2fixturedefs = create_super_closure(fm, parentnode, fixturenames, ignore_args)
if PYTEST37_OR_GREATER and not PYTEST8_OR_GREATER:
return _init_fixnames, super_closure, arg2fixturedefs
else:
return super_closure, arg2fixturedefs

# Compare with the previous behaviour TODO remove when in 'production' ?
# NOTE different order happens all the time because of our "prepend" strategy in the closure building
# which makes much more sense/intuition than pytest default
assert set(super_closure) == set(ref_fixturenames)
assert dict(arg2fixturedefs) == ref_arg2fixturedefs

if PYTEST37_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,
Expand Down Expand Up @@ -1132,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],
Expand All @@ -1157,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
Expand Down
24 changes: 23 additions & 1 deletion tests/cases/issues/test_issue_126.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@
# + All contributors to <https://github.com/smarie/python-pytest-cases>
#
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
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
Expand Down Expand Up @@ -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]',
Expand Down
18 changes: 16 additions & 2 deletions tests/pytest_extension/parametrize_plus/test_getcallspecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
# + All contributors to <https://github.com/smarie/python-pytest-cases>
#
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
from packaging.version import Version

import pytest

from pytest_cases import parametrize
from pytest_cases.common_pytest import get_callspecs
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):
Expand Down Expand Up @@ -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'

0 comments on commit 9d2a902

Please sign in to comment.