Merge pull request #7619 from bluetech/py-to-pathlib

Some py.path -> pathlib conversions
This commit is contained in:
Ran Benita 2020-08-07 11:33:21 +03:00 committed by GitHub
commit 384b6f6866
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 324 additions and 200 deletions

View File

@ -1,6 +1,6 @@
import json import json
from pathlib import Path
import py
import requests import requests
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues" issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"
@ -31,12 +31,12 @@ def get_issues():
def main(args): def main(args):
cachefile = py.path.local(args.cache) cachefile = Path(args.cache)
if not cachefile.exists() or args.refresh: if not cachefile.exists() or args.refresh:
issues = get_issues() issues = get_issues()
cachefile.write(json.dumps(issues)) cachefile.write_text(json.dumps(issues), "utf-8")
else: else:
issues = json.loads(cachefile.read()) issues = json.loads(cachefile.read_text("utf-8"))
open_issues = [x for x in issues if x["state"] == "open"] open_issues = [x for x in issues if x["state"] == "open"]

View File

@ -41,6 +41,7 @@ from _pytest.compat import ATTRS_EQ_FIELD
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import overload from _pytest.compat import overload
from _pytest.compat import TYPE_CHECKING from _pytest.compat import TYPE_CHECKING
from _pytest.pathlib import Path
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Type from typing import Type
@ -1190,12 +1191,12 @@ def getfslineno(obj: object) -> Tuple[Union[str, py.path.local], int]:
# note: if we need to add more paths than what we have now we should probably use a list # note: if we need to add more paths than what we have now we should probably use a list
# for better maintenance. # for better maintenance.
_PLUGGY_DIR = py.path.local(pluggy.__file__.rstrip("oc")) _PLUGGY_DIR = Path(pluggy.__file__.rstrip("oc"))
# pluggy is either a package or a single module depending on the version # pluggy is either a package or a single module depending on the version
if _PLUGGY_DIR.basename == "__init__.py": if _PLUGGY_DIR.name == "__init__.py":
_PLUGGY_DIR = _PLUGGY_DIR.dirpath() _PLUGGY_DIR = _PLUGGY_DIR.parent
_PYTEST_DIR = py.path.local(_pytest.__file__).dirpath() _PYTEST_DIR = Path(_pytest.__file__).parent
_PY_DIR = py.path.local(py.__file__).dirpath() _PY_DIR = Path(py.__file__).parent
def filter_traceback(entry: TracebackEntry) -> bool: def filter_traceback(entry: TracebackEntry) -> bool:
@ -1213,9 +1214,17 @@ def filter_traceback(entry: TracebackEntry) -> bool:
is_generated = "<" in raw_filename and ">" in raw_filename is_generated = "<" in raw_filename and ">" in raw_filename
if is_generated: if is_generated:
return False return False
# entry.path might point to a non-existing file, in which case it will # entry.path might point to a non-existing file, in which case it will
# also return a str object. See #1133. # also return a str object. See #1133.
p = py.path.local(entry.path) p = Path(entry.path)
return (
not p.relto(_PLUGGY_DIR) and not p.relto(_PYTEST_DIR) and not p.relto(_PY_DIR) parents = p.parents
) if _PLUGGY_DIR in parents:
return False
if _PYTEST_DIR in parents:
return False
if _PY_DIR in parents:
return False
return True

View File

@ -18,7 +18,6 @@ from typing import TypeVar
from typing import Union from typing import Union
import attr import attr
import py
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.outcomes import fail from _pytest.outcomes import fail
@ -104,13 +103,18 @@ def is_async_function(func: object) -> bool:
) )
def getlocation(function, curdir=None) -> str: def getlocation(function, curdir: Optional[str] = None) -> str:
from _pytest.pathlib import Path
function = get_real_func(function) function = get_real_func(function)
fn = py.path.local(inspect.getfile(function)) fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno lineno = function.__code__.co_firstlineno
if curdir is not None: if curdir is not None:
relfn = fn.relto(curdir) try:
if relfn: relfn = fn.relative_to(curdir)
except ValueError:
pass
else:
return "%s:%d" % (relfn, lineno + 1) return "%s:%d" % (relfn, lineno + 1)
return "%s:%d" % (fn, lineno + 1) return "%s:%d" % (fn, lineno + 1)

View File

