Type annotate ParameterSet

This commit is contained in:
Ran Benita 2020-05-01 14:40:15 +03:00
parent 43fa1ee8f9
commit ff8b7884e8
4 changed files with 83 additions and 18 deletions

View File

@ -1,6 +1,7 @@
""" """
python version compatibility code python version compatibility code
""" """
import enum
import functools import functools
import inspect import inspect
import os import os
@ -33,13 +34,20 @@ else:
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Type from typing import Type
from typing_extensions import Final
_T = TypeVar("_T") _T = TypeVar("_T")
_S = TypeVar("_S") _S = TypeVar("_S")
NOTSET = object() # fmt: off
# Singleton type for NOTSET, as described in:
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class NotSetType(enum.Enum):
token = 0
NOTSET = NotSetType.token # type: Final # noqa: E305
# fmt: on
MODULE_NOT_FOUND_ERROR = ( MODULE_NOT_FOUND_ERROR = (
"ModuleNotFoundError" if sys.version_info[:2] >= (3, 6) else "ImportError" "ModuleNotFoundError" if sys.version_info[:2] >= (3, 6) else "ImportError"

View File

@ -1,7 +1,9 @@
""" generic mechanism for marking and selecting python functions. """ """ generic mechanism for marking and selecting python functions. """
import typing
import warnings import warnings
from typing import AbstractSet from typing import AbstractSet
from typing import Optional from typing import Optional
from typing import Union
import attr import attr
@ -31,7 +33,11 @@ __all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mar
old_mark_config_key = StoreKey[Optional[Config]]() old_mark_config_key = StoreKey[Optional[Config]]()
def param(*values, **kw): def param(
*values: object,
marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (),
id: Optional[str] = None
) -> ParameterSet:
"""Specify a parameter in `pytest.mark.parametrize`_ calls or """Specify a parameter in `pytest.mark.parametrize`_ calls or
:ref:`parametrized fixtures <fixture-parametrize-marks>`. :ref:`parametrized fixtures <fixture-parametrize-marks>`.
@ -48,7 +54,7 @@ def param(*values, **kw):
:keyword marks: a single mark or a list of marks to be applied to this parameter set. :keyword marks: a single mark or a list of marks to be applied to this parameter set.
:keyword str id: the id to attribute to this parameter set. :keyword str id: the id to attribute to this parameter set.
""" """
return ParameterSet.param(*values, **kw) return ParameterSet.param(*values, marks=marks, id=id)
def pytest_addoption(parser): def pytest_addoption(parser):

View File

@ -1,11 +1,12 @@
import collections.abc
import inspect import inspect
import typing
import warnings import warnings
from collections import namedtuple
from collections.abc import MutableMapping
from typing import Any from typing import Any
from typing import Iterable from typing import Iterable
from typing import List from typing import List
from typing import Mapping from typing import Mapping
from typing import NamedTuple
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
@ -17,20 +18,29 @@ import attr
from .._code import getfslineno from .._code import getfslineno
from ..compat import ascii_escaped from ..compat import ascii_escaped
from ..compat import NOTSET from ..compat import NOTSET
from ..compat import NotSetType
from ..compat import TYPE_CHECKING
from _pytest.config import Config
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestUnknownMarkWarning
if TYPE_CHECKING:
from _pytest.python import FunctionDefinition
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
def istestfunc(func): def istestfunc(func) -> bool:
return ( return (
hasattr(func, "__call__") hasattr(func, "__call__")
and getattr(func, "__name__", "<lambda>") != "<lambda>" and getattr(func, "__name__", "<lambda>") != "<lambda>"
) )
def get_empty_parameterset_mark(config, argnames, func): def get_empty_parameterset_mark(
config: Config, argnames: Sequence[str], func
) -> "MarkDecorator":
from ..nodes import Collector from ..nodes import Collector
requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION) requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
@ -53,16 +63,33 @@ def get_empty_parameterset_mark(config, argnames, func):
fs, fs,
lineno, lineno,
) )
return mark(reason=reason) # Type ignored because MarkDecorator.__call__() is a bit tough to
# annotate ATM.
return mark(reason=reason) # type: ignore[no-any-return] # noqa: F723
class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): class ParameterSet(
NamedTuple(
"ParameterSet",
[
("values", Sequence[Union[object, NotSetType]]),
("marks", "typing.Collection[Union[MarkDecorator, Mark]]"),
("id", Optional[str]),
],
)
):
@classmethod @classmethod
def param(cls, *values, marks=(), id=None): def param(
cls,
*values: object,
marks: "Union[MarkDecorator, typing.Collection[Union[MarkDecorator, Mark]]]" = (),
id: Optional[str] = None
) -> "ParameterSet":
if isinstance(marks, MarkDecorator): if isinstance(marks, MarkDecorator):
marks = (marks,) marks = (marks,)
else: else:
assert isinstance(marks, (tuple, list, set)) # TODO(py36): Change to collections.abc.Collection.
assert isinstance(marks, (collections.abc.Sequence, set))
if id is not None: if id is not None:
if not isinstance(id, str): if not isinstance(id, str):
@ -73,7 +100,11 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
return cls(values, marks, id) return cls(values, marks, id)
@classmethod @classmethod
def extract_from(cls, parameterset, force_tuple=False): def extract_from(
cls,
parameterset: Union["ParameterSet", Sequence[object], object],
force_tuple: bool = False,
) -> "ParameterSet":
""" """
:param parameterset: :param parameterset:
a legacy style parameterset that may or may not be a tuple, a legacy style parameterset that may or may not be a tuple,
@ -89,10 +120,20 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
if force_tuple: if force_tuple:
return cls.param(parameterset) return cls.param(parameterset)
else: else:
return cls(parameterset, marks=[], id=None) # TODO: Refactor to fix this type-ignore. Currently the following
# type-checks but crashes:
#
# @pytest.mark.parametrize(('x', 'y'), [1, 2])
# def test_foo(x, y): pass
return cls(parameterset, marks=[], id=None) # type: ignore[arg-type] # noqa: F821
@staticmethod @staticmethod
def _parse_parametrize_args(argnames, argvalues, *args, **kwargs): def _parse_parametrize_args(
argnames: Union[str, List[str], Tuple[str, ...]],
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
*args,
**kwargs
) -> Tuple[Union[List[str], Tuple[str, ...]], bool]:
if not isinstance(argnames, (tuple, list)): if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()] argnames = [x.strip() for x in argnames.split(",") if x.strip()]
force_tuple = len(argnames) == 1 force_tuple = len(argnames) == 1
@ -101,13 +142,23 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
return argnames, force_tuple return argnames, force_tuple
@staticmethod @staticmethod
def _parse_parametrize_parameters(argvalues, force_tuple): def _parse_parametrize_parameters(
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
force_tuple: bool,
) -> List["ParameterSet"]:
return [ return [
ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues
] ]
@classmethod @classmethod
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition): def _for_parametrize(
cls,
argnames: Union[str, List[str], Tuple[str, ...]],
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
func,
config: Config,
function_definition: "FunctionDefinition",
) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]:
argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
del argvalues del argvalues
@ -370,7 +421,7 @@ class MarkGenerator:
MARK_GEN = MarkGenerator() MARK_GEN = MarkGenerator()
class NodeKeywords(MutableMapping): class NodeKeywords(collections.abc.MutableMapping):
def __init__(self, node): def __init__(self, node):
self.node = node self.node = node
self.parent = node.parent self.parent = node.parent

View File

@ -1051,7 +1051,7 @@ class TestLiterals:
("1e3", "999"), ("1e3", "999"),
# The current implementation doesn't understand that numbers inside # The current implementation doesn't understand that numbers inside
# strings shouldn't be treated as numbers: # strings shouldn't be treated as numbers:
pytest.param("'3.1416'", "'3.14'", marks=pytest.mark.xfail), pytest.param("'3.1416'", "'3.14'", marks=pytest.mark.xfail), # type: ignore
], ],
) )
def test_number_non_matches(self, testdir, expression, output): def test_number_non_matches(self, testdir, expression, output):