From b0505788821604f0b0787683d47a0ca693fd0426 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 9 Nov 2020 18:27:40 +0200 Subject: [PATCH 1/2] pytester: split asserts to a separate plugin, don't rewrite pytester itself An upcoming commit wants to import from `_pytest.pytester` in the public `pytest` module. This means that `_pytest.pytester` would start to get imported during import time, which it hasn't up to now -- it was imported by the plugin loader (if requested). When a plugin is loaded, it is subjected to assertion rewriting, but only if the module isn't imported yet, it issues a warning "Module already imported so cannot be rewritten" and skips the rewriting. So we'd end up with the pytester plugin not being rewritten, but it wants to be. Absent better ideas, the solution here is to split the pytester assertions to their own plugin (which will always only be imported by the plugin loader) and exclude pytester itself from plugin rewriting. --- src/_pytest/config/__init__.py | 1 + src/_pytest/pytester.py | 51 +++++++++++------------ src/_pytest/pytester_assertions.py | 66 ++++++++++++++++++++++++++++++ 3 files changed, 90 insertions(+), 28 deletions(-) create mode 100644 src/_pytest/pytester_assertions.py diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6c1d9c69a50..74168efd572 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -256,6 +256,7 @@ def directory_arg(path: str, optname: str) -> str: builtin_plugins = set(default_plugins) builtin_plugins.add("pytester") +builtin_plugins.add("pytester_assertions") def get_config( diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 0e1dffb9d8a..158b71b3a52 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1,4 +1,7 @@ -"""(Disabled by default) support for testing pytest and pytest plugins.""" +"""(Disabled by default) support for testing pytest and pytest plugins. + +PYTEST_DONT_REWRITE +""" import collections.abc import contextlib import gc @@ -66,6 +69,9 @@ import pexpect +pytest_plugins = ["pytester_assertions"] + + IGNORE_PAM = [ # filenames added when obtaining details about the current user "/var/lib/sss/mc/passwd" ] @@ -408,16 +414,12 @@ def countoutcomes(self) -> List[int]: def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: __tracebackhide__ = True + from _pytest.pytester_assertions import assertoutcome outcomes = self.listoutcomes() - realpassed, realskipped, realfailed = outcomes - obtained = { - "passed": len(realpassed), - "skipped": len(realskipped), - "failed": len(realfailed), - } - expected = {"passed": passed, "skipped": skipped, "failed": failed} - assert obtained == expected, outcomes + assertoutcome( + outcomes, passed=passed, skipped=skipped, failed=failed, + ) def clear(self) -> None: self.calls[:] = [] @@ -574,25 +576,18 @@ def assert_outcomes( """Assert that the specified outcomes appear with the respective numbers (0 means it didn't occur) in the text output from a test run.""" __tracebackhide__ = True - - d = self.parseoutcomes() - obtained = { - "passed": d.get("passed", 0), - "skipped": d.get("skipped", 0), - "failed": d.get("failed", 0), - "errors": d.get("errors", 0), - "xpassed": d.get("xpassed", 0), - "xfailed": d.get("xfailed", 0), - } - expected = { - "passed": passed, - "skipped": skipped, - "failed": failed, - "errors": errors, - "xpassed": xpassed, - "xfailed": xfailed, - } - assert obtained == expected + from _pytest.pytester_assertions import assert_outcomes + + outcomes = self.parseoutcomes() + assert_outcomes( + outcomes, + passed=passed, + skipped=skipped, + failed=failed, + errors=errors, + xpassed=xpassed, + xfailed=xfailed, + ) class CwdSnapshot: diff --git a/src/_pytest/pytester_assertions.py b/src/_pytest/pytester_assertions.py new file mode 100644 index 00000000000..630c1d3331c --- /dev/null +++ b/src/_pytest/pytester_assertions.py @@ -0,0 +1,66 @@ +"""Helper plugin for pytester; should not be loaded on its own.""" +# This plugin contains assertions used by pytester. pytester cannot +# contain them itself, since it is imported by the `pytest` module, +# hence cannot be subject to assertion rewriting, which requires a +# module to not be already imported. +from typing import Dict +from typing import Sequence +from typing import Tuple +from typing import Union + +from _pytest.reports import CollectReport +from _pytest.reports import TestReport + + +def assertoutcome( + outcomes: Tuple[ + Sequence[TestReport], + Sequence[Union[CollectReport, TestReport]], + Sequence[Union[CollectReport, TestReport]], + ], + passed: int = 0, + skipped: int = 0, + failed: int = 0, +) -> None: + __tracebackhide__ = True + + realpassed, realskipped, realfailed = outcomes + obtained = { + "passed": len(realpassed), + "skipped": len(realskipped), + "failed": len(realfailed), + } + expected = {"passed": passed, "skipped": skipped, "failed": failed} + assert obtained == expected, outcomes + + +def assert_outcomes( + outcomes: Dict[str, int], + passed: int = 0, + skipped: int = 0, + failed: int = 0, + errors: int = 0, + xpassed: int = 0, + xfailed: int = 0, +) -> None: + """Assert that the specified outcomes appear with the respective + numbers (0 means it didn't occur) in the text output from a test run.""" + __tracebackhide__ = True + + obtained = { + "passed": outcomes.get("passed", 0), + "skipped": outcomes.get("skipped", 0), + "failed": outcomes.get("failed", 0), + "errors": outcomes.get("errors", 0), + "xpassed": outcomes.get("xpassed", 0), + "xfailed": outcomes.get("xfailed", 0), + } + expected = { + "passed": passed, + "skipped": skipped, + "failed": failed, + "errors": errors, + "xpassed": xpassed, + "xfailed": xfailed, + } + assert obtained == expected From f1e6fdcddbfe8991935685ccc5049dd957ec4382 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 27 Sep 2020 22:20:31 +0300 Subject: [PATCH 2/2] 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. --- changelog/7469.deprecation.rst | 18 ++++++++ changelog/7469.improvement.rst | 23 ++++++++++ doc/en/reference.rst | 77 +++++++++++++--------------------- src/_pytest/cacheprovider.py | 51 ++++++++++++++++------ src/_pytest/capture.py | 18 ++++---- src/_pytest/deprecated.py | 26 ++++++++++++ src/_pytest/doctest.py | 2 +- src/_pytest/fixtures.py | 15 +++++-- src/_pytest/logging.py | 6 ++- src/_pytest/pytester.py | 18 +++++--- src/_pytest/python.py | 2 +- src/_pytest/recwarn.py | 15 ++++--- src/_pytest/tmpdir.py | 61 +++++++++++++++++++-------- src/pytest/__init__.py | 18 ++++++++ testing/deprecated_test.py | 14 +++++++ testing/python/fixtures.py | 22 +++++----- testing/test_cacheprovider.py | 6 +-- testing/test_recwarn.py | 16 +++---- testing/test_tmpdir.py | 6 ++- 19 files changed, 290 insertions(+), 124 deletions(-) create mode 100644 changelog/7469.deprecation.rst create mode 100644 changelog/7469.improvement.rst diff --git a/changelog/7469.deprecation.rst b/changelog/7469.deprecation.rst new file mode 100644 index 00000000000..67d0b2bba46 --- /dev/null +++ b/changelog/7469.deprecation.rst @@ -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. diff --git a/changelog/7469.improvement.rst b/changelog/7469.improvement.rst new file mode 100644 index 00000000000..cbd75f05419 --- /dev/null +++ b/changelog/7469.improvement.rst @@ -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. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index cbe89fe0bf0..6973043ccfe 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -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] `. 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] `. 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] `. 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] `. 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: - -.. currentmodule:: _pytest.tmpdir +``tmp_path_factory`` is an instance of :class:`~pytest.TempdirFactory`: -.. automethod:: TempdirFactory.mktemp -.. automethod:: TempdirFactory.getbasetemp +.. autoclass:: pytest.TempdirFactory() .. _`hook-reference`: diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 1689b9a410f..03acd03109e 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -25,6 +25,7 @@ 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 @@ @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" + def __init__( + self, cachedir: Path, config: Config, *, _ispytest: bool = False + ) -> None: + check_ispytest(_ispytest) + self._cachedir = cachedir + self._config = config + @classmethod - def for_config(cls, config: Config) -> "Cache": - cachedir = cls.cache_dir_from_config(config) + 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) - return cls(cachedir, config) + cls.clear_cache(cachedir, _ispytest=True) + return cls(cachedir, config, _ispytest=True) @classmethod - def clear_cache(cls, cachedir: Path) -> None: - """Clear the sub-directories used to hold cached directories and values.""" + 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 @@ def set(self, key: str, value: object) -> None: 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 @@ def set(self, key: str, value: object) -> None: 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") diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 1c3a2b81959..086302658cb 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -21,6 +21,7 @@ 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 @@ def pytest_internalerror(self) -> None: 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 diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 2e9154e8380..19b31d66538 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -8,6 +8,8 @@ :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 @@ 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) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index fd9434a9215..64e8f0e0eee 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -563,7 +563,7 @@ def func() -> None: 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 diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index cef998c03b7..273bcafd393 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -49,6 +49,7 @@ 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 @@ def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None: 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 @@ def __init__( 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 @@ def __repr__(self) -> str: return f"" 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( diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 2f5da8e7a00..2e4847328ab 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -27,6 +27,7 @@ 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 @@ def handleError(self, record: logging.LogRecord) -> None: 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() diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 158b71b3a52..20ea71edc64 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -48,6 +48,7 @@ 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 TimeoutExpired(Exception): 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 @@ def assert_contains_lines(self, lines2: Sequence[str]) -> None: @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: diff --git a/src/_pytest/python.py b/src/_pytest/python.py index a3eaa58238e..e48e7531c19 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1620,7 +1620,7 @@ def from_parent(cls, parent, **kw): # todo: determine sound type limitations 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): diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 49f1e590296..58b449114c0 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -16,6 +16,7 @@ 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 @@ def __init__( 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: diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 4ca1dd6e136..e62d08db5f5 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -14,37 +14,56 @@ 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 @@ def getbasetemp(self) -> Path: @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) diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index d7a5b22997f..8af095ea8ae 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -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.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 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_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 @@ "__version__", "_fillfuncargs", "approx", + "Cache", + "CaptureFixture", "Class", "cmdline", "collect", @@ -65,6 +76,7 @@ "File", "fixture", "FixtureLookupError", + "FixtureRequest", "freeze_includes", "Function", "hookimpl", @@ -72,6 +84,7 @@ "importorskip", "Instance", "Item", + "LogCaptureFixture", "main", "mark", "Module", @@ -84,6 +97,7 @@ "PytestConfigWarning", "PytestDeprecationWarning", "PytestExperimentalApiWarning", + "Pytester", "PytestUnhandledCoroutineWarning", "PytestUnknownMarkWarning", "PytestWarning", @@ -92,7 +106,11 @@ "Session", "set_trace", "skip", + "TempPathFactory", + "Testdir", + "TempdirFactory", "UsageError", + "WarningsRecorder", "warns", "xfail", "yield_fixture", diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 0d1b58ad16a..d213414ee45 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -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) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index a5637b47642..94547dd245c 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -621,7 +621,7 @@ def something(request): pass 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 @@ def test_method(self, something): ) (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 @@ def test_second(): 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 @@ def test_func2(self, something): 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 @@ def test_func(m1): """ ) 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 @@ def test_foo(f1, p1, m1, f2, s1): pass """ ) 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 @@ def test_func(f1, m1): """ ) 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 @@ def test_func(self, f2, f1, c1, m1, s1): """ ) 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 @@ def test_func(m_test, f1): } ) 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 @@ def test_func(self, f2, f1, m2): """ ) 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): diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 37253b8b5db..ccc7304b02a 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -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 diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index f61f8586f9c..05970061ed3 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -28,7 +28,7 @@ def test_method(recwarn): 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 @@ def test_recording(self) -> None: def test_warn_stacklevel(self) -> None: """#4243""" - rec = WarningsRecorder() + rec = WarningsRecorder(_ispytest=True) with rec: warnings.warn("test", DeprecationWarning, 2) @@ -53,21 +53,21 @@ def test_typechecking(self) -> None: 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 diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index bd6e7b968b0..1df0e2207b3 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -48,7 +48,9 @@ def option(self): 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 @@ def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch): """#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()