Export types of builtin fixture for type annotations

In order to allow users to type annotate fixtures they request, the
types need to be imported from the `pytest` namespace. They are/were
always available to import from the `_pytest` namespace, but that is
not guaranteed to be stable.

These types are only exported for the purpose of typing. Specifically,
the following are *not* public:

- Construction (`__init__`)
- Subclassing
- staticmethods and classmethods

We try to combat them being used anyway by:

- Marking the classes as `@final` when possible (already done).

- Not documenting private stuff in the API Reference.

- Using `_`-prefixed names or marking as `:meta private:` for private
  stuff.

- Adding a keyword-only `_ispytest=False` to private constructors,
  warning if False, and changing pytest itself to pass True. In the
  future it will (hopefully) become a hard error.

Hopefully that will be enough.
This commit is contained in:
Ran Benita 2020-09-27 22:20:31 +03:00
parent b050578882
commit f1e6fdcddb
19 changed files with 292 additions and 126 deletions

View File

@ -0,0 +1,18 @@
Directly constructing/calling the following classes/functions is now deprecated:
- ``_pytest.cacheprovider.Cache``
- ``_pytest.cacheprovider.Cache.for_config()``
- ``_pytest.cacheprovider.Cache.clear_cache()``
- ``_pytest.cacheprovider.Cache.cache_dir_from_config()``
- ``_pytest.capture.CaptureFixture``
- ``_pytest.fixtures.FixtureRequest``
- ``_pytest.fixtures.SubRequest``
- ``_pytest.logging.LogCaptureFixture``
- ``_pytest.pytester.Pytester``
- ``_pytest.pytester.Testdir``
- ``_pytest.recwarn.WarningsRecorder``
- ``_pytest.recwarn.WarningsChecker``
- ``_pytest.tmpdir.TempPathFactory``
- ``_pytest.tmpdir.TempdirFactory``
These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0.

View File

@ -0,0 +1,23 @@
It is now possible to construct a :class:`MonkeyPatch` object directly as ``pytest.MonkeyPatch()``,
in cases when the :fixture:`monkeypatch` fixture cannot be used. Previously some users imported it
from the private `_pytest.monkeypatch.MonkeyPatch` namespace.
The types of builtin pytest fixtures are now exported so they may be used in type annotations of test functions.
The newly-exported types are:
- ``pytest.FixtureRequest`` for the :fixture:`request` fixture.
- ``pytest.Cache`` for the :fixture:`cache` fixture.
- ``pytest.CaptureFixture[str]`` for the :fixture:`capfd` and :fixture:`capsys` fixtures.
- ``pytest.CaptureFixture[bytes]`` for the :fixture:`capfdbinary` and :fixture:`capsysbinary` fixtures.
- ``pytest.LogCaptureFixture`` for the :fixture:`caplog` fixture.
- ``pytest.Pytester`` for the :fixture:`pytester` fixture.
- ``pytest.Testdir`` for the :fixture:`testdir` fixture.
- ``pytest.TempdirFactory`` for the :fixture:`tmpdir_factory` fixture.
- ``pytest.TempPathFactory`` for the :fixture:`tmp_path_factory` fixture.
- ``pytest.MonkeyPatch`` for the :fixture:`monkeypatch` fixture.
- ``pytest.WarningsRecorder`` for the :fixture:`recwarn` fixture.
Constructing them is not supported (except for `MonkeyPatch`); 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.
Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.

View File

