wip: more type annotations, too many Any still

This commit is contained in:
Ronny Pfannschmidt 2024-06-22 11:41:22 +02:00
parent 608436cda1
commit cbd9e8996f
6 changed files with 91 additions and 60 deletions

View File

@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Python version compatibility code.""" """Python version compatibility code."""
from __future__ import annotations from __future__ import annotations
@ -12,8 +11,11 @@ from inspect import signature
import os import os
from pathlib import Path from pathlib import Path
import sys import sys
from types import FunctionType
from types import MethodType
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast
from typing import Final from typing import Final
from typing import NoReturn from typing import NoReturn
@ -66,7 +68,8 @@ def is_async_function(func: object) -> bool:
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) 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) function = get_real_func(function)
fn = Path(inspect.getfile(function)) fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno 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) 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).""" """Return number of arguments used up by mock arguments (if any)."""
patchings = getattr(function, "patchings", None) patchings = getattr(function, "patchings", None)
if not patchings: if not patchings:
@ -222,7 +225,7 @@ class _PytestWrapper:
obj: Any 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 """Get the real function object of the (possibly) wrapped object by
functools.wraps or functools.partial.""" functools.wraps or functools.partial."""
start_obj = obj start_obj = obj
@ -249,7 +252,7 @@ def get_real_func(obj):
return 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 """Attempt to obtain the real function object that might be wrapping
``obj``, while at the same time returning a bound method to ``holder`` if ``obj``, while at the same time returning a bound method to ``holder`` if
the original object was a bound method.""" the original object was a bound method."""
@ -263,11 +266,8 @@ def get_real_method(obj, holder):
return obj return obj
def getimfunc(func): def getimfunc(func: FunctionType | MethodType | Callable[..., Any]) -> FunctionType:
try: return cast(FunctionType, getattr(func, "__func__", func))
return func.__func__
except AttributeError:
return func
def safe_getattr(object: Any, name: str, default: Any) -> Any: def safe_getattr(object: Any, name: str, default: Any) -> Any:

View File

@ -417,7 +417,8 @@ def _get_continue_on_failure(config: Config) -> bool:
class DoctestTextfile(Module): 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]:
import doctest import doctest

View File

