Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment on lines +849 to +852
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

current best 2 tracks


# 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'