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."""
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:

View File

@ -417,7 +417,8 @@ 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]:
import doctest

View File

@ -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(

View File

@ -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
@ -255,19 +258,19 @@ class PyobjMixin(nodes.Node):
_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,
@ -277,7 +280,7 @@ class PyobjMixin(nodes.Node):
return None
@property
def obj(self):
def obj(self) -> Any:
"""Underlying Python object."""
obj = getattr(self, "_obj", None)
if obj is None:
@ -292,10 +295,10 @@ class PyobjMixin(nodes.Node):
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
@ -434,14 +437,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 +471,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 +483,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 +495,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,7 +551,9 @@ 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]:
@ -568,7 +579,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 +612,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 +657,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,38 +720,47 @@ 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]:
@ -780,7 +804,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 +839,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:
@ -1101,8 +1129,8 @@ class Metafunc:
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 +1551,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 +1606,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 +1615,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
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 +1645,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

View File

@ -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():

View File

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