pytest-lazy-fixture icon indicating copy to clipboard operation
pytest-lazy-fixture copied to clipboard

Raise error with `pytest==8.0.0`

Open OttoAndrey opened this issue 1 year ago • 22 comments

Hi! pytest-lazy-fixture doesn't work with pytest==8.0.0

 AttributeError: 'CallSpec2' object has no attribute 'funcargs'

OttoAndrey avatar Jan 29 '24 02:01 OttoAndrey

Since the main branch of this repo hasn't seen an update in two years, I've been looking for an alternative. In my case, I was using something like this:

# In conftest.py:
@pytest.fixture
def test_api_sqlite_mp(test_sqlite_mp):
    return Platform(_backend=RestTestBackend(test_sqlite_mp.backend))


@pytest.fixture
def test_api_pgsql_mp(test_pgsql_mp):
    return Platform(_backend=RestTestBackend(test_pgsql_mp.backend))

# In another file:
api_platforms = pytest.mark.parametrize(
    "test_mp",
    [
        pytest.lazy_fixture("test_api_sqlite_mp"),
        pytest.lazy_fixture("test_api_pgsql_mp"),
    ],
)

# And finally, for the function:
@api_platforms
def test_index_model(test_mp):
    ...

So following suggestions on StackOverflow and here, I changed that to

# In conftest.py (everything stays the same):
@pytest.fixture
def test_api_sqlite_mp(test_sqlite_mp):
    return Platform(_backend=RestTestBackend(test_sqlite_mp.backend))


@pytest.fixture
def test_api_pgsql_mp(test_pgsql_mp):
    return Platform(_backend=RestTestBackend(test_pgsql_mp.backend))

# In another file:
api_platforms = pytest.mark.parametrize(
    "test_mp",
    [
        "test_api_sqlite_mp",
        "test_api_pgsql_mp",
    ],
)

# And finally, for the function:
@api_platforms
def test_index_model(test_mp, request):
    test_mp = request.getfixturevalue(test_mp)
    ...

After uninstalling pytest-lazy-fixture, my tests are running just fine again. Hope this helps :)

glatterf42 avatar Jan 29 '24 11:01 glatterf42

Hi All,

As mentioned by @glatterf42, it looks like this issue might never get fixed. So what I did was to come out with my own local pytest-lazy-fixture plugin like this:

import dataclasses
import typing

import pytest