@ -123,7 +123,7 @@ def filter_traceback_for_conftest_import_failure(
def main( def main(
args: Optional[List[str]] = None, args: Optional[Union[List[str], py.path.local]] = None,
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None,
) -> Union[int, ExitCode]: ) -> Union[int, ExitCode]:
"""Perform an in-process test run. """Perform an in-process test run.
@ -1006,12 +1006,15 @@ class Config:
ns, unknown_args = self._parser.parse_known_and_unknown_args( ns, unknown_args = self._parser.parse_known_and_unknown_args(
args, namespace=copy.copy(self.option) args, namespace=copy.copy(self.option)
) )
self.rootdir, self.inifile, self.inicfg = determine_setup( rootpath, inipath, inicfg = determine_setup(
ns.inifilename, ns.inifilename,
ns.file_or_dir + unknown_args, ns.file_or_dir + unknown_args,
rootdir_cmd_arg=ns.rootdir or None, rootdir_cmd_arg=ns.rootdir or None,
config=self, config=self,
) )
self.rootdir = py.path.local(str(rootpath))
self.inifile = py.path.local(str(inipath)) if inipath else None
self.inicfg = inicfg
self._parser.extra_info["rootdir"] = self.rootdir self._parser.extra_info["rootdir"] = self.rootdir
self._parser.extra_info["inifile"] = self.inifile self._parser.extra_info["inifile"] = self.inifile
self._parser.addini("addopts", "extra command line options", "args") self._parser.addini("addopts", "extra command line options", "args")
@ -1305,7 +1308,7 @@ class Config:
values = [] # type: List[py.path.local] values = [] # type: List[py.path.local]
for relroot in relroots: for relroot in relroots:
if not isinstance(relroot, py.path.local): if not isinstance(relroot, py.path.local):
relroot = relroot.replace("/", py.path.local.sep) relroot = relroot.replace("/", os.sep)
relroot = modpath.join(relroot, abs=True) relroot = modpath.join(relroot, abs=True)
values.append(relroot) values.append(relroot)
return values return values

View File

@ -1,23 +1,28 @@
import itertools
import os import os
import sys
from typing import Dict from typing import Dict
from typing import Iterable from typing import Iterable
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Sequence
from typing import Tuple from typing import Tuple
from typing import Union from typing import Union
import iniconfig import iniconfig
import py
from .exceptions import UsageError from .exceptions import UsageError
from _pytest.compat import TYPE_CHECKING from _pytest.compat import TYPE_CHECKING
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.pathlib import absolutepath
from _pytest.pathlib import commonpath
from _pytest.pathlib import Path
if TYPE_CHECKING: if TYPE_CHECKING:
from . import Config from . import Config
def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig: def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
"""Parse the given generic '.ini' file using legacy IniConfig parser, returning """Parse the given generic '.ini' file using legacy IniConfig parser, returning
the parsed object. the parsed object.
@ -30,7 +35,7 @@ def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig:
def load_config_dict_from_file( def load_config_dict_from_file(
filepath: py.path.local, filepath: Path,
) -> Optional[Dict[str, Union[str, List[str]]]]: ) -> Optional[Dict[str, Union[str, List[str]]]]:
"""Load pytest configuration from the given file path, if supported. """Load pytest configuration from the given file path, if supported.
@ -38,18 +43,18 @@ def load_config_dict_from_file(
""" """
# Configuration from ini files are obtained from the [pytest] section, if present. # Configuration from ini files are obtained from the [pytest] section, if present.
if filepath.ext == ".ini": if filepath.suffix == ".ini":
iniconfig = _parse_ini_config(filepath) iniconfig = _parse_ini_config(filepath)
if "pytest" in iniconfig: if "pytest" in iniconfig:
return dict(iniconfig["pytest"].items()) return dict(iniconfig["pytest"].items())
else: else:
# "pytest.ini" files are always the source of configuration, even if empty. # "pytest.ini" files are always the source of configuration, even if empty.
if filepath.basename == "pytest.ini": if filepath.name == "pytest.ini":
return {} return {}
# '.cfg' files are considered if they contain a "[tool:pytest]" section. # '.cfg' files are considered if they contain a "[tool:pytest]" section.
elif filepath.ext == ".cfg": elif filepath.suffix == ".cfg":
iniconfig = _parse_ini_config(filepath) iniconfig = _parse_ini_config(filepath)
if "tool:pytest" in iniconfig.sections: if "tool:pytest" in iniconfig.sections:
@ -60,7 +65,7 @@ def load_config_dict_from_file(
fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False) fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
# '.toml' files are considered if they contain a [tool.pytest.ini_options] table. # '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
elif filepath.ext == ".toml": elif filepath.suffix == ".toml":
import toml import toml
config = toml.load(str(filepath)) config = toml.load(str(filepath))
@ -79,9 +84,9 @@ def load_config_dict_from_file(
def locate_config( def locate_config(
args: Iterable[Union[str, py.path.local]] args: Iterable[Path],
) -> Tuple[ ) -> Tuple[
Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]], Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]],
]: ]:
"""Search in the list of arguments for a valid ini-file for pytest, """Search in the list of arguments for a valid ini-file for pytest,
and return a tuple of (rootdir, inifile, cfg-dict).""" and return a tuple of (rootdir, inifile, cfg-dict)."""
@ -93,62 +98,77 @@ def locate_config(
] ]
args = [x for x in args if not str(x).startswith("-")] args = [x for x in args if not str(x).startswith("-")]
if not args: if not args:
args = [py.path.local()] args = [Path.cwd()]
for arg in args: for arg in args:
arg = py.path.local(arg) argpath = absolutepath(arg)
for base in arg.parts(reverse=True): for base in itertools.chain((argpath,), reversed(argpath.parents)):
for config_name in config_names: for config_name in config_names:
p = base.join(config_name) p = base / config_name
if p.isfile(): if p.is_file():
ini_config = load_config_dict_from_file(p) ini_config = load_config_dict_from_file(p)
if ini_config is not None: if ini_config is not None:
return base, p, ini_config return base, p, ini_config
return None, None, {} return None, None, {}
def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local: def get_common_ancestor(paths: Iterable[Path]) -> Path:
common_ancestor = None # type: Optional[py.path.local] common_ancestor = None # type: Optional[Path]
for path in paths: for path in paths:
if not path.exists(): if not path.exists():
continue continue
if common_ancestor is None: if common_ancestor is None:
common_ancestor = path common_ancestor = path
else: else:
if path.relto(common_ancestor) or path == common_ancestor: if common_ancestor in path.parents or path == common_ancestor:
continue continue
elif common_ancestor.relto(path): elif path in common_ancestor.parents:
common_ancestor = path common_ancestor = path
else: else:
shared = path.common(common_ancestor) shared = commonpath(path, common_ancestor)
if shared is not None: if shared is not None:
common_ancestor = shared common_ancestor = shared
if common_ancestor is None: if common_ancestor is None:
common_ancestor = py.path.local() common_ancestor = Path.cwd()
elif common_ancestor.isfile(): elif common_ancestor.is_file():
common_ancestor = common_ancestor.dirpath() common_ancestor = common_ancestor.parent
return common_ancestor return common_ancestor
def get_dirs_from_args(args: Iterable[str]) -> List[py.path.local]: def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
def is_option(x: str) -> bool: def is_option(x: str) -> bool:
return x.startswith("-") return x.startswith("-")
def get_file_part_from_node_id(x: str) -> str: def get_file_part_from_node_id(x: str) -> str:
return x.split("::")[0] return x.split("::")[0]
def get_dir_from_path(path: py.path.local) -> py.path.local: def get_dir_from_path(path: Path) -> Path:
if path.isdir(): if path.is_dir():
return path return path
return py.path.local(path.dirname) return path.parent
if sys.version_info < (3, 8):
def safe_exists(path: Path) -> bool:
# On Python<3.8, this can throw on paths that contain characters
# unrepresentable at the OS level.
try:
return path.exists()
except OSError:
return False
else:
def safe_exists(path: Path) -> bool:
return path.exists()
# These look like paths but may not exist # These look like paths but may not exist
possible_paths = ( possible_paths = (
py.path.local(get_file_part_from_node_id(arg)) absolutepath(get_file_part_from_node_id(arg))
for arg in args for arg in args
if not is_option(arg) if not is_option(arg)
) )
return [get_dir_from_path(path) for path in possible_paths if path.exists()] return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
@ -156,15 +176,15 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte
def determine_setup( def determine_setup(
inifile: Optional[str], inifile: Optional[str],
args: List[str], args: Sequence[str],
rootdir_cmd_arg: Optional[str] = None, rootdir_cmd_arg: Optional[str] = None,
config: Optional["Config"] = None, config: Optional["Config"] = None,
) -> Tuple[py.path.local, Optional[py.path.local], Dict[str, Union[str, List[str]]]]: ) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
rootdir = None rootdir = None
dirs = get_dirs_from_args(args) dirs = get_dirs_from_args(args)
if inifile: if inifile:
inipath_ = py.path.local(inifile) inipath_ = absolutepath(inifile)
inipath = inipath_ # type: Optional[py.path.local] inipath = inipath_ # type: Optional[Path]
inicfg = load_config_dict_from_file(inipath_) or {} inicfg = load_config_dict_from_file(inipath_) or {}
if rootdir_cmd_arg is None: if rootdir_cmd_arg is None:
rootdir = get_common_ancestor(dirs) rootdir = get_common_ancestor(dirs)
@ -172,8 +192,10 @@ def determine_setup(
ancestor = get_common_ancestor(dirs) ancestor = get_common_ancestor(dirs)
rootdir, inipath, inicfg = locate_config([ancestor]) rootdir, inipath, inicfg = locate_config([ancestor])
if rootdir is None and rootdir_cmd_arg is None: if rootdir is None and rootdir_cmd_arg is None:
for possible_rootdir in ancestor.parts(reverse=True): for possible_rootdir in itertools.chain(
if possible_rootdir.join("setup.py").exists(): (ancestor,), reversed(ancestor.parents)
):
if (possible_rootdir / "setup.py").is_file():
rootdir = possible_rootdir rootdir = possible_rootdir
break break
else: else:
@ -181,16 +203,16 @@ def determine_setup(
rootdir, inipath, inicfg = locate_config(dirs) rootdir, inipath, inicfg = locate_config(dirs)
if rootdir is None: if rootdir is None:
if config is not None: if config is not None:
cwd = config.invocation_dir cwd = config.invocation_params.dir
else: else:
cwd = py.path.local() cwd = Path.cwd()
rootdir = get_common_ancestor([cwd, ancestor]) rootdir = get_common_ancestor([cwd, ancestor])
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
if is_fs_root: if is_fs_root:
rootdir = ancestor rootdir = ancestor
if rootdir_cmd_arg: if rootdir_cmd_arg:
rootdir = py.path.local(os.path.expandvars(rootdir_cmd_arg)) rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
if not rootdir.isdir(): if not rootdir.is_dir():
raise UsageError( raise UsageError(
"Directory '{}' not found. Check your '--rootdir' option.".format( "Directory '{}' not found. Check your '--rootdir' option.".format(
rootdir rootdir

View File

@ -1,5 +1,6 @@
import functools import functools
import inspect import inspect
import os
import sys import sys
import warnings import warnings
from collections import defaultdict from collections import defaultdict
@ -1515,8 +1516,8 @@ class FixtureManager:
# by their test id). # by their test id).
if p.basename.startswith("conftest.py"): if p.basename.startswith("conftest.py"):
nodeid = p.dirpath().relto(self.config.rootdir) nodeid = p.dirpath().relto(self.config.rootdir)
if p.sep != nodes.SEP: if os.sep != nodes.SEP:
nodeid = nodeid.replace(p.sep, nodes.SEP) nodeid = nodeid.replace(os.sep, nodes.SEP)
self.parsefactories(plugin, nodeid) self.parsefactories(plugin, nodeid)

View File

@ -569,3 +569,44 @@ def visit(
for entry in entries: for entry in entries:
if entry.is_dir(follow_symlinks=False) and recurse(entry): if entry.is_dir(follow_symlinks=False) and recurse(entry):
yield from visit(entry.path, recurse) yield from visit(entry.path, recurse)
def absolutepath(path: Union[Path, str]) -> Path:
"""Convert a path to an absolute path using os.path.abspath.
Prefer this over Path.resolve() (see #6523).
Prefer this over Path.absolute() (not public, doesn't normalize).
"""
return Path(os.path.abspath(str(path)))
def commonpath(path1: Path, path2: Path) -> Optional[Path]:
"""Return the common part shared with the other path, or None if there is
no common part."""
try:
return Path(os.path.commonpath((str(path1), str(path2))))
except ValueError:
return None
def bestrelpath(directory: Path, dest: Path) -> str:
"""Return a string which is a relative path from directory to dest such
that directory/bestrelpath == dest.
If no such path can be determined, returns dest.
"""
if dest == directory:
return os.curdir
# Find the longest common directory.
base = commonpath(directory, dest)
# Can be the case on Windows.
if not base:
return str(dest)
reldirectory = directory.relative_to(base)
reldest = dest.relative_to(base)
return os.path.join(
# Back from directory to base.
*([os.pardir] * len(reldirectory.parts)),
# Forward from base to dest.
*reldest.parts,
)

View File

@ -1339,7 +1339,7 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
verbose = config.getvalue("verbose") verbose = config.getvalue("verbose")
def get_best_relpath(func): def get_best_relpath(func):
loc = getlocation(func, curdir) loc = getlocation(func, str(curdir))
return curdir.bestrelpath(py.path.local(loc)) return curdir.bestrelpath(py.path.local(loc))
def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None: def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
@ -1404,7 +1404,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
if not fixturedefs: if not fixturedefs:
continue continue
for fixturedef in fixturedefs: for fixturedef in fixturedefs:
loc = getlocation(fixturedef.func, curdir) loc = getlocation(fixturedef.func, str(curdir))
if (fixturedef.argname, loc) in seen: if (fixturedef.argname, loc) in seen:
continue continue
seen.add((fixturedef.argname, loc)) seen.add((fixturedef.argname, loc))
@ -1434,7 +1434,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
if verbose > 0: if verbose > 0:
tw.write(" -- %s" % bestrel, yellow=True) tw.write(" -- %s" % bestrel, yellow=True)
tw.write("\n") tw.write("\n")
loc = getlocation(fixturedef.func, curdir) loc = getlocation(fixturedef.func, str(curdir))
doc = inspect.getdoc(fixturedef.func) doc = inspect.getdoc(fixturedef.func)
if doc: if doc:
write_docstring(tw, doc) write_docstring(tw, doc)

View File

@ -3,8 +3,6 @@ import os
from typing import IO from typing import IO
from typing import Union from typing import Union
import py
from _pytest._code.code import ExceptionRepr from _pytest._code.code import ExceptionRepr
from _pytest.config import Config from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
@ -106,5 +104,5 @@ class ResultLog:
if excrepr.reprcrash is not None: if excrepr.reprcrash is not None:
path = excrepr.reprcrash.path path = excrepr.reprcrash.path
else: else:
path = "cwd:%s" % py.path.local() path = "cwd:%s" % os.getcwd()
self.write_log_entry(path, "!", str(excrepr)) self.write_log_entry(path, "!", str(excrepr))

View File

@ -586,7 +586,7 @@ class TestInvocationVariants:
): ):
pytest.main("-h") # type: ignore[arg-type] pytest.main("-h") # type: ignore[arg-type]
def test_invoke_with_path(self, tmpdir, capsys): def test_invoke_with_path(self, tmpdir: py.path.local, capsys) -> None:
retcode = pytest.main(tmpdir) retcode = pytest.main(tmpdir)
assert retcode == ExitCode.NO_TESTS_COLLECTED assert retcode == ExitCode.NO_TESTS_COLLECTED
out, err = capsys.readouterr() out, err = capsys.readouterr()

View File

@ -21,17 +21,27 @@ from _pytest.config.exceptions import UsageError
from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import determine_setup
from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import get_common_ancestor
from _pytest.config.findpaths import locate_config from _pytest.config.findpaths import locate_config
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pathlib import Path from _pytest.pathlib import Path
from _pytest.pytester import Testdir
class TestParseIni: class TestParseIni:
@pytest.mark.parametrize( @pytest.mark.parametrize(
"section, filename", [("pytest", "pytest.ini"), ("tool:pytest", "setup.cfg")] "section, filename", [("pytest", "pytest.ini"), ("tool:pytest", "setup.cfg")]
) )
def test_getcfg_and_config(self, testdir, tmpdir, section, filename): def test_getcfg_and_config(
sub = tmpdir.mkdir("sub") self,
sub.chdir() testdir: Testdir,
tmpdir.join(filename).write( tmp_path: Path,
section: str,
filename: str,
monkeypatch: MonkeyPatch,
) -> None:
sub = tmp_path / "sub"
sub.mkdir()
monkeypatch.chdir(sub)
(tmp_path / filename).write_text(
textwrap.dedent( textwrap.dedent(
"""\ """\
[{section}] [{section}]
@ -39,17 +49,14 @@ class TestParseIni:
""".format( """.format(
section=section section=section
) )
) ),
encoding="utf-8",
) )
_, _, cfg = locate_config([sub]) _, _, cfg = locate_config([sub])
assert cfg["name"] == "value" assert cfg["name"] == "value"
config = testdir.parseconfigure(sub) config = testdir.parseconfigure(str(sub))
assert config.inicfg["name"] == "value" assert config.inicfg["name"] == "value"
def test_getcfg_empty_path(self):
"""Correctly handle zero length arguments (a la pytest '')."""
locate_config([""])
def test_setupcfg_uses_toolpytest_with_pytest(self, testdir): def test_setupcfg_uses_toolpytest_with_pytest(self, testdir):
p1 = testdir.makepyfile("def test(): pass") p1 = testdir.makepyfile("def test(): pass")
testdir.makefile( testdir.makefile(
@ -1168,16 +1175,17 @@ def test_collect_pytest_prefix_bug(pytestconfig):
class TestRootdir: class TestRootdir:
def test_simple_noini(self, tmpdir): def test_simple_noini(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
assert get_common_ancestor([tmpdir]) == tmpdir assert get_common_ancestor([tmp_path]) == tmp_path
a = tmpdir.mkdir("a") a = tmp_path / "a"
assert get_common_ancestor([a, tmpdir]) == tmpdir a.mkdir()
assert get_common_ancestor([tmpdir, a]) == tmpdir assert get_common_ancestor([a, tmp_path]) == tmp_path
with tmpdir.as_cwd(): assert get_common_ancestor([tmp_path, a]) == tmp_path
assert get_common_ancestor([]) == tmpdir monkeypatch.chdir(tmp_path)
no_path = tmpdir.join("does-not-exist") assert get_common_ancestor([]) == tmp_path
assert get_common_ancestor([no_path]) == tmpdir no_path = tmp_path / "does-not-exist"
assert get_common_ancestor([no_path.join("a")]) == tmpdir assert get_common_ancestor([no_path]) == tmp_path
assert get_common_ancestor([no_path / "a"]) == tmp_path
@pytest.mark.parametrize( @pytest.mark.parametrize(
"name, contents", "name, contents",
@ -1190,44 +1198,49 @@ class TestRootdir:
pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"), pytest.param("setup.cfg", "[tool:pytest]\nx=10", id="setup.cfg"),
], ],
) )
def test_with_ini(self, tmpdir: py.path.local, name: str, contents: str) -> None: def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None:
inifile = tmpdir.join(name) inipath = tmp_path / name
inifile.write(contents) inipath.write_text(contents, "utf-8")
a = tmpdir.mkdir("a") a = tmp_path / "a"
b = a.mkdir("b") a.mkdir()
for args in ([str(tmpdir)], [str(a)], [str(b)]): b = a / "b"
rootdir, parsed_inifile, _ = determine_setup(None, args) b.mkdir()
assert rootdir == tmpdir for args in ([str(tmp_path)], [str(a)], [str(b)]):
assert parsed_inifile == inifile rootpath, parsed_inipath, _ = determine_setup(None, args)
rootdir, parsed_inifile, ini_config = determine_setup(None, [str(b), str(a)]) assert rootpath == tmp_path
assert rootdir == tmpdir assert parsed_inipath == inipath
assert parsed_inifile == inifile rootpath, parsed_inipath, ini_config = determine_setup(None, [str(b), str(a)])
assert rootpath == tmp_path
assert parsed_inipath == inipath
assert ini_config == {"x": "10"} assert ini_config == {"x": "10"}
@pytest.mark.parametrize("name", "setup.cfg tox.ini".split()) @pytest.mark.parametrize("name", ["setup.cfg", "tox.ini"])
def test_pytestini_overrides_empty_other(self, tmpdir: py.path.local, name) -> None: def test_pytestini_overrides_empty_other(self, tmp_path: Path, name: str) -> None:
inifile = tmpdir.ensure("pytest.ini") inipath = tmp_path / "pytest.ini"
a = tmpdir.mkdir("a") inipath.touch()
a.ensure(name) a = tmp_path / "a"
rootdir, parsed_inifile, _ = determine_setup(None, [str(a)]) a.mkdir()
assert rootdir == tmpdir (a / name).touch()
assert parsed_inifile == inifile rootpath, parsed_inipath, _ = determine_setup(None, [str(a)])
assert rootpath == tmp_path
assert parsed_inipath == inipath
def test_setuppy_fallback(self, tmpdir: py.path.local) -> None: def test_setuppy_fallback(self, tmp_path: Path) -> None:
a = tmpdir.mkdir("a") a = tmp_path / "a"
a.ensure("setup.cfg") a.mkdir()
tmpdir.ensure("setup.py") (a / "setup.cfg").touch()
rootdir, inifile, inicfg = determine_setup(None, [str(a)]) (tmp_path / "setup.py").touch()
assert rootdir == tmpdir rootpath, inipath, inicfg = determine_setup(None, [str(a)])
assert inifile is None assert rootpath == tmp_path
assert inipath is None
assert inicfg == {} assert inicfg == {}
def test_nothing(self, tmpdir: py.path.local, monkeypatch) -> None: def test_nothing(self, tmp_path: Path, monkeypatch: MonkeyPatch) -> None:
monkeypatch.chdir(str(tmpdir)) monkeypatch.chdir(tmp_path)
rootdir, inifile, inicfg = determine_setup(None, [str(tmpdir)]) rootpath, inipath, inicfg = determine_setup(None, [str(tmp_path)])
assert rootdir == tmpdir assert rootpath == tmp_path
assert inifile is None assert inipath is None
assert inicfg == {} assert inicfg == {}
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -1242,45 +1255,58 @@ class TestRootdir:
], ],
) )
def test_with_specific_inifile( def test_with_specific_inifile(
self, tmpdir: py.path.local, name: str, contents: str self, tmp_path: Path, name: str, contents: str
) -> None: ) -> None:
p = tmpdir.ensure(name) p = tmp_path / name
p.write(contents) p.touch()
rootdir, inifile, ini_config = determine_setup(str(p), [str(tmpdir)]) p.write_text(contents, "utf-8")
assert rootdir == tmpdir rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)])
assert inifile == p assert rootpath == tmp_path
assert inipath == p
assert ini_config == {"x": "10"} assert ini_config == {"x": "10"}
def test_with_arg_outside_cwd_without_inifile(self, tmpdir, monkeypatch) -> None: def test_with_arg_outside_cwd_without_inifile(
monkeypatch.chdir(str(tmpdir)) self, tmp_path: Path, monkeypatch: MonkeyPatch
a = tmpdir.mkdir("a") ) -> None:
b = tmpdir.mkdir("b") monkeypatch.chdir(tmp_path)
rootdir, inifile, _ = determine_setup(None, [str(a), str(b)]) a = tmp_path / "a"
assert rootdir == tmpdir a.mkdir()
b = tmp_path / "b"
b.mkdir()
rootpath, inifile, _ = determine_setup(None, [str(a), str(b)])
assert rootpath == tmp_path
assert inifile is None assert inifile is None
def test_with_arg_outside_cwd_with_inifile(self, tmpdir) -> None: def test_with_arg_outside_cwd_with_inifile(self, tmp_path: Path) -> None:
a = tmpdir.mkdir("a") a = tmp_path / "a"
b = tmpdir.mkdir("b") a.mkdir()
inifile = a.ensure("pytest.ini") b = tmp_path / "b"
rootdir, parsed_inifile, _ = determine_setup(None, [str(a), str(b)]) b.mkdir()
assert rootdir == a inipath = a / "pytest.ini"
assert inifile == parsed_inifile inipath.touch()
rootpath, parsed_inipath, _ = determine_setup(None, [str(a), str(b)])
assert rootpath == a
assert inipath == parsed_inipath
@pytest.mark.parametrize("dirs", ([], ["does-not-exist"], ["a/does-not-exist"])) @pytest.mark.parametrize("dirs", ([], ["does-not-exist"], ["a/does-not-exist"]))
def test_with_non_dir_arg(self, dirs, tmpdir) -> None: def test_with_non_dir_arg(
with tmpdir.ensure(dir=True).as_cwd(): self, dirs: Sequence[str], tmp_path: Path, monkeypatch: MonkeyPatch
rootdir, inifile, _ = determine_setup(None, dirs) ) -> None:
assert rootdir == tmpdir monkeypatch.chdir(tmp_path)
assert inifile is None rootpath, inipath, _ = determine_setup(None, dirs)
assert rootpath == tmp_path
assert inipath is None
def test_with_existing_file_in_subdir(self, tmpdir) -> None: def test_with_existing_file_in_subdir(
a = tmpdir.mkdir("a") self, tmp_path: Path, monkeypatch: MonkeyPatch
a.ensure("exist") ) -> None:
with tmpdir.as_cwd(): a = tmp_path / "a"
rootdir, inifile, _ = determine_setup(None, ["a/exist"]) a.mkdir()
assert rootdir == tmpdir (a / "exists").touch()
assert inifile is None monkeypatch.chdir(tmp_path)
rootpath, inipath, _ = determine_setup(None, ["a/exist"])
assert rootpath == tmp_path
assert inipath is None
class TestOverrideIniArgs: class TestOverrideIniArgs:

