Skip to content

Commit 0d257b2

Browse files
smarielarsonerSylvain MARIE
authored
pytest8 (#335)
* WIP: Work on pytest 8 * In progress... towards #330 * Fixed compliance with pytest 8. Fixed #330 --------- Co-authored-by: Eric Larson <[email protected]> Co-authored-by: Sylvain MARIE <[email protected]>
1 parent 3a8a5bc commit 0d257b2

File tree

6 files changed

+102
-19
lines changed

6 files changed

+102
-19
lines changed

docs/changelog.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Changelog
22

3-
### 3.8.3 (in progress) - TBD
3+
### 3.8.3 - Support for `pytest` version 8
44

5-
- tbd
5+
- Fixed compliance with pytest 8. Fixed [#330](https://github.com/smarie/python-pytest-cases/issues/330). PR
6+
[#335](https://github.com/smarie/python-pytest-cases/pull/335) by [smarie](https://github.com/smarie) and
7+
[larsoner](https://github.com/larsoner).
68

79
### 3.8.2 - bugfixes and project improvements
810

src/pytest_cases/common_pytest.py

+32-5
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
from .common_others import get_function_host
3131
from .common_pytest_marks import make_marked_parameter_value, get_param_argnames_as_list, \
3232
get_pytest_parametrize_marks, get_pytest_usefixture_marks, PYTEST3_OR_GREATER, PYTEST6_OR_GREATER, \
33-
PYTEST38_OR_GREATER, PYTEST34_OR_GREATER, PYTEST33_OR_GREATER, PYTEST32_OR_GREATER, PYTEST71_OR_GREATER
33+
PYTEST38_OR_GREATER, PYTEST34_OR_GREATER, PYTEST33_OR_GREATER, PYTEST32_OR_GREATER, PYTEST71_OR_GREATER, \
34+
PYTEST8_OR_GREATER
3435
from .common_pytest_lazy_values import is_lazy_value, is_lazy
3536

3637

@@ -554,6 +555,14 @@ def set_callspec_arg_scope_to_function(callspec, arg_name):
554555
callspec._arg2scopenum[arg_name] = get_pytest_function_scopeval() # noqa
555556

556557

558+
def in_callspec_explicit_args(
559+
callspec, # type: CallSpec2
560+
name # type: str
561+
): # type: (...) -> bool
562+
"""Return True if name is explicitly used in callspec args"""
563+
return (name in callspec.params) or (not PYTEST8_OR_GREATER and name in callspec.funcargs)
564+
565+
557566
if PYTEST71_OR_GREATER:
558567
from _pytest.python import IdMaker # noqa
559568

@@ -653,14 +662,27 @@ def getfuncargnames(function, cls=None):
653662
return arg_names
654663

655664

665+
class FakeSession(object):
666+
__slots__ = ('_fixturemanager',)
667+
668+
def __init__(self):
669+
self._fixturemanager = None
670+
671+
656672
class MiniFuncDef(object):
657-
__slots__ = ('nodeid',)
673+
__slots__ = ('nodeid', 'session')
658674

659675
def __init__(self, nodeid):
660676
self.nodeid = nodeid
677+
if PYTEST8_OR_GREATER:
678+
self.session = FakeSession()
661679

662680

663681
class MiniMetafunc(Metafunc):
682+
"""
683+
A class to know what pytest *would* do for a given function in terms of callspec.
684+
It is used in function `case_to_argvalues`
685+
"""
664686
# noinspection PyMissingConstructor
665687
def __init__(self, func):
666688
from .plugin import PYTEST_CONFIG # late import to ensure config has been loaded by now
@@ -685,12 +707,18 @@ def __init__(self, func):
685707
self.fixturenames_not_in_sig = [f for f in get_pytest_usefixture_marks(func) if f not in self.fixturenames]
686708
if self.fixturenames_not_in_sig:
687709
self.fixturenames = tuple(self.fixturenames_not_in_sig + list(self.fixturenames))
710+
711+
if PYTEST8_OR_GREATER:
712+
# dummy
713+
self._arg2fixturedefs = dict() # type: dict[str, Sequence["FixtureDef[Any]"]]
714+
688715
# get parametrization marks
689716
self.pmarks = get_pytest_parametrize_marks(self.function)
690717
if self.is_parametrized:
691718
self.update_callspecs()
692719
# preserve order
693-
self.required_fixtures = tuple(f for f in self.fixturenames if f not in self._calls[0].funcargs)
720+
ref_names = self._calls[0].params if PYTEST8_OR_GREATER else self._calls[0].funcargs
721+
self.required_fixtures = tuple(f for f in self.fixturenames if f not in ref_names)
694722
else:
695723
self.required_fixtures = self.fixturenames
696724

@@ -773,8 +801,7 @@ def get_callspecs(func):
773801
Returns a list of pytest CallSpec objects corresponding to calls that should be made for this parametrized function.
774802
This mini-helper assumes no complex things (scope='function', indirect=False, no fixtures, no custom configuration)
775803
776-
:param func:
777-
:return:
804+
Note that this function is currently only used in tests.
778805
"""
779806
meta = MiniMetafunc(func)
780807
# meta.update_callspecs()

src/pytest_cases/common_pytest_marks.py

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
PYTEST6_OR_GREATER = PYTEST_VERSION >= Version('6.0.0')
4444
PYTEST7_OR_GREATER = PYTEST_VERSION >= Version('7.0.0')
4545
PYTEST71_OR_GREATER = PYTEST_VERSION >= Version('7.1.0')
46+
PYTEST8_OR_GREATER = PYTEST_VERSION >= Version('8.0.0')
4647

4748

4849
def get_param_argnames_as_list(argnames):

src/pytest_cases/plugin.py

+26-9
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828

2929
from .common_mini_six import string_types
3030
from .common_pytest_lazy_values import get_lazy_args
31-
from .common_pytest_marks import PYTEST35_OR_GREATER, PYTEST46_OR_GREATER, PYTEST37_OR_GREATER, PYTEST7_OR_GREATER
31+
from .common_pytest_marks import PYTEST35_OR_GREATER, PYTEST46_OR_GREATER, PYTEST37_OR_GREATER, PYTEST7_OR_GREATER, PYTEST8_OR_GREATER
3232
from .common_pytest import get_pytest_nodeid, get_pytest_function_scopeval, is_function_node, get_param_names, \
33-
get_param_argnames_as_list, has_function_scope, set_callspec_arg_scope_to_function
33+
get_param_argnames_as_list, has_function_scope, set_callspec_arg_scope_to_function, in_callspec_explicit_args
3434

3535
from .fixture_core1_unions import NOT_USED, USED, is_fixture_union_params, UnionFixtureAlternative
3636

@@ -41,7 +41,8 @@
4141
from .case_parametrizer_new import get_current_cases
4242

4343

44-
_DEBUG = False
44+
_DEBUG = True
45+
"""Note: this is a manual flag to turn when developing (do not forget to also call pytest with -s)"""
4546

4647

4748
# @pytest.hookimpl(hookwrapper=True, tryfirst=True)
@@ -753,7 +754,7 @@ def remove_all(self, values):
753754
self._update_fixture_defs()
754755

755756

756-
def getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()):
757+
def _getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()):
757758
"""
758759
Replaces pytest's getfixtureclosure method to handle unions.
759760
"""
@@ -764,7 +765,10 @@ def getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()):
764765
# new argument "ignore_args" in 4.6+
765766
kwargs['ignore_args'] = ignore_args
766767

767-
if PYTEST37_OR_GREATER:
768+
if PYTEST8_OR_GREATER:
769+
# two outputs and sig change
770+
ref_fixturenames, ref_arg2fixturedefs = fm.__class__.getfixtureclosure(fm, parentnode, fixturenames, **kwargs)
771+
elif PYTEST37_OR_GREATER:
768772
# three outputs
769773
initial_names, ref_fixturenames, ref_arg2fixturedefs = \
770774
fm.__class__.getfixtureclosure(fm, fixturenames, parentnode, **kwargs)
@@ -781,12 +785,19 @@ def getfixtureclosure(fm, fixturenames, parentnode, ignore_args=()):
781785
assert set(super_closure) == set(ref_fixturenames)
782786
assert dict(arg2fixturedefs) == ref_arg2fixturedefs
783787

784-
if PYTEST37_OR_GREATER:
788+
if PYTEST37_OR_GREATER and not PYTEST8_OR_GREATER:
785789
return _init_fixnames, super_closure, arg2fixturedefs
786790
else:
787791
return super_closure, arg2fixturedefs
788792

789793

794+
if PYTEST8_OR_GREATER:
795+
def getfixtureclosure(fm, parentnode, initialnames, ignore_args):
796+
return _getfixtureclosure(fm, fixturenames=initialnames, parentnode=parentnode, ignore_args=ignore_args)
797+
else:
798+
getfixtureclosure = _getfixtureclosure
799+
800+
790801
def create_super_closure(fm,
791802
parentnode,
792803
fixturenames,
@@ -835,6 +846,11 @@ def _merge(new_items, into_list):
835846
# we cannot sort yet - merge the fixture names into the _init_fixnames
836847
_merge(fixturenames, _init_fixnames)
837848

849+
# Bugfix GH#330 in progress...
850+
# TODO analyze why in the test "fixture_union_0simplest
851+
# the first node contains second, and the second contains first
852+
# or TODO check the test for get_callspecs, it is maybe simpler
853+
838854
# Finally create the closure
839855
fixture_defs_mgr = FixtureDefsCache(fm, parentnode)
840856
closure_tree = FixtureClosureNode(fixture_defs_mgr=fixture_defs_mgr)
@@ -1035,7 +1051,8 @@ def create_call_list_from_pending_parametrizations(self):
10351051

10361052
if _DEBUG:
10371053
print("\n".join(["%s[%s]: funcargs=%s, params=%s" % (get_pytest_nodeid(self.metafunc),
1038-
c.id, c.funcargs, c.params)
1054+
c.id, c.params if PYTEST8_OR_GREATER else c.funcargs,
1055+
c.params)
10391056
for c in calls]) + "\n")
10401057

10411058
# clean EMPTY_ID set by @parametrize when there is at least a MultiParamsAlternative
@@ -1107,7 +1124,7 @@ def _cleanup_calls_list(metafunc,
11071124

11081125
# A/ set to "not used" all parametrized fixtures that were not used in some branches
11091126
for fixture, p_to_apply in pending_dct.items():
1110-
if fixture not in c.params and fixture not in c.funcargs:
1127+
if not in_callspec_explicit_args(c, fixture):
11111128
# parametrize with a single "not used" value and discard the id
11121129
if isinstance(p_to_apply, UnionParamz):
11131130
c_with_dummy = _parametrize_calls(metafunc, [c], p_to_apply.union_fixture_name, [NOT_USED],
@@ -1132,7 +1149,7 @@ def _cleanup_calls_list(metafunc,
11321149
# For this we use a dirty hack: we add a parameter with they name in the callspec, it seems to be propagated
11331150
# in the `request`. TODO is there a better way?
11341151
for fixture_name in _not_always_used_func_scoped:
1135-
if fixture_name not in c.params and fixture_name not in c.funcargs:
1152+
if not in_callspec_explicit_args(c, fixture_name):
11361153
if not n.requires(fixture_name):
11371154
# explicitly add it as discarded by creating a parameter value for it.
11381155
c.params[fixture_name] = NOT_USED

tests/cases/issues/test_issue_126.py

+23-1
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22
# + All contributors to <https://github.com/smarie/python-pytest-cases>
33
#
44
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
5+
from packaging.version import Version
6+
57
import pytest
68

79
from pytest_cases.common_pytest_marks import PYTEST3_OR_GREATER
810
from pytest_cases import parametrize_with_cases
911

1012

13+
PYTEST_VERSION = Version(pytest.__version__)
14+
PYTEST8_OR_GREATER = PYTEST_VERSION >= Version('8.0.0')
15+
16+
1117
@pytest.fixture()
1218
def dependent_fixture():
1319
return 0
@@ -66,7 +72,23 @@ def test_synthesis(module_results_dct):
6672
for host in (test_functionality, test_functionality_again, TestNested.test_functionality_again2):
6773
assert markers_dict[host] == (set(), set())
6874

69-
if PYTEST3_OR_GREATER:
75+
if PYTEST8_OR_GREATER:
76+
# in version 8 they added a smart suffix in case last char of id is already a numeric
77+
assert list(module_results_dct) == [
78+
'test_functionality[_requirement_1_0]',
79+
'test_functionality[_requirement_2_0]',
80+
'test_functionality[_requirement_1_1]',
81+
'test_functionality[_requirement_2_1]',
82+
'test_functionality_again[_requirement_1_0]', # <- note: same fixtures than previously
83+
'test_functionality_again[_requirement_2_0]', # idem
84+
'test_functionality_again[_requirement_1_1]', # idem
85+
'test_functionality_again[_requirement_2_1]', # idem
86+
'test_functionality_again2[_requirement_1_0]', # idem
87+
'test_functionality_again2[_requirement_2_0]', # idem
88+
'test_functionality_again2[_requirement_1_1]', # idem
89+
'test_functionality_again2[_requirement_2_1]' # idem
90+
]
91+
elif PYTEST3_OR_GREATER:
7092
assert list(module_results_dct) == [
7193
'test_functionality[_requirement_10]',
7294
'test_functionality[_requirement_20]',

tests/pytest_extension/parametrize_plus/test_getcallspecs.py

+16-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22
# + All contributors to <https://github.com/smarie/python-pytest-cases>
33
#
44
# License: 3-clause BSD, <https://github.com/smarie/python-pytest-cases/blob/master/LICENSE>
5+
from packaging.version import Version
6+
57
import pytest
68

79
from pytest_cases import parametrize
810
from pytest_cases.common_pytest import get_callspecs
911
from pytest_cases.common_pytest_marks import has_pytest_param
1012

1113

14+
PYTEST_VERSION = Version(pytest.__version__)
15+
PYTEST8_OR_GREATER = PYTEST_VERSION >= Version('8.0.0')
16+
17+
1218
if not has_pytest_param:
1319
@pytest.mark.parametrize('new_style', [False, True])
1420
def test_getcallspecs(new_style):
@@ -48,10 +54,18 @@ def test_foo(a):
4854
calls = get_callspecs(test_foo)
4955

5056
assert len(calls) == 2
51-
assert calls[0].funcargs == dict(a=1)
57+
if PYTEST8_OR_GREATER:
58+
# funcargs disappears in version 8
59+
assert calls[0].params == dict(a=1)
60+
else:
61+
assert calls[0].funcargs == dict(a=1)
5262
assert calls[0].id == 'a=1' if new_style else 'oh'
5363
assert calls[0].marks == []
5464

55-
assert calls[1].funcargs == dict(a='12')
65+
if PYTEST8_OR_GREATER:
66+
# funcargs disappears in version 8
67+
assert calls[1].params == dict(a='12')
68+
else:
69+
assert calls[1].funcargs == dict(a='12')
5670
assert calls[1].id == 'a=12' if new_style else 'hey'
5771
assert calls[1].marks[0].name == 'skip'

0 commit comments

Comments
 (0)