From 224fe1e7dcedd746812d72b69952f8158cb2e738 Mon Sep 17 00:00:00 2001 From: Simone Bacchio Date: Sat, 23 Mar 2024 18:28:40 +0200 Subject: [PATCH 1/4] Working version of lazy_fixture --- lyncs_utils/pytest.py | 169 +++++++++++++++++++++++++----------------- 1 file changed, 99 insertions(+), 70 deletions(-) diff --git a/lyncs_utils/pytest.py b/lyncs_utils/pytest.py index 00c4427..574922a 100644 --- a/lyncs_utils/pytest.py +++ b/lyncs_utils/pytest.py @@ -10,11 +10,14 @@ import pytest +def is_dyn_param(val): + "Checks if is DynParam" + return isinstance(val, DynParam) + + @dataclass class DynParam: - """ - An object that allows to generate test parameters dynamically. - """ + """An object that allows to generate test parameters dynamically.""" arg: None ids: callable = lambda val: str(val) @@ -25,9 +28,7 @@ def __call__(self, test): @dataclass class GetMark(DynParam): - """ - Takes a dictionary as input and returns the value corresponding to the first matching mark. - """ + """Takes a dictionary as input and returns the value corresponding to the first matching mark.""" default: str = None @@ -43,31 +44,100 @@ def __call__(self, test): return tuple(out) +def normalize_dyn_param(callspec, metafunc, used_keys, idx, key, val): + """Replaces DynParam with its output""" + ids = val.ids + test = metafunc.function + vals = val(test) + newcalls = [] + for val in vals: + newcallspec = copy_callspec(callspec) + newcallspec.params[key] = val + newcallspec._idlist[idx] = ids(val) + calls = normalize_call(newcallspec, metafunc) + newcalls.extend(calls) + return newcalls + + +@dataclass +class LazyFixture: + """Lazy fixture dataclass.""" + + name: str + + +def lazy_fixture(name: str) -> LazyFixture: + """Mark a fixture as lazy.""" + return LazyFixture(name) + + +def is_lazy_fixture(value: object) -> bool: + """Check whether a value is a lazy fixture.""" + return isinstance(value, LazyFixture) + + +def normalize_lazy_fixture(callspec, metafunc, used_keys, idx, key, val): + "Replaces lazy with its output" + fm = metafunc.config.pluginmanager.get_plugin("funcmanage") + try: + if pytest.version_tuple >= (8, 0, 0): + fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure( + metafunc.definition.parent, [val.name], {} + ) + else: + _, fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure( + [val.name], metafunc.definition.parent + ) + except ValueError: + # 3.6.0 <= pytest < 3.7.0; `FixtureManager.getfixtureclosure` returns 2 values + fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure( + [val.name], metafunc.definition.parent + ) + except AttributeError: + # pytest < 3.6.0; `Metafunc` has no `definition` attribute + fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure( + [val.name], current_node + ) + + extra_fixturenames = [ + fname for fname in fixturenames_closure if fname not in callspec.params + ] # and fname not in callspec.funcargs] + + newmetafunc = copy_metafunc(metafunc) + newmetafunc.fixturenames = extra_fixturenames + newmetafunc._arg2fixturedefs.update(arg2fixturedefs) + newmetafunc._calls = [callspec] + fm.pytest_generate_tests(newmetafunc) + normalize_metafunc_calls(newmetafunc, used_keys) + return newmetafunc._calls + + @pytest.hookimpl(hookwrapper=True) def pytest_generate_tests(metafunc): "Runs normalize_call for all calls" yield + normalize_metafunc_calls(metafunc) + + +def normalize_metafunc_calls(metafunc, used_keys=None): newcalls = [] for callspec in metafunc._calls: - calls = normalize_call(callspec, metafunc.function) + calls = normalize_call(callspec, metafunc, used_keys) newcalls.extend(calls) metafunc._calls = newcalls -def normalize_call(callspec, test): +def normalize_call(callspec, metafunc, used_keys=None): "Replaces DynParam with its output" + used_keys = used_keys or set() for idx, (key, val) in enumerate(callspec.params.items()): + if key in used_keys: + continue + used_keys.add(key) if is_dyn_param(val): - ids = val.ids - vals = val(test) - newcalls = [] - for val in vals: - newcallspec = copy_callspec(callspec) - newcallspec.params[key] = val - newcallspec._idlist[idx] = ids(val) - calls = normalize_call(newcallspec, test) - newcalls.extend(calls) - return newcalls + return normalize_dyn_param(callspec, metafunc, used_keys, idx, key, val) + if is_lazy_fixture(val): + return normalize_lazy_fixture(callspec, metafunc, used_keys, idx, key, val) return [callspec] @@ -79,60 +149,19 @@ def copy_callspec(callspec): return new -def is_dyn_param(val): - "Checks if is DynParam" - return isinstance(val, DynParam) +def copy_metafunc(metafunc): + copied = copy(metafunc) + copied.fixturenames = copy(metafunc.fixturenames) + copied._calls = [] + try: + copied._ids = copy(metafunc._ids) + except AttributeError: + # pytest>=5.3.0 + pass -@dataclass -class LazyFixture: - """Lazy fixture dataclass.""" - - name: str - - -def lazy_fixture(name: str) -> LazyFixture: - """Mark a fixture as lazy. - - Credit: - - https://github.com/TvoroG/pytest-lazy-fixture/issues/65#issuecomment-1914581161 - """ - return LazyFixture(name) - - -def is_lazy_fixture(value: object) -> bool: - """Check whether a value is a lazy fixture. - - Credit: - - https://github.com/TvoroG/pytest-lazy-fixture/issues/65#issuecomment-1914581161 - """ - return isinstance(value, LazyFixture) - - -def pytest_make_parametrize_id( - config: pytest.Config, - val: object, - argname: str, -) -> (str, None): - """Inject lazy fixture parametrized id. - - Reference: - - https://bit.ly/48Off6r - - Args: - config (pytest.Config): pytest configuration. - value (object): fixture value. - argname (str): automatic parameter name. - - Returns: - str: new parameter id. - - Credit: - - https://github.com/TvoroG/pytest-lazy-fixture/issues/65#issuecomment-1914581161 - """ - if is_lazy_fixture(val): - return typing.cast(LazyFixture, val).name - return None + copied._arg2fixturedefs = copy(metafunc._arg2fixturedefs) + return copied @pytest.hookimpl(tryfirst=True) From 97b9ac47d038ee5052f938d4c33ef1cc1d987b0e Mon Sep 17 00:00:00 2001 From: Simone Bacchio Date: Sat, 23 Mar 2024 18:36:36 +0200 Subject: [PATCH 2/4] Finalizing changes --- lyncs_utils/pytest.py | 69 +++++++++---------------------------------- 1 file changed, 14 insertions(+), 55 deletions(-) diff --git a/lyncs_utils/pytest.py b/lyncs_utils/pytest.py index 574922a..b15a15b 100644 --- a/lyncs_utils/pytest.py +++ b/lyncs_utils/pytest.py @@ -77,7 +77,7 @@ def is_lazy_fixture(value: object) -> bool: def normalize_lazy_fixture(callspec, metafunc, used_keys, idx, key, val): - "Replaces lazy with its output" + "Replaces LazyFixture with its output" fm = metafunc.config.pluginmanager.get_plugin("funcmanage") try: if pytest.version_tuple >= (8, 0, 0): @@ -112,14 +112,23 @@ def normalize_lazy_fixture(callspec, metafunc, used_keys, idx, key, val): return newmetafunc._calls +@pytest.hookimpl(tryfirst=True) +def pytest_fixture_setup(fixturedef, request): + """Replaces LazyFixture with its name""" + val = getattr(request, "param", None) + if is_lazy_fixture(val): + request.param = request.getfixturevalue(val.name) + + @pytest.hookimpl(hookwrapper=True) def pytest_generate_tests(metafunc): - "Runs normalize_call for all calls" + """Runs normalize_call for all calls""" yield normalize_metafunc_calls(metafunc) def normalize_metafunc_calls(metafunc, used_keys=None): + """Runs normalize_call for all calls""" newcalls = [] for callspec in metafunc._calls: calls = normalize_call(callspec, metafunc, used_keys) @@ -128,7 +137,7 @@ def normalize_metafunc_calls(metafunc, used_keys=None): def normalize_call(callspec, metafunc, used_keys=None): - "Replaces DynParam with its output" + "Replaces special fixtures with their output" used_keys = used_keys or set() for idx, (key, val) in enumerate(callspec.params.items()): if key in used_keys: @@ -142,7 +151,7 @@ def normalize_call(callspec, metafunc, used_keys=None): def copy_callspec(callspec): - "Creating a copy of callspec" + """Creates a copy of callspec""" new = copy(callspec) object.__setattr__(new, "params", copy(callspec.params)) object.__setattr__(new, "_idlist", copy(callspec._idlist)) @@ -150,6 +159,7 @@ def copy_callspec(callspec): def copy_metafunc(metafunc): + """Creates a copy of metafunc""" copied = copy(metafunc) copied.fixturenames = copy(metafunc.fixturenames) copied._calls = [] @@ -162,54 +172,3 @@ def copy_metafunc(metafunc): copied._arg2fixturedefs = copy(metafunc._arg2fixturedefs) return copied - - -@pytest.hookimpl(tryfirst=True) -def pytest_fixture_setup( - fixturedef: pytest.FixtureDef, - request: pytest.FixtureRequest, -) -> (object, None): - """Lazy fixture setup hook. - - This hook will never take over a fixture setup but just simply will - try to resolve recursively any lazy fixture found in request.param. - - Reference: - - https://bit.ly/3SyvsXJ - - Args: - fixturedef (pytest.FixtureDef): fixture definition object. - request (pytest.FixtureRequest): fixture request object. - - Returns: - object | None: fixture value or None otherwise. - - Credit: - - https://github.com/TvoroG/pytest-lazy-fixture/issues/65#issuecomment-1914581161 - """ - if hasattr(request, "param") and request.param: - request.param = _resolve_lazy_fixture(request.param, request) - return None - - -def _resolve_lazy_fixture(__val: object, request: pytest.FixtureRequest) -> object: - """Lazy fixture resolver. - - Args: - __val (object): fixture value object. - request (pytest.FixtureRequest): pytest fixture request object. - - Returns: - object: resolved fixture value. - - Credit: - - https://github.com/TvoroG/pytest-lazy-fixture/issues/65#issuecomment-1914581161 - """ - if isinstance(__val, (list, tuple)): - return tuple(_resolve_lazy_fixture(v, request) for v in __val) - if isinstance(__val, typing.Mapping): - return {k: _resolve_lazy_fixture(v, request) for k, v in __val.items()} - if not is_lazy_fixture(__val): - return __val - lazy_obj = typing.cast(LazyFixture, __val) - return request.getfixturevalue(lazy_obj.name) From b096cdec19bd3e18d5b460b46c97fea6a4ed999f Mon Sep 17 00:00:00 2001 From: Simone Bacchio Date: Sat, 23 Mar 2024 18:37:43 +0200 Subject: [PATCH 3/4] Incrementing version number --- lyncs_utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lyncs_utils/__init__.py b/lyncs_utils/__init__.py index f783d23..8848bab 100644 --- a/lyncs_utils/__init__.py +++ b/lyncs_utils/__init__.py @@ -1,6 +1,6 @@ "Collection of generic-purpose and stand-alone functions" -__version__ = "0.5.0" +__version__ = "0.5.1" from .math import * from .logical import * From 1bdd12acf49f16f50ad21d82095ca26fb710fd7d Mon Sep 17 00:00:00 2001 From: Simone Bacchio Date: Sat, 23 Mar 2024 16:39:09 +0000 Subject: [PATCH 4/4] Updating pylint score (from Github Action) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f8ac3c..e128d42 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![license](https://img.shields.io/github/license/Lyncs-API/lyncs.utils?logo=github&logoColor=white)](https://github.com/Lyncs-API/lyncs.utils/blob/master/LICENSE) [![build & test](https://img.shields.io/github/actions/workflow/status/Lyncs-API/lyncs.utils/ci_cd.yml?logo=github&logoColor=white&branch=master)](https://github.com/Lyncs-API/lyncs.utils/actions) [![codecov](https://img.shields.io/codecov/c/github/Lyncs-API/lyncs.utils?logo=codecov&logoColor=white)](https://codecov.io/gh/Lyncs-API/lyncs.utils) -[![pylint](https://img.shields.io/badge/pylint%20score-9.6%2F10-green?logo=python&logoColor=white)](http://pylint.pycqa.org/) +[![pylint](https://img.shields.io/badge/pylint%20score-9.4%2F10-green?logo=python&logoColor=white)](http://pylint.pycqa.org/) [![black](https://img.shields.io/badge/code%20style-black-000000.svg?logo=codefactor&logoColor=white)](https://github.com/ambv/black) This package provides a collection of generic-purpose and stand-alone functions that are of common use.