From c2ece58aa0f0e75b49840fe8860cea505bbc6a05 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Oct 2021 10:29:21 +0300 Subject: [PATCH 01/11] Add legacypath plugin --- src/_pytest/config/__init__.py | 1 + src/_pytest/legacypath.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 src/_pytest/legacypath.py diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 8bbb2720d..34a7a80e1 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -240,6 +240,7 @@ default_plugins = essential_plugins + ( "unittest", "capture", "skipping", + "legacypath", "tmpdir", "monkeypatch", "recwarn", diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py new file mode 100644 index 000000000..602fe1de8 --- /dev/null +++ b/src/_pytest/legacypath.py @@ -0,0 +1 @@ +"""Add backward compatibility support for the legacy py path type.""" From 1df28a4450ae6deecd4f94b775a8e03022be4020 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Oct 2021 10:28:45 +0300 Subject: [PATCH 02/11] Move testdir to legacypath plugin --- doc/en/conf.py | 4 + src/_pytest/legacypath.py | 253 +++++++++++++++++++++++++++++++++++++ src/_pytest/pytester.py | 231 --------------------------------- src/pytest/__init__.py | 2 - testing/test_collection.py | 1 - testing/test_legacypath.py | 27 ++++ testing/test_pytester.py | 26 ---- 7 files changed, 284 insertions(+), 260 deletions(-) create mode 100644 testing/test_legacypath.py diff --git a/doc/en/conf.py b/doc/en/conf.py index 51ab612ed..e183c3e74 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -446,3 +446,7 @@ def setup(app: "sphinx.application.Sphinx") -> None: ) sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final + + # legacypath.py monkey-patches pytest.Testdir in. Import the file so + # that autodoc can discover references to it. + import _pytest.legacypath # noqa: F401 diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 602fe1de8..cebcd1c3c 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -1 +1,254 @@ """Add backward compatibility support for the legacy py path type.""" +import subprocess +from typing import List +from typing import Optional +from typing import TYPE_CHECKING +from typing import Union + +from iniconfig import SectionWrapper + +import pytest +from _pytest.compat import final +from _pytest.compat import LEGACY_PATH +from _pytest.compat import legacy_path +from _pytest.deprecated import check_ispytest + +if TYPE_CHECKING: + from typing_extensions import Final + + import pexpect + + +@final +class Testdir: + """ + Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead. + + All methods just forward to an internal :class:`Pytester` instance, converting results + to `legacy_path` objects as necessary. + """ + + __test__ = False + + CLOSE_STDIN: "Final" = pytest.Pytester.CLOSE_STDIN + TimeoutExpired: "Final" = pytest.Pytester.TimeoutExpired + + def __init__(self, pytester: pytest.Pytester, *, _ispytest: bool = False) -> None: + check_ispytest(_ispytest) + self._pytester = pytester + + @property + def tmpdir(self) -> LEGACY_PATH: + """Temporary directory where tests are executed.""" + return legacy_path(self._pytester.path) + + @property + def test_tmproot(self) -> LEGACY_PATH: + return legacy_path(self._pytester._test_tmproot) + + @property + def request(self): + return self._pytester._request + + @property + def plugins(self): + return self._pytester.plugins + + @plugins.setter + def plugins(self, plugins): + self._pytester.plugins = plugins + + @property + def monkeypatch(self) -> pytest.MonkeyPatch: + return self._pytester._monkeypatch + + def make_hook_recorder(self, pluginmanager) -> pytest.HookRecorder: + """See :meth:`Pytester.make_hook_recorder`.""" + return self._pytester.make_hook_recorder(pluginmanager) + + def chdir(self) -> None: + """See :meth:`Pytester.chdir`.""" + return self._pytester.chdir() + + def finalize(self) -> None: + """See :meth:`Pytester._finalize`.""" + return self._pytester._finalize() + + def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH: + """See :meth:`Pytester.makefile`.""" + if ext and not ext.startswith("."): + # pytester.makefile is going to throw a ValueError in a way that + # testdir.makefile did not, because + # pathlib.Path is stricter suffixes than py.path + # This ext arguments is likely user error, but since testdir has + # allowed this, we will prepend "." as a workaround to avoid breaking + # testdir usage that worked before + ext = "." + ext + return legacy_path(self._pytester.makefile(ext, *args, **kwargs)) + + def makeconftest(self, source) -> LEGACY_PATH: + """See :meth:`Pytester.makeconftest`.""" + return legacy_path(self._pytester.makeconftest(source)) + + def makeini(self, source) -> LEGACY_PATH: + """See :meth:`Pytester.makeini`.""" + return legacy_path(self._pytester.makeini(source)) + + def getinicfg(self, source: str) -> SectionWrapper: + """See :meth:`Pytester.getinicfg`.""" + return self._pytester.getinicfg(source) + + def makepyprojecttoml(self, source) -> LEGACY_PATH: + """See :meth:`Pytester.makepyprojecttoml`.""" + return legacy_path(self._pytester.makepyprojecttoml(source)) + + def makepyfile(self, *args, **kwargs) -> LEGACY_PATH: + """See :meth:`Pytester.makepyfile`.""" + return legacy_path(self._pytester.makepyfile(*args, **kwargs)) + + def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH: + """See :meth:`Pytester.maketxtfile`.""" + return legacy_path(self._pytester.maketxtfile(*args, **kwargs)) + + def syspathinsert(self, path=None) -> None: + """See :meth:`Pytester.syspathinsert`.""" + return self._pytester.syspathinsert(path) + + def mkdir(self, name) -> LEGACY_PATH: + """See :meth:`Pytester.mkdir`.""" + return legacy_path(self._pytester.mkdir(name)) + + def mkpydir(self, name) -> LEGACY_PATH: + """See :meth:`Pytester.mkpydir`.""" + return legacy_path(self._pytester.mkpydir(name)) + + def copy_example(self, name=None) -> LEGACY_PATH: + """See :meth:`Pytester.copy_example`.""" + return legacy_path(self._pytester.copy_example(name)) + + def getnode( + self, config: pytest.Config, arg + ) -> Optional[Union[pytest.Item, pytest.Collector]]: + """See :meth:`Pytester.getnode`.""" + return self._pytester.getnode(config, arg) + + def getpathnode(self, path): + """See :meth:`Pytester.getpathnode`.""" + return self._pytester.getpathnode(path) + + def genitems( + self, colitems: List[Union[pytest.Item, pytest.Collector]] + ) -> List[pytest.Item]: + """See :meth:`Pytester.genitems`.""" + return self._pytester.genitems(colitems) + + def runitem(self, source): + """See :meth:`Pytester.runitem`.""" + return self._pytester.runitem(source) + + def inline_runsource(self, source, *cmdlineargs): + """See :meth:`Pytester.inline_runsource`.""" + return self._pytester.inline_runsource(source, *cmdlineargs) + + def inline_genitems(self, *args): + """See :meth:`Pytester.inline_genitems`.""" + return self._pytester.inline_genitems(*args) + + def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): + """See :meth:`Pytester.inline_run`.""" + return self._pytester.inline_run( + *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc + ) + + def runpytest_inprocess(self, *args, **kwargs) -> pytest.RunResult: + """See :meth:`Pytester.runpytest_inprocess`.""" + return self._pytester.runpytest_inprocess(*args, **kwargs) + + def runpytest(self, *args, **kwargs) -> pytest.RunResult: + """See :meth:`Pytester.runpytest`.""" + return self._pytester.runpytest(*args, **kwargs) + + def parseconfig(self, *args) -> pytest.Config: + """See :meth:`Pytester.parseconfig`.""" + return self._pytester.parseconfig(*args) + + def parseconfigure(self, *args) -> pytest.Config: + """See :meth:`Pytester.parseconfigure`.""" + return self._pytester.parseconfigure(*args) + + def getitem(self, source, funcname="test_func"): + """See :meth:`Pytester.getitem`.""" + return self._pytester.getitem(source, funcname) + + def getitems(self, source): + """See :meth:`Pytester.getitems`.""" + return self._pytester.getitems(source) + + def getmodulecol(self, source, configargs=(), withinit=False): + """See :meth:`Pytester.getmodulecol`.""" + return self._pytester.getmodulecol( + source, configargs=configargs, withinit=withinit + ) + + def collect_by_name( + self, modcol: pytest.Collector, name: str + ) -> Optional[Union[pytest.Item, pytest.Collector]]: + """See :meth:`Pytester.collect_by_name`.""" + return self._pytester.collect_by_name(modcol, name) + + def popen( + self, + cmdargs, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=CLOSE_STDIN, + **kw, + ): + """See :meth:`Pytester.popen`.""" + return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw) + + def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> pytest.RunResult: + """See :meth:`Pytester.run`.""" + return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin) + + def runpython(self, script) -> pytest.RunResult: + """See :meth:`Pytester.runpython`.""" + return self._pytester.runpython(script) + + def runpython_c(self, command): + """See :meth:`Pytester.runpython_c`.""" + return self._pytester.runpython_c(command) + + def runpytest_subprocess(self, *args, timeout=None) -> pytest.RunResult: + """See :meth:`Pytester.runpytest_subprocess`.""" + return self._pytester.runpytest_subprocess(*args, timeout=timeout) + + def spawn_pytest( + self, string: str, expect_timeout: float = 10.0 + ) -> "pexpect.spawn": + """See :meth:`Pytester.spawn_pytest`.""" + return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout) + + def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": + """See :meth:`Pytester.spawn`.""" + return self._pytester.spawn(cmd, expect_timeout=expect_timeout) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return str(self.tmpdir) + + +pytest.Testdir = Testdir # type: ignore[attr-defined] + + +@pytest.fixture +def testdir(pytester: pytest.Pytester) -> Testdir: + """ + Identical to :fixture:`pytester`, and provides an instance whose methods return + legacy ``LEGACY_PATH`` objects instead when applicable. + + New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. + """ + return Testdir(pytester, _ispytest=True) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 146606976..42e71ff91 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -40,8 +40,6 @@ from _pytest import timing from _pytest._code import Source from _pytest.capture import _get_multicapture from _pytest.compat import final -from _pytest.compat import LEGACY_PATH -from _pytest.compat import legacy_path from _pytest.compat import NOTSET from _pytest.compat import NotSetType from _pytest.config import _PluggyPlugin @@ -493,17 +491,6 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt return Pytester(request, tmp_path_factory, _ispytest=True) -@fixture -def testdir(pytester: "Pytester") -> "Testdir": - """ - Identical to :fixture:`pytester`, and provides an instance whose methods return - legacy ``LEGACY_PATH`` objects instead when applicable. - - New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. - """ - return Testdir(pytester, _ispytest=True) - - @fixture def _sys_snapshot() -> Generator[None, None, None]: snappaths = SysPathsSnapshot() @@ -1531,224 +1518,6 @@ class LineComp: LineMatcher(lines1).fnmatch_lines(lines2) -@final -class Testdir: - """ - Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead. - - All methods just forward to an internal :class:`Pytester` instance, converting results - to `legacy_path` objects as necessary. - """ - - __test__ = False - - CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN - TimeoutExpired: "Final" = Pytester.TimeoutExpired - - def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None: - check_ispytest(_ispytest) - self._pytester = pytester - - @property - def tmpdir(self) -> LEGACY_PATH: - """Temporary directory where tests are executed.""" - return legacy_path(self._pytester.path) - - @property - def test_tmproot(self) -> LEGACY_PATH: - return legacy_path(self._pytester._test_tmproot) - - @property - def request(self): - return self._pytester._request - - @property - def plugins(self): - return self._pytester.plugins - - @plugins.setter - def plugins(self, plugins): - self._pytester.plugins = plugins - - @property - def monkeypatch(self) -> MonkeyPatch: - return self._pytester._monkeypatch - - def make_hook_recorder(self, pluginmanager) -> HookRecorder: - """See :meth:`Pytester.make_hook_recorder`.""" - return self._pytester.make_hook_recorder(pluginmanager) - - def chdir(self) -> None: - """See :meth:`Pytester.chdir`.""" - return self._pytester.chdir() - - def finalize(self) -> None: - """See :meth:`Pytester._finalize`.""" - return self._pytester._finalize() - - def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH: - """See :meth:`Pytester.makefile`.""" - if ext and not ext.startswith("."): - # pytester.makefile is going to throw a ValueError in a way that - # testdir.makefile did not, because - # pathlib.Path is stricter suffixes than py.path - # This ext arguments is likely user error, but since testdir has - # allowed this, we will prepend "." as a workaround to avoid breaking - # testdir usage that worked before - ext = "." + ext - return legacy_path(self._pytester.makefile(ext, *args, **kwargs)) - - def makeconftest(self, source) -> LEGACY_PATH: - """See :meth:`Pytester.makeconftest`.""" - return legacy_path(self._pytester.makeconftest(source)) - - def makeini(self, source) -> LEGACY_PATH: - """See :meth:`Pytester.makeini`.""" - return legacy_path(self._pytester.makeini(source)) - - def getinicfg(self, source: str) -> SectionWrapper: - """See :meth:`Pytester.getinicfg`.""" - return self._pytester.getinicfg(source) - - def makepyprojecttoml(self, source) -> LEGACY_PATH: - """See :meth:`Pytester.makepyprojecttoml`.""" - return legacy_path(self._pytester.makepyprojecttoml(source)) - - def makepyfile(self, *args, **kwargs) -> LEGACY_PATH: - """See :meth:`Pytester.makepyfile`.""" - return legacy_path(self._pytester.makepyfile(*args, **kwargs)) - - def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH: - """See :meth:`Pytester.maketxtfile`.""" - return legacy_path(self._pytester.maketxtfile(*args, **kwargs)) - - def syspathinsert(self, path=None) -> None: - """See :meth:`Pytester.syspathinsert`.""" - return self._pytester.syspathinsert(path) - - def mkdir(self, name) -> LEGACY_PATH: - """See :meth:`Pytester.mkdir`.""" - return legacy_path(self._pytester.mkdir(name)) - - def mkpydir(self, name) -> LEGACY_PATH: - """See :meth:`Pytester.mkpydir`.""" - return legacy_path(self._pytester.mkpydir(name)) - - def copy_example(self, name=None) -> LEGACY_PATH: - """See :meth:`Pytester.copy_example`.""" - return legacy_path(self._pytester.copy_example(name)) - - def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]: - """See :meth:`Pytester.getnode`.""" - return self._pytester.getnode(config, arg) - - def getpathnode(self, path): - """See :meth:`Pytester.getpathnode`.""" - return self._pytester.getpathnode(path) - - def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]: - """See :meth:`Pytester.genitems`.""" - return self._pytester.genitems(colitems) - - def runitem(self, source): - """See :meth:`Pytester.runitem`.""" - return self._pytester.runitem(source) - - def inline_runsource(self, source, *cmdlineargs): - """See :meth:`Pytester.inline_runsource`.""" - return self._pytester.inline_runsource(source, *cmdlineargs) - - def inline_genitems(self, *args): - """See :meth:`Pytester.inline_genitems`.""" - return self._pytester.inline_genitems(*args) - - def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): - """See :meth:`Pytester.inline_run`.""" - return self._pytester.inline_run( - *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc - ) - - def runpytest_inprocess(self, *args, **kwargs) -> RunResult: - """See :meth:`Pytester.runpytest_inprocess`.""" - return self._pytester.runpytest_inprocess(*args, **kwargs) - - def runpytest(self, *args, **kwargs) -> RunResult: - """See :meth:`Pytester.runpytest`.""" - return self._pytester.runpytest(*args, **kwargs) - - def parseconfig(self, *args) -> Config: - """See :meth:`Pytester.parseconfig`.""" - return self._pytester.parseconfig(*args) - - def parseconfigure(self, *args) -> Config: - """See :meth:`Pytester.parseconfigure`.""" - return self._pytester.parseconfigure(*args) - - def getitem(self, source, funcname="test_func"): - """See :meth:`Pytester.getitem`.""" - return self._pytester.getitem(source, funcname) - - def getitems(self, source): - """See :meth:`Pytester.getitems`.""" - return self._pytester.getitems(source) - - def getmodulecol(self, source, configargs=(), withinit=False): - """See :meth:`Pytester.getmodulecol`.""" - return self._pytester.getmodulecol( - source, configargs=configargs, withinit=withinit - ) - - def collect_by_name( - self, modcol: Collector, name: str - ) -> Optional[Union[Item, Collector]]: - """See :meth:`Pytester.collect_by_name`.""" - return self._pytester.collect_by_name(modcol, name) - - def popen( - self, - cmdargs, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=CLOSE_STDIN, - **kw, - ): - """See :meth:`Pytester.popen`.""" - return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw) - - def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: - """See :meth:`Pytester.run`.""" - return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin) - - def runpython(self, script) -> RunResult: - """See :meth:`Pytester.runpython`.""" - return self._pytester.runpython(script) - - def runpython_c(self, command): - """See :meth:`Pytester.runpython_c`.""" - return self._pytester.runpython_c(command) - - def runpytest_subprocess(self, *args, timeout=None) -> RunResult: - """See :meth:`Pytester.runpytest_subprocess`.""" - return self._pytester.runpytest_subprocess(*args, timeout=timeout) - - def spawn_pytest( - self, string: str, expect_timeout: float = 10.0 - ) -> "pexpect.spawn": - """See :meth:`Pytester.spawn_pytest`.""" - return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout) - - def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": - """See :meth:`Pytester.spawn`.""" - return self._pytester.spawn(cmd, expect_timeout=expect_timeout) - - def __repr__(self) -> str: - return f"" - - def __str__(self) -> str: - return str(self.tmpdir) - - -@final class LineMatcher: """Flexible matching of text. diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 37ef8fda3..3d19680db 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -46,7 +46,6 @@ from _pytest.pytester import LineMatcher from _pytest.pytester import Pytester from _pytest.pytester import RecordedHookCall from _pytest.pytester import RunResult -from _pytest.pytester import Testdir from _pytest.python import Class from _pytest.python import Function from _pytest.python import Instance @@ -145,7 +144,6 @@ __all__ = [ "StashKey", "version_tuple", "TempPathFactory", - "Testdir", "TempdirFactory", "UsageError", "WarningsRecorder", diff --git a/testing/test_collection.py b/testing/test_collection.py index 6532959cb..98e5ec070 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -614,7 +614,6 @@ class TestSession: items2, hookrec = pytester.inline_genitems(item.nodeid) (item2,) = items2 assert item2.name == item.name - assert item2.fspath == item.fspath assert item2.path == item.path def test_find_byid_without_instance_parents(self, pytester: Pytester) -> None: diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py new file mode 100644 index 000000000..0550fe7dc --- /dev/null +++ b/testing/test_legacypath.py @@ -0,0 +1,27 @@ +import pytest +from _pytest.legacypath import Testdir + + +def test_testdir_testtmproot(testdir: Testdir) -> None: + """Check test_tmproot is a py.path attribute for backward compatibility.""" + assert testdir.test_tmproot.check(dir=1) + + +def test_testdir_makefile_dot_prefixes_extension_silently( + testdir: Testdir, +) -> None: + """For backwards compat #8192""" + p1 = testdir.makefile("foo.bar", "") + assert ".foo.bar" in str(p1) + + +def test_testdir_makefile_ext_none_raises_type_error(testdir: Testdir) -> None: + """For backwards compat #8192""" + with pytest.raises(TypeError): + testdir.makefile(None, "") + + +def test_testdir_makefile_ext_empty_string_makes_file(testdir: Testdir) -> None: + """For backwards compat #8192""" + p1 = testdir.makefile("", "") + assert "test_testdir_makefile" in str(p1) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 15d777d1f..bc6e52aba 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -17,7 +17,6 @@ from _pytest.pytester import LineMatcher from _pytest.pytester import Pytester from _pytest.pytester import SysModulesSnapshot from _pytest.pytester import SysPathsSnapshot -from _pytest.pytester import Testdir def test_make_hook_recorder(pytester: Pytester) -> None: @@ -814,19 +813,6 @@ def test_makefile_joins_absolute_path(pytester: Pytester) -> None: assert str(p1) == str(pytester.path / "absfile.py") -def test_testtmproot(testdir) -> None: - """Check test_tmproot is a py.path attribute for backward compatibility.""" - assert testdir.test_tmproot.check(dir=1) - - -def test_testdir_makefile_dot_prefixes_extension_silently( - testdir: Testdir, -) -> None: - """For backwards compat #8192""" - p1 = testdir.makefile("foo.bar", "") - assert ".foo.bar" in str(p1) - - def test_pytester_makefile_dot_prefixes_extension_with_warning( pytester: Pytester, ) -> None: @@ -837,18 +823,6 @@ def test_pytester_makefile_dot_prefixes_extension_with_warning( pytester.makefile("foo.bar", "") -def test_testdir_makefile_ext_none_raises_type_error(testdir) -> None: - """For backwards compat #8192""" - with pytest.raises(TypeError): - testdir.makefile(None, "") - - -def test_testdir_makefile_ext_empty_string_makes_file(testdir) -> None: - """For backwards compat #8192""" - p1 = testdir.makefile("", "") - assert "test_testdir_makefile" in str(p1) - - @pytest.mark.filterwarnings("default") def test_pytester_assert_outcomes_warnings(pytester: Pytester) -> None: pytester.makepyfile( From 5e883f51959f900fcf985493ce2d09e07bad7e00 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Oct 2021 10:37:02 +0300 Subject: [PATCH 03/11] Move tmpdir to legacypath plugin --- doc/en/reference/reference.rst | 2 +- src/_pytest/legacypath.py | 72 ++++++++++++++++++++++++++++++++++ src/_pytest/tmpdir.py | 52 ------------------------ src/pytest/__init__.py | 2 - testing/test_legacypath.py | 42 ++++++++++++++++++++ testing/test_tmpdir.py | 24 +----------- 6 files changed, 117 insertions(+), 77 deletions(-) diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 304c789de..9bd242c0b 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -638,7 +638,7 @@ tmpdir :ref:`tmpdir and tmpdir_factory` -.. autofunction:: _pytest.tmpdir.tmpdir() +.. autofunction:: _pytest.legacypath.tmpdir() :no-auto-options: diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index cebcd1c3c..e8a239cf2 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -1,10 +1,12 @@ """Add backward compatibility support for the legacy py path type.""" import subprocess +from pathlib import Path from typing import List from typing import Optional from typing import TYPE_CHECKING from typing import Union +import attr from iniconfig import SectionWrapper import pytest @@ -252,3 +254,73 @@ def testdir(pytester: pytest.Pytester) -> Testdir: New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. """ return Testdir(pytester, _ispytest=True) + + +@final +@attr.s(init=False, auto_attribs=True) +class TempdirFactory: + """Backward compatibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH`` + for :class:``TempPathFactory``.""" + + _tmppath_factory: pytest.TempPathFactory + + def __init__( + self, tmppath_factory: pytest.TempPathFactory, *, _ispytest: bool = False + ) -> None: + check_ispytest(_ispytest) + self._tmppath_factory = tmppath_factory + + def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH: + """Same as :meth:`TempPathFactory.mktemp`, but returns a ``_pytest.compat.LEGACY_PATH`` object.""" + return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve()) + + def getbasetemp(self) -> LEGACY_PATH: + """Backward compat wrapper for ``_tmppath_factory.getbasetemp``.""" + return legacy_path(self._tmppath_factory.getbasetemp().resolve()) + + +pytest.TempdirFactory = TempdirFactory # type: ignore[attr-defined] + + +@pytest.fixture(scope="session") +def tmpdir_factory(request: pytest.FixtureRequest) -> TempdirFactory: + """Return a :class:`pytest.TempdirFactory` instance for the test session.""" + # Set dynamically by pytest_configure(). + return request.config._tmpdirhandler # type: ignore + + +@pytest.fixture +def tmpdir(tmp_path: Path) -> LEGACY_PATH: + """Return a temporary directory path object which is unique to each test + function invocation, created as a sub directory of the base temporary + directory. + + By default, a new base temporary directory is created each test session, + and old bases are removed after 3 sessions, to aid in debugging. If + ``--basetemp`` is used then it is cleared each session. See :ref:`base + temporary directory`. + + The returned object is a `legacy_path`_ object. + + .. _legacy_path: https://py.readthedocs.io/en/latest/path.html + """ + return legacy_path(tmp_path) + + +def pytest_configure(config: pytest.Config) -> None: + mp = pytest.MonkeyPatch() + config.add_cleanup(mp.undo) + + # Create TmpdirFactory and attach it to the config object. + # + # This is to comply with existing plugins which expect the handler to be + # available at pytest_configure time, but ideally should be moved entirely + # to the tmpdir_factory session fixture. + try: + tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined] + except AttributeError: + # tmpdir plugin is blocked. + pass + else: + _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True) + mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 905cf0e5a..f901fd572 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -13,8 +13,6 @@ from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup from .pathlib import rm_rf from _pytest.compat import final -from _pytest.compat import LEGACY_PATH -from _pytest.compat import legacy_path from _pytest.config import Config from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture @@ -157,29 +155,6 @@ class TempPathFactory: return basetemp -@final -@attr.s(init=False, auto_attribs=True) -class TempdirFactory: - """Backward compatibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH`` - for :class:``TempPathFactory``.""" - - _tmppath_factory: 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) -> LEGACY_PATH: - """Same as :meth:`TempPathFactory.mktemp`, but returns a ``_pytest.compat.LEGACY_PATH`` object.""" - return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve()) - - def getbasetemp(self) -> LEGACY_PATH: - """Backward compat wrapper for ``_tmppath_factory.getbasetemp``.""" - return legacy_path(self._tmppath_factory.getbasetemp().resolve()) - - def get_user() -> Optional[str]: """Return the current user name, or None if getuser() does not work in the current environment (see #1010).""" @@ -201,16 +176,7 @@ def pytest_configure(config: Config) -> None: mp = MonkeyPatch() config.add_cleanup(mp.undo) _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True) - _tmpdirhandler = TempdirFactory(_tmp_path_factory, _ispytest=True) mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False) - mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False) - - -@fixture(scope="session") -def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: - """Return a :class:`pytest.TempdirFactory` instance for the test session.""" - # Set dynamically by pytest_configure() above. - return request.config._tmpdirhandler # type: ignore @fixture(scope="session") @@ -228,24 +194,6 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: return factory.mktemp(name, numbered=True) -@fixture -def tmpdir(tmp_path: Path) -> LEGACY_PATH: - """Return a temporary directory path object which is unique to each test - function invocation, created as a sub directory of the base temporary - directory. - - By default, a new base temporary directory is created each test session, - and old bases are removed after 3 sessions, to aid in debugging. If - ``--basetemp`` is used then it is cleared each session. See :ref:`base - temporary directory`. - - The returned object is a `legacy_path`_ object. - - .. _legacy_path: https://py.readthedocs.io/en/latest/path.html - """ - return legacy_path(tmp_path) - - @fixture def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path: """Return a temporary directory path object which is unique to each test diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 3d19680db..83a0df11c 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -60,7 +60,6 @@ from _pytest.recwarn import warns from _pytest.runner import CallInfo from _pytest.stash import Stash from _pytest.stash import StashKey -from _pytest.tmpdir import TempdirFactory from _pytest.tmpdir import TempPathFactory from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warning_types import PytestCacheWarning @@ -144,7 +143,6 @@ __all__ = [ "StashKey", "version_tuple", "TempPathFactory", - "TempdirFactory", "UsageError", "WarningsRecorder", "warns", diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index 0550fe7dc..89c4fa862 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -1,4 +1,8 @@ +from pathlib import Path + import pytest +from _pytest.compat import LEGACY_PATH +from _pytest.legacypath import TempdirFactory from _pytest.legacypath import Testdir @@ -25,3 +29,41 @@ def test_testdir_makefile_ext_empty_string_makes_file(testdir: Testdir) -> None: """For backwards compat #8192""" p1 = testdir.makefile("", "") assert "test_testdir_makefile" in str(p1) + + +def attempt_symlink_to(path: str, to_path: str) -> None: + """Try to make a symlink from "path" to "to_path", skipping in case this platform + does not support it or we don't have sufficient privileges (common on Windows).""" + try: + Path(path).symlink_to(Path(to_path)) + except OSError: + pytest.skip("could not create symbolic link") + + +def test_tmpdir_factory( + tmpdir_factory: TempdirFactory, + tmp_path_factory: pytest.TempPathFactory, +) -> None: + assert str(tmpdir_factory.getbasetemp()) == str(tmp_path_factory.getbasetemp()) + dir = tmpdir_factory.mktemp("foo") + assert dir.exists() + + +def test_tmpdir_equals_tmp_path(tmpdir: LEGACY_PATH, tmp_path: Path) -> None: + assert Path(tmpdir) == tmp_path + + +def test_tmpdir_always_is_realpath(pytester: pytest.Pytester) -> None: + # See test_tmp_path_always_is_realpath. + realtemp = pytester.mkdir("myrealtemp") + linktemp = pytester.path.joinpath("symlinktemp") + attempt_symlink_to(str(linktemp), str(realtemp)) + p = pytester.makepyfile( + """ + def test_1(tmpdir): + import os + assert os.path.realpath(str(tmpdir)) == str(tmpdir) + """ + ) + result = pytester.runpytest("-s", p, "--basetemp=%s/bt" % linktemp) + assert not result.ret diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 4dff9dff0..4f7c53847 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -118,8 +118,8 @@ def test_mktemp(pytester: Pytester, basename: str, is_ok: bool) -> None: result.stdout.fnmatch_lines("*ValueError*") -def test_tmpdir_always_is_realpath(pytester: Pytester) -> None: - # the reason why tmpdir should be a realpath is that +def test_tmp_path_always_is_realpath(pytester: Pytester, monkeypatch) -> None: + # the reason why tmp_path should be a realpath is that # when you cd to it and do "os.getcwd()" you will anyway # get the realpath. Using the symlinked path can thus # easily result in path-inequality @@ -128,22 +128,6 @@ def test_tmpdir_always_is_realpath(pytester: Pytester) -> None: realtemp = pytester.mkdir("myrealtemp") linktemp = pytester.path.joinpath("symlinktemp") attempt_symlink_to(linktemp, str(realtemp)) - p = pytester.makepyfile( - """ - def test_1(tmpdir): - import os - assert os.path.realpath(str(tmpdir)) == str(tmpdir) - """ - ) - result = pytester.runpytest("-s", p, "--basetemp=%s/bt" % linktemp) - assert not result.ret - - -def test_tmp_path_always_is_realpath(pytester: Pytester, monkeypatch) -> None: - # for reasoning see: test_tmpdir_always_is_realpath test-case - realtemp = pytester.mkdir("myrealtemp") - linktemp = pytester.path.joinpath("symlinktemp") - attempt_symlink_to(linktemp, str(realtemp)) monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(linktemp)) pytester.makepyfile( """ @@ -423,10 +407,6 @@ def attempt_symlink_to(path, to_path): pytest.skip("could not create symbolic link") -def test_tmpdir_equals_tmp_path(tmpdir, tmp_path): - assert Path(tmpdir) == tmp_path - - def test_basetemp_with_read_only_files(pytester: Pytester) -> None: """Integration test for #5524""" pytester.makepyfile( From a1a605a63ed01a1ff50d08540925df98d9564ec4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Oct 2021 10:39:46 +0300 Subject: [PATCH 04/11] Move Cache.makedir to legacypath plugin --- src/_pytest/cacheprovider.py | 9 --------- src/_pytest/legacypath.py | 11 +++++++++++ testing/test_legacypath.py | 6 ++++++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 78edf9ac5..681d02b40 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -20,8 +20,6 @@ from .reports import CollectReport from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.compat import final -from _pytest.compat import LEGACY_PATH -from _pytest.compat import legacy_path from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl @@ -142,13 +140,6 @@ class Cache: res.mkdir(exist_ok=True, parents=True) return res - def makedir(self, name: str) -> LEGACY_PATH: - """Return a directory path object with the given name. - - Same as :func:`mkdir`, but returns a legacy py path instance. - """ - return legacy_path(self.mkdir(name)) - def _getvaluepath(self, key: str) -> Path: return self._cachedir.joinpath(self._CACHE_PREFIX_VALUES, Path(key)) diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index e8a239cf2..d53a0d315 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -307,6 +307,14 @@ def tmpdir(tmp_path: Path) -> LEGACY_PATH: return legacy_path(tmp_path) +def Cache_makedir(self: pytest.Cache, name: str) -> LEGACY_PATH: + """Return a directory path object with the given name. + + Same as :func:`mkdir`, but returns a legacy py path instance. + """ + return legacy_path(self.mkdir(name)) + + def pytest_configure(config: pytest.Config) -> None: mp = pytest.MonkeyPatch() config.add_cleanup(mp.undo) @@ -324,3 +332,6 @@ def pytest_configure(config: pytest.Config) -> None: else: _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True) mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False) + + # Add Cache.makedir(). + mp.setattr(pytest.Cache, "makedir", Cache_makedir, raising=False) diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index 89c4fa862..04cd61f0e 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -67,3 +67,9 @@ def test_tmpdir_always_is_realpath(pytester: pytest.Pytester) -> None: ) result = pytester.runpytest("-s", p, "--basetemp=%s/bt" % linktemp) assert not result.ret + + +def test_cache_makedir(cache: pytest.Cache) -> None: + dir = cache.makedir("foo") # type: ignore[attr-defined] + assert dir.exists() + dir.remove() From 7c0011374c99a3738324db2c783d4e5d369cff23 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Oct 2021 10:45:05 +0300 Subject: [PATCH 05/11] Move FixtureRequest.fspath to legacypath plugin --- src/_pytest/fixtures.py | 7 ------- src/_pytest/legacypath.py | 10 ++++++++++ testing/python/fixtures.py | 6 ++---- testing/test_legacypath.py | 21 +++++++++++++++++++++ 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index bcb53552a..fddff931c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -46,8 +46,6 @@ from _pytest.compat import getfuncargnames from _pytest.compat import getimfunc from _pytest.compat import getlocation from _pytest.compat import is_generator -from _pytest.compat import LEGACY_PATH -from _pytest.compat import legacy_path from _pytest.compat import NOTSET from _pytest.compat import safe_getattr from _pytest.config import _PluggyPlugin @@ -528,11 +526,6 @@ class FixtureRequest: raise AttributeError(f"module not available in {self.scope}-scoped context") return self._pyfuncitem.getparent(_pytest.python.Module).obj - @property - def fspath(self) -> LEGACY_PATH: - """(deprecated) The file system path of the test module which collected this test.""" - return legacy_path(self.path) - @property def path(self) -> Path: if self.scope not in ("function", "class", "module", "package"): diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index d53a0d315..08d668f46 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -315,6 +315,11 @@ def Cache_makedir(self: pytest.Cache, name: str) -> LEGACY_PATH: return legacy_path(self.mkdir(name)) +def FixtureRequest_fspath(self: pytest.FixtureRequest) -> LEGACY_PATH: + """(deprecated) The file system path of the test module which collected this test.""" + return legacy_path(self.path) + + def pytest_configure(config: pytest.Config) -> None: mp = pytest.MonkeyPatch() config.add_cleanup(mp.undo) @@ -335,3 +340,8 @@ def pytest_configure(config: pytest.Config) -> None: # Add Cache.makedir(). mp.setattr(pytest.Cache, "makedir", Cache_makedir, raising=False) + + # Add FixtureRequest.fspath property. + mp.setattr( + pytest.FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False + ) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index ea66f50f0..d8708b9ed 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -967,7 +967,6 @@ class TestRequestBasic: (item,) = pytester.genitems([modcol]) req = fixtures.FixtureRequest(item, _ispytest=True) assert req.path == modcol.path - assert req.fspath == modcol.fspath def test_request_fixturenames(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -1098,12 +1097,11 @@ class TestRequestSessionScoped: def session_request(self, request): return request - @pytest.mark.parametrize("name", ["path", "fspath", "module"]) + @pytest.mark.parametrize("name", ["path", "module"]) def test_session_scoped_unavailable_attributes(self, session_request, name): - expected = "path" if name == "fspath" else name with pytest.raises( AttributeError, - match=f"{expected} not available in session-scoped context", + match=f"{name} not available in session-scoped context", ): getattr(session_request, name) diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index 04cd61f0e..ed0435c1c 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -73,3 +73,24 @@ def test_cache_makedir(cache: pytest.Cache) -> None: dir = cache.makedir("foo") # type: ignore[attr-defined] assert dir.exists() dir.remove() + + +def test_fixturerequest_getmodulepath(pytester: pytest.Pytester) -> None: + modcol = pytester.getmodulecol("def test_somefunc(): pass") + (item,) = pytester.genitems([modcol]) + req = pytest.FixtureRequest(item, _ispytest=True) + assert req.path == modcol.path + assert req.fspath == modcol.fspath # type: ignore[attr-defined] + + +class TestFixtureRequestSessionScoped: + @pytest.fixture(scope="session") + def session_request(self, request): + return request + + def test_session_scoped_unavailable_attributes(self, session_request): + with pytest.raises( + AttributeError, + match="path not available in session-scoped context", + ): + session_request.fspath From d979f82fb0f7a05a41d8afb45e2448665ab20a01 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Oct 2021 10:54:28 +0300 Subject: [PATCH 06/11] Move TerminalReporter.stardir to legacypath plugin --- src/_pytest/legacypath.py | 16 ++++++++++++++++ src/_pytest/terminal.py | 12 ------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 08d668f46..cac22927c 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -14,6 +14,7 @@ from _pytest.compat import final from _pytest.compat import LEGACY_PATH from _pytest.compat import legacy_path from _pytest.deprecated import check_ispytest +from _pytest.terminal import TerminalReporter if TYPE_CHECKING: from typing_extensions import Final @@ -320,6 +321,16 @@ def FixtureRequest_fspath(self: pytest.FixtureRequest) -> LEGACY_PATH: return legacy_path(self.path) +def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH: + """The directory from which pytest was invoked. + + Prefer to use ``startpath`` which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(self.startpath) + + def pytest_configure(config: pytest.Config) -> None: mp = pytest.MonkeyPatch() config.add_cleanup(mp.undo) @@ -345,3 +356,8 @@ def pytest_configure(config: pytest.Config) -> None: mp.setattr( pytest.FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False ) + + # Add TerminalReporter.startdir property. + mp.setattr( + TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False + ) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 8c5be3b78..ba540eef8 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -37,8 +37,6 @@ from _pytest._code import ExceptionInfo from _pytest._code.code import ExceptionRepr from _pytest._io.wcwidth import wcswidth from _pytest.compat import final -from _pytest.compat import LEGACY_PATH -from _pytest.compat import legacy_path from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode @@ -383,16 +381,6 @@ class TerminalReporter: def showlongtestinfo(self) -> bool: return self.verbosity > 0 - @property - def startdir(self) -> LEGACY_PATH: - """The directory from which pytest was invoked. - - Prefer to use ``startpath`` which is a :class:`pathlib.Path`. - - :type: LEGACY_PATH - """ - return legacy_path(self.startpath) - def hasopt(self, char: str) -> bool: char = {"xfailed": "x", "skipped": "s"}.get(char, char) return char in self.reportchars From 84722a7904af41191165dcb5458eb641fd2c4d8b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Oct 2021 10:58:31 +0300 Subject: [PATCH 07/11] Move Config.{invocation_dir,rootdir,inifile} to legacypath plugin --- src/_pytest/config/__init__.py | 32 ---------------------------- src/_pytest/legacypath.py | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 34a7a80e1..bee2fb22a 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -49,7 +49,6 @@ from _pytest._code import filter_traceback from _pytest._io import TerminalWriter from _pytest.compat import final from _pytest.compat import importlib_metadata -from _pytest.compat import LEGACY_PATH from _pytest.compat import legacy_path from _pytest.outcomes import fail from _pytest.outcomes import Skipped @@ -950,17 +949,6 @@ class Config: self.cache: Optional[Cache] = None - @property - def invocation_dir(self) -> LEGACY_PATH: - """The directory from which pytest was invoked. - - Prefer to use :attr:`invocation_params.dir `, - which is a :class:`pathlib.Path`. - - :type: LEGACY_PATH - """ - return legacy_path(str(self.invocation_params.dir)) - @property def rootpath(self) -> Path: """The path to the :ref:`rootdir `. @@ -971,16 +959,6 @@ class Config: """ return self._rootpath - @property - def rootdir(self) -> LEGACY_PATH: - """The path to the :ref:`rootdir `. - - Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. - - :type: LEGACY_PATH - """ - return legacy_path(str(self.rootpath)) - @property def inipath(self) -> Optional[Path]: """The path to the :ref:`configfile `. @@ -991,16 +969,6 @@ class Config: """ return self._inipath - @property - def inifile(self) -> Optional[LEGACY_PATH]: - """The path to the :ref:`configfile `. - - Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. - - :type: Optional[LEGACY_PATH] - """ - return legacy_path(str(self.inipath)) if self.inipath else None - def add_cleanup(self, func: Callable[[], None]) -> None: """Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index cac22927c..78afeb7d1 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -331,6 +331,37 @@ def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH: return legacy_path(self.startpath) +def Config_invocation_dir(self: pytest.Config) -> LEGACY_PATH: + """The directory from which pytest was invoked. + + Prefer to use :attr:`invocation_params.dir `, + which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(str(self.invocation_params.dir)) + + +def Config_rootdir(self: pytest.Config) -> LEGACY_PATH: + """The path to the :ref:`rootdir `. + + Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(str(self.rootpath)) + + +def Config_inifile(self: pytest.Config) -> Optional[LEGACY_PATH]: + """The path to the :ref:`configfile `. + + Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. + + :type: Optional[LEGACY_PATH] + """ + return legacy_path(str(self.inipath)) if self.inipath else None + + def pytest_configure(config: pytest.Config) -> None: mp = pytest.MonkeyPatch() config.add_cleanup(mp.undo) @@ -361,3 +392,10 @@ def pytest_configure(config: pytest.Config) -> None: mp.setattr( TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False ) + + # Add Config.{invocation_dir,rootdir,inifile} properties. + mp.setattr( + pytest.Config, "invocation_dir", property(Config_invocation_dir), raising=False + ) + mp.setattr(pytest.Config, "rootdir", property(Config_rootdir), raising=False) + mp.setattr(pytest.Config, "inifile", property(Config_inifile), raising=False) From d9ca55c648266afff0fb3820704d69289faa10d2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Oct 2021 11:01:18 +0300 Subject: [PATCH 08/11] Move Session.startdir to legacypath plugin --- src/_pytest/legacypath.py | 13 +++++++++++++ src/_pytest/main.py | 12 ------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 78afeb7d1..3fca65b25 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -362,6 +362,16 @@ def Config_inifile(self: pytest.Config) -> Optional[LEGACY_PATH]: return legacy_path(str(self.inipath)) if self.inipath else None +def Session_stardir(self: pytest.Session) -> LEGACY_PATH: + """The path from which pytest was invoked. + + Prefer to use ``startpath`` which is a :class:`pathlib.Path`. + + :type: LEGACY_PATH + """ + return legacy_path(self.startpath) + + def pytest_configure(config: pytest.Config) -> None: mp = pytest.MonkeyPatch() config.add_cleanup(mp.undo) @@ -399,3 +409,6 @@ def pytest_configure(config: pytest.Config) -> None: ) mp.setattr(pytest.Config, "rootdir", property(Config_rootdir), raising=False) mp.setattr(pytest.Config, "inifile", property(Config_inifile), raising=False) + + # Add Session.startdir property. + mp.setattr(pytest.Session, "startdir", property(Session_stardir), raising=False) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index c48222409..b10794e57 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -25,8 +25,6 @@ import attr import _pytest._code from _pytest import nodes from _pytest.compat import final -from _pytest.compat import LEGACY_PATH -from _pytest.compat import legacy_path from _pytest.config import Config from _pytest.config import directory_arg from _pytest.config import ExitCode @@ -504,16 +502,6 @@ class Session(nodes.FSCollector): """ return self.config.invocation_params.dir - @property - def stardir(self) -> LEGACY_PATH: - """The path from which pytest was invoked. - - Prefer to use ``startpath`` which is a :class:`pathlib.Path`. - - :type: LEGACY_PATH - """ - return legacy_path(self.startpath) - def _node_location_to_relpath(self, node_path: Path) -> str: # bestrelpath is a quite slow function. return self._bestrelpathcache[node_path] From ce7cff9f8e29d691a272fd057a3a1053f222805b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Oct 2021 11:21:02 +0300 Subject: [PATCH 09/11] Move pathlist support to legacypath plugin --- src/_pytest/config/__init__.py | 22 +++++++------- src/_pytest/legacypath.py | 17 +++++++++++ testing/test_config.py | 23 +++++--------- testing/test_legacypath.py | 55 ++++++++++++++++++++++++++++++++++ 4 files changed, 92 insertions(+), 25 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index bee2fb22a..eadce78fa 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -49,7 +49,6 @@ from _pytest._code import filter_traceback from _pytest._io import TerminalWriter from _pytest.compat import final from _pytest.compat import importlib_metadata -from _pytest.compat import legacy_path from _pytest.outcomes import fail from _pytest.outcomes import Skipped from _pytest.pathlib import absolutepath @@ -1369,6 +1368,12 @@ class Config: self._inicache[name] = val = self._getini(name) return val + # Meant for easy monkeypatching by legacypath plugin. + # Can be inlined back (with no cover removed) once legacypath is gone. + def _getini_unknown_type(self, name: str, type: str, value: Union[str, List[str]]): + msg = f"unknown configuration type: {type}" + raise ValueError(msg, value) # pragma: no cover + def _getini(self, name: str): try: description, type, default = self._parser._inidict[name] @@ -1401,13 +1406,7 @@ class Config: # a_line_list = ["tests", "acceptance"] # in this case, we already have a list ready to use. # - if type == "pathlist": - # TODO: This assert is probably not valid in all cases. - assert self.inipath is not None - dp = self.inipath.parent - input_values = shlex.split(value) if isinstance(value, str) else value - return [legacy_path(str(dp / x)) for x in input_values] - elif type == "paths": + if type == "paths": # TODO: This assert is probably not valid in all cases. assert self.inipath is not None dp = self.inipath.parent @@ -1422,9 +1421,12 @@ class Config: return value elif type == "bool": return _strtobool(str(value).strip()) - else: - assert type in [None, "string"] + elif type == "string": return value + elif type is None: + return value + else: + return self._getini_unknown_type(name, type, value) def _getconftest_pathlist( self, name: str, path: Path, rootpath: Path diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 3fca65b25..2af20856a 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -1,4 +1,5 @@ """Add backward compatibility support for the legacy py path type.""" +import shlex import subprocess from pathlib import Path from typing import List @@ -372,6 +373,19 @@ def Session_stardir(self: pytest.Session) -> LEGACY_PATH: return legacy_path(self.startpath) +def Config__getini_unknown_type( + self, name: str, type: str, value: Union[str, List[str]] +): + if type == "pathlist": + # TODO: This assert is probably not valid in all cases. + assert self.inipath is not None + dp = self.inipath.parent + input_values = shlex.split(value) if isinstance(value, str) else value + return [legacy_path(str(dp / x)) for x in input_values] + else: + raise ValueError(f"unknown configuration type: {type}", value) + + def pytest_configure(config: pytest.Config) -> None: mp = pytest.MonkeyPatch() config.add_cleanup(mp.undo) @@ -412,3 +426,6 @@ def pytest_configure(config: pytest.Config) -> None: # Add Session.startdir property. mp.setattr(pytest.Session, "startdir", property(Session_stardir), raising=False) + + # Add pathlist configuration type. + mp.setattr(pytest.Config, "_getini_unknown_type", Config__getini_unknown_type) diff --git a/testing/test_config.py b/testing/test_config.py index 766d5a849..443559116 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -635,14 +635,11 @@ class TestConfigAPI: pytest.raises(ValueError, config.getini, "other") @pytest.mark.parametrize("config_type", ["ini", "pyproject"]) - @pytest.mark.parametrize("ini_type", ["paths", "pathlist"]) - def test_addini_paths( - self, pytester: Pytester, config_type: str, ini_type: str - ) -> None: + def test_addini_paths(self, pytester: Pytester, config_type: str) -> None: pytester.makeconftest( - f""" + """ def pytest_addoption(parser): - parser.addini("paths", "my new ini value", type="{ini_type}") + parser.addini("paths", "my new ini value", type="paths") parser.addini("abc", "abc value") """ ) @@ -1521,12 +1518,11 @@ class TestOverrideIniArgs: assert result.ret == 0 result.stdout.fnmatch_lines(["custom_option:3.0"]) - @pytest.mark.parametrize("ini_type", ["paths", "pathlist"]) - def test_override_ini_paths(self, pytester: Pytester, ini_type: str) -> None: + def test_override_ini_paths(self, pytester: Pytester) -> None: pytester.makeconftest( - f""" + """ def pytest_addoption(parser): - parser.addini("paths", "my new ini value", type="{ini_type}")""" + parser.addini("paths", "my new ini value", type="paths")""" ) pytester.makeini( """ @@ -1534,15 +1530,12 @@ class TestOverrideIniArgs: paths=blah.py""" ) pytester.makepyfile( - rf""" + r""" def test_overriden(pytestconfig): config_paths = pytestconfig.getini("paths") print(config_paths) for cpf in config_paths: - if "{ini_type}" == "pathlist": - print('\nuser_path:%s' % cpf.basename) - else: - print('\nuser_path:%s' % cpf.name) + print('\nuser_path:%s' % cpf.name) """ ) result = pytester.runpytest( diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index ed0435c1c..08568d8f7 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -94,3 +94,58 @@ class TestFixtureRequestSessionScoped: match="path not available in session-scoped context", ): session_request.fspath + + +@pytest.mark.parametrize("config_type", ["ini", "pyproject"]) +def test_addini_paths(pytester: pytest.Pytester, config_type: str) -> None: + pytester.makeconftest( + """ + def pytest_addoption(parser): + parser.addini("paths", "my new ini value", type="pathlist") + parser.addini("abc", "abc value") + """ + ) + if config_type == "ini": + inipath = pytester.makeini( + """ + [pytest] + paths=hello world/sub.py + """ + ) + elif config_type == "pyproject": + inipath = pytester.makepyprojecttoml( + """ + [tool.pytest.ini_options] + paths=["hello", "world/sub.py"] + """ + ) + config = pytester.parseconfig() + values = config.getini("paths") + assert len(values) == 2 + assert values[0] == inipath.parent.joinpath("hello") + assert values[1] == inipath.parent.joinpath("world/sub.py") + pytest.raises(ValueError, config.getini, "other") + + +def test_override_ini_paths(pytester: pytest.Pytester) -> None: + pytester.makeconftest( + """ + def pytest_addoption(parser): + parser.addini("paths", "my new ini value", type="pathlist")""" + ) + pytester.makeini( + """ + [pytest] + paths=blah.py""" + ) + pytester.makepyfile( + r""" + def test_overriden(pytestconfig): + config_paths = pytestconfig.getini("paths") + print(config_paths) + for cpf in config_paths: + print('\nuser_path:%s' % cpf.basename) + """ + ) + result = pytester.runpytest("--override-ini", "paths=foo/bar1.py foo/bar2.py", "-s") + result.stdout.fnmatch_lines(["user_path:bar1.py", "user_path:bar2.py"]) From c3dff755af7f603e6ec4f74ea284c280286060da Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Oct 2021 11:27:38 +0300 Subject: [PATCH 10/11] Move Node.fspath to legacypath plugin --- src/_pytest/legacypath.py | 13 +++++++++++++ src/_pytest/nodes.py | 10 ---------- testing/test_legacypath.py | 12 ++++++++++++ 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 2af20856a..12676a418 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -15,6 +15,7 @@ from _pytest.compat import final from _pytest.compat import LEGACY_PATH from _pytest.compat import legacy_path from _pytest.deprecated import check_ispytest +from _pytest.nodes import Node from _pytest.terminal import TerminalReporter if TYPE_CHECKING: @@ -386,6 +387,15 @@ def Config__getini_unknown_type( raise ValueError(f"unknown configuration type: {type}", value) +def Node_fspath(self: Node) -> LEGACY_PATH: + """(deprecated) returns a legacy_path copy of self.path""" + return legacy_path(self.path) + + +def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None: + self.path = Path(value) + + def pytest_configure(config: pytest.Config) -> None: mp = pytest.MonkeyPatch() config.add_cleanup(mp.undo) @@ -429,3 +439,6 @@ def pytest_configure(config: pytest.Config) -> None: # Add pathlist configuration type. mp.setattr(pytest.Config, "_getini_unknown_type", Config__getini_unknown_type) + + # Add Node.fspath property. + mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 05cf01fc6..09bbda0a2 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -24,7 +24,6 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest.compat import cached_property from _pytest.compat import LEGACY_PATH -from _pytest.compat import legacy_path from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH @@ -238,15 +237,6 @@ class Node(metaclass=NodeMeta): # Deprecated alias. Was never public. Can be removed in a few releases. self._store = self.stash - @property - def fspath(self) -> LEGACY_PATH: - """(deprecated) returns a legacy_path copy of self.path""" - return legacy_path(self.path) - - @fspath.setter - def fspath(self, value: LEGACY_PATH) -> None: - self.path = Path(value) - @classmethod def from_parent(cls, parent: "Node", **kw): """Public constructor for Nodes. diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index 08568d8f7..9ab139df4 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -6,6 +6,18 @@ from _pytest.legacypath import TempdirFactory from _pytest.legacypath import Testdir +def test_item_fspath(pytester: pytest.Pytester) -> None: + pytester.makepyfile("def test_func(): pass") + items, hookrec = pytester.inline_genitems() + assert len(items) == 1 + (item,) = items + items2, hookrec = pytester.inline_genitems(item.nodeid) + (item2,) = items2 + assert item2.name == item.name + assert item2.fspath == item.fspath # type: ignore[attr-defined] + assert item2.path == item.path + + def test_testdir_testtmproot(testdir: Testdir) -> None: """Check test_tmproot is a py.path attribute for backward compatibility.""" assert testdir.test_tmproot.check(dir=1) From e6eac28f0eefd6eca641111f14a565f9ea15da1b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 29 Oct 2021 13:11:12 +0300 Subject: [PATCH 11/11] legacypath: only add `testdir` and `tmpdir` fixtures if corresponding plugins are registered This preserves the existing behavior and gives a proper error message in case e.g. `testdir` is requested without the `pytester` plugin being loaded. --- doc/en/reference/reference.rst | 2 +- src/_pytest/legacypath.py | 90 +++++++++++++++++++--------------- 2 files changed, 51 insertions(+), 41 deletions(-) diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 9bd242c0b..f90070acb 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -638,7 +638,7 @@ tmpdir :ref:`tmpdir and tmpdir_factory` -.. autofunction:: _pytest.legacypath.tmpdir() +.. autofunction:: _pytest.legacypath.LegacyTmpdirPlugin.tmpdir() :no-auto-options: diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 12676a418..15bb98fb0 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -248,15 +248,17 @@ class Testdir: pytest.Testdir = Testdir # type: ignore[attr-defined] -@pytest.fixture -def testdir(pytester: pytest.Pytester) -> Testdir: - """ - Identical to :fixture:`pytester`, and provides an instance whose methods return - legacy ``LEGACY_PATH`` objects instead when applicable. +class LegacyTestdirPlugin: + @staticmethod + @pytest.fixture + def testdir(pytester: pytest.Pytester) -> Testdir: + """ + Identical to :fixture:`pytester`, and provides an instance whose methods return + legacy ``LEGACY_PATH`` objects instead when applicable. - New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. - """ - return Testdir(pytester, _ispytest=True) + New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. + """ + return Testdir(pytester, _ispytest=True) @final @@ -285,29 +287,31 @@ class TempdirFactory: pytest.TempdirFactory = TempdirFactory # type: ignore[attr-defined] -@pytest.fixture(scope="session") -def tmpdir_factory(request: pytest.FixtureRequest) -> TempdirFactory: - """Return a :class:`pytest.TempdirFactory` instance for the test session.""" - # Set dynamically by pytest_configure(). - return request.config._tmpdirhandler # type: ignore +class LegacyTmpdirPlugin: + @staticmethod + @pytest.fixture(scope="session") + def tmpdir_factory(request: pytest.FixtureRequest) -> TempdirFactory: + """Return a :class:`pytest.TempdirFactory` instance for the test session.""" + # Set dynamically by pytest_configure(). + return request.config._tmpdirhandler # type: ignore + @staticmethod + @pytest.fixture + def tmpdir(tmp_path: Path) -> LEGACY_PATH: + """Return a temporary directory path object which is unique to each test + function invocation, created as a sub directory of the base temporary + directory. -@pytest.fixture -def tmpdir(tmp_path: Path) -> LEGACY_PATH: - """Return a temporary directory path object which is unique to each test - function invocation, created as a sub directory of the base temporary - directory. + By default, a new base temporary directory is created each test session, + and old bases are removed after 3 sessions, to aid in debugging. If + ``--basetemp`` is used then it is cleared each session. See :ref:`base + temporary directory`. - By default, a new base temporary directory is created each test session, - and old bases are removed after 3 sessions, to aid in debugging. If - ``--basetemp`` is used then it is cleared each session. See :ref:`base - temporary directory`. + The returned object is a `legacy_path`_ object. - The returned object is a `legacy_path`_ object. - - .. _legacy_path: https://py.readthedocs.io/en/latest/path.html - """ - return legacy_path(tmp_path) + .. _legacy_path: https://py.readthedocs.io/en/latest/path.html + """ + return legacy_path(tmp_path) def Cache_makedir(self: pytest.Cache, name: str) -> LEGACY_PATH: @@ -400,19 +404,25 @@ def pytest_configure(config: pytest.Config) -> None: mp = pytest.MonkeyPatch() config.add_cleanup(mp.undo) - # Create TmpdirFactory and attach it to the config object. - # - # This is to comply with existing plugins which expect the handler to be - # available at pytest_configure time, but ideally should be moved entirely - # to the tmpdir_factory session fixture. - try: - tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined] - except AttributeError: - # tmpdir plugin is blocked. - pass - else: - _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True) - mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False) + if config.pluginmanager.has_plugin("pytester"): + config.pluginmanager.register(LegacyTestdirPlugin, "legacypath-pytester") + + if config.pluginmanager.has_plugin("tmpdir"): + # Create TmpdirFactory and attach it to the config object. + # + # This is to comply with existing plugins which expect the handler to be + # available at pytest_configure time, but ideally should be moved entirely + # to the tmpdir_factory session fixture. + try: + tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined] + except AttributeError: + # tmpdir plugin is blocked. + pass + else: + _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True) + mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False) + + config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir") # Add Cache.makedir(). mp.setattr(pytest.Cache, "makedir", Cache_makedir, raising=False)