From 6d3a66d947a57fed99dcb4bae47062cd9ce6a5f2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Dec 2020 19:54:07 +0200 Subject: [PATCH 1/3] nodes: avoid needing to expose NodeKeywords for typing It adds no value over exporting just the ABC so do that to reduce the API surface. --- src/_pytest/nodes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index da2a0a7ea..c6eb49dec 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,10 +1,12 @@ import os import warnings from pathlib import Path +from typing import Any from typing import Callable from typing import Iterable from typing import Iterator from typing import List +from typing import MutableMapping from typing import Optional from typing import overload from typing import Set @@ -148,8 +150,9 @@ class Node(metaclass=NodeMeta): #: Filesystem path where this node was collected from (can be None). self.fspath = fspath or getattr(parent, "fspath", None) + # The explicit annotation is to avoid publicly exposing NodeKeywords. #: Keywords/markers collected from all scopes. - self.keywords = NodeKeywords(self) + self.keywords: MutableMapping[str, Any] = NodeKeywords(self) #: The marker objects belonging to this node. self.own_markers: List[Mark] = [] From bd76042344b3c3318dddf991c08d49bbce2251bb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Dec 2020 20:49:17 +0200 Subject: [PATCH 2/3] python: export pytest.Metafunc for typing purposes The type cannot be constructed directly, but is exported for use in type annotations, since it is reachable through existing public API. --- changelog/7469.deprecation.rst | 1 + changelog/7469.feature.rst | 1 + doc/en/deprecations.rst | 4 ++-- doc/en/funcarg_compare.rst | 4 ++-- doc/en/reference.rst | 4 ++-- src/_pytest/python.py | 12 +++++++++++- src/pytest/__init__.py | 2 ++ testing/python/metafunc.py | 2 +- 8 files changed, 22 insertions(+), 8 deletions(-) diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst index 6bbc80755..bcf4266d8 100644 --- a/changelog/7469.deprecation.rst +++ b/changelog/7469.deprecation.rst @@ -3,5 +3,6 @@ Directly constructing the following classes is now deprecated: - ``_pytest.mark.structures.Mark`` - ``_pytest.mark.structures.MarkDecorator`` - ``_pytest.mark.structures.MarkGenerator`` +- ``_pytest.python.Metafunc`` These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0. diff --git a/changelog/7469.feature.rst b/changelog/7469.feature.rst index 81f93d1f7..0ab2b48c4 100644 --- a/changelog/7469.feature.rst +++ b/changelog/7469.feature.rst @@ -5,6 +5,7 @@ The newly-exported types are: - ``pytest.Mark`` for :class:`marks `. - ``pytest.MarkDecorator`` for :class:`mark decorators `. - ``pytest.MarkGenerator`` for the :class:`pytest.mark ` singleton. +- ``pytest.Metafunc`` for the :class:`metafunc ` argument to the `pytest_generate_tests ` hook. Constructing them directly is not supported; they are only meant for use in type annotations. Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 5ef1053e0..ec2397e59 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -397,8 +397,8 @@ Metafunc.addcall .. versionremoved:: 4.0 -``_pytest.python.Metafunc.addcall`` was a precursor to the current parametrized mechanism. Users should use -:meth:`_pytest.python.Metafunc.parametrize` instead. +``Metafunc.addcall`` was a precursor to the current parametrized mechanism. Users should use +:meth:`pytest.Metafunc.parametrize` instead. Example: diff --git a/doc/en/funcarg_compare.rst b/doc/en/funcarg_compare.rst index 0c4913edf..5e2a05006 100644 --- a/doc/en/funcarg_compare.rst +++ b/doc/en/funcarg_compare.rst @@ -47,7 +47,7 @@ There are several limitations and difficulties with this approach: 2. parametrizing the "db" resource is not straight forward: you need to apply a "parametrize" decorator or implement a :py:func:`~hookspec.pytest_generate_tests` hook - calling :py:func:`~python.Metafunc.parametrize` which + calling :py:func:`~pytest.Metafunc.parametrize` which performs parametrization at the places where the resource is used. Moreover, you need to modify the factory to use an ``extrakey`` parameter containing ``request.param`` to the @@ -113,7 +113,7 @@ This new way of parametrizing funcarg factories should in many cases allow to re-use already written factories because effectively ``request.param`` was already used when test functions/classes were parametrized via -:py:func:`metafunc.parametrize(indirect=True) <_pytest.python.Metafunc.parametrize>` calls. +:py:func:`metafunc.parametrize(indirect=True) ` calls. Of course it's perfectly fine to combine parametrization and scoping: diff --git a/doc/en/reference.rst b/doc/en/reference.rst index c8e8dca75..7f2ae0105 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -138,7 +138,7 @@ pytest.mark.parametrize **Tutorial**: :doc:`parametrize`. -This mark has the same signature as :py:meth:`_pytest.python.Metafunc.parametrize`; see there. +This mark has the same signature as :py:meth:`pytest.Metafunc.parametrize`; see there. .. _`pytest.mark.skip ref`: @@ -870,7 +870,7 @@ Mark Metafunc ~~~~~~~~ -.. autoclass:: _pytest.python.Metafunc +.. autoclass:: pytest.Metafunc() :members: Module diff --git a/src/_pytest/python.py b/src/_pytest/python.py index b46050920..31d91853f 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -55,6 +55,7 @@ from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH from _pytest.fixtures import FuncFixtureInfo from _pytest.main import Session @@ -467,7 +468,12 @@ class PyCollector(PyobjMixin, nodes.Collector): fixtureinfo = definition._fixtureinfo metafunc = Metafunc( - definition, fixtureinfo, self.config, cls=cls, module=module + definition=definition, + fixtureinfo=fixtureinfo, + config=self.config, + cls=cls, + module=module, + _ispytest=True, ) methods = [] if hasattr(module, "pytest_generate_tests"): @@ -971,7 +977,11 @@ class Metafunc: config: Config, cls=None, module=None, + *, + _ispytest: bool = False, ) -> None: + check_ispytest(_ispytest) + #: Access to the underlying :class:`_pytest.python.FunctionDefinition`. self.definition = definition diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 74cf00ee2..f97b0ac2e 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -40,6 +40,7 @@ from _pytest.pytester import Testdir from _pytest.python import Class from _pytest.python import Function from _pytest.python import Instance +from _pytest.python import Metafunc from _pytest.python import Module from _pytest.python import Package from _pytest.python_api import approx @@ -95,6 +96,7 @@ __all__ = [ "Mark", "MarkDecorator", "MarkGenerator", + "Metafunc", "Module", "MonkeyPatch", "Package", diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index c50ea53d2..58a902a3a 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -47,7 +47,7 @@ class TestMetafunc: names = getfuncargnames(func) fixtureinfo: Any = FuncFixtureInfoMock(names) definition: Any = DefinitionMock._create(func, "mock::nodeid") - return python.Metafunc(definition, fixtureinfo, config) + return python.Metafunc(definition, fixtureinfo, config, _ispytest=True) def test_no_funcargs(self) -> None: def function(): From 96ea867fec556a8d0e2b60392927572da38c88df Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Dec 2020 21:23:23 +0200 Subject: [PATCH 3/3] runner: export pytest.CallInfo for typing purposes The type cannot be constructed directly, but is exported for use in type annotations, since it is reachable through existing public API. This also documents `from_call` as public, because at least pytest-forked uses it, so we must treat it as public already anyway. --- changelog/7469.deprecation.rst | 1 + changelog/7469.feature.rst | 1 + doc/en/reference.rst | 2 +- src/_pytest/runner.py | 69 +++++++++++++++++++++++----------- src/pytest/__init__.py | 2 + 5 files changed, 52 insertions(+), 23 deletions(-) diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst index bcf4266d8..0d7908ef8 100644 --- a/changelog/7469.deprecation.rst +++ b/changelog/7469.deprecation.rst @@ -4,5 +4,6 @@ Directly constructing the following classes is now deprecated: - ``_pytest.mark.structures.MarkDecorator`` - ``_pytest.mark.structures.MarkGenerator`` - ``_pytest.python.Metafunc`` +- ``_pytest.runner.CallInfo`` These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0. diff --git a/changelog/7469.feature.rst b/changelog/7469.feature.rst index 0ab2b48c4..f9948d686 100644 --- a/changelog/7469.feature.rst +++ b/changelog/7469.feature.rst @@ -6,6 +6,7 @@ The newly-exported types are: - ``pytest.MarkDecorator`` for :class:`mark decorators `. - ``pytest.MarkGenerator`` for the :class:`pytest.mark ` singleton. - ``pytest.Metafunc`` for the :class:`metafunc ` argument to the `pytest_generate_tests ` hook. +- ``pytest.runner.CallInfo`` for the :class:`CallInfo ` type passed to various hooks. Constructing them directly is not supported; they are only meant for use in type annotations. Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 7f2ae0105..bc6c5670a 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -758,7 +758,7 @@ Full reference to objects accessible from :ref:`fixtures ` or :ref:`hoo CallInfo ~~~~~~~~ -.. autoclass:: _pytest.runner.CallInfo() +.. autoclass:: pytest.CallInfo() :members: diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 794690ddb..df046a78a 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -26,6 +26,7 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest.compat import final from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.nodes import Node @@ -260,34 +261,47 @@ TResult = TypeVar("TResult", covariant=True) @final -@attr.s(repr=False) +@attr.s(repr=False, init=False, auto_attribs=True) class CallInfo(Generic[TResult]): - """Result/Exception info a function invocation. + """Result/Exception info of a function invocation.""" - :param T result: - The return value of the call, if it didn't raise. Can only be - accessed if excinfo is None. - :param Optional[ExceptionInfo] excinfo: - The captured exception of the call, if it raised. - :param float start: - The system time when the call started, in seconds since the epoch. - :param float stop: - The system time when the call ended, in seconds since the epoch. - :param float duration: - The call duration, in seconds. - :param str when: - The context of invocation: "setup", "call", "teardown", ... - """ + _result: Optional[TResult] + #: The captured exception of the call, if it raised. + excinfo: Optional[ExceptionInfo[BaseException]] + #: The system time when the call started, in seconds since the epoch. + start: float + #: The system time when the call ended, in seconds since the epoch. + stop: float + #: The call duration, in seconds. + duration: float + #: The context of invocation: "collect", "setup", "call" or "teardown". + when: "Literal['collect', 'setup', 'call', 'teardown']" - _result = attr.ib(type="Optional[TResult]") - excinfo = attr.ib(type=Optional[ExceptionInfo[BaseException]]) - start = attr.ib(type=float) - stop = attr.ib(type=float) - duration = attr.ib(type=float) - when = attr.ib(type="Literal['collect', 'setup', 'call', 'teardown']") + def __init__( + self, + result: Optional[TResult], + excinfo: Optional[ExceptionInfo[BaseException]], + start: float, + stop: float, + duration: float, + when: "Literal['collect', 'setup', 'call', 'teardown']", + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._result = result + self.excinfo = excinfo + self.start = start + self.stop = stop + self.duration = duration + self.when = when @property def result(self) -> TResult: + """The return value of the call, if it didn't raise. + + Can only be accessed if excinfo is None. + """ if self.excinfo is not None: raise AttributeError(f"{self!r} has no valid result") # The cast is safe because an exception wasn't raised, hence @@ -304,6 +318,16 @@ class CallInfo(Generic[TResult]): Union[Type[BaseException], Tuple[Type[BaseException], ...]] ] = None, ) -> "CallInfo[TResult]": + """Call func, wrapping the result in a CallInfo. + + :param func: + The function to call. Called without arguments. + :param when: + The phase in which the function is called. + :param reraise: + Exception or exceptions that shall propagate if raised by the + function, instead of being wrapped in the CallInfo. + """ excinfo = None start = timing.time() precise_start = timing.perf_counter() @@ -325,6 +349,7 @@ class CallInfo(Generic[TResult]): when=when, result=result, excinfo=excinfo, + _ispytest=True, ) def __repr__(self) -> str: diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index f97b0ac2e..53917340f 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -48,6 +48,7 @@ from _pytest.python_api import raises from _pytest.recwarn import deprecated_call from _pytest.recwarn import WarningsRecorder from _pytest.recwarn import warns +from _pytest.runner import CallInfo from _pytest.tmpdir import TempdirFactory from _pytest.tmpdir import TempPathFactory from _pytest.warning_types import PytestAssertRewriteWarning @@ -69,6 +70,7 @@ __all__ = [ "_fillfuncargs", "approx", "Cache", + "CallInfo", "CaptureFixture", "Class", "cmdline",