Skip to content

Commit

Permalink
pytest8 (#335)
Browse files Browse the repository at this point in the history
* WIP: Work on pytest 8

* In progress... towards #330

* Fixed compliance with pytest 8. Fixed #330

---------

Co-authored-by: Eric Larson <larson.eric.d@gmail.com>
Co-authored-by: Sylvain MARIE <sylvain.marie@se.com>
  • Loading branch information
3 people authored Mar 8, 2024
1 parent 3a8a5bc commit 0d257b2
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 19 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
37 changes: 32 additions & 5 deletions src/pytest_cases/common_pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions src/pytest_cases/common_pytest_marks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
35 changes: 26 additions & 9 deletions src/pytest_cases/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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.
"""
Expand All @@ -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)
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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],
Expand All @@ -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
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 0d257b2

Please sign in to comment.