diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index e74528257..2ff222b9c 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -210,7 +210,11 @@ class TracebackEntry: @property def lineno(self) -> int: - return self._rawentry.tb_lineno - 1 + if self._rawentry.tb_lineno is None: + # how did i trigger this 😱 + return -1 # type: ignore[unreachable] + else: + return self._rawentry.tb_lineno - 1 @property def frame(self) -> Frame: diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 614848e0d..401e9417e 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -1,4 +1,3 @@ -# mypy: allow-untyped-defs """Python version compatibility code.""" from __future__ import annotations @@ -12,8 +11,11 @@ from inspect import signature import os from pathlib import Path import sys +from types import FunctionType +from types import MethodType from typing import Any from typing import Callable +from typing import cast from typing import Final from typing import NoReturn @@ -66,7 +68,8 @@ def is_async_function(func: object) -> bool: return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) -def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str: +def getlocation(function: Any, curdir: str | os.PathLike[str] | None = None) -> str: + # todo: declare a type alias for function, fixturefunction and callables/generators function = get_real_func(function) fn = Path(inspect.getfile(function)) lineno = function.__code__.co_firstlineno @@ -80,7 +83,7 @@ def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str: return "%s:%d" % (fn, lineno + 1) -def num_mock_patch_args(function) -> int: +def num_mock_patch_args(function: Callable[..., object]) -> int: """Return number of arguments used up by mock arguments (if any).""" patchings = getattr(function, "patchings", None) if not patchings: @@ -222,7 +225,7 @@ class _PytestWrapper: obj: Any -def get_real_func(obj): +def get_real_func(obj: Any) -> Any: """Get the real function object of the (possibly) wrapped object by functools.wraps or functools.partial.""" start_obj = obj @@ -249,7 +252,7 @@ def get_real_func(obj): return obj -def get_real_method(obj, holder): +def get_real_method(obj: Any, holder: object) -> Any: """Attempt to obtain the real function object that might be wrapping ``obj``, while at the same time returning a bound method to ``holder`` if the original object was a bound method.""" @@ -263,11 +266,8 @@ def get_real_method(obj, holder): return obj -def getimfunc(func): - try: - return func.__func__ - except AttributeError: - return func +def getimfunc(func: FunctionType | MethodType | Callable[..., Any]) -> FunctionType: + return cast(FunctionType, getattr(func, "__func__", func)) def safe_getattr(object: Any, name: str, default: Any) -> Any: diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index cb46d9a3b..c0fac8aa5 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -417,9 +417,10 @@ def _get_continue_on_failure(config: Config) -> bool: class DoctestTextfile(Module): - obj = None + # todo: this shouldnt be a module + obj: None = None # type: ignore[assignment] - def collect(self) -> Iterable[DoctestItem]: + def collect(self) -> Iterable[DoctestItem]: # type: ignore[override] import doctest # Inspired by doctest.testfile; ideally we would use it directly, @@ -497,7 +498,7 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]: class DoctestModule(Module): - def collect(self) -> Iterable[DoctestItem]: + def collect(self) -> Iterable[DoctestItem]: # type: ignore[override] import doctest class MockAwareDocTestFinder(doctest.DocTestFinder): diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0151a4d9c..1e3385e3c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1100,7 +1100,8 @@ def resolve_fixture_function( ) -> _FixtureFunc[FixtureValue]: """Get the actual callable that can be called to obtain the fixture value.""" - fixturefunc = fixturedef.func + # absuing any for the differences between FunctionTpye and Callable + fixturefunc: Any = fixturedef.func # The fixture function needs to be bound to the actual # request.instance so that code working with "fixturedef" behaves # as expected. @@ -1112,11 +1113,11 @@ def resolve_fixture_function( instance, fixturefunc.__self__.__class__, ): - return fixturefunc + return cast(_FixtureFunc[FixtureValue], fixturefunc) fixturefunc = getimfunc(fixturedef.func) if fixturefunc != fixturedef.func: fixturefunc = fixturefunc.__get__(instance) - return fixturefunc + return cast(_FixtureFunc[FixtureValue], fixturefunc) def pytest_fixture_setup( diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 8ec269060..9818fe409 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -535,6 +535,7 @@ class Session(nodes.Collector): ``Session`` collects the initial paths given as arguments to pytest. """ + parent: None Interrupted = Interrupted Failed = Failed # Set on the session by runner.pytest_sessionstart. diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index bbde2664b..3076484a3 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,4 +1,3 @@ -# mypy: allow-untyped-defs from __future__ import annotations import abc @@ -20,6 +19,7 @@ from typing import TypeVar import warnings import pluggy +from typing_extensions import Self import _pytest._code from _pytest._code import getfslineno @@ -54,9 +54,6 @@ SEP = "/" tracebackcutdir = Path(_pytest.__file__).parent -_T = TypeVar("_T") - - def _imply_path( node_type: type[Node], path: Path | None, @@ -96,7 +93,7 @@ class NodeMeta(abc.ABCMeta): progress on detangling the :class:`Node` classes. """ - def __call__(cls, *k, **kw) -> NoReturn: + def __call__(cls, *k: object, **kw: object) -> NoReturn: msg = ( "Direct construction of {name} has been deprecated, please use {name}.from_parent.\n" "See " @@ -105,25 +102,6 @@ class NodeMeta(abc.ABCMeta): ).format(name=f"{cls.__module__}.{cls.__name__}") fail(msg, pytrace=False) - def _create(cls: type[_T], *k, **kw) -> _T: - try: - return super().__call__(*k, **kw) # type: ignore[no-any-return,misc] - except TypeError: - sig = signature(getattr(cls, "__init__")) - known_kw = {k: v for k, v in kw.items() if k in sig.parameters} - from .warning_types import PytestDeprecationWarning - - warnings.warn( - PytestDeprecationWarning( - f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n" - "See https://docs.pytest.org/en/stable/deprecations.html" - "#constructors-of-custom-pytest-node-subclasses-should-take-kwargs " - "for more details." - ) - ) - - return super().__call__(*k, **known_kw) # type: ignore[no-any-return,misc] - class Node(abc.ABC, metaclass=NodeMeta): r"""Base class of :class:`Collector` and :class:`Item`, the components of @@ -138,8 +116,13 @@ class Node(abc.ABC, metaclass=NodeMeta): #: for methods not migrated to ``pathlib.Path`` yet, such as #: :meth:`Item.reportinfo `. Will be deprecated in #: a future release, prefer using :attr:`path` instead. + name: str + parent: Node | None + config: Config + session: Session fspath: LEGACY_PATH + _nodeid: str # Use __slots__ to make attribute access faster. # Note that __dict__ is still available. __slots__ = ( @@ -156,7 +139,7 @@ class Node(abc.ABC, metaclass=NodeMeta): def __init__( self, name: str, - parent: Node | None = None, + parent: Node | None, config: Config | None = None, session: Session | None = None, fspath: LEGACY_PATH | None = None, @@ -200,13 +183,9 @@ class Node(abc.ABC, metaclass=NodeMeta): #: Allow adding of extra keywords to use for matching. self.extra_keyword_matches: set[str] = set() - if nodeid is not None: - assert "::()" not in nodeid - self._nodeid = nodeid - else: - if not self.parent: - raise TypeError("nodeid or parent must be provided") - self._nodeid = self.parent.nodeid + "::" + self.name + self._nodeid = self._make_nodeid( + name=self.name, parent=self.parent, given=nodeid + ) #: A place where plugins can store information on the node for their #: own use. @@ -215,7 +194,38 @@ class Node(abc.ABC, metaclass=NodeMeta): self._store = self.stash @classmethod - def from_parent(cls, parent: Node, **kw) -> Self: + def _make_nodeid(cls, name: str, parent: Node | None, given: str | None) -> str: + if given is not None: + assert "::()" not in given + return given + else: + assert parent is not None + return f"{parent.nodeid}::{name}" + + @classmethod + def _create(cls, *k: object, **kw: object) -> Self: + callit = super(type(cls), NodeMeta).__call__ # type: ignore[misc] + try: + return cast(Self, callit(cls, *k, **kw)) + except TypeError as e: + sig = signature(getattr(cls, "__init__")) + known_kw = {k: v for k, v in kw.items() if k in sig.parameters} + from .warning_types import PytestDeprecationWarning + + warnings.warn( + PytestDeprecationWarning( + f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n" + f"Exception: {e}\n" + "See https://docs.pytest.org/en/stable/deprecations.html" + "#constructors-of-custom-pytest-node-subclasses-should-take-kwargs " + "for more details." + ) + ) + + return cast(Self, callit(cls, *k, **known_kw)) + + @classmethod + def from_parent(cls, parent: Node, **kw: Any) -> Self: """Public constructor for Nodes. This indirection got introduced in order to enable removing @@ -238,7 +248,7 @@ class Node(abc.ABC, metaclass=NodeMeta): return self.session.gethookproxy(self.path) def __repr__(self) -> str: - return "<{} {}>".format(self.__class__.__name__, getattr(self, "name", None)) + return f'<{self.__class__.__name__} { getattr(self, "name", None)}>' def warn(self, warning: Warning) -> None: """Issue a warning for this Node. @@ -598,7 +608,6 @@ class FSCollector(Collector, abc.ABC): if nodeid and os.sep != SEP: nodeid = nodeid.replace(os.sep, SEP) - super().__init__( name=name, parent=parent, @@ -611,11 +620,11 @@ class FSCollector(Collector, abc.ABC): @classmethod def from_parent( cls, - parent, + parent: Node, *, fspath: LEGACY_PATH | None = None, path: Path | None = None, - **kw, + **kw: Any, ) -> Self: """The public constructor.""" return super().from_parent(parent=parent, fspath=fspath, path=path, **kw) @@ -646,22 +655,25 @@ class Directory(FSCollector, abc.ABC): """ +class Definition(Collector, abc.ABC): + @abc.abstractmethod + def collect(self) -> Iterable[Item]: ... + + class Item(Node, abc.ABC): """Base class of all test invocation items. Note that for a single function there might be multiple test invocation items. """ - nextitem = None - def __init__( self, - name, - parent=None, + name: str, + parent: Node | None = None, config: Config | None = None, session: Session | None = None, nodeid: str | None = None, - **kw, + **kw: Any, ) -> None: # The first two arguments are intentionally passed positionally, # to keep plugins who define a node type which inherits from diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 9182ce7df..3e1f74d19 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1,4 +1,3 @@ -# mypy: allow-untyped-defs """Python test discovery, setup and run of test functions.""" from __future__ import annotations @@ -17,6 +16,7 @@ from pathlib import Path import types from typing import Any from typing import Callable +from typing import cast from typing import Dict from typing import final from typing import Generator @@ -27,6 +27,8 @@ from typing import Mapping from typing import Pattern from typing import Sequence from typing import TYPE_CHECKING +from typing import TypeVar +from typing import Union import warnings import _pytest @@ -203,7 +205,7 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: return any(fnmatch_ex(pattern, path) for pattern in patterns) -def pytest_pycollect_makemodule(module_path: Path, parent) -> Module: +def pytest_pycollect_makemodule(module_path: Path, parent: nodes.FSCollector) -> Module: return Module.from_parent(parent, path=module_path) @@ -242,6 +244,7 @@ def pytest_pycollect_makeitem( res.warn(PytestCollectionWarning(reason)) return res else: + assert isinstance(obj, (types.FunctionType)) return list(collector._genfunctions(name, obj)) return None @@ -252,22 +255,26 @@ class PyobjMixin(nodes.Node): as its intended to always mix in before a node its position in the mro is unaffected""" + def __init__(self, *k: Any, obj: Any | None = None, **kw: Any) -> None: + super().__init__(*k, **kw) + self._assign_obj_with_markers(obj) + _ALLOW_MARKERS = True @property - def module(self): + def module(self) -> types.ModuleType | None: """Python module object this node was collected from (can be None).""" node = self.getparent(Module) return node.obj if node is not None else None @property - def cls(self): + def cls(self) -> type | None: """Python class object this node was collected from (can be None).""" node = self.getparent(Class) return node.obj if node is not None else None @property - def instance(self): + def instance(self) -> object | None: """Python instance object the function is bound to. Returns None if not a test method, e.g. for a standalone test function, @@ -276,26 +283,30 @@ class PyobjMixin(nodes.Node): # Overridden by Function. return None + def _assign_obj_with_markers(self, obj: Any | None) -> None: + self._obj = obj + # XXX evil hack + # used to avoid Function marker duplication + if self._ALLOW_MARKERS and obj is not None: + self.own_markers.extend(get_unpacked_marks(self.obj)) + # This assumes that `obj` is called before there is a chance + # to add custom keys to `self.keywords`, so no fear of overriding. + self.keywords.update((mark.name, mark) for mark in self.own_markers) + @property - def obj(self): + def obj(self) -> Any: """Underlying Python object.""" obj = getattr(self, "_obj", None) if obj is None: - self._obj = obj = self._getobj() - # XXX evil hack - # used to avoid Function marker duplication - if self._ALLOW_MARKERS: - self.own_markers.extend(get_unpacked_marks(self.obj)) - # This assumes that `obj` is called before there is a chance - # to add custom keys to `self.keywords`, so no fear of overriding. - self.keywords.update((mark.name, mark) for mark in self.own_markers) + obj = self._getobj() + self._assign_obj_with_markers(obj) return obj @obj.setter - def obj(self, value): + def obj(self, value: Any) -> None: self._obj = value - def _getobj(self): + def _getobj(self) -> Any: """Get the underlying Python object. May be overwritten by subclasses.""" # TODO: Improve the type of `parent` such that assert/ignore aren't needed. assert self.parent is not None @@ -391,7 +402,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): return True return False - def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + def collect(self) -> Iterable[nodes.Definition | nodes.Collector]: if not getattr(self.obj, "__test__", True): return [] @@ -404,10 +415,10 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): # In each class, nodes should be definition ordered. # __dict__ is definition ordered. seen: set[str] = set() - dict_values: list[list[nodes.Item | nodes.Collector]] = [] + dict_values: list[list[nodes.Definition | nodes.Collector]] = [] ihook = self.ihook for dic in dicts: - values: list[nodes.Item | nodes.Collector] = [] + values: list[nodes.Definition | nodes.Collector] = [] # Note: seems like the dict can change during iteration - # be careful not to remove the list() without consideration. for name, obj in list(dic.items()): @@ -434,14 +445,20 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): result.extend(values) return result - def _genfunctions(self, name: str, funcobj) -> Iterator[Function]: + def _genfunctions( + self, name: str, funcobj: types.FunctionType + ) -> Iterator[Function]: modulecol = self.getparent(Module) assert modulecol is not None module = modulecol.obj clscol = self.getparent(Class) - cls = clscol and clscol.obj or None + cls: type | None = getattr(clscol, "obj", None) - definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj) + definition = FunctionDefinition.from_parent( + self, # type: ignore[arg-type] + name=name, + callobj=funcobj, + ) fixtureinfo = definition._fixtureinfo # pytest_generate_tests impls call metafunc.parametrize() which fills @@ -462,7 +479,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) if not metafunc._calls: - yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) + yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo) # type: ignore[arg-type] else: # Direct parametrizations taking place in module/class-specific # `metafunc.parametrize` calls may have shadowed some fixtures, so make sure @@ -474,7 +491,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): for callspec in metafunc._calls: subname = f"{name}[{callspec.id}]" yield Function.from_parent( - self, + self, # type: ignore[arg-type] name=subname, callspec=callspec, fixtureinfo=fixtureinfo, @@ -486,7 +503,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): def importtestmodule( path: Path, config: Config, -): +) -> types.ModuleType: # We assume we are only called once per module. importmode = config.getoption("--import-mode") try: @@ -542,10 +559,12 @@ def importtestmodule( class Module(nodes.File, PyCollector): """Collector for test classes and functions in a Python module.""" - def _getobj(self): + obj: types.ModuleType + + def _getobj(self) -> types.ModuleType: return importtestmodule(self.path, self.config) - def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + def collect(self) -> Iterable[nodes.Collector]: self._register_setup_module_fixture() self._register_setup_function_fixture() self.session._fixturemanager.parsefactories(self) @@ -568,7 +587,9 @@ class Module(nodes.File, PyCollector): if setup_module is None and teardown_module is None: return - def xunit_setup_module_fixture(request) -> Generator[None, None, None]: + def xunit_setup_module_fixture( + request: FixtureRequest, + ) -> Generator[None, None, None]: module = request.module if setup_module is not None: _call_with_optional_argument(setup_module, module) @@ -599,7 +620,9 @@ class Module(nodes.File, PyCollector): if setup_function is None and teardown_function is None: return - def xunit_setup_function_fixture(request) -> Generator[None, None, None]: + def xunit_setup_function_fixture( + request: FixtureRequest, + ) -> Generator[None, None, None]: if request.instance is not None: # in this case we are bound to an instance, so we need to let # setup_method handle this @@ -642,9 +665,9 @@ class Package(nodes.Directory): fspath: LEGACY_PATH | None, parent: nodes.Collector, # NOTE: following args are unused: - config=None, - session=None, - nodeid=None, + config: Config | None = None, + session: Session | None = None, + nodeid: str | None = None, path: Path | None = None, ) -> None: # NOTE: Could be just the following, but kept as-is for compat. @@ -705,41 +728,50 @@ class Package(nodes.Directory): yield from cols -def _call_with_optional_argument(func, arg) -> None: +T = TypeVar("T") + + +def _call_with_optional_argument( + func: Callable[[T], None] | Callable[[], None], arg: T +) -> None: """Call the given function with the given argument if func accepts one argument, otherwise calls func without arguments.""" arg_count = func.__code__.co_argcount if inspect.ismethod(func): arg_count -= 1 if arg_count: - func(arg) + func(arg) # type: ignore[call-arg] else: - func() + func() # type: ignore[call-arg] -def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> object | None: +def _get_first_non_fixture_func( + obj: object, names: Iterable[str] +) -> types.FunctionType | types.MethodType | None: """Return the attribute from the given object to be used as a setup/teardown xunit-style function, but only if not marked as a fixture to avoid calling it twice. """ for name in names: meth: object | None = getattr(obj, name, None) if meth is not None and fixtures.getfixturemarker(meth) is None: - return meth + return cast(Union[types.FunctionType, types.MethodType], meth) return None class Class(PyCollector): """Collector for test methods (and nested classes) in a Python class.""" + obj: type + @classmethod - def from_parent(cls, parent, *, name, obj=None, **kw) -> Self: # type: ignore[override] + def from_parent(cls, parent: nodes.Node, *, name: str, **kw: Any) -> Self: # type: ignore[override] """The public constructor.""" return super().from_parent(name=name, parent=parent, **kw) - def newinstance(self): + def newinstance(self) -> Any: return self.obj() - def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + def collect(self) -> Iterable[nodes.Collector]: if not safe_getattr(self.obj, "__test__", True): return [] if hasinit(self.obj): @@ -780,7 +812,9 @@ class Class(PyCollector): if setup_class is None and teardown_class is None: return - def xunit_setup_class_fixture(request) -> Generator[None, None, None]: + def xunit_setup_class_fixture( + request: FixtureRequest, + ) -> Generator[None, None, None]: cls = request.cls if setup_class is not None: func = getimfunc(setup_class) @@ -813,7 +847,9 @@ class Class(PyCollector): if setup_method is None and teardown_method is None: return - def xunit_setup_method_fixture(request) -> Generator[None, None, None]: + def xunit_setup_method_fixture( + request: FixtureRequest, + ) -> Generator[None, None, None]: instance = request.instance method = request.function if setup_method is not None: @@ -1096,13 +1132,15 @@ class Metafunc: test function is defined. """ + definition: FunctionDefinition + def __init__( self, definition: FunctionDefinition, fixtureinfo: fixtures.FuncFixtureInfo, config: Config, - cls=None, - module=None, + cls: type | None = None, + module: types.ModuleType | None = None, *, _ispytest: bool = False, ) -> None: @@ -1523,13 +1561,15 @@ class Function(PyobjMixin, nodes.Item): # Disable since functions handle it themselves. _ALLOW_MARKERS = False + obj: Callable[..., object] + def __init__( self, name: str, - parent, + parent: PyCollector | Module | Class, config: Config | None = None, callspec: CallSpec2 | None = None, - callobj=NOTSET, + callobj: Any = NOTSET, keywords: Mapping[str, Any] | None = None, session: Session | None = None, fixtureinfo: FuncFixtureInfo | None = None, @@ -1576,7 +1616,7 @@ class Function(PyobjMixin, nodes.Item): # todo: determine sound type limitations @classmethod - def from_parent(cls, parent, **kw) -> Self: + def from_parent(cls, parent: Module | Class, **kw: Any) -> Self: # type: ignore[override] """The public constructor.""" return super().from_parent(parent=parent, **kw) @@ -1585,30 +1625,27 @@ class Function(PyobjMixin, nodes.Item): self._request = fixtures.TopRequest(self, _ispytest=True) @property - def function(self): + def function(self) -> types.FunctionType: """Underlying python 'function' object.""" return getimfunc(self.obj) @property - def instance(self): + def instance(self) -> Any | None: try: return self._instance except AttributeError: - if isinstance(self.parent, Class): - # Each Function gets a fresh class instance. - self._instance = self._getinstance() - else: - self._instance = None + # Each Function gets a fresh class instance. + self._instance = self._getinstance() return self._instance - def _getinstance(self): + def _getinstance(self) -> Any | None: if isinstance(self.parent, Class): # Each Function gets a fresh class instance. return self.parent.newinstance() else: return None - def _getobj(self): + def _getobj(self) -> object: instance = self.instance if instance is not None: parent_obj = instance @@ -1618,7 +1655,7 @@ class Function(PyobjMixin, nodes.Item): return getattr(parent_obj, self.originalname) @property - def _pyfuncitem(self): + def _pyfuncitem(self) -> Self: """(compatonly) for code expecting pytest-2.2 style request objects.""" return self @@ -1673,6 +1710,8 @@ class FunctionDefinition(Function): """This class is a stop gap solution until we evolve to have actual function definition nodes and manage to get rid of ``metafunc``.""" + parent: Module | Class + def runtest(self) -> None: raise RuntimeError("function definitions are not supposed to be run as tests") diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index aefea1333..e1202aaa5 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -81,7 +81,7 @@ class UnitTestCase(Class): # it. return self.obj("runTest") - def collect(self) -> Iterable[Item | Collector]: + def collect(self) -> Iterable[Item | Collector]: # type: ignore[override] from unittest import TestLoader cls = self.obj diff --git a/testing/python/collect.py b/testing/python/collect.py index 063866112..e91808bd3 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -325,7 +325,8 @@ class TestFunction: session = Session.from_config(config) session._fixturemanager = FixtureManager(session) - return pytest.Function.from_parent(parent=session, **kwargs) + # todo: implement intermediate node for testing + return pytest.Function.from_parent(parent=session, **kwargs) # type: ignore[arg-type] def test_function_equality(self, pytester: Pytester) -> None: def func1(): diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 2dd85607e..9f0286425 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -7,6 +7,7 @@ import re import sys import textwrap from typing import Any +from typing import Callable from typing import cast from typing import Dict from typing import Iterator @@ -49,7 +50,7 @@ class TestMetafunc: @dataclasses.dataclass class DefinitionMock(python.FunctionDefinition): _nodeid: str - obj: object + obj: Callable[..., Any] names = getfuncargnames(func) fixtureinfo: Any = FuncFixtureInfoMock(names) diff --git a/testing/test_mark.py b/testing/test_mark.py index 89eef7920..16ca96711 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -634,6 +634,14 @@ class TestFunctional: has_own, has_inherited = items has_own_marker = has_own.get_closest_marker("c") has_inherited_marker = has_inherited.get_closest_marker("c") + + for item in items: + print(item) + for node in item.iter_parents(): + print(" ", node) + for marker in node.own_markers: + print(" ", marker) + assert has_own_marker is not None assert has_inherited_marker is not None assert has_own_marker.kwargs == {"location": "function"} diff --git a/testing/test_nodes.py b/testing/test_nodes.py index f039acf24..e85f21578 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -31,7 +31,7 @@ def test_node_direct_construction_deprecated() -> None: " for more details." ), ): - nodes.Node(None, session=None) # type: ignore[arg-type] + nodes.Node(None, parent=None, session=None) # type: ignore[arg-type] def test_subclassing_both_item_and_collector_deprecated(