@ -1100,7 +1100,8 @@ def resolve_fixture_function(
) -> _FixtureFunc[FixtureValue]: ) -> _FixtureFunc[FixtureValue]:
"""Get the actual callable that can be called to obtain the fixture """Get the actual callable that can be called to obtain the fixture
value.""" 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 # The fixture function needs to be bound to the actual
# request.instance so that code working with "fixturedef" behaves # request.instance so that code working with "fixturedef" behaves
# as expected. # as expected.
@ -1112,11 +1113,11 @@ def resolve_fixture_function(
instance, instance,
fixturefunc.__self__.__class__, fixturefunc.__self__.__class__,
): ):
return fixturefunc return cast(_FixtureFunc[FixtureValue], fixturefunc)
fixturefunc = getimfunc(fixturedef.func) fixturefunc = getimfunc(fixturedef.func)
if fixturefunc != fixturedef.func: if fixturefunc != fixturedef.func:
fixturefunc = fixturefunc.__get__(instance) fixturefunc = fixturefunc.__get__(instance)
return fixturefunc return cast(_FixtureFunc[FixtureValue], fixturefunc)
def pytest_fixture_setup( def pytest_fixture_setup(

View File

@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Python test discovery, setup and run of test functions.""" """Python test discovery, setup and run of test functions."""
from __future__ import annotations from __future__ import annotations
@ -17,6 +16,7 @@ from pathlib import Path
import types import types
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast
from typing import Dict from typing import Dict
from typing import final from typing import final
from typing import Generator from typing import Generator
@ -27,6 +27,8 @@ from typing import Mapping
from typing import Pattern from typing import Pattern
from typing import Sequence from typing import Sequence
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import warnings import warnings
import _pytest 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) 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) return Module.from_parent(parent, path=module_path)
@ -242,6 +244,7 @@ def pytest_pycollect_makeitem(
res.warn(PytestCollectionWarning(reason)) res.warn(PytestCollectionWarning(reason))
return res return res
else: else:
assert isinstance(obj, (types.FunctionType))
return list(collector._genfunctions(name, obj)) return list(collector._genfunctions(name, obj))
return None return None
@ -255,19 +258,19 @@ class PyobjMixin(nodes.Node):
_ALLOW_MARKERS = True _ALLOW_MARKERS = True
@property @property
def module(self): def module(self) -> types.ModuleType | None:
"""Python module object this node was collected from (can be None).""" """Python module object this node was collected from (can be None)."""
node = self.getparent(Module) node = self.getparent(Module)
return node.obj if node is not None else None return node.obj if node is not None else None
@property @property
def cls(self): def cls(self) -> type | None:
"""Python class object this node was collected from (can be None).""" """Python class object this node was collected from (can be None)."""
node = self.getparent(Class) node = self.getparent(Class)
return node.obj if node is not None else None return node.obj if node is not None else None
@property @property
def instance(self): def instance(self) -> object | None:
"""Python instance object the function is bound to. """Python instance object the function is bound to.
Returns None if not a test method, e.g. for a standalone test function, Returns None if not a test method, e.g. for a standalone test function,
@ -277,7 +280,7 @@ class PyobjMixin(nodes.Node):
return None return None
@property @property
def obj(self): def obj(self) -> Any:
"""Underlying Python object.""" """Underlying Python object."""
obj = getattr(self, "_obj", None) obj = getattr(self, "_obj", None)
if obj is None: if obj is None:
@ -292,10 +295,10 @@ class PyobjMixin(nodes.Node):
return obj return obj
@obj.setter @obj.setter
def obj(self, value): def obj(self, value: Any) -> None:
self._obj = value self._obj = value
def _getobj(self): def _getobj(self) -> Any:
"""Get the underlying Python object. May be overwritten by subclasses.""" """Get the underlying Python object. May be overwritten by subclasses."""
# TODO: Improve the type of `parent` such that assert/ignore aren't needed. # TODO: Improve the type of `parent` such that assert/ignore aren't needed.
assert self.parent is not None assert self.parent is not None
@ -434,14 +437,20 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
result.extend(values) result.extend(values)
return result return result
def _genfunctions(self, name: str, funcobj) -> Iterator[Function]: def _genfunctions(
self, name: str, funcobj: types.FunctionType
) -> Iterator[Function]:
modulecol = self.getparent(Module) modulecol = self.getparent(Module)
assert modulecol is not None assert modulecol is not None
module = modulecol.obj module = modulecol.obj
clscol = self.getparent(Class) 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 fixtureinfo = definition._fixtureinfo
# pytest_generate_tests impls call metafunc.parametrize() which fills # pytest_generate_tests impls call metafunc.parametrize() which fills
@ -462,7 +471,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc))
if not metafunc._calls: 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: else:
# Direct parametrizations taking place in module/class-specific # Direct parametrizations taking place in module/class-specific
# `metafunc.parametrize` calls may have shadowed some fixtures, so make sure # `metafunc.parametrize` calls may have shadowed some fixtures, so make sure
@ -474,7 +483,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
for callspec in metafunc._calls: for callspec in metafunc._calls:
subname = f"{name}[{callspec.id}]" subname = f"{name}[{callspec.id}]"
yield Function.from_parent( yield Function.from_parent(
self, self, # type: ignore[arg-type]
name=subname, name=subname,
callspec=callspec, callspec=callspec,
fixtureinfo=fixtureinfo, fixtureinfo=fixtureinfo,
@ -486,7 +495,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
def importtestmodule( def importtestmodule(
path: Path, path: Path,
config: Config, config: Config,
): ) -> types.ModuleType:
# We assume we are only called once per module. # We assume we are only called once per module.
importmode = config.getoption("--import-mode") importmode = config.getoption("--import-mode")
try: try:
@ -542,7 +551,9 @@ def importtestmodule(
class Module(nodes.File, PyCollector): class Module(nodes.File, PyCollector):
"""Collector for test classes and functions in a Python module.""" """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) return importtestmodule(self.path, self.config)
def collect(self) -> Iterable[nodes.Item | nodes.Collector]: def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
@ -568,7 +579,9 @@ class Module(nodes.File, PyCollector):
if setup_module is None and teardown_module is None: if setup_module is None and teardown_module is None:
return 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 module = request.module
if setup_module is not None: if setup_module is not None:
_call_with_optional_argument(setup_module, module) _call_with_optional_argument(setup_module, module)
@ -599,7 +612,9 @@ class Module(nodes.File, PyCollector):
if setup_function is None and teardown_function is None: if setup_function is None and teardown_function is None:
return 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: if request.instance is not None:
# in this case we are bound to an instance, so we need to let # in this case we are bound to an instance, so we need to let
# setup_method handle this # setup_method handle this
@ -642,9 +657,9 @@ class Package(nodes.Directory):
fspath: LEGACY_PATH | None, fspath: LEGACY_PATH | None,
parent: nodes.Collector, parent: nodes.Collector,
# NOTE: following args are unused: # NOTE: following args are unused:
config=None, config: Config | None = None,
session=None, session: Session | None = None,
nodeid=None, nodeid: str | None = None,
path: Path | None = None, path: Path | None = None,
) -> None: ) -> None:
# NOTE: Could be just the following, but kept as-is for compat. # NOTE: Could be just the following, but kept as-is for compat.
@ -705,38 +720,47 @@ class Package(nodes.Directory):
yield from cols 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 """Call the given function with the given argument if func accepts one argument, otherwise
calls func without arguments.""" calls func without arguments."""
arg_count = func.__code__.co_argcount arg_count = func.__code__.co_argcount
if inspect.ismethod(func): if inspect.ismethod(func):
arg_count -= 1 arg_count -= 1
if arg_count: if arg_count:
func(arg) func(arg) # type: ignore[call-arg]
else: 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 """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. xunit-style function, but only if not marked as a fixture to avoid calling it twice.
""" """
for name in names: for name in names:
meth: object | None = getattr(obj, name, None) meth: object | None = getattr(obj, name, None)
if meth is not None and fixtures.getfixturemarker(meth) is None: if meth is not None and fixtures.getfixturemarker(meth) is None:
return meth return cast(Union[types.FunctionType, types.MethodType], meth)
return None return None
class Class(PyCollector): class Class(PyCollector):
"""Collector for test methods (and nested classes) in a Python class.""" """Collector for test methods (and nested classes) in a Python class."""
obj: type
@classmethod @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.""" """The public constructor."""
return super().from_parent(name=name, parent=parent, **kw) return super().from_parent(name=name, parent=parent, **kw)
def newinstance(self): def newinstance(self) -> Any:
return self.obj() return self.obj()
def collect(self) -> Iterable[nodes.Item | nodes.Collector]: def collect(self) -> Iterable[nodes.Item | nodes.Collector]:
@ -780,7 +804,9 @@ class Class(PyCollector):
if setup_class is None and teardown_class is None: if setup_class is None and teardown_class is None:
return 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 cls = request.cls
if setup_class is not None: if setup_class is not None:
func = getimfunc(setup_class) func = getimfunc(setup_class)
@ -813,7 +839,9 @@ class Class(PyCollector):
if setup_method is None and teardown_method is None: if setup_method is None and teardown_method is None:
return 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 instance = request.instance
method = request.function method = request.function
if setup_method is not None: if setup_method is not None:
@ -1101,8 +1129,8 @@ class Metafunc:
definition: FunctionDefinition, definition: FunctionDefinition,
fixtureinfo: fixtures.FuncFixtureInfo, fixtureinfo: fixtures.FuncFixtureInfo,
config: Config, config: Config,
cls=None, cls: type | None = None,
module=None, module: types.ModuleType | None = None,
*, *,
_ispytest: bool = False, _ispytest: bool = False,
) -> None: ) -> None:
@ -1523,13 +1551,15 @@ class Function(PyobjMixin, nodes.Item):
# Disable since functions handle it themselves. # Disable since functions handle it themselves.
_ALLOW_MARKERS = False _ALLOW_MARKERS = False
obj: Callable[..., object]
def __init__( def __init__(
self, self,
name: str, name: str,
parent, parent: PyCollector | Module | Class,
config: Config | None = None, config: Config | None = None,
callspec: CallSpec2 | None = None, callspec: CallSpec2 | None = None,
callobj=NOTSET, callobj: Any = NOTSET,
keywords: Mapping[str, Any] | None = None, keywords: Mapping[str, Any] | None = None,
session: Session | None = None, session: Session | None = None,
fixtureinfo: FuncFixtureInfo | None = None, fixtureinfo: FuncFixtureInfo | None = None,
@ -1576,7 +1606,7 @@ class Function(PyobjMixin, nodes.Item):
# todo: determine sound type limitations # todo: determine sound type limitations
@classmethod @classmethod
def from_parent(cls, parent, **kw) -> Self: def from_parent(cls, parent: Module | Class, **kw: Any) -> Self: # type: ignore[override]
"""The public constructor.""" """The public constructor."""
return super().from_parent(parent=parent, **kw) return super().from_parent(parent=parent, **kw)
@ -1585,30 +1615,27 @@ class Function(PyobjMixin, nodes.Item):
self._request = fixtures.TopRequest(self, _ispytest=True) self._request = fixtures.TopRequest(self, _ispytest=True)
@property @property
def function(self): def function(self) -> types.FunctionType:
"""Underlying python 'function' object.""" """Underlying python 'function' object."""
return getimfunc(self.obj) return getimfunc(self.obj)
@property @property
def instance(self): def instance(self) -> Any | None:
try: try:
return self._instance return self._instance
except AttributeError: except AttributeError:
if isinstance(self.parent, Class):
# Each Function gets a fresh class instance.
self._instance = self._getinstance() self._instance = self._getinstance()
else:
self._instance = None
return self._instance return self._instance
def _getinstance(self): def _getinstance(self) -> Any | None:
if isinstance(self.parent, Class): if isinstance(self.parent, Class):
# Each Function gets a fresh class instance. # Each Function gets a fresh class instance.
return self.parent.newinstance() return self.parent.newinstance()
else: else:
return None return None
def _getobj(self): def _getobj(self) -> object:
instance = self.instance instance = self.instance
if instance is not None: if instance is not None:
parent_obj = instance parent_obj = instance
@ -1618,7 +1645,7 @@ class Function(PyobjMixin, nodes.Item):
return getattr(parent_obj, self.originalname) return getattr(parent_obj, self.originalname)
@property @property
def _pyfuncitem(self): def _pyfuncitem(self) -> Self:
"""(compatonly) for code expecting pytest-2.2 style request objects.""" """(compatonly) for code expecting pytest-2.2 style request objects."""
return self return self

View File

@ -325,7 +325,8 @@ class TestFunction:
session = Session.from_config(config) session = Session.from_config(config)
session._fixturemanager = FixtureManager(session) 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 test_function_equality(self, pytester: Pytester) -> None:
def func1(): def func1():

View File

@ -7,6 +7,7 @@ import re
import sys import sys
import textwrap import textwrap
from typing import Any from typing import Any
from typing import Callable
from typing import cast from typing import cast
from typing import Dict from typing import Dict
from typing import Iterator from typing import Iterator
@ -49,7 +50,7 @@ class TestMetafunc:
@dataclasses.dataclass @dataclasses.dataclass
class DefinitionMock(python.FunctionDefinition): class DefinitionMock(python.FunctionDefinition):
_nodeid: str _nodeid: str
obj: object obj: Callable[..., Any]
names = getfuncargnames(func) names = getfuncargnames(func)
fixtureinfo: Any = FuncFixtureInfoMock(names) fixtureinfo: Any = FuncFixtureInfoMock(names)