@ -304,11 +304,10 @@ request ``pytestconfig`` into your fixture and get it with ``pytestconfig.cache`
Under the hood, the cache plugin uses the simple
``dumps``/``loads`` API of the :py:mod:`json` stdlib module.
.. currentmodule:: _pytest.cacheprovider
``config.cache`` is an instance of :class:`pytest.Cache`:
.. automethod:: Cache.get
.. automethod:: Cache.set
.. automethod:: Cache.makedir
.. autoclass:: pytest.Cache()
:members:
.. fixture:: capsys
@ -318,12 +317,10 @@ capsys
**Tutorial**: :doc:`capture`.
.. currentmodule:: _pytest.capture
.. autofunction:: capsys()
.. autofunction:: _pytest.capture.capsys()
:no-auto-options:
Returns an instance of :py:class:`CaptureFixture`.
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
@ -334,7 +331,7 @@ capsys
captured = capsys.readouterr()
assert captured.out == "hello\n"
.. autoclass:: CaptureFixture()
.. autoclass:: pytest.CaptureFixture()
:members:
@ -345,10 +342,10 @@ capsysbinary
**Tutorial**: :doc:`capture`.
.. autofunction:: capsysbinary()
.. autofunction:: _pytest.capture.capsysbinary()
:no-auto-options:
Returns an instance of :py:class:`CaptureFixture`.
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
@ -367,10 +364,10 @@ capfd
**Tutorial**: :doc:`capture`.
.. autofunction:: capfd()
.. autofunction:: _pytest.capture.capfd()
:no-auto-options:
Returns an instance of :py:class:`CaptureFixture`.
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
@ -389,10 +386,10 @@ capfdbinary
**Tutorial**: :doc:`capture`.
.. autofunction:: capfdbinary()
.. autofunction:: _pytest.capture.capfdbinary()
:no-auto-options:
Returns an instance of :py:class:`CaptureFixture`.
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
@ -433,7 +430,7 @@ request
The ``request`` fixture is a special fixture providing information of the requesting test function.
.. autoclass:: _pytest.fixtures.FixtureRequest()
.. autoclass:: pytest.FixtureRequest()
:members:
@ -475,9 +472,9 @@ caplog
.. autofunction:: _pytest.logging.caplog()
:no-auto-options:
Returns a :class:`_pytest.logging.LogCaptureFixture` instance.
Returns a :class:`pytest.LogCaptureFixture` instance.
.. autoclass:: _pytest.logging.LogCaptureFixture
.. autoclass:: pytest.LogCaptureFixture()
:members:
@ -504,9 +501,7 @@ pytester
.. versionadded:: 6.2
.. currentmodule:: _pytest.pytester
Provides a :class:`Pytester` instance that can be used to run and test pytest itself.
Provides a :class:`~pytest.Pytester` instance that can be used to run and test pytest itself.
It provides an empty directory where pytest can be executed in isolation, and contains facilities
to write tests, configuration files, and match against expected output.
@ -519,16 +514,16 @@ To use it, include in your topmost ``conftest.py`` file:
.. autoclass:: Pytester()
.. autoclass:: pytest.Pytester()
:members:
.. autoclass:: RunResult()
.. autoclass:: _pytest.pytester.RunResult()
:members:
.. autoclass:: LineMatcher()
.. autoclass:: _pytest.pytester.LineMatcher()
:members:
.. autoclass:: HookRecorder()
.. autoclass:: _pytest.pytester.HookRecorder()
:members:
.. fixture:: testdir
@ -541,7 +536,7 @@ legacy ``py.path.local`` objects instead when applicable.
New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
.. autoclass:: Testdir()
.. autoclass:: pytest.Testdir()
:members:
@ -552,12 +547,10 @@ recwarn
**Tutorial**: :ref:`assertwarnings`
.. currentmodule:: _pytest.recwarn
.. autofunction:: recwarn()
.. autofunction:: _pytest.recwarn.recwarn()
:no-auto-options:
.. autoclass:: WarningsRecorder()
.. autoclass:: pytest.WarningsRecorder()
:members:
Each recorded warning is an instance of :class:`warnings.WarningMessage`.
@ -574,13 +567,11 @@ tmp_path
**Tutorial**: :doc:`tmpdir`
.. currentmodule:: _pytest.tmpdir
.. autofunction:: tmp_path()
.. autofunction:: _pytest.tmpdir.tmp_path()
:no-auto-options:
.. fixture:: tmp_path_factory
.. fixture:: _pytest.tmpdir.tmp_path_factory
tmp_path_factory
~~~~~~~~~~~~~~~~
@ -589,12 +580,9 @@ tmp_path_factory
.. _`tmp_path_factory factory api`:
``tmp_path_factory`` instances have the following methods:
``tmp_path_factory`` is an instance of :class:`~pytest.TempPathFactory`:
.. currentmodule:: _pytest.tmpdir
.. automethod:: TempPathFactory.mktemp
.. automethod:: TempPathFactory.getbasetemp
.. autoclass:: pytest.TempPathFactory()
.. fixture:: tmpdir
@ -604,9 +592,7 @@ tmpdir
**Tutorial**: :doc:`tmpdir`
.. currentmodule:: _pytest.tmpdir
.. autofunction:: tmpdir()
.. autofunction:: _pytest.tmpdir.tmpdir()
:no-auto-options:
@ -619,12 +605,9 @@ tmpdir_factory
.. _`tmpdir factory api`:
``tmpdir_factory`` instances have the following methods:
``tmp_path_factory`` is an instance of :class:`~pytest.TempdirFactory`:
.. currentmodule:: _pytest.tmpdir
.. automethod:: TempdirFactory.mktemp
.. automethod:: TempdirFactory.getbasetemp
.. autoclass:: pytest.TempdirFactory()
.. _`hook-reference`:

View File

@ -25,6 +25,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.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.main import Session
@ -53,7 +54,7 @@ Signature: 8a477f597d28d172789f06886806bc55
@final
@attr.s
@attr.s(init=False)
class Cache:
_cachedir = attr.ib(type=Path, repr=False)
_config = attr.ib(type=Config, repr=False)
@ -64,26 +65,52 @@ class Cache:
# sub-directory under cache-dir for values created by "set"
_CACHE_PREFIX_VALUES = "v"
@classmethod
def for_config(cls, config: Config) -> "Cache":
cachedir = cls.cache_dir_from_config(config)
if config.getoption("cacheclear") and cachedir.is_dir():
cls.clear_cache(cachedir)
return cls(cachedir, config)
def __init__(
self, cachedir: Path, config: Config, *, _ispytest: bool = False
) -> None:
check_ispytest(_ispytest)
self._cachedir = cachedir
self._config = config
@classmethod
def clear_cache(cls, cachedir: Path) -> None:
"""Clear the sub-directories used to hold cached directories and values."""
def for_config(cls, config: Config, *, _ispytest: bool = False) -> "Cache":
"""Create the Cache instance for a Config.
:meta private:
"""
check_ispytest(_ispytest)
cachedir = cls.cache_dir_from_config(config, _ispytest=True)
if config.getoption("cacheclear") and cachedir.is_dir():
cls.clear_cache(cachedir, _ispytest=True)
return cls(cachedir, config, _ispytest=True)
@classmethod
def clear_cache(cls, cachedir: Path, _ispytest: bool = False) -> None:
"""Clear the sub-directories used to hold cached directories and values.
:meta private:
"""
check_ispytest(_ispytest)
for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
d = cachedir / prefix
if d.is_dir():
rm_rf(d)
@staticmethod
def cache_dir_from_config(config: Config) -> Path:
def cache_dir_from_config(config: Config, *, _ispytest: bool = False) -> Path:
"""Get the path to the cache directory for a Config.
:meta private:
"""
check_ispytest(_ispytest)
return resolve_from_str(config.getini("cache_dir"), config.rootpath)
def warn(self, fmt: str, **args: object) -> None:
def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None:
"""Issue a cache warning.
:meta private:
"""
check_ispytest(_ispytest)
import warnings
from _pytest.warning_types import PytestCacheWarning
@ -152,7 +179,7 @@ class Cache:
cache_dir_exists_already = self._cachedir.exists()
path.parent.mkdir(exist_ok=True, parents=True)
except OSError:
self.warn("could not create cache path {path}", path=path)
self.warn("could not create cache path {path}", path=path, _ispytest=True)
return
if not cache_dir_exists_already:
self._ensure_supporting_files()
@ -160,7 +187,7 @@ class Cache:
try:
f = path.open("w")
except OSError:
self.warn("cache could not write path {path}", path=path)
self.warn("cache could not write path {path}", path=path, _ispytest=True)
else:
with f:
f.write(data)
@ -469,7 +496,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
@hookimpl(tryfirst=True)
def pytest_configure(config: Config) -> None:
config.cache = Cache.for_config(config)
config.cache = Cache.for_config(config, _ispytest=True)
config.pluginmanager.register(LFPlugin(config), "lfplugin")
config.pluginmanager.register(NFPlugin(config), "nfplugin")

View File

@ -21,6 +21,7 @@ from _pytest.compat import final
from _pytest.config import Config
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import SubRequest
from _pytest.nodes import Collector
@ -826,10 +827,13 @@ class CaptureManager:
class CaptureFixture(Generic[AnyStr]):
"""Object returned by the :py:func:`capsys`, :py:func:`capsysbinary`,
:py:func:`capfd` and :py:func:`capfdbinary` fixtures."""
"""Object returned by the :fixture:`capsys`, :fixture:`capsysbinary`,
:fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
def __init__(self, captureclass, request: SubRequest) -> None:
def __init__(
self, captureclass, request: SubRequest, *, _ispytest: bool = False
) -> None:
check_ispytest(_ispytest)
self.captureclass = captureclass
self.request = request
self._capture: Optional[MultiCapture[AnyStr]] = None
@ -904,7 +908,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
``out`` and ``err`` will be ``text`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](SysCapture, request)
capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@ -921,7 +925,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
``out`` and ``err`` will be ``bytes`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request)
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@ -938,7 +942,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
``out`` and ``err`` will be ``text`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](FDCapture, request)
capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@ -955,7 +959,7 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
``out`` and ``err`` will be ``byte`` objects.
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request)
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture

View File

@ -8,6 +8,8 @@ All constants defined in this module should be either instances of
:class:`PytestWarning`, or :class:`UnformattedWarning`
in case of warnings which need to format their messages.
"""
from warnings import warn
from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import UnformattedWarning
@ -59,3 +61,27 @@ FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestDeprecationWarning(
STRICT_OPTION = PytestDeprecationWarning(
"The --strict option is deprecated, use --strict-markers instead."
)
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
# You want to make some `__init__` or function "private".
#
# def my_private_function(some, args):
# ...
#
# Do this:
#
# def my_private_function(some, args, *, _ispytest: bool = False):
# check_ispytest(_ispytest)
# ...
#
# Change all internal/allowed calls to
#
# my_private_function(some, args, _ispytest=True)
#
# All other calls will get the default _ispytest=False and trigger
# the warning (possibly error in the future).
def check_ispytest(ispytest: bool) -> None:
if not ispytest:
warn(PRIVATE, stacklevel=3)

View File

@ -563,7 +563,7 @@ def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
node=doctest_item, func=func, cls=None, funcargs=False
)
fixture_request = FixtureRequest(doctest_item)
fixture_request = FixtureRequest(doctest_item, _ispytest=True)
fixture_request._fillfixtures()
return fixture_request

View File

@ -49,6 +49,7 @@ from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import FILLFUNCARGS
from _pytest.deprecated import YIELD_FIXTURE
from _pytest.mark import Mark
@ -367,7 +368,7 @@ def _fill_fixtures_impl(function: "Function") -> None:
assert function.parent is not None
fi = fm.getfixtureinfo(function.parent, function.obj, None)
function._fixtureinfo = fi
request = function._request = FixtureRequest(function)
request = function._request = FixtureRequest(function, _ispytest=True)
request._fillfixtures()
# Prune out funcargs for jstests.
newfuncargs = {}
@ -429,7 +430,8 @@ class FixtureRequest:
indirectly.
"""
def __init__(self, pyfuncitem) -> None:
def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
self._pyfuncitem = pyfuncitem
#: Fixture for which this request is being performed.
self.fixturename: Optional[str] = None
@ -674,7 +676,9 @@ class FixtureRequest:
if paramscopenum is not None:
scope = scopes[paramscopenum]
subrequest = SubRequest(self, scope, param, param_index, fixturedef)
subrequest = SubRequest(
self, scope, param, param_index, fixturedef, _ispytest=True
)
# Check if a higher-level scoped fixture accesses a lower level one.
subrequest._check_scope(argname, self.scope, scope)
@ -751,7 +755,10 @@ class SubRequest(FixtureRequest):
param,
param_index: int,
fixturedef: "FixtureDef[object]",
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self._parent_request = request
self.fixturename = fixturedef.argname
if param is not NOTSET:
@ -769,6 +776,8 @@ class SubRequest(FixtureRequest):
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
"""Add finalizer/teardown function to be called after the last test
within the requesting test context finished execution."""
self._fixturedef.addfinalizer(finalizer)
def _schedule_finalizers(

View File

@ -27,6 +27,7 @@ from _pytest.config import create_terminal_writer
from _pytest.config import hookimpl
from _pytest.config import UsageError
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.main import Session
@ -346,7 +347,8 @@ class LogCaptureHandler(logging.StreamHandler):
class LogCaptureFixture:
"""Provides access and control of log capturing."""
def __init__(self, item: nodes.Node) -> None:
def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
self._item = item
self._initial_handler_level: Optional[int] = None
# Dict of log name -> log level.
@ -482,7 +484,7 @@ def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]:
* caplog.record_tuples -> list of (logger_name, level, message) tuples
* caplog.clear() -> clear captured records and formatted log output string
"""
result = LogCaptureFixture(request.node)
result = LogCaptureFixture(request.node, _ispytest=True)
yield result
result._finalize()

View File

@ -48,6 +48,7 @@ from _pytest.config import hookimpl
from _pytest.config import main
from _pytest.config import PytestPluginManager
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.main import Session
@ -454,7 +455,7 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt
It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path`
fixture but provides methods which aid in testing pytest itself.
"""
return Pytester(request, tmp_path_factory)
return Pytester(request, tmp_path_factory, _ispytest=True)
@fixture
@ -465,7 +466,7 @@ def testdir(pytester: "Pytester") -> "Testdir":
New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
"""
return Testdir(pytester)
return Testdir(pytester, _ispytest=True)
@fixture
@ -648,8 +649,13 @@ class Pytester:
pass
def __init__(
self, request: FixtureRequest, tmp_path_factory: TempPathFactory
self,
request: FixtureRequest,
tmp_path_factory: TempPathFactory,
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self._request = request
self._mod_collections: WeakKeyDictionary[
Collector, List[Union[Item, Collector]]
@ -1480,7 +1486,7 @@ class LineComp:
@final
@attr.s(repr=False, str=False)
@attr.s(repr=False, str=False, init=False)
class Testdir:
"""
Similar to :class:`Pytester`, but this class works with legacy py.path.local objects instead.
@ -1495,7 +1501,9 @@ class Testdir:
TimeoutExpired = Pytester.TimeoutExpired
Session = Pytester.Session
_pytester: Pytester = attr.ib()
def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
self._pytester = pytester
@property
def tmpdir(self) -> py.path.local:

View File

@ -1620,7 +1620,7 @@ class Function(PyobjMixin, nodes.Item):
def _initrequest(self) -> None:
self.funcargs: Dict[str, object] = {}
self._request = fixtures.FixtureRequest(self)
self._request = fixtures.FixtureRequest(self, _ispytest=True)
@property
def function(self):

View File

@ -16,6 +16,7 @@ from typing import TypeVar
from typing import Union
from _pytest.compat import final
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.outcomes import fail
@ -30,7 +31,7 @@ def recwarn() -> Generator["WarningsRecorder", None, None]:
See http://docs.python.org/library/warnings.html for information
on warning categories.
"""
wrec = WarningsRecorder()
wrec = WarningsRecorder(_ispytest=True)
with wrec:
warnings.simplefilter("default")
yield wrec
@ -142,14 +143,14 @@ def warns(
msg += ", ".join(sorted(kwargs))
msg += "\nUse context-manager form instead?"
raise TypeError(msg)
return WarningsChecker(expected_warning, match_expr=match)
return WarningsChecker(expected_warning, match_expr=match, _ispytest=True)
else:
func = args[0]
if not callable(func):
raise TypeError(
"{!r} object (type: {}) must be callable".format(func, type(func))
)
with WarningsChecker(expected_warning):
with WarningsChecker(expected_warning, _ispytest=True):
return func(*args[1:], **kwargs)
@ -159,7 +160,8 @@ class WarningsRecorder(warnings.catch_warnings):
Adapted from `warnings.catch_warnings`.
"""
def __init__(self) -> None:
def __init__(self, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
# Type ignored due to the way typeshed handles warnings.catch_warnings.
super().__init__(record=True) # type: ignore[call-arg]
self._entered = False
@ -232,8 +234,11 @@ class WarningsChecker(WarningsRecorder):
Union[Type[Warning], Tuple[Type[Warning], ...]]
] = None,
match_expr: Optional[Union[str, Pattern[str]]] = None,
*,
_ispytest: bool = False,
) -> None:
super().__init__()
check_ispytest(_ispytest)
super().__init__(_ispytest=True)
msg = "exceptions must be derived from Warning, not %s"
if expected_warning is None:

View File

@ -14,37 +14,56 @@ from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup
from _pytest.compat import final
from _pytest.config import Config
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch
@final
@attr.s
@attr.s(init=False)
class TempPathFactory:
"""Factory for temporary directories under the common base temp directory.
The base directory can be configured using the ``--basetemp`` option.
"""
_given_basetemp = attr.ib(
type=Optional[Path],
# Use os.path.abspath() to get absolute path instead of resolve() as it
# does not work the same in all platforms (see #4427).
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
# Ignore type because of https://github.com/python/mypy/issues/6172.
converter=attr.converters.optional(
lambda p: Path(os.path.abspath(str(p))) # type: ignore
),
)
_given_basetemp = attr.ib(type=Optional[Path])
_trace = attr.ib()
_basetemp = attr.ib(type=Optional[Path], default=None)
_basetemp = attr.ib(type=Optional[Path])
def __init__(
self,
given_basetemp: Optional[Path],
trace,
basetemp: Optional[Path] = None,
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
if given_basetemp is None:
self._given_basetemp = None
else:
# Use os.path.abspath() to get absolute path instead of resolve() as it
# does not work the same in all platforms (see #4427).
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
self._trace = trace
self._basetemp = basetemp
@classmethod
def from_config(cls, config: Config) -> "TempPathFactory":
"""Create a factory according to pytest configuration."""
def from_config(
cls, config: Config, *, _ispytest: bool = False,
) -> "TempPathFactory":
"""Create a factory according to pytest configuration.
:meta private:
"""
check_ispytest(_ispytest)
return cls(
given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir")
given_basetemp=config.option.basetemp,
trace=config.trace.get("tmpdir"),
_ispytest=True,
)
def _ensure_relative_to_basetemp(self, basename: str) -> str:
@ -104,13 +123,19 @@ class TempPathFactory:
@final
@attr.s
@attr.s(init=False)
class TempdirFactory:
"""Backward comptibility wrapper that implements :class:``py.path.local``
for :class:``TempPathFactory``."""
_tmppath_factory = attr.ib(type=TempPathFactory)
def __init__(
self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False
) -> None:
check_ispytest(_ispytest)
self._tmppath_factory = tmppath_factory
def mktemp(self, basename: str, numbered: bool = True) -> py.path.local:
"""Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object."""
return py.path.local(self._tmppath_factory.mktemp(basename, numbered).resolve())
@ -139,8 +164,8 @@ def pytest_configure(config: Config) -> None:
to the tmpdir_factory session fixture.
"""
mp = MonkeyPatch()
tmppath_handler = TempPathFactory.from_config(config)
t = TempdirFactory(tmppath_handler)
tmppath_handler = TempPathFactory.from_config(config, _ispytest=True)
t = TempdirFactory(tmppath_handler, _ispytest=True)
config._cleanup.append(mp.undo)
mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False)
mp.setattr(config, "_tmpdirhandler", t, raising=False)

View File

@ -3,6 +3,8 @@
from . import collect
from _pytest import __version__
from _pytest.assertion import register_assert_rewrite
from _pytest.cacheprovider import Cache
from _pytest.capture import CaptureFixture
from _pytest.config import cmdline
from _pytest.config import console_main
from _pytest.config import ExitCode
@ -14,8 +16,10 @@ from _pytest.debugging import pytestPDB as __pytestPDB
from _pytest.fixtures import _fillfuncargs
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureLookupError
from _pytest.fixtures import FixtureRequest
from _pytest.fixtures import yield_fixture
from _pytest.freeze_support import freeze_includes
from _pytest.logging import LogCaptureFixture
from _pytest.main import Session
from _pytest.mark import MARK_GEN as mark
from _pytest.mark import param
@ -28,6 +32,8 @@ from _pytest.outcomes import fail
from _pytest.outcomes import importorskip
from _pytest.outcomes import skip
from _pytest.outcomes import xfail
from _pytest.pytester import Pytester
from _pytest.pytester import Testdir
from _pytest.python import Class
from _pytest.python import Function
from _pytest.python import Instance
@ -36,7 +42,10 @@ from _pytest.python import Package
from _pytest.python_api import approx
from _pytest.python_api import raises
from _pytest.recwarn import deprecated_call
from _pytest.recwarn import WarningsRecorder
from _pytest.recwarn import warns
from _pytest.tmpdir import TempdirFactory
from _pytest.tmpdir import TempPathFactory
from _pytest.warning_types import PytestAssertRewriteWarning
from _pytest.warning_types import PytestCacheWarning
from _pytest.warning_types import PytestCollectionWarning
@ -53,6 +62,8 @@ __all__ = [
"__version__",
"_fillfuncargs",
"approx",
"Cache",
"CaptureFixture",
"Class",
"cmdline",
"collect",
@ -65,6 +76,7 @@ __all__ = [
"File",
"fixture",
"FixtureLookupError",
"FixtureRequest",
"freeze_includes",
"Function",
"hookimpl",
@ -72,6 +84,7 @@ __all__ = [
"importorskip",
"Instance",
"Item",
"LogCaptureFixture",
"main",
"mark",
"Module",
@ -84,6 +97,7 @@ __all__ = [
"PytestConfigWarning",
"PytestDeprecationWarning",
"PytestExperimentalApiWarning",
"Pytester",
"PytestUnhandledCoroutineWarning",
"PytestUnknownMarkWarning",
"PytestWarning",
@ -92,7 +106,11 @@ __all__ = [
"Session",
"set_trace",
"skip",
"TempPathFactory",
"Testdir",
"TempdirFactory",
"UsageError",
"WarningsRecorder",
"warns",
"xfail",
"yield_fixture",

View File

@ -123,3 +123,17 @@ def test_yield_fixture_is_deprecated() -> None:
@pytest.yield_fixture
def fix():
assert False
def test_private_is_deprecated() -> None:
class PrivateInit:
def __init__(self, foo: int, *, _ispytest: bool = False) -> None:
deprecated.check_ispytest(_ispytest)
with pytest.warns(
pytest.PytestDeprecationWarning, match="private pytest class or function"
):
PrivateInit(10)
# Doesn't warn.
PrivateInit(10, _ispytest=True)

View File

@ -621,7 +621,7 @@ class TestRequestBasic:
def test_func(something): pass
"""
)
req = fixtures.FixtureRequest(item)
req = fixtures.FixtureRequest(item, _ispytest=True)
assert req.function == item.obj
assert req.keywords == item.keywords
assert hasattr(req.module, "test_func")
@ -661,7 +661,9 @@ class TestRequestBasic:
)
(item1,) = testdir.genitems([modcol])
assert item1.name == "test_method"
arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs
arg2fixturedefs = fixtures.FixtureRequest(
item1, _ispytest=True
)._arg2fixturedefs
assert len(arg2fixturedefs) == 1
assert arg2fixturedefs["something"][0].argname == "something"
@ -910,7 +912,7 @@ class TestRequestBasic:
def test_request_getmodulepath(self, testdir):
modcol = testdir.getmodulecol("def test_somefunc(): pass")
(item,) = testdir.genitems([modcol])
req = fixtures.FixtureRequest(item)
req = fixtures.FixtureRequest(item, _ispytest=True)
assert req.fspath == modcol.fspath
def test_request_fixturenames(self, testdir):
@ -1052,7 +1054,7 @@ class TestRequestMarking:
pass
"""
)
req1 = fixtures.FixtureRequest(item1)
req1 = fixtures.FixtureRequest(item1, _ispytest=True)
assert "xfail" not in item1.keywords
req1.applymarker(pytest.mark.xfail)
assert "xfail" in item1.keywords
@ -3882,7 +3884,7 @@ class TestScopeOrdering:
"""
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
assert request.fixturenames == "m1 f1".split()
def test_func_closure_with_native_fixtures(self, testdir, monkeypatch) -> None:
@ -3928,7 +3930,7 @@ class TestScopeOrdering:
"""
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
# order of fixtures based on their scope and position in the parameter list
assert (
request.fixturenames == "s1 my_tmpdir_factory p1 m1 f1 f2 my_tmpdir".split()
@ -3954,7 +3956,7 @@ class TestScopeOrdering:
"""
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
assert request.fixturenames == "m1 f1".split()
def test_func_closure_scopes_reordered(self, testdir):
@ -3987,7 +3989,7 @@ class TestScopeOrdering:
"""
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
assert request.fixturenames == "s1 m1 c1 f2 f1".split()
def test_func_closure_same_scope_closer_root_first(self, testdir):
@ -4027,7 +4029,7 @@ class TestScopeOrdering:
}
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
assert request.fixturenames == "p_sub m_conf m_sub m_test f1".split()
def test_func_closure_all_scopes_complex(self, testdir):
@ -4071,7 +4073,7 @@ class TestScopeOrdering:
"""
)
items, _ = testdir.inline_genitems()
request = FixtureRequest(items[0])
request = FixtureRequest(items[0], _ispytest=True)
assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split()
def test_multiple_packages(self, testdir):

