diff --git a/changelog/7259.feature.rst b/changelog/7259.feature.rst new file mode 100644 index 00000000000..e19aaca5291 --- /dev/null +++ b/changelog/7259.feature.rst @@ -0,0 +1,2 @@ +Added :meth:`cache.mkdir() `, which is similar to the existing :meth:`cache.makedir() `, +but returns a :class:`pathlib.Path` instead of a legacy ``py.path.local``. diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py index bdcc8b76222..8a32814edf8 100644 --- a/doc/en/example/nonpython/conftest.py +++ b/doc/en/example/nonpython/conftest.py @@ -2,9 +2,9 @@ import pytest -def pytest_collect_file(parent, path): - if path.ext == ".yaml" and path.basename.startswith("test"): - return YamlFile.from_parent(parent, fspath=path) +def pytest_collect_file(parent, fspath): + if fspath.suffix == ".yaml" and fspath.name.startswith("test"): + return YamlFile.from_parent(parent, path=fspath) class YamlFile(pytest.File): @@ -12,7 +12,7 @@ def collect(self): # We need a yaml parser, e.g. PyYAML. import yaml - raw = yaml.safe_load(self.fspath.open()) + raw = yaml.safe_load(self.path.open()) for name, spec in sorted(raw.items()): yield YamlItem.from_parent(self, name=name, spec=spec) diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index d9968b8ab51..ce0c4efa509 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -936,7 +936,7 @@ pytest treats some global variables in a special manner when defined in a test m **Tutorial**: :ref:`customizing-test-collection` Can be declared in *conftest.py files* to exclude test directories or modules. -Needs to be ``list[str]``. +Needs to be a list of paths (``str``, :class:`pathlib.Path` or any :class:`os.PathLike`). .. code-block:: python diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index a7ec7989184..62112c6e343 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -60,10 +60,10 @@ class Cache: _cachedir = attr.ib(type=Path, repr=False) _config = attr.ib(type=Config, repr=False) - # sub-directory under cache-dir for directories created by "makedir" + # Sub-directory under cache-dir for directories created by `mkdir()`. _CACHE_PREFIX_DIRS = "d" - # sub-directory under cache-dir for values created by "set" + # Sub-directory under cache-dir for values created by `set()`. _CACHE_PREFIX_VALUES = "v" def __init__( @@ -121,13 +121,15 @@ def warn(self, fmt: str, *, _ispytest: bool = False, **args: object) -> None: stacklevel=3, ) - def makedir(self, name: str) -> LEGACY_PATH: + def mkdir(self, name: str) -> Path: """Return a directory path object with the given name. If the directory does not yet exist, it will be created. You can use it to manage files to e.g. store/retrieve database dumps across test sessions. + .. versionadded:: 6.3 + :param name: Must be a string not containing a ``/`` separator. Make sure the name contains your plugin or application @@ -138,7 +140,14 @@ def makedir(self, name: str) -> LEGACY_PATH: raise ValueError("name is not allowed to contain path separators") res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path) res.mkdir(exist_ok=True, parents=True) - return legacy_path(res) + 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)) @@ -572,8 +581,8 @@ def cacheshow(config: Config, session: Session) -> int: contents = sorted(ddir.rglob(glob)) tw.sep("-", "cache directories for %r" % glob) for p in contents: - # if p.check(dir=1): - # print("%s/" % p.relto(basedir)) + # if p.is_dir(): + # print("%s/" % p.relative_to(basedir)) if p.is_file(): key = str(p.relative_to(basedir)) tw.line(f"{key} is a file of length {p.stat().st_size:d}") diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 144f1c9d112..624fbd02f8c 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1445,9 +1445,7 @@ def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]: modpath = Path(mod.__file__).parent values: List[Path] = [] for relroot in relroots: - if isinstance(relroot, Path): - pass - elif isinstance(relroot, LEGACY_PATH): + if isinstance(relroot, os.PathLike): relroot = Path(relroot) else: relroot = relroot.replace("/", os.sep) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index b8e46297a81..5eaeccc4766 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -28,7 +28,6 @@ from _pytest._code.code import ReprFileLocation from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter -from _pytest.compat import LEGACY_PATH from _pytest.compat import legacy_path from _pytest.compat import safe_getattr from _pytest.config import Config @@ -122,7 +121,6 @@ def pytest_unconfigure() -> None: def pytest_collect_file( fspath: Path, - path: LEGACY_PATH, parent: Collector, ) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: config = parent.config diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 3e7213489ff..b6de7a8ddc3 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -465,7 +465,7 @@ class Session(nodes.FSCollector): def __init__(self, config: Config) -> None: super().__init__( path=config.rootpath, - fspath=config.rootdir, + fspath=None, parent=None, config=config, session=self, diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index df106abb330..febae07857e 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -912,7 +912,7 @@ def copy_example(self, name: Optional[str] = None) -> Path: example_dir = self._request.config.getini("pytester_example_dir") if example_dir is None: raise ValueError("pytester_example_dir is unset, can't copy examples") - example_dir = Path(str(self._request.config.rootdir)) / example_dir + example_dir = self._request.config.rootpath / example_dir for extra_element in self._request.node.iter_markers("pytester_example_path"): assert extra_element.args diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ccd685f54a9..905b40d893f 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -210,11 +210,11 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: return any(fnmatch_ex(pattern, path) for pattern in patterns) -def pytest_pycollect_makemodule(fspath: Path, path: LEGACY_PATH, parent) -> "Module": +def pytest_pycollect_makemodule(fspath: Path, parent) -> "Module": if fspath.name == "__init__.py": - pkg: Package = Package.from_parent(parent, fspath=path) + pkg: Package = Package.from_parent(parent, path=fspath) return pkg - mod: Module = Module.from_parent(parent, fspath=path) + mod: Module = Module.from_parent(parent, path=fspath) return mod @@ -691,7 +691,7 @@ def _collectfile( assert ( fspath.is_file() ), "{!r} is not a file (isdir={!r}, exists={!r}, islink={!r})".format( - path, fspath.is_dir(), fspath.exists(), fspath.is_symlink() + fspath, fspath.is_dir(), fspath.exists(), fspath.is_symlink() ) ihook = self.session.gethookproxy(fspath) if not self.session.isinitpath(fspath): diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index b7ec18a9cb6..452849f2bc6 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -304,9 +304,9 @@ def runtest(self): class MyCollector(pytest.File): def collect(self): return [MyItem.from_parent(name="xyz", parent=self)] - def pytest_collect_file(path, parent): - if path.basename.startswith("conftest"): - return MyCollector.from_parent(fspath=path, parent=parent) + def pytest_collect_file(fspath, parent): + if fspath.name.startswith("conftest"): + return MyCollector.from_parent(path=fspath, parent=parent) """ ) result = pytester.runpytest(c.name + "::" + "xyz") diff --git a/testing/conftest.py b/testing/conftest.py index 2dc20bcb2fd..63817b9ad1b 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -114,13 +114,13 @@ def dummy_yaml_custom_test(pytester: Pytester): """ import pytest - def pytest_collect_file(parent, path): - if path.ext == ".yaml" and path.basename.startswith("test"): - return YamlFile.from_parent(fspath=path, parent=parent) + def pytest_collect_file(parent, fspath): + if fspath.suffix == ".yaml" and fspath.name.startswith("test"): + return YamlFile.from_parent(path=fspath, parent=parent) class YamlFile(pytest.File): def collect(self): - yield YamlItem.from_parent(name=self.fspath.basename, parent=self) + yield YamlItem.from_parent(name=self.path.name, parent=self) class YamlItem(pytest.Item): def runtest(self): diff --git a/testing/example_scripts/collect/package_infinite_recursion/conftest.py b/testing/example_scripts/collect/package_infinite_recursion/conftest.py index 9629fa646af..d9e7a89bdaa 100644 --- a/testing/example_scripts/collect/package_infinite_recursion/conftest.py +++ b/testing/example_scripts/collect/package_infinite_recursion/conftest.py @@ -1,2 +1,2 @@ -def pytest_ignore_collect(path): +def pytest_ignore_collect(fspath): return False diff --git a/testing/example_scripts/fixtures/custom_item/conftest.py b/testing/example_scripts/fixtures/custom_item/conftest.py index 161934b58f7..1b3940e95a4 100644 --- a/testing/example_scripts/fixtures/custom_item/conftest.py +++ b/testing/example_scripts/fixtures/custom_item/conftest.py @@ -11,5 +11,5 @@ def collect(self): yield CustomItem.from_parent(name="foo", parent=self) -def pytest_collect_file(path, parent): - return CustomFile.from_parent(fspath=path, parent=parent) +def pytest_collect_file(fspath, parent): + return CustomFile.from_parent(path=fspath, parent=parent) diff --git a/testing/example_scripts/issue88_initial_file_multinodes/conftest.py b/testing/example_scripts/issue88_initial_file_multinodes/conftest.py index a053a638a9f..7227a53b783 100644 --- a/testing/example_scripts/issue88_initial_file_multinodes/conftest.py +++ b/testing/example_scripts/issue88_initial_file_multinodes/conftest.py @@ -6,8 +6,8 @@ def collect(self): return [MyItem.from_parent(name="hello", parent=self)] -def pytest_collect_file(path, parent): - return MyFile.from_parent(fspath=path, parent=parent) +def pytest_collect_file(fspath, parent): + return MyFile.from_parent(path=fspath, parent=parent) class MyItem(pytest.Item): diff --git a/testing/python/collect.py b/testing/python/collect.py index bb4c937c01e..633212d9511 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -778,9 +778,9 @@ def test_pytest_pycollect_module(self, pytester: Pytester) -> None: import pytest class MyModule(pytest.Module): pass - def pytest_pycollect_makemodule(path, parent): - if path.basename == "test_xyz.py": - return MyModule.from_parent(fspath=path, parent=parent) + def pytest_pycollect_makemodule(fspath, parent): + if fspath.name == "test_xyz.py": + return MyModule.from_parent(path=fspath, parent=parent) """ ) pytester.makepyfile("def test_some(): pass") @@ -882,9 +882,9 @@ def find_module(self, name, path=None): return Loader() sys.meta_path.append(Finder()) - def pytest_collect_file(path, parent): - if path.ext == ".narf": - return Module.from_parent(fspath=path, parent=parent)""" + def pytest_collect_file(fspath, parent): + if fspath.suffix == ".narf": + return Module.from_parent(path=fspath, parent=parent)""" ) pytester.makefile( ".narf", diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8d0dc538aa5..3eb4b80f39e 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3208,10 +3208,10 @@ class TestRequestScopeAccess: pytestmark = pytest.mark.parametrize( ("scope", "ok", "error"), [ - ["session", "", "fspath class function module"], - ["module", "module fspath", "cls function"], - ["class", "module fspath cls", "function"], - ["function", "module fspath cls function", ""], + ["session", "", "path class function module"], + ["module", "module path", "cls function"], + ["class", "module path cls", "function"], + ["function", "module path cls function", ""], ], ) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 2cb657efc16..e631e4ad854 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -14,15 +14,15 @@ class TestNewAPI: - def test_config_cache_makedir(self, pytester: Pytester) -> None: + def test_config_cache_mkdir(self, pytester: Pytester) -> None: pytester.makeini("[pytest]") config = pytester.parseconfigure() assert config.cache is not None with pytest.raises(ValueError): - config.cache.makedir("key/name") + config.cache.mkdir("key/name") - p = config.cache.makedir("name") - assert p.check() + p = config.cache.mkdir("name") + assert p.is_dir() def test_config_cache_dataerror(self, pytester: Pytester) -> None: pytester.makeini("[pytest]") @@ -217,9 +217,9 @@ def pytest_configure(config): config.cache.set("my/name", [1,2,3]) config.cache.set("my/hello", "world") config.cache.set("other/some", {1:2}) - dp = config.cache.makedir("mydb") - dp.ensure("hello") - dp.ensure("world") + dp = config.cache.mkdir("mydb") + dp.joinpath("hello").touch() + dp.joinpath("world").touch() """ ) result = pytester.runpytest() diff --git a/testing/test_collection.py b/testing/test_collection.py index f015578e2ba..98d0e174440 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -95,9 +95,9 @@ def test_getcustomfile_roundtrip(self, pytester: Pytester) -> None: import pytest class CustomFile(pytest.File): pass - def pytest_collect_file(path, parent): - if path.ext == ".xxx": - return CustomFile.from_parent(fspath=path, parent=parent) + def pytest_collect_file(fspath, parent): + if fspath.suffix == ".xxx": + return CustomFile.from_parent(path=fspath, parent=parent) """ ) node = pytester.getpathnode(hello) @@ -271,15 +271,15 @@ def test_pytest_collect_file(self, pytester: Pytester) -> None: wascalled = [] class Plugin: - def pytest_collect_file(self, path): - if not path.basename.startswith("."): + def pytest_collect_file(self, fspath: Path) -> None: + if not fspath.name.startswith("."): # Ignore hidden files, e.g. .testmondata. - wascalled.append(path) + wascalled.append(fspath) pytester.makefile(".abc", "xyz") pytest.main(pytester.path, plugins=[Plugin()]) assert len(wascalled) == 1 - assert wascalled[0].ext == ".abc" + assert wascalled[0].suffix == ".abc" class TestPrunetraceback: @@ -292,15 +292,15 @@ def test_custom_repr_failure(self, pytester: Pytester) -> None: pytester.makeconftest( """ import pytest - def pytest_collect_file(path, parent): - return MyFile.from_parent(fspath=path, parent=parent) + def pytest_collect_file(fspath, parent): + return MyFile.from_parent(path=fspath, parent=parent) class MyError(Exception): pass class MyFile(pytest.File): def collect(self): raise MyError() def repr_failure(self, excinfo): - if excinfo.errisinstance(MyError): + if isinstance(excinfo.value, MyError): return "hello world" return pytest.File.repr_failure(self, excinfo) """ @@ -335,9 +335,8 @@ class TestCustomConftests: def test_ignore_collect_path(self, pytester: Pytester) -> None: pytester.makeconftest( """ - def pytest_ignore_collect(path, config): - return path.basename.startswith("x") or \ - path.basename == "test_one.py" + def pytest_ignore_collect(fspath, config): + return fspath.name.startswith("x") or fspath.name == "test_one.py" """ ) sub = pytester.mkdir("xy123") @@ -352,7 +351,7 @@ def pytest_ignore_collect(path, config): def test_ignore_collect_not_called_on_argument(self, pytester: Pytester) -> None: pytester.makeconftest( """ - def pytest_ignore_collect(path, config): + def pytest_ignore_collect(fspath, config): return True """ ) @@ -367,12 +366,19 @@ def pytest_ignore_collect(path, config): def test_collectignore_exclude_on_option(self, pytester: Pytester) -> None: pytester.makeconftest( """ - # potentially avoid dependency on pylib - from _pytest.compat import legacy_path from pathlib import Path - collect_ignore = [legacy_path('hello'), 'test_world.py', Path('bye')] + + class MyPathLike: + def __init__(self, path): + self.path = path + def __fspath__(self): + return "path" + + collect_ignore = [MyPathLike('hello'), 'test_world.py', Path('bye')] + def pytest_addoption(parser): parser.addoption("--XX", action="store_true", default=False) + def pytest_configure(config): if config.getvalue("XX"): collect_ignore[:] = [] @@ -413,9 +419,9 @@ def test_pytest_fs_collect_hooks_are_seen(self, pytester: Pytester) -> None: import pytest class MyModule(pytest.Module): pass - def pytest_collect_file(path, parent): - if path.ext == ".py": - return MyModule.from_parent(fspath=path, parent=parent) + def pytest_collect_file(fspath, parent): + if fspath.suffix == ".py": + return MyModule.from_parent(path=fspath, parent=parent) """ ) pytester.mkdir("sub") @@ -431,9 +437,9 @@ def test_pytest_collect_file_from_sister_dir(self, pytester: Pytester) -> None: import pytest class MyModule1(pytest.Module): pass - def pytest_collect_file(path, parent): - if path.ext == ".py": - return MyModule1.from_parent(fspath=path, parent=parent) + def pytest_collect_file(fspath, parent): + if fspath.suffix == ".py": + return MyModule1.from_parent(path=fspath, parent=parent) """ ) conf1.replace(sub1.joinpath(conf1.name)) @@ -442,9 +448,9 @@ def pytest_collect_file(path, parent): import pytest class MyModule2(pytest.Module): pass - def pytest_collect_file(path, parent): - if path.ext == ".py": - return MyModule2.from_parent(fspath=path, parent=parent) + def pytest_collect_file(fspath, parent): + if fspath.suffix == ".py": + return MyModule2.from_parent(path=fspath, parent=parent) """ ) conf2.replace(sub2.joinpath(conf2.name)) @@ -533,9 +539,9 @@ def runtest(self): class SpecialFile(pytest.File): def collect(self): return [SpecialItem.from_parent(name="check", parent=self)] - def pytest_collect_file(path, parent): - if path.basename == %r: - return SpecialFile.from_parent(fspath=path, parent=parent) + def pytest_collect_file(fspath, parent): + if fspath.name == %r: + return SpecialFile.from_parent(path=fspath, parent=parent) """ % p.name ) @@ -755,13 +761,13 @@ def pytest_configure(config): config.pluginmanager.register(Plugin2()) class Plugin2(object): - def pytest_collect_file(self, path, parent): - if path.ext == ".abc": - return MyFile2.from_parent(fspath=path, parent=parent) + def pytest_collect_file(self, fspath, parent): + if fspath.suffix == ".abc": + return MyFile2.from_parent(path=fspath, parent=parent) - def pytest_collect_file(path, parent): - if path.ext == ".abc": - return MyFile1.from_parent(fspath=path, parent=parent) + def pytest_collect_file(fspath, parent): + if fspath.suffix == ".abc": + return MyFile1.from_parent(path=fspath, parent=parent) class MyFile1(pytest.File): def collect(self): diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 3497b7cc4fd..344c9bc5162 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -629,7 +629,7 @@ def test_hook_proxy(pytester: Pytester) -> None: "root/demo-0/test_foo1.py": "def test1(): pass", "root/demo-a/test_foo2.py": "def test1(): pass", "root/demo-a/conftest.py": """\ - def pytest_ignore_collect(path, config): + def pytest_ignore_collect(fspath, config): return True """, "root/demo-b/test_foo3.py": "def test1(): pass", diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 1c76351eafc..139e2a9a705 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -979,9 +979,9 @@ def test_summing_simple( pytester.makeconftest( """ import pytest - def pytest_collect_file(path, parent): - if path.ext == ".xyz": - return MyItem.from_parent(name=path.basename, parent=parent) + def pytest_collect_file(fspath, parent): + if fspath.suffix == ".xyz": + return MyItem.from_parent(name=fspath.name, parent=parent) class MyItem(pytest.Item): def runtest(self): raise ValueError(42) @@ -1430,9 +1430,9 @@ def collect(self): NoFunItem.from_parent(name='b', parent=self), ] - def pytest_collect_file(path, parent): - if path.check(ext='.py'): - return FunCollector.from_parent(fspath=path, parent=parent) + def pytest_collect_file(fspath, parent): + if fspath.suffix == '.py': + return FunCollector.from_parent(path=fspath, parent=parent) """ ) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 349de6e080f..bba36421ace 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1303,7 +1303,7 @@ class MyItem(pytest.Item): def runtest(self): pytest.xfail("Expected Failure") - def pytest_collect_file(path, parent): + def pytest_collect_file(fspath, parent): return MyItem.from_parent(name="foo", parent=parent) """ ) @@ -1377,7 +1377,7 @@ def setup(self): def runtest(self): assert False - def pytest_collect_file(path, parent): + def pytest_collect_file(fspath, parent): return MyItem.from_parent(name="foo", parent=parent) """ ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 53bced8e68e..5dc5a1cb69f 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1036,8 +1036,8 @@ def test_more_quiet_reporting(self, pytester: Pytester) -> None: def test_report_collectionfinish_hook(self, pytester: Pytester, params) -> None: pytester.makeconftest( """ - def pytest_report_collectionfinish(config, startpath, startdir, items): - return ['hello from hook: {0} items'.format(len(items))] + def pytest_report_collectionfinish(config, startpath, items): + return [f'hello from hook: {len(items)} items'] """ ) pytester.makepyfile( @@ -1462,7 +1462,7 @@ def pytest_report_header(config): ) pytester.mkdir("a").joinpath("conftest.py").write_text( """ -def pytest_report_header(config, startdir, startpath): +def pytest_report_header(config, startpath): return ["line1", str(startpath)] """ )