python: more type annotations

This commit is contained in:
Ran Benita 2020-06-28 19:31:35 +03:00
parent fb2640b82f
commit e079ebbd57
2 changed files with 67 additions and 57 deletions

View File

@ -13,7 +13,9 @@ from collections.abc import Sequence
from functools import partial from functools import partial
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import Generator
from typing import Iterable from typing import Iterable
from typing import Iterator
from typing import List from typing import List
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
@ -196,8 +198,8 @@ def pytest_collect_file(path: py.path.local, parent) -> Optional["Module"]:
return None return None
def path_matches_patterns(path, patterns): def path_matches_patterns(path: py.path.local, patterns: Iterable[str]) -> bool:
"""Returns True if the given py.path.local matches one of the patterns in the list of globs given""" """Returns True if path matches any of the patterns in the list of globs given."""
return any(path.fnmatch(pattern) for pattern in patterns) return any(path.fnmatch(pattern) for pattern in patterns)
@ -300,7 +302,7 @@ class PyobjMixin:
obj = self.parent.obj # type: ignore[attr-defined] # noqa: F821 obj = self.parent.obj # type: ignore[attr-defined] # noqa: F821
return getattr(obj, self.name) return getattr(obj, self.name)
def getmodpath(self, stopatmodule=True, includemodule=False): def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
""" return python path relative to the containing module. """ """ return python path relative to the containing module. """
chain = self.listchain() chain = self.listchain()
chain.reverse() chain.reverse()
@ -338,10 +340,10 @@ class PyobjMixin:
class PyCollector(PyobjMixin, nodes.Collector): class PyCollector(PyobjMixin, nodes.Collector):
def funcnamefilter(self, name): def funcnamefilter(self, name: str) -> bool:
return self._matches_prefix_or_glob_option("python_functions", name) return self._matches_prefix_or_glob_option("python_functions", name)
def isnosetest(self, obj): def isnosetest(self, obj: object) -> bool:
""" Look for the __test__ attribute, which is applied by the """ Look for the __test__ attribute, which is applied by the
@nose.tools.istest decorator @nose.tools.istest decorator
""" """
@ -350,10 +352,10 @@ class PyCollector(PyobjMixin, nodes.Collector):
# function) as test classes. # function) as test classes.
return safe_getattr(obj, "__test__", False) is True return safe_getattr(obj, "__test__", False) is True
def classnamefilter(self, name): def classnamefilter(self, name: str) -> bool:
return self._matches_prefix_or_glob_option("python_classes", name) return self._matches_prefix_or_glob_option("python_classes", name)
def istestfunction(self, obj, name): def istestfunction(self, obj: object, name: str) -> bool:
if self.funcnamefilter(name) or self.isnosetest(obj): if self.funcnamefilter(name) or self.isnosetest(obj):
if isinstance(obj, staticmethod): if isinstance(obj, staticmethod):
# static methods need to be unwrapped # static methods need to be unwrapped
@ -365,10 +367,10 @@ class PyCollector(PyobjMixin, nodes.Collector):
else: else:
return False return False
def istestclass(self, obj, name): def istestclass(self, obj: object, name: str) -> bool:
return self.classnamefilter(name) or self.isnosetest(obj) return self.classnamefilter(name) or self.isnosetest(obj)
def _matches_prefix_or_glob_option(self, option_name, name): def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool:
""" """
checks if the given name matches the prefix or glob-pattern defined checks if the given name matches the prefix or glob-pattern defined
in ini configuration. in ini configuration.
@ -428,7 +430,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
) # type: Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]] ) # type: Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]
return item return item
def _genfunctions(self, name, funcobj): def _genfunctions(self, name: str, funcobj) -> 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
@ -486,7 +488,7 @@ class Module(nodes.File, PyCollector):
self.session._fixturemanager.parsefactories(self) self.session._fixturemanager.parsefactories(self)
return super().collect() return super().collect()
def _inject_setup_module_fixture(self): def _inject_setup_module_fixture(self) -> None:
"""Injects a hidden autouse, module scoped fixture into the collected module object """Injects a hidden autouse, module scoped fixture into the collected module object
that invokes setUpModule/tearDownModule if either or both are available. that invokes setUpModule/tearDownModule if either or both are available.
@ -504,7 +506,7 @@ class Module(nodes.File, PyCollector):
return return
@fixtures.fixture(autouse=True, scope="module") @fixtures.fixture(autouse=True, scope="module")
def xunit_setup_module_fixture(request): def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
if setup_module is not None: if setup_module is not None:
_call_with_optional_argument(setup_module, request.module) _call_with_optional_argument(setup_module, request.module)
yield yield
@ -513,7 +515,7 @@ class Module(nodes.File, PyCollector):
self.obj.__pytest_setup_module = xunit_setup_module_fixture self.obj.__pytest_setup_module = xunit_setup_module_fixture
def _inject_setup_function_fixture(self): def _inject_setup_function_fixture(self) -> None:
"""Injects a hidden autouse, function scoped fixture into the collected module object """Injects a hidden autouse, function scoped fixture into the collected module object
that invokes setup_function/teardown_function if either or both are available. that invokes setup_function/teardown_function if either or both are available.
@ -528,7 +530,7 @@ class Module(nodes.File, PyCollector):
return return
@fixtures.fixture(autouse=True, scope="function") @fixtures.fixture(autouse=True, scope="function")
def xunit_setup_function_fixture(request): def xunit_setup_function_fixture(request) -> 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
@ -608,7 +610,7 @@ class Package(Module):
) )
self.name = os.path.basename(str(fspath.dirname)) self.name = os.path.basename(str(fspath.dirname))
def setup(self): def setup(self) -> None:
# not using fixtures to call setup_module here because autouse fixtures # not using fixtures to call setup_module here because autouse fixtures
# from packages are not called automatically (#4085) # from packages are not called automatically (#4085)
setup_module = _get_first_non_fixture_func( setup_module = _get_first_non_fixture_func(
@ -661,7 +663,7 @@ class Package(Module):
pkg_prefixes.add(path) pkg_prefixes.add(path)
def _call_with_optional_argument(func, arg): def _call_with_optional_argument(func, arg) -> 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
@ -673,7 +675,7 @@ def _call_with_optional_argument(func, arg):
func() func()
def _get_first_non_fixture_func(obj, names): def _get_first_non_fixture_func(obj: object, names: Iterable[str]):
"""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 xunit-style function, but only if not marked as a fixture to
avoid calling it twice. avoid calling it twice.
@ -723,7 +725,7 @@ class Class(PyCollector):
return [Instance.from_parent(self, name="()")] return [Instance.from_parent(self, name="()")]
def _inject_setup_class_fixture(self): def _inject_setup_class_fixture(self) -> None:
"""Injects a hidden autouse, class scoped fixture into the collected class object """Injects a hidden autouse, class scoped fixture into the collected class object
that invokes setup_class/teardown_class if either or both are available. that invokes setup_class/teardown_class if either or both are available.
@ -736,7 +738,7 @@ class Class(PyCollector):
return return
@fixtures.fixture(autouse=True, scope="class") @fixtures.fixture(autouse=True, scope="class")
def xunit_setup_class_fixture(cls): def xunit_setup_class_fixture(cls) -> Generator[None, None, None]:
if setup_class is not None: if setup_class is not None:
func = getimfunc(setup_class) func = getimfunc(setup_class)
_call_with_optional_argument(func, self.obj) _call_with_optional_argument(func, self.obj)
@ -747,7 +749,7 @@ class Class(PyCollector):
self.obj.__pytest_setup_class = xunit_setup_class_fixture self.obj.__pytest_setup_class = xunit_setup_class_fixture
def _inject_setup_method_fixture(self): def _inject_setup_method_fixture(self) -> None:
"""Injects a hidden autouse, function scoped fixture into the collected class object """Injects a hidden autouse, function scoped fixture into the collected class object
that invokes setup_method/teardown_method if either or both are available. that invokes setup_method/teardown_method if either or both are available.
@ -760,7 +762,7 @@ class Class(PyCollector):
return return
@fixtures.fixture(autouse=True, scope="function") @fixtures.fixture(autouse=True, scope="function")
def xunit_setup_method_fixture(self, request): def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
method = request.function method = request.function
if setup_method is not None: if setup_method is not None:
func = getattr(self, "setup_method") func = getattr(self, "setup_method")
@ -794,16 +796,18 @@ class Instance(PyCollector):
return self.obj return self.obj
def hasinit(obj): def hasinit(obj: object) -> bool:
init = getattr(obj, "__init__", None) init = getattr(obj, "__init__", None) # type: object
if init: if init:
return init != object.__init__ return init != object.__init__
return False
def hasnew(obj): def hasnew(obj: object) -> bool:
new = getattr(obj, "__new__", None) new = getattr(obj, "__new__", None) # type: object
if new: if new:
return new != object.__new__ return new != object.__new__
return False
class CallSpec2: class CallSpec2:
@ -843,7 +847,7 @@ class CallSpec2:
def setmulti2( def setmulti2(
self, self,
valtypes: "Mapping[str, Literal['params', 'funcargs']]", valtypes: Mapping[str, "Literal['params', 'funcargs']"],
argnames: typing.Sequence[str], argnames: typing.Sequence[str],
valset: Iterable[object], valset: Iterable[object],
id: str, id: str,
@ -903,7 +907,7 @@ class Metafunc:
self._arg2fixturedefs = fixtureinfo.name2fixturedefs self._arg2fixturedefs = fixtureinfo.name2fixturedefs
@property @property
def funcargnames(self): def funcargnames(self) -> List[str]:
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility""" """ alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
warnings.warn(FUNCARGNAMES, stacklevel=2) warnings.warn(FUNCARGNAMES, stacklevel=2)
return self.fixturenames return self.fixturenames
@ -1170,7 +1174,11 @@ class Metafunc:
) )
def _find_parametrized_scope(argnames, arg2fixturedefs, indirect): def _find_parametrized_scope(
argnames: typing.Sequence[str],
arg2fixturedefs: Mapping[str, typing.Sequence[fixtures.FixtureDef]],
indirect: Union[bool, typing.Sequence[str]],
) -> "fixtures._Scope":
"""Find the most appropriate scope for a parametrized call based on its arguments. """Find the most appropriate scope for a parametrized call based on its arguments.
When there's at least one direct argument, always use "function" scope. When there's at least one direct argument, always use "function" scope.
@ -1180,9 +1188,7 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
Related to issue #1832, based on code posted by @Kingdread. Related to issue #1832, based on code posted by @Kingdread.
""" """
from _pytest.fixtures import scopes if isinstance(indirect, Sequence):
if isinstance(indirect, (list, tuple)):
all_arguments_are_fixtures = len(indirect) == len(argnames) all_arguments_are_fixtures = len(indirect) == len(argnames)
else: else:
all_arguments_are_fixtures = bool(indirect) all_arguments_are_fixtures = bool(indirect)
@ -1196,7 +1202,7 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
] ]
if used_scopes: if used_scopes:
# Takes the most narrow scope from used fixtures # Takes the most narrow scope from used fixtures
for scope in reversed(scopes): for scope in reversed(fixtures.scopes):
if scope in used_scopes: if scope in used_scopes:
return scope return scope
@ -1264,7 +1270,7 @@ def _idvalset(
ids: Optional[List[Union[None, str]]], ids: Optional[List[Union[None, str]]],
nodeid: Optional[str], nodeid: Optional[str],
config: Optional[Config], config: Optional[Config],
): ) -> str:
if parameterset.id is not None: if parameterset.id is not None:
return parameterset.id return parameterset.id
id = None if ids is None or idx >= len(ids) else ids[idx] id = None if ids is None or idx >= len(ids) else ids[idx]
@ -1318,7 +1324,7 @@ def show_fixtures_per_test(config):
return wrap_session(config, _show_fixtures_per_test) return wrap_session(config, _show_fixtures_per_test)
def _show_fixtures_per_test(config, session): def _show_fixtures_per_test(config: Config, session: Session) -> None:
import _pytest.config import _pytest.config
session.perform_collect() session.perform_collect()
@ -1330,7 +1336,7 @@ def _show_fixtures_per_test(config, session):
loc = getlocation(func, curdir) loc = getlocation(func, curdir)
return curdir.bestrelpath(py.path.local(loc)) return curdir.bestrelpath(py.path.local(loc))
def write_fixture(fixture_def): def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
argname = fixture_def.argname argname = fixture_def.argname
if verbose <= 0 and argname.startswith("_"): if verbose <= 0 and argname.startswith("_"):
return return
@ -1346,18 +1352,16 @@ def _show_fixtures_per_test(config, session):
else: else:
tw.line(" no docstring available", red=True) tw.line(" no docstring available", red=True)
def write_item(item): def write_item(item: nodes.Item) -> None:
try: # Not all items have _fixtureinfo attribute.
info = item._fixtureinfo info = getattr(item, "_fixtureinfo", None) # type: Optional[FuncFixtureInfo]
except AttributeError: if info is None or not info.name2fixturedefs:
# doctests items have no _fixtureinfo attribute # This test item does not use any fixtures.
return
if not info.name2fixturedefs:
# this test item does not use any fixtures
return return
tw.line() tw.line()
tw.sep("-", "fixtures used by {}".format(item.name)) tw.sep("-", "fixtures used by {}".format(item.name))
tw.sep("-", "({})".format(get_best_relpath(item.function))) # TODO: Fix this type ignore.
tw.sep("-", "({})".format(get_best_relpath(item.function))) # type: ignore[attr-defined]
# dict key not used in loop but needed for sorting # dict key not used in loop but needed for sorting
for _, fixturedefs in sorted(info.name2fixturedefs.items()): for _, fixturedefs in sorted(info.name2fixturedefs.items()):
assert fixturedefs is not None assert fixturedefs is not None
@ -1448,15 +1452,15 @@ class Function(PyobjMixin, nodes.Item):
def __init__( def __init__(
self, self,
name, name: str,
parent, parent,
config=None, config: Optional[Config] = None,
callspec: Optional[CallSpec2] = None, callspec: Optional[CallSpec2] = None,
callobj=NOTSET, callobj=NOTSET,
keywords=None, keywords=None,
session=None, session: Optional[Session] = None,
fixtureinfo: Optional[FuncFixtureInfo] = None, fixtureinfo: Optional[FuncFixtureInfo] = None,
originalname=None, originalname: Optional[str] = None,
) -> None: ) -> None:
""" """
param name: the full function name, including any decorations like those param name: the full function name, including any decorations like those
@ -1533,8 +1537,8 @@ class Function(PyobjMixin, nodes.Item):
""" """
return super().from_parent(parent=parent, **kw) return super().from_parent(parent=parent, **kw)
def _initrequest(self): def _initrequest(self) -> None:
self.funcargs = {} self.funcargs = {} # type: Dict[str, object]
self._request = fixtures.FixtureRequest(self) self._request = fixtures.FixtureRequest(self)
@property @property
@ -1552,7 +1556,7 @@ class Function(PyobjMixin, nodes.Item):
return self return self
@property @property
def funcargnames(self): def funcargnames(self) -> List[str]:
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility""" """ alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
warnings.warn(FUNCARGNAMES, stacklevel=2) warnings.warn(FUNCARGNAMES, stacklevel=2)
return self.fixturenames return self.fixturenames

View File

@ -3,9 +3,12 @@ import re
import sys import sys
import textwrap import textwrap
from typing import Any from typing import Any
from typing import cast
from typing import Dict
from typing import Iterator from typing import Iterator
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Sequence
from typing import Tuple from typing import Tuple
from typing import Union from typing import Union
@ -138,12 +141,15 @@ class TestMetafunc:
class DummyFixtureDef: class DummyFixtureDef:
scope = attr.ib() scope = attr.ib()
fixtures_defs = dict( fixtures_defs = cast(
session_fix=[DummyFixtureDef("session")], Dict[str, Sequence[fixtures.FixtureDef]],
package_fix=[DummyFixtureDef("package")], dict(
module_fix=[DummyFixtureDef("module")], session_fix=[DummyFixtureDef("session")],
class_fix=[DummyFixtureDef("class")], package_fix=[DummyFixtureDef("package")],
func_fix=[DummyFixtureDef("function")], module_fix=[DummyFixtureDef("module")],
class_fix=[DummyFixtureDef("class")],
func_fix=[DummyFixtureDef("function")],
),
) )
# use arguments to determine narrow scope; the cause of the bug is that it would look on all # use arguments to determine narrow scope; the cause of the bug is that it would look on all