Skip to content

Detect declared-only instance attributes #698

@graipher

Description

@graipher

Summary

When typing unit tests written with pytest, when parametrizing a fixture in the following way:

# /// script
# dependencies = [
#   "pytest",
# ]
# ///

import pytest


@pytest.fixture(params=["foo", "bar"])
def foo_fixture(request: pytest.FixtureRequest) -> str:
    return request.param

ty does not recognize that the pytest.FixtureRequest object does indeed have a param attribute:

uvx ty check .
error[unresolved-attribute]: Type `FixtureRequest` has no attribute `param`
  --> main.py:12:12
   |
10 | @pytest.fixture(params=["foo", "bar"])
11 | def foo_fixture(request: pytest.FixtureRequest) -> str:
12 |     return request.param
   |            ^^^^^^^^^^^^^
   |
info: rule `unresolved-attribute` is enabled by default

despite the fact that pytest.FixtureRequest declares this attribute in it's __init__ method:

class FixtureRequest(abc.ABC):
    """The type of the ``request`` fixture.

    A request object gives access to the requesting test context and has a
    ``param`` attribute in case the fixture is parametrized.
    """

    def __init__(
        self,
        pyfuncitem: Function,
        fixturename: str | None,
        arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]],
        fixture_defs: dict[str, FixtureDef[Any]],
        *,
        _ispytest: bool = False,
    ) -> None:
        check_ispytest(_ispytest)
        #: Fixture for which this request is being performed.
        self.fixturename: Final = fixturename
        self._pyfuncitem: Final = pyfuncitem
        # The FixtureDefs for each fixture name requested by this item.
        # Starts from the statically-known fixturedefs resolved during
        # collection. Dynamically requested fixtures (using
        # `request.getfixturevalue("foo")`) are added dynamically.
        self._arg2fixturedefs: Final = arg2fixturedefs
        # The evaluated argnames so far, mapping to the FixtureDef they resolved
        # to.
        self._fixture_defs: Final = fixture_defs
        # Notes on the type of `param`:
        # -`request.param` is only defined in parametrized fixtures, and will raise
        #   AttributeError otherwise. Python typing has no notion of "undefined", so
        #   this cannot be reflected in the type.
        # - Technically `param` is only (possibly) defined on SubRequest, not
        #   FixtureRequest, but the typing of that is still in flux so this cheats.
        # - In the future we might consider using a generic for the param type, but
        #   for now just using Any.
        self.param: Any

    ...

Both mypy and pyright do not emit an error for this (mypy does complain about the return annotation being a str and request.param returning Any in strict mode, though).

I guess that attributes assigned in __init__ methods are currently not considered or that there is at least no proper resolution to which __init__ gets called (since this is an ABC)? The comment above self.param may also be relevant here.

Version

ty 0.0.1-alpha.11

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions