Skip to content

Commit ee20d13

Browse files
committed
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.
1 parent 86765f9 commit ee20d13

19 files changed

+269
-124
lines changed

changelog/7469.deprecation.rst

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Directly constructing/calling the following classes/functions is now deprecated:
2+
3+
- ``_pytest.cacheprovider.Cache``
4+
- ``_pytest.cacheprovider.Cache.for_config()``
5+
- ``_pytest.cacheprovider.Cache.clear_cache()``
6+
- ``_pytest.cacheprovider.Cache.cache_dir_from_config()``
7+
- ``_pytest.capture.CaptureFixture``
8+
- ``_pytest.fixtures.FixtureRequest``
9+
- ``_pytest.fixtures.SubRequest``
10+
- ``_pytest.logging.LogCaptureFixture``
11+
- ``_pytest.pytester.Pytester``
12+
- ``_pytest.pytester.Testdir``
13+
- ``_pytest.recwarn.WarningsRecorder``
14+
- ``_pytest.recwarn.WarningsChecker``
15+
- ``_pytest.tmpdir.TempPathFactory``
16+
- ``_pytest.tmpdir.TempdirFactory``
17+
18+
These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 7.0.0.

changelog/7469.improvement.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
It is now possible to construct a :class:`MonkeyPatch` object directly as ``pytest.MonkeyPatch()``,
2+
in cases when the :fixture:`monkeypatch` fixture cannot be used. Previously some users imported it
3+
from the private `_pytest.monkeypatch.MonkeyPatch` namespace.
4+
5+
The types of builtin pytest fixtures are now exported so they may be used in type annotations of test functions.
6+
The newly-exported types are:
7+
8+
- ``pytest.FixtureRequest`` for the :fixture:`request` fixture.
9+
- ``pytest.Cache`` for the :fixture:`cache` fixture.
10+
- ``pytest.CaptureFixture[str]`` for the :fixture:`capfd` and :fixture:`capsys` fixtures.
11+
- ``pytest.CaptureFixture[bytes]`` for the :fixture:`capfdbinary` and :fixture:`capsysbinary` fixtures.
12+
- ``pytest.LogCaptureFixture`` for the :fixture:`caplog` fixture.
13+
- ``pytest.Pytester`` for the :fixture:`pytester` fixture.
14+
- ``pytest.Testdir`` for the :fixture:`testdir` fixture.
15+
- ``pytest.TempdirFactory`` for the :fixture:`tmpdir_factory` fixture.
16+
- ``pytest.TempPathFactory`` for the :fixture:`tmp_path_factory` fixture.
17+
- ``pytest.MonkeyPatch`` for the :fixture:`monkeypatch` fixture.
18+
- ``pytest.WarningsRecorder`` for the :fixture:`recwarn` fixture.
19+
20+
Constructing them is not supported (except for `MonkeyPatch`); they are only meant for use in type annotations.
21+
Doing so will emit a deprecation warning, and may become a hard-error in pytest 7.0.
22+
23+
Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.

doc/en/reference.rst

Lines changed: 30 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -304,11 +304,10 @@ request ``pytestconfig`` into your fixture and get it with ``pytestconfig.cache`
304304
Under the hood, the cache plugin uses the simple
305305
``dumps``/``loads`` API of the :py:mod:`json` stdlib module.
306306

307-
.. currentmodule:: _pytest.cacheprovider
307+
``config.cache`` is an instance of :class:`pytest.Cache`:
308308

309-
.. automethod:: Cache.get
310-
.. automethod:: Cache.set
311-
.. automethod:: Cache.makedir
309+
.. autoclass:: pytest.Cache()
310+
:members:
312311

313312

314313
.. fixture:: capsys
@@ -318,12 +317,10 @@ capsys
318317

319318
**Tutorial**: :doc:`capture`.
320319

321-
.. currentmodule:: _pytest.capture
322-
323-
.. autofunction:: capsys()
320+
.. autofunction:: _pytest.capture.capsys()
324321
:no-auto-options:
325322

326-
Returns an instance of :py:class:`CaptureFixture`.
323+
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
327324

328325
Example:
329326

@@ -334,7 +331,7 @@ capsys
334331
captured = capsys.readouterr()
335332
assert captured.out == "hello\n"
336333
337-
.. autoclass:: CaptureFixture()
334+
.. autoclass:: pytest.CaptureFixture()
338335
:members:
339336

340337

@@ -345,10 +342,10 @@ capsysbinary
345342

346343
**Tutorial**: :doc:`capture`.
347344

348-
.. autofunction:: capsysbinary()
345+
.. autofunction:: _pytest.capture.capsysbinary()
349346
:no-auto-options:
350347

351-
Returns an instance of :py:class:`CaptureFixture`.
348+
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
352349

353350
Example:
354351

@@ -367,10 +364,10 @@ capfd
367364

368365
**Tutorial**: :doc:`capture`.
369366

370-
.. autofunction:: capfd()
367+
.. autofunction:: _pytest.capture.capfd()
371368
:no-auto-options:
372369

373-
Returns an instance of :py:class:`CaptureFixture`.
370+
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
374371

375372
Example:
376373

@@ -389,10 +386,10 @@ capfdbinary
389386

390387
**Tutorial**: :doc:`capture`.
391388

392-
.. autofunction:: capfdbinary()
389+
.. autofunction:: _pytest.capture.capfdbinary()
393390
:no-auto-options:
394391

395-
Returns an instance of :py:class:`CaptureFixture`.
392+
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
396393

397394
Example:
398395

@@ -433,7 +430,7 @@ request
433430

434431
The ``request`` fixture is a special fixture providing information of the requesting test function.
435432

436-
.. autoclass:: _pytest.fixtures.FixtureRequest()
433+
.. autoclass:: pytest.FixtureRequest()
437434
:members:
438435

439436

@@ -475,9 +472,9 @@ caplog
475472
.. autofunction:: _pytest.logging.caplog()
476473
:no-auto-options:
477474

478-
Returns a :class:`_pytest.logging.LogCaptureFixture` instance.
475+
Returns a :class:`pytest.LogCaptureFixture` instance.
479476

480-
.. autoclass:: _pytest.logging.LogCaptureFixture
477+
.. autoclass:: pytest.LogCaptureFixture()
481478
:members:
482479

483480

@@ -504,9 +501,7 @@ pytester
504501

505502
.. versionadded:: 6.2
506503

507-
.. currentmodule:: _pytest.pytester
508-
509-
Provides a :class:`Pytester` instance that can be used to run and test pytest itself.
504+
Provides a :class:`~pytest.Pytester` instance that can be used to run and test pytest itself.
510505

511506
It provides an empty directory where pytest can be executed in isolation, and contains facilities
512507
to write tests, configuration files, and match against expected output.
@@ -519,16 +514,16 @@ To use it, include in your topmost ``conftest.py`` file:
519514
520515
521516
522-
.. autoclass:: Pytester()
517+
.. autoclass:: pytest.Pytester()
523518
:members:
524519

525-
.. autoclass:: RunResult()
520+
.. autoclass:: _pytest.pytester.RunResult()
526521
:members:
527522

528-
.. autoclass:: LineMatcher()
523+
.. autoclass:: _pytest.pytester.LineMatcher()
529524
:members:
530525

531-
.. autoclass:: HookRecorder()
526+
.. autoclass:: _pytest.pytester.HookRecorder()
532527
:members:
533528

534529
.. fixture:: testdir
@@ -541,7 +536,7 @@ legacy ``py.path.local`` objects instead when applicable.
541536

542537
New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
543538

544-
.. autoclass:: Testdir()
539+
.. autoclass:: pytest.Testdir()
545540
:members:
546541

547542

@@ -552,12 +547,10 @@ recwarn
552547

553548
**Tutorial**: :ref:`assertwarnings`
554549

555-
.. currentmodule:: _pytest.recwarn
556-
557-
.. autofunction:: recwarn()
550+
.. autofunction:: _pytest.recwarn.recwarn()
558551
:no-auto-options:
559552

560-
.. autoclass:: WarningsRecorder()
553+
.. autoclass:: pytest.WarningsRecorder()
561554
:members:
562555

563556
Each recorded warning is an instance of :class:`warnings.WarningMessage`.
@@ -574,13 +567,11 @@ tmp_path
574567

575568
**Tutorial**: :doc:`tmpdir`
576569

577-
.. currentmodule:: _pytest.tmpdir
578-
579-
.. autofunction:: tmp_path()
570+
.. autofunction:: _pytest.tmpdir.tmp_path()
580571
:no-auto-options:
581572

582573

583-
.. fixture:: tmp_path_factory
574+
.. fixture:: _pytest.tmpdir.tmp_path_factory
584575

585576
tmp_path_factory
586577
~~~~~~~~~~~~~~~~
@@ -589,12 +580,9 @@ tmp_path_factory
589580

590581
.. _`tmp_path_factory factory api`:
591582

592-
``tmp_path_factory`` instances have the following methods:
583+
``tmp_path_factory`` is an instance of :class:`~pytest.TempPathFactory`:
593584

594-
.. currentmodule:: _pytest.tmpdir
595-
596-
.. automethod:: TempPathFactory.mktemp
597-
.. automethod:: TempPathFactory.getbasetemp
585+
.. autoclass:: pytest.TempPathFactory()
598586

599587

600588
.. fixture:: tmpdir
@@ -604,9 +592,7 @@ tmpdir
604592

605593
**Tutorial**: :doc:`tmpdir`
606594

607-
.. currentmodule:: _pytest.tmpdir
608-
609-
.. autofunction:: tmpdir()
595+
.. autofunction:: _pytest.tmpdir.tmpdir()
610596
:no-auto-options:
611597

612598

@@ -619,12 +605,9 @@ tmpdir_factory
619605

620606
.. _`tmpdir factory api`:
621607

622-
``tmpdir_factory`` instances have the following methods:
623-
624-
.. currentmodule:: _pytest.tmpdir
608+
``tmp_path_factory`` is an instance of :class:`~pytest.TempdirFactory`:
625609

626-
.. automethod:: TempdirFactory.mktemp
627-
.. automethod:: TempdirFactory.getbasetemp
610+
.. autoclass:: pytest.TempdirFactory()
628611

629612

630613
.. _`hook-reference`:

src/_pytest/cacheprovider.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from _pytest.config import ExitCode
2626
from _pytest.config import hookimpl
2727
from _pytest.config.argparsing import Parser
28+
from _pytest.deprecated import check_ispytest
2829
from _pytest.fixtures import fixture
2930
from _pytest.fixtures import FixtureRequest
3031
from _pytest.main import Session
@@ -53,7 +54,7 @@
5354

5455

5556
@final
56-
@attr.s
57+
@attr.s(init=False)
5758
class Cache:
5859
_cachedir = attr.ib(type=Path, repr=False)
5960
_config = attr.ib(type=Config, repr=False)
@@ -64,26 +65,50 @@ class Cache:
6465
# sub-directory under cache-dir for values created by "set"
6566
_CACHE_PREFIX_VALUES = "v"
6667

68+
def __init__(self, cachedir: Path, config: Config, *, _ispytest: bool) -> None:
69+
check_ispytest(_ispytest)
70+
self._cachedir = cachedir
71+
self._config = config
72+
6773
@classmethod
68-
def for_config(cls, config: Config) -> "Cache":
69-
cachedir = cls.cache_dir_from_config(config)
74+
def for_config(cls, config: Config, *, _ispytest: bool) -> "Cache":
75+
"""Create the Cache instance for a Config.
76+
77+
:meta private:
78+
"""
79+
check_ispytest(_ispytest)
80+
cachedir = cls.cache_dir_from_config(config, _ispytest=True)
7081
if config.getoption("cacheclear") and cachedir.is_dir():
71-
cls.clear_cache(cachedir)
72-
return cls(cachedir, config)
82+
cls.clear_cache(cachedir, _ispytest=True)
83+
return cls(cachedir, config, _ispytest=True)
7384

7485
@classmethod
75-
def clear_cache(cls, cachedir: Path) -> None:
76-
"""Clear the sub-directories used to hold cached directories and values."""
86+
def clear_cache(cls, cachedir: Path, _ispytest: bool) -> None:
87+
"""Clear the sub-directories used to hold cached directories and values.
88+
89+
:meta private:
90+
"""
91+
check_ispytest(_ispytest)
7792
for prefix in (cls._CACHE_PREFIX_DIRS, cls._CACHE_PREFIX_VALUES):
7893
d = cachedir / prefix
7994
if d.is_dir():
8095
rm_rf(d)
8196

8297
@staticmethod
83-
def cache_dir_from_config(config: Config) -> Path:
98+
def cache_dir_from_config(config: Config, *, _ispytest: bool) -> Path:
99+
"""Get the path to the cache directory for a Config.
100+
101+
:meta private:
102+
"""
103+
check_ispytest(_ispytest)
84104
return resolve_from_str(config.getini("cache_dir"), config.rootpath)
85105

86-
def warn(self, fmt: str, **args: object) -> None:
106+
def warn(self, fmt: str, *, _ispytest: bool, **args: object) -> None:
107+
"""Issue a cache warning.
108+
109+
:meta private:
110+
"""
111+
check_ispytest(_ispytest)
87112
import warnings
88113
from _pytest.warning_types import PytestCacheWarning
89114

@@ -152,15 +177,15 @@ def set(self, key: str, value: object) -> None:
152177
cache_dir_exists_already = self._cachedir.exists()
153178
path.parent.mkdir(exist_ok=True, parents=True)
154179
except OSError:
155-
self.warn("could not create cache path {path}", path=path)
180+
self.warn("could not create cache path {path}", path=path, _ispytest=True)
156181
return
157182
if not cache_dir_exists_already:
158183
self._ensure_supporting_files()
159184
data = json.dumps(value, indent=2, sort_keys=True)
160185
try:
161186
f = path.open("w")
162187
except OSError:
163-
self.warn("cache could not write path {path}", path=path)
188+
self.warn("cache could not write path {path}", path=path, _ispytest=True)
164189
else:
165190
with f:
166191
f.write(data)
@@ -469,7 +494,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
469494

470495
@hookimpl(tryfirst=True)
471496
def pytest_configure(config: Config) -> None:
472-
config.cache = Cache.for_config(config)
497+
config.cache = Cache.for_config(config, _ispytest=True)
473498
config.pluginmanager.register(LFPlugin(config), "lfplugin")
474499
config.pluginmanager.register(NFPlugin(config), "nfplugin")
475500

0 commit comments

Comments
 (0)