@dataclasses.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 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.
    """
    if is_lazy_fixture(val):
        return typing.cast(LazyFixture, val).name
    return None


@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.
    """
    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.
    """
    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)

By now I am simply including this into my root conftest.py.

PrieJos avatar Jan 29 '24 12:01 PrieJos

For the record, the minimal set of changes to make things work again doesn't seem to be that massive. This works for me, passing pytest-lazy-fixture's test set (not running tox, just running Python 3.10 with pytest 8.0.0):

diff --git a/pytest_lazyfixture.py b/pytest_lazyfixture.py
index abf5db5..df83ce7 100644
--- a/pytest_lazyfixture.py
+++ b/pytest_lazyfixture.py
@@ -71,14 +71,13 @@ def pytest_make_parametrize_id(config, val, argname):
 def pytest_generate_tests(metafunc):
     yield
 
-    normalize_metafunc_calls(metafunc, 'funcargs')
-    normalize_metafunc_calls(metafunc, 'params')
+    normalize_metafunc_calls(metafunc)
 
 
-def normalize_metafunc_calls(metafunc, valtype, used_keys=None):
+def normalize_metafunc_calls(metafunc, used_keys=None):
     newcalls = []
     for callspec in metafunc._calls:
-        calls = normalize_call(callspec, metafunc, valtype, used_keys)
+        calls = normalize_call(callspec, metafunc, used_keys)
         newcalls.extend(calls)
     metafunc._calls = newcalls
 
@@ -98,17 +97,21 @@ def copy_metafunc(metafunc):
     return copied
 
 
-def normalize_call(callspec, metafunc, valtype, used_keys):
+def normalize_call(callspec, metafunc, used_keys):
     fm = metafunc.config.pluginmanager.get_plugin('funcmanage')
 
     used_keys = used_keys or set()
-    valtype_keys = set(getattr(callspec, valtype).keys()) - used_keys
+    keys = set(callspec.params.keys()) - used_keys
+    print(used_keys, keys)
 
-    for arg in valtype_keys:
-        val = getattr(callspec, valtype)[arg]
+    for arg in keys:
+        val = callspec.params[arg]
         if is_lazy_fixture(val):
             try:
-                _, fixturenames_closure, arg2fixturedefs = fm.getfixtureclosure([val.name], metafunc.definition.parent)
+                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)
@@ -117,14 +120,14 @@ def normalize_call(callspec, metafunc, valtype, used_keys):
                 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]
+                                  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, valtype, used_keys | set([arg]))
+            normalize_metafunc_calls(newmetafunc, used_keys | set([arg]))
             return newmetafunc._calls
 
         used_keys.add(arg)

But the bigger question is of course how much more overdue maintenance is waiting to happen, and whether whether @TvoroG would be interested to keep maintaining or to transfer maintenance to someone else (cfr #63).

YannickJadoul avatar Jan 30 '24 00:01 YannickJadoul

I didn't look at the code in detail, but if the only problem is the call to getfixtureclosure, https://github.com/pytest-dev/pytest/pull/11888 will fix the problem.

nicoddemus avatar Jan 30 '24 11:01 nicoddemus

I still get an error with https://github.com/pytest-dev/pytest/pull/11888:

______________________ ERROR collecting tests/test_prettytable.py _______________________
/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pluggy/_hooks.py:501: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pluggy/_manager.py:119: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
../pytest/src/_pytest/python.py:274: in pytest_pycollect_makeitem
    return list(collector._genfunctions(name, obj))
../pytest/src/_pytest/python.py:489: in _genfunctions
    self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pluggy/_hooks.py:562: in call_extra
    return self._hookexec(self.name, hookimpls, kwargs, firstresult)
/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pluggy/_manager.py:119: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pytest_lazyfixture.py:74: in pytest_generate_tests
    normalize_metafunc_calls(metafunc, 'funcargs')
/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pytest_lazyfixture.py:81: in normalize_metafunc_calls
    calls = normalize_call(callspec, metafunc, valtype, used_keys)
/Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/site-packages/pytest_lazyfixture.py:105: in normalize_call
    valtype_keys = set(getattr(callspec, valtype).keys()) - used_keys
E   AttributeError: 'CallSpec2' object has no attribute 'funcargs'

hugovk avatar Jan 30 '24 11:01 hugovk

I didn't look at the code in detail, but if the only problem is the call to getfixtureclosure, pytest-dev/pytest#11888 will fix the problem.

I believe the order of arguments also changed, or am I misreading the 3 different versions in pytest-lazy-fixutre?

I still get an error with pytest-dev/pytest#11888:

And yes, the other fix is that CallSpec2.funcargs was merged into CallSpec2.params? At least, stopping to access funcargs and only accessing params did the trick. (So yes, actually my patch up there is not backwards compatible, right now.)

YannickJadoul avatar Jan 30 '24 11:01 YannickJadoul

I was able to easily replace this plugin with https://github.com/dev-petrov/pytest-lazy-fixtures:

  • https://github.com/jazzband/prettytable/pull/279

hugovk avatar Feb 03 '24 14:02 hugovk

I mainly used this package because it alowed me to use parametrized fixtures in parametrized tests, producing a product of test cases. I have tested @glatterf42 's and @PrieJos 's solution but both produced errors Failed: The requested fixture has no parameter defined for test when I used them with parametrized fixtures. Does anyone have a solution for this?

harsanyidani avatar Feb 05 '24 15:02 harsanyidani

I mainly used this package because it alowed me to use parametrized fixtures in parametrized tests, producing a product of test cases. I have tested @glatterf42 's and @PrieJos 's solution but both produced errors Failed: The requested fixture has no parameter defined for test when I used them with parametrized fixtures. Does anyone have a solution for this?

I've already raised an issue about this. I'm sticking with pytest 7.x in affected projects until it's fixed.

agronholm avatar Feb 05 '24 18:02 agronholm

@harsanyidani, @agronholm: without knowing more about your error messages, I still found some possibly related issues. Please check:

  • https://github.com/pytest-dev/pytest/issues/11075 for a possible solution
  • https://stackoverflow.com/questions/58319619/pytest-request-param-of-fixture-to-be-used-in-tests for another possible solution (though the error message might be different)
  • https://github.com/pytest-dev/pytest/issues/4666 for some background for this error

glatterf42 avatar Feb 06 '24 06:02 glatterf42

What do you mean, without knowing? I posted trivial repro instructions on the linked issue, and I'm getting the same error as @harsanyidani. On pytest 7.x, all this works perfectly with this project but not with the new one.

agronholm avatar Feb 06 '24 07:02 agronholm

While @harsanyidani provided a crucial part of the error they are getting, it's by no means a full traceback (as is provided in the first related issue I linked above). The issue you created added some context, so thank you for that, but it's in another repository and I don't have the time right now to install this other code and debug it. Please keep in mind that I'm not maintaining this repo, I was just trying to help with a quick search.

glatterf42 avatar Feb 06 '24 07:02 glatterf42

There is no traceback. This was the output I got when trying to run that trivial repro test module with pytest 7:

collected 2 items                                                                                                                                                                                                                                                                                                                         

tests/test_foo.py::test_foo[service1] ERROR                                                                                                                                                                                                                                                                                         [ 50%]
tests/test_foo.py::test_foo[service2] ERROR                                                                                                                                                                                                                                                                                         [100%]

================================================================================================================================================================= ERRORS ==================================================================================================================================================================
__________________________________________________________________________________________________________________________________________________ ERROR at setup of test_foo[service1] ___________________________________________________________________________________________________________________________________________________
The requested fixture has no parameter defined for test:
    tests/test_foo.py::test_foo[service1]

Requested fixture 'fixture1' defined in:
tests/test_foo.py:7

Requested here:
venv38/lib64/python3.8/site-packages/_pytest/fixtures.py:693
__________________________________________________________________________________________________________________________________________________ ERROR at setup of test_foo[service2] ___________________________________________________________________________________________________________________________________________________
The requested fixture has no parameter defined for test:
    tests/test_foo.py::test_foo[service2]

Requested fixture 'fixture1' defined in:
tests/test_foo.py:7

Requested here:
venv38/lib64/python3.8/site-packages/_pytest/fixtures.py:693
============================================================================================================================================================ 2 errors in 0.04s ============================================================================================================================================================

agronholm avatar Feb 06 '24 07:02 agronholm

Thanks, and did you try giving def fixture2(requests, service1, service2) for example? Just guessing here based on the links above.

glatterf42 avatar Feb 06 '24 08:02 glatterf42

That's hardly equivalent with what I had before. That would activate both service1 and service2 at the same time which is not what I want.

agronholm avatar Feb 06 '24 08:02 agronholm

There is no traceback. This was the output I got when trying to run that trivial repro test module with pytest 7:

collected 2 items                                                                                                                                                                                                                                                                                                                         

tests/test_foo.py::test_foo[service1] ERROR                                                                                                                                                                                                                                                                                         [ 50%]
tests/test_foo.py::test_foo[service2] ERROR                                                                                                                                                                                                                                                                                         [100%]

================================================================================================================================================================= ERRORS ==================================================================================================================================================================
__________________________________________________________________________________________________________________________________________________ ERROR at setup of test_foo[service1] ___________________________________________________________________________________________________________________________________________________
The requested fixture has no parameter defined for test:
    tests/test_foo.py::test_foo[service1]

Requested fixture 'fixture1' defined in:
tests/test_foo.py:7

Requested here:
venv38/lib64/python3.8/site-packages/_pytest/fixtures.py:693
__________________________________________________________________________________________________________________________________________________ ERROR at setup of test_foo[service2] ___________________________________________________________________________________________________________________________________________________
The requested fixture has no parameter defined for test:
    tests/test_foo.py::test_foo[service2]

Requested fixture 'fixture1' defined in:
tests/test_foo.py:7

Requested here:
venv38/lib64/python3.8/site-packages/_pytest/fixtures.py:693
============================================================================================================================================================ 2 errors in 0.04s ============================================================================================================================================================

Yes, this is exactly what I'm getting.

harsanyidani avatar Feb 06 '24 08:02 harsanyidani

Alright then, sorry I can't be of more help right now.

glatterf42 avatar Feb 06 '24 08:02 glatterf42

Yeah, the only way forward is for the author of the new library to look at this project and fill in the missing functionality in theirs.

agronholm avatar Feb 06 '24 08:02 agronholm

But seriously though, this should be a built-in feature in pytest itself.

agronholm avatar Feb 06 '24 08:02 agronholm

There might be one more thing you can do: show the pytest devs in their existing issue to integrate pytest-lazy-fixture in core pytest that you're interested in that :)

glatterf42 avatar Feb 06 '24 08:02 glatterf42

The package seems to be fully replaced with lazy-fixtures which seems to have all the features we get used to.

RashidRysaev avatar Aug 02 '24 11:08 RashidRysaev

The package seems to be fully replaced with lazy-fixtures which seems to have all the features we get used to.

It is .... in Debian at least. All reverse-dependencies have been sorted out & pytest-lazy-fixture has been removed. Patches have been sent to upstream projects.

a-detiste avatar Aug 02 '24 11:08 a-detiste