View File

@ -1,74 +1,74 @@
from textwrap import dedent from textwrap import dedent
import py
import pytest import pytest
from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import get_common_ancestor
from _pytest.config.findpaths import load_config_dict_from_file from _pytest.config.findpaths import load_config_dict_from_file
from _pytest.pathlib import Path
class TestLoadConfigDictFromFile: class TestLoadConfigDictFromFile:
def test_empty_pytest_ini(self, tmpdir): def test_empty_pytest_ini(self, tmp_path: Path) -> None:
"""pytest.ini files are always considered for configuration, even if empty""" """pytest.ini files are always considered for configuration, even if empty"""
fn = tmpdir.join("pytest.ini") fn = tmp_path / "pytest.ini"
fn.write("") fn.write_text("", encoding="utf-8")
assert load_config_dict_from_file(fn) == {} assert load_config_dict_from_file(fn) == {}
def test_pytest_ini(self, tmpdir): def test_pytest_ini(self, tmp_path: Path) -> None:
"""[pytest] section in pytest.ini files is read correctly""" """[pytest] section in pytest.ini files is read correctly"""
fn = tmpdir.join("pytest.ini") fn = tmp_path / "pytest.ini"
fn.write("[pytest]\nx=1") fn.write_text("[pytest]\nx=1", encoding="utf-8")
assert load_config_dict_from_file(fn) == {"x": "1"} assert load_config_dict_from_file(fn) == {"x": "1"}
def test_custom_ini(self, tmpdir): def test_custom_ini(self, tmp_path: Path) -> None:
"""[pytest] section in any .ini file is read correctly""" """[pytest] section in any .ini file is read correctly"""
fn = tmpdir.join("custom.ini") fn = tmp_path / "custom.ini"
fn.write("[pytest]\nx=1") fn.write_text("[pytest]\nx=1", encoding="utf-8")
assert load_config_dict_from_file(fn) == {"x": "1"} assert load_config_dict_from_file(fn) == {"x": "1"}
def test_custom_ini_without_section(self, tmpdir): def test_custom_ini_without_section(self, tmp_path: Path) -> None:
"""Custom .ini files without [pytest] section are not considered for configuration""" """Custom .ini files without [pytest] section are not considered for configuration"""
fn = tmpdir.join("custom.ini") fn = tmp_path / "custom.ini"
fn.write("[custom]") fn.write_text("[custom]", encoding="utf-8")
assert load_config_dict_from_file(fn) is None assert load_config_dict_from_file(fn) is None
def test_custom_cfg_file(self, tmpdir): def test_custom_cfg_file(self, tmp_path: Path) -> None:
"""Custom .cfg files without [tool:pytest] section are not considered for configuration""" """Custom .cfg files without [tool:pytest] section are not considered for configuration"""
fn = tmpdir.join("custom.cfg") fn = tmp_path / "custom.cfg"
fn.write("[custom]") fn.write_text("[custom]", encoding="utf-8")
assert load_config_dict_from_file(fn) is None assert load_config_dict_from_file(fn) is None
def test_valid_cfg_file(self, tmpdir): def test_valid_cfg_file(self, tmp_path: Path) -> None:
"""Custom .cfg files with [tool:pytest] section are read correctly""" """Custom .cfg files with [tool:pytest] section are read correctly"""
fn = tmpdir.join("custom.cfg") fn = tmp_path / "custom.cfg"
fn.write("[tool:pytest]\nx=1") fn.write_text("[tool:pytest]\nx=1", encoding="utf-8")
assert load_config_dict_from_file(fn) == {"x": "1"} assert load_config_dict_from_file(fn) == {"x": "1"}
def test_unsupported_pytest_section_in_cfg_file(self, tmpdir): def test_unsupported_pytest_section_in_cfg_file(self, tmp_path: Path) -> None:
""".cfg files with [pytest] section are no longer supported and should fail to alert users""" """.cfg files with [pytest] section are no longer supported and should fail to alert users"""
fn = tmpdir.join("custom.cfg") fn = tmp_path / "custom.cfg"
fn.write("[pytest]") fn.write_text("[pytest]", encoding="utf-8")
with pytest.raises(pytest.fail.Exception): with pytest.raises(pytest.fail.Exception):
load_config_dict_from_file(fn) load_config_dict_from_file(fn)
def test_invalid_toml_file(self, tmpdir): def test_invalid_toml_file(self, tmp_path: Path) -> None:
""".toml files without [tool.pytest.ini_options] are not considered for configuration.""" """.toml files without [tool.pytest.ini_options] are not considered for configuration."""
fn = tmpdir.join("myconfig.toml") fn = tmp_path / "myconfig.toml"
fn.write( fn.write_text(
dedent( dedent(
""" """
[build_system] [build_system]
x = 1 x = 1
""" """
) ),
encoding="utf-8",
) )
assert load_config_dict_from_file(fn) is None assert load_config_dict_from_file(fn) is None
def test_valid_toml_file(self, tmpdir): def test_valid_toml_file(self, tmp_path: Path) -> None:
""".toml files with [tool.pytest.ini_options] are read correctly, including changing """.toml files with [tool.pytest.ini_options] are read correctly, including changing
data types to str/list for compatibility with other configuration options.""" data types to str/list for compatibility with other configuration options."""
fn = tmpdir.join("myconfig.toml") fn = tmp_path / "myconfig.toml"
fn.write( fn.write_text(
dedent( dedent(
""" """
[tool.pytest.ini_options] [tool.pytest.ini_options]
@ -77,7 +77,8 @@ class TestLoadConfigDictFromFile:
values = ["tests", "integration"] values = ["tests", "integration"]
name = "foo" name = "foo"
""" """
) ),
encoding="utf-8",
) )
assert load_config_dict_from_file(fn) == { assert load_config_dict_from_file(fn) == {
"x": "1", "x": "1",
@ -88,23 +89,22 @@ class TestLoadConfigDictFromFile:
class TestCommonAncestor: class TestCommonAncestor:
def test_has_ancestor(self, tmpdir): def test_has_ancestor(self, tmp_path: Path) -> None:
fn1 = tmpdir.join("foo/bar/test_1.py").ensure(file=1) fn1 = tmp_path / "foo" / "bar" / "test_1.py"
fn2 = tmpdir.join("foo/zaz/test_2.py").ensure(file=1) fn1.parent.mkdir(parents=True)
assert get_common_ancestor([fn1, fn2]) == tmpdir.join("foo") fn1.touch()
assert get_common_ancestor([py.path.local(fn1.dirname), fn2]) == tmpdir.join( fn2 = tmp_path / "foo" / "zaz" / "test_2.py"
"foo" fn2.parent.mkdir(parents=True)
) fn2.touch()
assert get_common_ancestor( assert get_common_ancestor([fn1, fn2]) == tmp_path / "foo"
[py.path.local(fn1.dirname), py.path.local(fn2.dirname)] assert get_common_ancestor([fn1.parent, fn2]) == tmp_path / "foo"
) == tmpdir.join("foo") assert get_common_ancestor([fn1.parent, fn2.parent]) == tmp_path / "foo"
assert get_common_ancestor([fn1, py.path.local(fn2.dirname)]) == tmpdir.join( assert get_common_ancestor([fn1, fn2.parent]) == tmp_path / "foo"
"foo"
)
def test_single_dir(self, tmpdir): def test_single_dir(self, tmp_path: Path) -> None:
assert get_common_ancestor([tmpdir]) == tmpdir assert get_common_ancestor([tmp_path]) == tmp_path
def test_single_file(self, tmpdir): def test_single_file(self, tmp_path: Path) -> None:
fn = tmpdir.join("foo.py").ensure(file=1) fn = tmp_path / "foo.py"
assert get_common_ancestor([fn]) == tmpdir fn.touch()
assert get_common_ancestor([fn]) == tmp_path

View File

@ -6,6 +6,8 @@ from textwrap import dedent
import py import py
import pytest import pytest
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import commonpath
from _pytest.pathlib import ensure_deletable from _pytest.pathlib import ensure_deletable
from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import get_extended_length_path_str from _pytest.pathlib import get_extended_length_path_str
@ -381,3 +383,21 @@ def test_suppress_error_removing_lock(tmp_path):
# check now that we can remove the lock file in normal circumstances # check now that we can remove the lock file in normal circumstances
assert ensure_deletable(path, consider_lock_dead_if_created_before=mtime + 30) assert ensure_deletable(path, consider_lock_dead_if_created_before=mtime + 30)
assert not lock.is_file() assert not lock.is_file()
def test_bestrelpath() -> None:
curdir = Path("/foo/bar/baz/path")
assert bestrelpath(curdir, curdir) == "."
assert bestrelpath(curdir, curdir / "hello" / "world") == "hello" + os.sep + "world"
assert bestrelpath(curdir, curdir.parent / "sister") == ".." + os.sep + "sister"
assert bestrelpath(curdir, curdir.parent) == ".."
assert bestrelpath(curdir, Path("hello")) == "hello"
def test_commonpath() -> None:
path = Path("/foo/bar/baz/path")
subpath = path / "sampledir"
assert commonpath(path, subpath) == path
assert commonpath(subpath, path) == path
assert commonpath(Path(str(path) + "suffix"), path) == path.parent
assert commonpath(path, path.parent.parent) == path.parent.parent