View File

@ -1156,7 +1156,7 @@ def test_gitignore(testdir):
from _pytest.cacheprovider import Cache
config = testdir.parseconfig()
cache = Cache.for_config(config)
cache = Cache.for_config(config, _ispytest=True)
cache.set("foo", "bar")
msg = "# Created by pytest automatically.\n*\n"
gitignore_path = cache._cachedir.joinpath(".gitignore")
@ -1178,7 +1178,7 @@ def test_does_not_create_boilerplate_in_existing_dirs(testdir):
"""
)
config = testdir.parseconfig()
cache = Cache.for_config(config)
cache = Cache.for_config(config, _ispytest=True)
cache.set("foo", "bar")
assert os.path.isdir("v") # cache contents
@ -1192,7 +1192,7 @@ def test_cachedir_tag(testdir):
from _pytest.cacheprovider import CACHEDIR_TAG_CONTENT
config = testdir.parseconfig()
cache = Cache.for_config(config)
cache = Cache.for_config(config, _ispytest=True)
cache.set("foo", "bar")
cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG")
assert cachedir_tag_path.read_bytes() == CACHEDIR_TAG_CONTENT

View File

@ -28,7 +28,7 @@ def test_recwarn_functional(testdir) -> None:
class TestWarningsRecorderChecker:
def test_recording(self) -> None:
rec = WarningsRecorder()
rec = WarningsRecorder(_ispytest=True)
with rec:
assert not rec.list
warnings.warn_explicit("hello", UserWarning, "xyz", 13)
@ -45,7 +45,7 @@ class TestWarningsRecorderChecker:
def test_warn_stacklevel(self) -> None:
"""#4243"""
rec = WarningsRecorder()
rec = WarningsRecorder(_ispytest=True)
with rec:
warnings.warn("test", DeprecationWarning, 2)
@ -53,21 +53,21 @@ class TestWarningsRecorderChecker:
from _pytest.recwarn import WarningsChecker
with pytest.raises(TypeError):
WarningsChecker(5) # type: ignore
WarningsChecker(5, _ispytest=True) # type: ignore[arg-type]
with pytest.raises(TypeError):
WarningsChecker(("hi", RuntimeWarning)) # type: ignore
WarningsChecker(("hi", RuntimeWarning), _ispytest=True) # type: ignore[arg-type]
with pytest.raises(TypeError):
WarningsChecker([DeprecationWarning, RuntimeWarning]) # type: ignore
WarningsChecker([DeprecationWarning, RuntimeWarning], _ispytest=True) # type: ignore[arg-type]
def test_invalid_enter_exit(self) -> None:
# wrap this test in WarningsRecorder to ensure warning state gets reset
with WarningsRecorder():
with WarningsRecorder(_ispytest=True):
with pytest.raises(RuntimeError):
rec = WarningsRecorder()
rec = WarningsRecorder(_ispytest=True)
rec.__exit__(None, None, None) # can't exit before entering
with pytest.raises(RuntimeError):
rec = WarningsRecorder()
rec = WarningsRecorder(_ispytest=True)
with rec:
with rec:
pass # can't enter twice

View File

@ -48,7 +48,9 @@ class FakeConfig:
class TestTempdirHandler:
def test_mktemp(self, tmp_path):
config = cast(Config, FakeConfig(tmp_path))
t = TempdirFactory(TempPathFactory.from_config(config))
t = TempdirFactory(
TempPathFactory.from_config(config, _ispytest=True), _ispytest=True
)
tmp = t.mktemp("world")
assert tmp.relto(t.getbasetemp()) == "world0"
tmp = t.mktemp("this")
@ -61,7 +63,7 @@ class TestTempdirHandler:
"""#4425"""
monkeypatch.chdir(tmp_path)
config = cast(Config, FakeConfig("hello"))
t = TempPathFactory.from_config(config)
t = TempPathFactory.from_config(config, _ispytest=True)
assert t.getbasetemp().resolve() == (tmp_path / "hello").resolve()