diff --git a/docs/changelog.md b/docs/changelog.md index d27ca45e..08bb4a9f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,10 @@ # Changelog -### 3.6.7 In Progress - Minor improvements +### 3.6.7 - Minor improvements and preparing for pytest 7 - Improved error message when a case function nested in a class has no `self` argument and is not static. Fixes [#243](https://github.com/smarie/python-pytest-cases/issues/243) + - Added support for the new Scopes enum in pytest 7. Fixed [#241](https://github.com/smarie/python-pytest-cases/issues/241) + - Fixed `__version__` in development mode. ### 3.6.6 - Layout change diff --git a/src/pytest_cases/__init__.py b/src/pytest_cases/__init__.py index d0c08db6..172584fb 100644 --- a/src/pytest_cases/__init__.py +++ b/src/pytest_cases/__init__.py @@ -23,7 +23,7 @@ # use setuptools_scm to get the current version from src using git from setuptools_scm import get_version as _gv from os import path as _path - __version__ = _gv(_path.join(_path.dirname(__file__), _path.pardir)) + __version__ = _gv(_path.join(_path.dirname(__file__), _path.pardir, _path.pardir)) AUTO2 = AUTO diff --git a/src/pytest_cases/case_parametrizer_new.py b/src/pytest_cases/case_parametrizer_new.py index 6743b1f1..80029184 100644 --- a/src/pytest_cases/case_parametrizer_new.py +++ b/src/pytest_cases/case_parametrizer_new.py @@ -824,7 +824,7 @@ def _of_interest(x): # noqa # ignore any error here, this is optional. pass else: - if len(s.parameters) < 1: + if len(s.parameters) < 1 or (tuple(s.parameters.keys())[0] != "self"): raise TypeError("case method is missing 'self' argument but is not static: %s" % m) # partialize the function to get one without the 'self' argument new_m = functools.partial(m, cls()) diff --git a/src/pytest_cases/common_pytest.py b/src/pytest_cases/common_pytest.py index 0674ae3c..17dbd891 100644 --- a/src/pytest_cases/common_pytest.py +++ b/src/pytest_cases/common_pytest.py @@ -521,18 +521,37 @@ def get_pytest_nodeid(metafunc): try: - from _pytest.fixtures import scopes as pt_scopes + # pytest 7+ : scopes is an enum + from _pytest.scope import Scope + + def get_pytest_function_scopeval(): + return Scope.Function + + def has_function_scope(fixdef): + return fixdef._scope is Scope.Function + + def set_callspec_arg_scope_to_function(callspec, arg_name): + callspec._arg2scope[arg_name] = Scope.Function + except ImportError: - # pytest 2 - from _pytest.python import scopes as pt_scopes, Metafunc # noqa + try: + # pytest 3+ + from _pytest.fixtures import scopes as pt_scopes + except ImportError: + # pytest 2 + from _pytest.python import scopes as pt_scopes + # def get_pytest_scopenum(scope_str): + # return pt_scopes.index(scope_str) -def get_pytest_scopenum(scope_str): - return pt_scopes.index(scope_str) + def get_pytest_function_scopeval(): + return pt_scopes.index("function") + def has_function_scope(fixdef): + return fixdef.scopenum == get_pytest_function_scopeval() -def get_pytest_function_scopenum(): - return pt_scopes.index("function") + def set_callspec_arg_scope_to_function(callspec, arg_name): + callspec._arg2scopenum[arg_name] = get_pytest_function_scopeval() # noqa from _pytest.python import _idval # noqa diff --git a/src/pytest_cases/common_pytest_marks.py b/src/pytest_cases/common_pytest_marks.py index 832f4a5e..865e3f26 100644 --- a/src/pytest_cases/common_pytest_marks.py +++ b/src/pytest_cases/common_pytest_marks.py @@ -41,6 +41,7 @@ PYTEST54_OR_GREATER = PYTEST_VERSION >= LooseVersion('5.4.0') PYTEST421_OR_GREATER = PYTEST_VERSION >= LooseVersion('4.2.1') PYTEST6_OR_GREATER = PYTEST_VERSION >= LooseVersion('6.0.0') +PYTEST7_OR_GREATER = PYTEST_VERSION >= LooseVersion('7.0.0') def get_param_argnames_as_list(argnames): diff --git a/src/pytest_cases/plugin.py b/src/pytest_cases/plugin.py index 888c197e..f18e7b49 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 -from .common_pytest import get_pytest_nodeid, get_pytest_function_scopenum, is_function_node, get_param_names, \ - get_param_argnames_as_list +from .common_pytest_marks import PYTEST35_OR_GREATER, PYTEST46_OR_GREATER, PYTEST37_OR_GREATER, PYTEST7_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 from .fixture_core1_unions import NOT_USED, USED, is_fixture_union_params, UnionFixtureAlternative @@ -187,12 +187,22 @@ def get_all_fixture_defs(self, drop_fake_fixtures=True, try_to_sort=True): items = self.gen_all_fixture_defs(drop_fake_fixtures=drop_fake_fixtures) # sort by scope as in pytest fixture closure creator (pytest did not do it in early versions, align with this) - if try_to_sort and PYTEST35_OR_GREATER: - f_scope = get_pytest_function_scopenum() - def sort_by_scope(kv_pair): # noqa - fixture_name, fixture_defs = kv_pair - return fixture_defs[-1].scopenum if fixture_defs is not None else f_scope - items = sorted(list(items), key=sort_by_scope) + if try_to_sort: + if PYTEST7_OR_GREATER: + # Scope is an enum, values are in reversed order, and the field is _scope + f_scope = get_pytest_function_scopeval() + def sort_by_scope(kv_pair): + fixture_name, fixture_defs = kv_pair + return fixture_defs[-1]._scope if fixture_defs is not None else f_scope + items = sorted(list(items), key=sort_by_scope, reverse=True) + + elif PYTEST35_OR_GREATER: + # scopes is a list, values are indices in the list, and the field is scopenum + f_scope = get_pytest_function_scopeval() + def sort_by_scope(kv_pair): # noqa + fixture_name, fixture_defs = kv_pair + return fixture_defs[-1].scopenum if fixture_defs is not None else f_scope + items = sorted(list(items), key=sort_by_scope) return OrderedDict(items) @@ -562,7 +572,7 @@ def _update_fixture_defs(self): # # also sort all partitions (note that we cannot rely on the order in all_fixture_defs when scopes are same!) # if LooseVersion(pytest.__version__) >= LooseVersion('3.5.0'): - # f_scope = get_pytest_function_scopenum() + # f_scope = get_pytest_function_scopeval() # for p in self.partitions: # def sort_by_scope2(fixture_name): # noqa # fixture_defs = all_fixture_defs[fixture_name] @@ -1031,14 +1041,13 @@ def _cleanup_calls_list(metafunc, # create ref lists of fixtures per scope _not_always_used_func_scoped = [] # _not_always_used_other_scoped = [] - _function_scope_num = get_pytest_function_scopenum() for fixture_name in fix_closure_tree.get_not_always_used(): try: fixdef = metafunc._arg2fixturedefs[fixture_name] # noqa except KeyError: continue # dont raise any error here and let pytest say "not found" later else: - if fixdef[-1].scopenum == _function_scope_num: + if has_function_scope(fixdef[-1]): _not_always_used_func_scoped.append(fixture_name) # else: # _not_always_used_other_scoped.append(fixture_name) @@ -1078,12 +1087,12 @@ def _cleanup_calls_list(metafunc, # explicitly add it as discarded by creating a parameter value for it. c.params[fixture_name] = NOT_USED c.indices[fixture_name] = 1 - c._arg2scopenum[fixture_name] = _function_scope_num # get_pytest_scopenum(fixdef[-1].scope) # noqa + set_callspec_arg_scope_to_function(c, fixture_name) else: # explicitly add it as active c.params[fixture_name] = USED c.indices[fixture_name] = 0 - c._arg2scopenum[fixture_name] = _function_scope_num # get_pytest_scopenum(fixdef[-1].scope) # noqa + set_callspec_arg_scope_to_function(c, fixture_name) # finally, if there are some session or module-scoped fixtures that # are used in *none* of the calls, they could be deactivated too diff --git a/tests/cases/issues/test_issue_243.py b/tests/cases/issues/test_issue_243.py deleted file mode 100644 index a3e8c0e9..00000000 --- a/tests/cases/issues/test_issue_243.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest - -from pytest_cases import parametrize_with_cases - - -def test_missing_self(): - class MyCases: - def case_one(self) -> int: - return 123 - - def case_two_forgot_self() -> int: - return 456 - - with pytest.raises(TypeError) as exc_info: - @parametrize_with_cases(argnames="expected", cases=MyCases) - def test_foo(expected): - pass - - assert str(exc_info.value) == ("case method is missing 'self' argument but is not static: %s" - % MyCases.case_two_forgot_self) diff --git a/tests/cases/issues/test_py35_issue_243.py b/tests/cases/issues/test_py35_issue_243.py new file mode 100644 index 00000000..66621cd9 --- /dev/null +++ b/tests/cases/issues/test_py35_issue_243.py @@ -0,0 +1,36 @@ +import pytest + +from pytest_cases import parametrize_with_cases, fixture + + +def test_missing_self(): + class MyCases: + def case_forgot_self() -> int: + return 456 + + with pytest.raises(TypeError) as exc_info: + @parametrize_with_cases(argnames="expected", cases=MyCases) + def test_foo(expected): + pass + + assert str(exc_info.value) == ("case method is missing 'self' argument but is not static: %s" + % MyCases.case_forgot_self) + + +@fixture +def a(): + return + + +def test_missing_self_params(): + class MyCases: + def case_fix_forgot_self(a) -> int: + return a + + with pytest.raises(TypeError) as exc_info: + @parametrize_with_cases(argnames="expected", cases=MyCases) + def test_foo(expected): + pass + + assert str(exc_info.value) == ("case method is missing 'self' argument but is not static: %s" + % MyCases.case_fix_forgot_self)