Merge pull request #7685 from bluetech/py-to-pathlib-2

config: start migrating Config.{rootdir,inifile} from py.path.local to pathlib
This commit is contained in:
Ran Benita 2020-09-04 18:42:52 +03:00 committed by GitHub
commit 885d969484
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 204 additions and 119 deletions

View File

@ -0,0 +1,3 @@
Added two new attributes :attr:`rootpath <_pytest.config.Config.rootpath>` and :attr:`inipath <_pytest.config.Config.inipath>` to :class:`Config <_pytest.config.Config>`.
These attributes are :class:`pathlib.Path` versions of the existing :attr:`rootdir <_pytest.config.Config.rootdir>` and :attr:`inifile <_pytest.config.Config.inifile>` attributes,
and should be preferred over them when possible.

View File

@ -180,10 +180,15 @@ are never merged - the first match wins.
The internal :class:`Config <_pytest.config.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture) The internal :class:`Config <_pytest.config.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
will subsequently carry these attributes: will subsequently carry these attributes:
- ``config.rootdir``: the determined root directory, guaranteed to exist. - :attr:`config.rootpath <_pytest.config.Config.rootpath>`: the determined root directory, guaranteed to exist.
- ``config.inifile``: the determined ``configfile``, may be ``None`` (it is named ``inifile`` - :attr:`config.inipath <_pytest.config.Config.inipath>`: the determined ``configfile``, may be ``None``
for historical reasons). (it is named ``inipath`` for historical reasons).
.. versionadded:: 6.1
The ``config.rootpath`` and ``config.inipath`` properties. They are :class:`pathlib.Path`
versions of the older ``config.rootdir`` and ``config.inifile``, which have type
``py.path.local``, and still exist for backward compatibility.
The ``rootdir`` is used as a reference directory for constructing test The ``rootdir`` is used as a reference directory for constructing test
addresses ("nodeids") and can be used also by plugins for storing addresses ("nodeids") and can be used also by plugins for storing

View File

@ -78,7 +78,7 @@ class Cache:
@staticmethod @staticmethod
def cache_dir_from_config(config: Config) -> Path: def cache_dir_from_config(config: Config) -> Path:
return resolve_from_str(config.getini("cache_dir"), config.rootdir) return resolve_from_str(config.getini("cache_dir"), config.rootpath)
def warn(self, fmt: str, **args: object) -> None: def warn(self, fmt: str, **args: object) -> None:
import warnings import warnings
@ -264,7 +264,7 @@ class LFPlugin:
def get_last_failed_paths(self) -> Set[Path]: def get_last_failed_paths(self) -> Set[Path]:
"""Return a set with all Paths()s of the previously failed nodeids.""" """Return a set with all Paths()s of the previously failed nodeids."""
rootpath = Path(str(self.config.rootdir)) rootpath = self.config.rootpath
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed} result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
return {x for x in result if x.exists()} return {x for x in result if x.exists()}
@ -495,7 +495,7 @@ def pytest_report_header(config: Config) -> Optional[str]:
# starting with .., ../.. if sensible # starting with .., ../.. if sensible
try: try:
displaypath = cachedir.relative_to(str(config.rootdir)) displaypath = cachedir.relative_to(config.rootpath)
except ValueError: except ValueError:
displaypath = cachedir displaypath = cachedir
return "cachedir: {}".format(displaypath) return "cachedir: {}".format(displaypath)

View File

@ -47,6 +47,7 @@ from _pytest.compat import importlib_metadata
from _pytest.compat import TYPE_CHECKING from _pytest.compat import TYPE_CHECKING
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import Skipped from _pytest.outcomes import Skipped
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import import_path from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode from _pytest.pathlib import ImportMode
from _pytest.pathlib import Path from _pytest.pathlib import Path
@ -520,7 +521,7 @@ class PytestPluginManager(PluginManager):
else: else:
directory = path directory = path
# XXX these days we may rather want to use config.rootdir # XXX these days we may rather want to use config.rootpath
# and allow users to opt into looking into the rootdir parent # and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir. # directories instead of requiring to specify confcutdir.
clist = [] clist = []
@ -820,13 +821,13 @@ class Config:
:param PytestPluginManager pluginmanager: :param PytestPluginManager pluginmanager:
:param InvocationParams invocation_params: :param InvocationParams invocation_params:
Object containing the parameters regarding the ``pytest.main`` Object containing parameters regarding the :func:`pytest.main`
invocation. invocation.
""" """
@attr.s(frozen=True) @attr.s(frozen=True)
class InvocationParams: class InvocationParams:
"""Holds parameters passed during ``pytest.main()`` """Holds parameters passed during :func:`pytest.main`.
The object attributes are read-only. The object attributes are read-only.
@ -841,11 +842,20 @@ class Config:
""" """
args = attr.ib(type=Tuple[str, ...], converter=_args_converter) args = attr.ib(type=Tuple[str, ...], converter=_args_converter)
"""Tuple of command-line arguments as passed to ``pytest.main()``.""" """The command-line arguments as passed to :func:`pytest.main`.
:type: Tuple[str, ...]
"""
plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]]) plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]])
"""List of extra plugins, might be `None`.""" """Extra plugins, might be `None`.
:type: Optional[Sequence[Union[str, plugin]]]
"""
dir = attr.ib(type=Path) dir = attr.ib(type=Path)
"""Directory from which ``pytest.main()`` was invoked.""" """The directory from which :func:`pytest.main` was invoked.
:type: pathlib.Path
"""
def __init__( def __init__(
self, self,
@ -867,6 +877,10 @@ class Config:
""" """
self.invocation_params = invocation_params self.invocation_params = invocation_params
"""The parameters with which pytest was invoked.
:type: InvocationParams
"""
_a = FILE_OR_DIR _a = FILE_OR_DIR
self._parser = Parser( self._parser = Parser(
@ -876,7 +890,7 @@ class Config:
self.pluginmanager = pluginmanager self.pluginmanager = pluginmanager
"""The plugin manager handles plugin registration and hook invocation. """The plugin manager handles plugin registration and hook invocation.
:type: PytestPluginManager. :type: PytestPluginManager
""" """
self.trace = self.pluginmanager.trace.root.get("config") self.trace = self.pluginmanager.trace.root.get("config")
@ -901,9 +915,55 @@ class Config:
@property @property
def invocation_dir(self) -> py.path.local: def invocation_dir(self) -> py.path.local:
"""Backward compatibility.""" """The directory from which pytest was invoked.
Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`,
which is a :class:`pathlib.Path`.
:type: py.path.local
"""
return py.path.local(str(self.invocation_params.dir)) return py.path.local(str(self.invocation_params.dir))
@property
def rootpath(self) -> Path:
"""The path to the :ref:`rootdir <rootdir>`.
:type: pathlib.Path
.. versionadded:: 6.1
"""
return self._rootpath
@property
def rootdir(self) -> py.path.local:
"""The path to the :ref:`rootdir <rootdir>`.
Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.
:type: py.path.local
"""
return py.path.local(str(self.rootpath))
@property
def inipath(self) -> Optional[Path]:
"""The path to the :ref:`configfile <configfiles>`.
:type: Optional[pathlib.Path]
.. versionadded:: 6.1
"""
return self._inipath
@property
def inifile(self) -> Optional[py.path.local]:
"""The path to the :ref:`configfile <configfiles>`.
Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
:type: Optional[py.path.local]
"""
return py.path.local(str(self.inipath)) if self.inipath else None
def add_cleanup(self, func: Callable[[], None]) -> None: def add_cleanup(self, func: Callable[[], None]) -> None:
"""Add a function to be called when the config object gets out of """Add a function to be called when the config object gets out of
use (usually coninciding with pytest_unconfigure).""" use (usually coninciding with pytest_unconfigure)."""
@ -977,9 +1037,9 @@ class Config:
def cwd_relative_nodeid(self, nodeid: str) -> str: def cwd_relative_nodeid(self, nodeid: str) -> str:
# nodeid's are relative to the rootpath, compute relative to cwd. # nodeid's are relative to the rootpath, compute relative to cwd.
if self.invocation_dir != self.rootdir: if self.invocation_params.dir != self.rootpath:
fullpath = self.rootdir.join(nodeid) fullpath = self.rootpath / nodeid
nodeid = self.invocation_dir.bestrelpath(fullpath) nodeid = bestrelpath(self.invocation_params.dir, fullpath)
return nodeid return nodeid
@classmethod @classmethod
@ -1014,11 +1074,11 @@ class Config:
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._rootpath = rootpath
self.inifile = py.path.local(str(inipath)) if inipath else None self._inipath = inipath
self.inicfg = inicfg self.inicfg = inicfg
self._parser.extra_info["rootdir"] = self.rootdir self._parser.extra_info["rootdir"] = str(self.rootpath)
self._parser.extra_info["inifile"] = self.inifile self._parser.extra_info["inifile"] = str(self.inipath)
self._parser.addini("addopts", "extra command line options", "args") self._parser.addini("addopts", "extra command line options", "args")
self._parser.addini("minversion", "minimally required pytest version") self._parser.addini("minversion", "minimally required pytest version")
self._parser.addini( self._parser.addini(
@ -1110,8 +1170,8 @@ class Config:
self._validate_plugins() self._validate_plugins()
self._warn_about_skipped_plugins() self._warn_about_skipped_plugins()
if self.known_args_namespace.confcutdir is None and self.inifile: if self.known_args_namespace.confcutdir is None and self.inipath is not None:
confcutdir = py.path.local(self.inifile).dirname confcutdir = str(self.inipath.parent)
self.known_args_namespace.confcutdir = confcutdir self.known_args_namespace.confcutdir = confcutdir
try: try:
self.hook.pytest_load_initial_conftests( self.hook.pytest_load_initial_conftests(
@ -1147,13 +1207,13 @@ class Config:
if not isinstance(minver, str): if not isinstance(minver, str):
raise pytest.UsageError( raise pytest.UsageError(
"%s: 'minversion' must be a single value" % self.inifile "%s: 'minversion' must be a single value" % self.inipath
) )
if Version(minver) > Version(pytest.__version__): if Version(minver) > Version(pytest.__version__):
raise pytest.UsageError( raise pytest.UsageError(
"%s: 'minversion' requires pytest-%s, actual pytest-%s'" "%s: 'minversion' requires pytest-%s, actual pytest-%s'"
% (self.inifile, minver, pytest.__version__,) % (self.inipath, minver, pytest.__version__,)
) )
def _validate_config_options(self) -> None: def _validate_config_options(self) -> None:
@ -1218,10 +1278,10 @@ class Config:
args, self.option, namespace=self.option args, self.option, namespace=self.option
) )
if not args: if not args:
if self.invocation_dir == self.rootdir: if self.invocation_params.dir == self.rootpath:
args = self.getini("testpaths") args = self.getini("testpaths")
if not args: if not args:
args = [str(self.invocation_dir)] args = [str(self.invocation_params.dir)]
self.args = args self.args = args
except PrintHelp: except PrintHelp:
pass pass
@ -1324,10 +1384,10 @@ class Config:
# #
if type == "pathlist": if type == "pathlist":
# TODO: This assert is probably not valid in all cases. # TODO: This assert is probably not valid in all cases.
assert self.inifile is not None assert self.inipath is not None
dp = py.path.local(self.inifile).dirpath() dp = self.inipath.parent
input_values = shlex.split(value) if isinstance(value, str) else value input_values = shlex.split(value) if isinstance(value, str) else value
return [dp.join(x, abs=True) for x in input_values] return [py.path.local(str(dp / x)) for x in input_values]
elif type == "args": elif type == "args":
return shlex.split(value) if isinstance(value, str) else value return shlex.split(value) if isinstance(value, str) else value
elif type == "linelist": elif type == "linelist":

View File

@ -50,6 +50,7 @@ from _pytest.deprecated import FILLFUNCARGS
from _pytest.mark import ParameterSet from _pytest.mark import ParameterSet
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
from _pytest.pathlib import absolutepath
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Deque from typing import Deque
@ -1443,7 +1444,7 @@ class FixtureManager:
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
nodeid = None nodeid = None
try: try:
p = py.path.local(plugin.__file__) # type: ignore[attr-defined] p = absolutepath(plugin.__file__) # type: ignore[attr-defined]
except AttributeError: except AttributeError:
pass pass
else: else:
@ -1452,8 +1453,13 @@ class FixtureManager:
# Construct the base nodeid which is later used to check # Construct the base nodeid which is later used to check
# what fixtures are visible for particular tests (as denoted # what fixtures are visible for particular tests (as denoted
# by their test id). # by their test id).
if p.basename.startswith("conftest.py"): if p.name.startswith("conftest.py"):
nodeid = p.dirpath().relto(self.config.rootdir) try:
nodeid = str(p.parent.relative_to(self.config.rootpath))
except ValueError:
nodeid = ""
if nodeid == ".":
nodeid = ""
if os.sep != nodes.SEP: if os.sep != nodes.SEP:
nodeid = nodeid.replace(os.sep, nodes.SEP) nodeid = nodeid.replace(os.sep, nodes.SEP)

View File

@ -603,7 +603,7 @@ class LoggingPlugin:
fpath = Path(fname) fpath = Path(fname)
if not fpath.is_absolute(): if not fpath.is_absolute():
fpath = Path(str(self._config.rootdir), fpath) fpath = self._config.rootpath / fpath
if not fpath.parent.exists(): if not fpath.parent.exists():
fpath.parent.mkdir(exist_ok=True, parents=True) fpath.parent.mkdir(exist_ok=True, parents=True)

View File

@ -33,6 +33,7 @@ from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureManager from _pytest.fixtures import FixtureManager
from _pytest.outcomes import exit from _pytest.outcomes import exit
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import Path from _pytest.pathlib import Path
from _pytest.pathlib import visit from _pytest.pathlib import visit
from _pytest.reports import CollectReport from _pytest.reports import CollectReport
@ -425,11 +426,11 @@ class Failed(Exception):
@attr.s @attr.s
class _bestrelpath_cache(Dict[py.path.local, str]): class _bestrelpath_cache(Dict[Path, str]):
path = attr.ib(type=py.path.local) path = attr.ib(type=Path)
def __missing__(self, path: py.path.local) -> str: def __missing__(self, path: Path) -> str:
r = self.path.bestrelpath(path) # type: str r = bestrelpath(self.path, path)
self[path] = r self[path] = r
return r return r
@ -444,8 +445,8 @@ class Session(nodes.FSCollector):
exitstatus = None # type: Union[int, ExitCode] exitstatus = None # type: Union[int, ExitCode]
def __init__(self, config: Config) -> None: def __init__(self, config: Config) -> None:
nodes.FSCollector.__init__( super().__init__(
self, config.rootdir, parent=None, config=config, session=self, nodeid="" config.rootdir, parent=None, config=config, session=self, nodeid=""
) )
self.testsfailed = 0 self.testsfailed = 0
self.testscollected = 0 self.testscollected = 0
@ -456,8 +457,8 @@ class Session(nodes.FSCollector):
self._initialpaths = frozenset() # type: FrozenSet[py.path.local] self._initialpaths = frozenset() # type: FrozenSet[py.path.local]
self._bestrelpathcache = _bestrelpath_cache( self._bestrelpathcache = _bestrelpath_cache(
config.rootdir config.rootpath
) # type: Dict[py.path.local, str] ) # type: Dict[Path, str]
self.config.pluginmanager.register(self, name="session") self.config.pluginmanager.register(self, name="session")
@ -475,7 +476,7 @@ class Session(nodes.FSCollector):
self.testscollected, self.testscollected,
) )
def _node_location_to_relpath(self, node_path: py.path.local) -> str: def _node_location_to_relpath(self, node_path: Path) -> str:
# bestrelpath is a quite slow function. # bestrelpath is a quite slow function.
return self._bestrelpathcache[node_path] return self._bestrelpathcache[node_path]
@ -599,7 +600,9 @@ class Session(nodes.FSCollector):
initialpaths = [] # type: List[py.path.local] initialpaths = [] # type: List[py.path.local]
for arg in args: for arg in args:
fspath, parts = resolve_collection_argument( fspath, parts = resolve_collection_argument(
self.config.invocation_dir, arg, as_pypath=self.config.option.pyargs self.config.invocation_params.dir,
arg,
as_pypath=self.config.option.pyargs,
) )
self._initial_parts.append((fspath, parts)) self._initial_parts.append((fspath, parts))
initialpaths.append(fspath) initialpaths.append(fspath)
@ -817,7 +820,7 @@ def search_pypath(module_name: str) -> str:
def resolve_collection_argument( def resolve_collection_argument(
invocation_dir: py.path.local, arg: str, *, as_pypath: bool = False invocation_path: Path, arg: str, *, as_pypath: bool = False
) -> Tuple[py.path.local, List[str]]: ) -> Tuple[py.path.local, List[str]]:
"""Parse path arguments optionally containing selection parts and return (fspath, names). """Parse path arguments optionally containing selection parts and return (fspath, names).
@ -844,7 +847,7 @@ def resolve_collection_argument(
strpath, *parts = str(arg).split("::") strpath, *parts = str(arg).split("::")
if as_pypath: if as_pypath:
strpath = search_pypath(strpath) strpath = search_pypath(strpath)
fspath = Path(str(invocation_dir), strpath) fspath = invocation_path / strpath
fspath = absolutepath(fspath) fspath = absolutepath(fspath)
if not fspath.exists(): if not fspath.exists():
msg = ( msg = (

View File

@ -31,6 +31,7 @@ from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords from _pytest.mark.structures import NodeKeywords
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.pathlib import absolutepath
from _pytest.pathlib import Path from _pytest.pathlib import Path
from _pytest.store import Store from _pytest.store import Store
@ -401,7 +402,7 @@ class Node(metaclass=NodeMeta):
# It will be better to just always display paths relative to invocation_dir, but # It will be better to just always display paths relative to invocation_dir, but
# this requires a lot of plumbing (#6428). # this requires a lot of plumbing (#6428).
try: try:
abspath = Path(os.getcwd()) != Path(str(self.config.invocation_dir)) abspath = Path(os.getcwd()) != self.config.invocation_params.dir
except OSError: except OSError:
abspath = True abspath = True
@ -597,10 +598,7 @@ class Item(Node):
@cached_property @cached_property
def location(self) -> Tuple[str, Optional[int], str]: def location(self) -> Tuple[str, Optional[int], str]:
location = self.reportinfo() location = self.reportinfo()
if isinstance(location[0], py.path.local): fspath = absolutepath(str(location[0]))
fspath = location[0]
else:
fspath = py.path.local(location[0])
relfspath = self.session._node_location_to_relpath(fspath) relfspath = self.session._node_location_to_relpath(fspath)
assert type(location[2]) is str assert type(location[2]) is str
return (relfspath, location[1], location[2]) return (relfspath, location[1], location[2])

View File

@ -366,8 +366,7 @@ def make_numbered_dir_with_cleanup(
raise e raise e
def resolve_from_str(input: str, root: py.path.local) -> Path: def resolve_from_str(input: str, rootpath: Path) -> Path:
rootpath = Path(root)
input = expanduser(input) input = expanduser(input)
input = expandvars(input) input = expandvars(input)
if isabs(input): if isabs(input):

View File

@ -40,6 +40,9 @@ from _pytest.config import ExitCode
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.nodes import Node from _pytest.nodes import Node
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import Path
from _pytest.reports import BaseReport from _pytest.reports import BaseReport
from _pytest.reports import CollectReport from _pytest.reports import CollectReport
from _pytest.reports import TestReport from _pytest.reports import TestReport
@ -297,9 +300,9 @@ class WarningReport:
if self.fslocation: if self.fslocation:
if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2: if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2:
filename, linenum = self.fslocation[:2] filename, linenum = self.fslocation[:2]
relpath = py.path.local(filename).relto(config.invocation_dir) relpath = bestrelpath(
if not relpath: config.invocation_params.dir, absolutepath(filename)
relpath = str(filename) )
return "{}:{}".format(relpath, linenum) return "{}:{}".format(relpath, linenum)
else: else:
return str(self.fslocation) return str(self.fslocation)
@ -319,11 +322,12 @@ class TerminalReporter:
self._main_color = None # type: Optional[str] self._main_color = None # type: Optional[str]
self._known_types = None # type: Optional[List[str]] self._known_types = None # type: Optional[List[str]]
self.startdir = config.invocation_dir self.startdir = config.invocation_dir
self.startpath = config.invocation_params.dir
if file is None: if file is None:
file = sys.stdout file = sys.stdout
self._tw = _pytest.config.create_terminal_writer(config, file) self._tw = _pytest.config.create_terminal_writer(config, file)
self._screen_width = self._tw.fullwidth self._screen_width = self._tw.fullwidth
self.currentfspath = None # type: Any self.currentfspath = None # type: Union[None, Path, str, int]
self.reportchars = getreportopt(config) self.reportchars = getreportopt(config)
self.hasmarkup = self._tw.hasmarkup self.hasmarkup = self._tw.hasmarkup
self.isatty = file.isatty() self.isatty = file.isatty()
@ -385,19 +389,17 @@ class TerminalReporter:
return char in self.reportchars return char in self.reportchars
def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None: def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None:
fspath = self.config.rootdir.join(nodeid.split("::")[0]) fspath = self.config.rootpath / nodeid.split("::")[0]
# NOTE: explicitly check for None to work around py bug, and for less
# overhead in general (https://github.com/pytest-dev/py/pull/207).
if self.currentfspath is None or fspath != self.currentfspath: if self.currentfspath is None or fspath != self.currentfspath:
if self.currentfspath is not None and self._show_progress_info: if self.currentfspath is not None and self._show_progress_info:
self._write_progress_information_filling_space() self._write_progress_information_filling_space()
self.currentfspath = fspath self.currentfspath = fspath
relfspath = self.startdir.bestrelpath(fspath) relfspath = bestrelpath(self.startpath, fspath)
self._tw.line() self._tw.line()
self._tw.write(relfspath + " ") self._tw.write(relfspath + " ")
self._tw.write(res, flush=True, **markup) self._tw.write(res, flush=True, **markup)
def write_ensure_prefix(self, prefix, extra: str = "", **kwargs) -> None: def write_ensure_prefix(self, prefix: str, extra: str = "", **kwargs) -> None:
if self.currentfspath != prefix: if self.currentfspath != prefix:
self._tw.line() self._tw.line()
self.currentfspath = prefix self.currentfspath = prefix
@ -709,14 +711,14 @@ class TerminalReporter:
self.write_line(line) self.write_line(line)
def pytest_report_header(self, config: Config) -> List[str]: def pytest_report_header(self, config: Config) -> List[str]:
line = "rootdir: %s" % config.rootdir line = "rootdir: %s" % config.rootpath
if config.inifile: if config.inipath:
line += ", configfile: " + config.rootdir.bestrelpath(config.inifile) line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
testpaths = config.getini("testpaths") testpaths = config.getini("testpaths")
if testpaths and config.args == testpaths: if testpaths and config.args == testpaths:
rel_paths = [config.rootdir.bestrelpath(x) for x in testpaths] rel_paths = [bestrelpath(config.rootpath, x) for x in testpaths]
line += ", testpaths: {}".format(", ".join(rel_paths)) line += ", testpaths: {}".format(", ".join(rel_paths))
result = [line] result = [line]
@ -860,7 +862,7 @@ class TerminalReporter:
if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace( if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace(
"\\", nodes.SEP "\\", nodes.SEP
): ):
res += " <- " + self.startdir.bestrelpath(fspath) res += " <- " + bestrelpath(self.startpath, fspath)
else: else:
res = "[location]" res = "[location]"
return res + " " return res + " "
@ -1102,7 +1104,7 @@ class TerminalReporter:
def show_skipped(lines: List[str]) -> None: def show_skipped(lines: List[str]) -> None:
skipped = self.stats.get("skipped", []) # type: List[CollectReport] skipped = self.stats.get("skipped", []) # type: List[CollectReport]
fskips = _folded_skips(self.startdir, skipped) if skipped else [] fskips = _folded_skips(self.startpath, skipped) if skipped else []
if not fskips: if not fskips:
return return
verbose_word = skipped[0]._get_verbose_word(self.config) verbose_word = skipped[0]._get_verbose_word(self.config)
@ -1230,7 +1232,7 @@ def _get_line_with_reprcrash_message(
def _folded_skips( def _folded_skips(
startdir: py.path.local, skipped: Sequence[CollectReport], startpath: Path, skipped: Sequence[CollectReport],
) -> List[Tuple[int, str, Optional[int], str]]: ) -> List[Tuple[int, str, Optional[int], str]]:
d = {} # type: Dict[Tuple[str, Optional[int], str], List[CollectReport]] d = {} # type: Dict[Tuple[str, Optional[int], str], List[CollectReport]]
for event in skipped: for event in skipped:
@ -1239,7 +1241,7 @@ def _folded_skips(
assert len(event.longrepr) == 3, (event, event.longrepr) assert len(event.longrepr) == 3, (event, event.longrepr)
fspath, lineno, reason = event.longrepr fspath, lineno, reason = event.longrepr
# For consistency, report all fspaths in relative form. # For consistency, report all fspaths in relative form.
fspath = startdir.bestrelpath(py.path.local(fspath)) fspath = bestrelpath(startpath, Path(fspath))
keywords = getattr(event, "keywords", {}) keywords = getattr(event, "keywords", {})
# Folding reports with global pytestmark variable. # Folding reports with global pytestmark variable.
# This is a workaround, because for now we cannot identify the scope of a skip marker # This is a workaround, because for now we cannot identify the scope of a skip marker

View File

@ -10,6 +10,7 @@ from _pytest.config import ExitCode
from _pytest.config import UsageError from _pytest.config import UsageError
from _pytest.main import resolve_collection_argument from _pytest.main import resolve_collection_argument
from _pytest.main import validate_basetemp from _pytest.main import validate_basetemp
from _pytest.pathlib import Path
from _pytest.pytester import Testdir from _pytest.pytester import Testdir
@ -108,73 +109,79 @@ def test_validate_basetemp_integration(testdir):
class TestResolveCollectionArgument: class TestResolveCollectionArgument:
@pytest.fixture @pytest.fixture
def root(self, testdir): def invocation_dir(self, testdir: Testdir) -> py.path.local:
testdir.syspathinsert(str(testdir.tmpdir / "src")) testdir.syspathinsert(str(testdir.tmpdir / "src"))
testdir.chdir() testdir.chdir()
pkg = testdir.tmpdir.join("src/pkg").ensure_dir() pkg = testdir.tmpdir.join("src/pkg").ensure_dir()
pkg.join("__init__.py").ensure(file=True) pkg.join("__init__.py").ensure()
pkg.join("test.py").ensure(file=True) pkg.join("test.py").ensure()
return testdir.tmpdir return testdir.tmpdir
def test_file(self, root): @pytest.fixture
def invocation_path(self, invocation_dir: py.path.local) -> Path:
return Path(str(invocation_dir))
def test_file(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
"""File and parts.""" """File and parts."""
assert resolve_collection_argument(root, "src/pkg/test.py") == ( assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == (
root / "src/pkg/test.py", invocation_dir / "src/pkg/test.py",
[], [],
) )
assert resolve_collection_argument(root, "src/pkg/test.py::") == ( assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == (
root / "src/pkg/test.py", invocation_dir / "src/pkg/test.py",
[""], [""],
) )
assert resolve_collection_argument(root, "src/pkg/test.py::foo::bar") == ( assert resolve_collection_argument(
root / "src/pkg/test.py", invocation_path, "src/pkg/test.py::foo::bar"
["foo", "bar"], ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"])
) assert resolve_collection_argument(
assert resolve_collection_argument(root, "src/pkg/test.py::foo::bar::") == ( invocation_path, "src/pkg/test.py::foo::bar::"
root / "src/pkg/test.py", ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar", ""])
["foo", "bar", ""],
)
def test_dir(self, root: py.path.local) -> None: def test_dir(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
"""Directory and parts.""" """Directory and parts."""
assert resolve_collection_argument(root, "src/pkg") == (root / "src/pkg", []) assert resolve_collection_argument(invocation_path, "src/pkg") == (
invocation_dir / "src/pkg",
with pytest.raises(
UsageError, match=r"directory argument cannot contain :: selection parts"
):
resolve_collection_argument(root, "src/pkg::")
with pytest.raises(
UsageError, match=r"directory argument cannot contain :: selection parts"
):
resolve_collection_argument(root, "src/pkg::foo::bar")
def test_pypath(self, root: py.path.local) -> None:
"""Dotted name and parts."""
assert resolve_collection_argument(root, "pkg.test", as_pypath=True) == (
root / "src/pkg/test.py",
[], [],
) )
with pytest.raises(
UsageError, match=r"directory argument cannot contain :: selection parts"
):
resolve_collection_argument(invocation_path, "src/pkg::")
with pytest.raises(
UsageError, match=r"directory argument cannot contain :: selection parts"
):
resolve_collection_argument(invocation_path, "src/pkg::foo::bar")
def test_pypath(self, invocation_dir: py.path.local, invocation_path: Path) -> None:
"""Dotted name and parts."""
assert resolve_collection_argument( assert resolve_collection_argument(
root, "pkg.test::foo::bar", as_pypath=True invocation_path, "pkg.test", as_pypath=True
) == (root / "src/pkg/test.py", ["foo", "bar"],) ) == (invocation_dir / "src/pkg/test.py", [])
assert resolve_collection_argument(root, "pkg", as_pypath=True) == ( assert resolve_collection_argument(
root / "src/pkg", invocation_path, "pkg.test::foo::bar", as_pypath=True
) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"])
assert resolve_collection_argument(invocation_path, "pkg", as_pypath=True) == (
invocation_dir / "src/pkg",
[], [],
) )
with pytest.raises( with pytest.raises(
UsageError, match=r"package argument cannot contain :: selection parts" UsageError, match=r"package argument cannot contain :: selection parts"
): ):
resolve_collection_argument(root, "pkg::foo::bar", as_pypath=True) resolve_collection_argument(
invocation_path, "pkg::foo::bar", as_pypath=True
)
def test_does_not_exist(self, root): def test_does_not_exist(self, invocation_path: Path) -> None:
"""Given a file/module that does not exist raises UsageError.""" """Given a file/module that does not exist raises UsageError."""
with pytest.raises( with pytest.raises(
UsageError, match=re.escape("file or directory not found: foobar") UsageError, match=re.escape("file or directory not found: foobar")
): ):
resolve_collection_argument(root, "foobar") resolve_collection_argument(invocation_path, "foobar")
with pytest.raises( with pytest.raises(
UsageError, UsageError,
@ -182,12 +189,14 @@ class TestResolveCollectionArgument:
"module or package not found: foobar (missing __init__.py?)" "module or package not found: foobar (missing __init__.py?)"
), ),
): ):
resolve_collection_argument(root, "foobar", as_pypath=True) resolve_collection_argument(invocation_path, "foobar", as_pypath=True)
def test_absolute_paths_are_resolved_correctly(self, root): def test_absolute_paths_are_resolved_correctly(
self, invocation_dir: py.path.local, invocation_path: Path
) -> None:
"""Absolute paths resolve back to absolute paths.""" """Absolute paths resolve back to absolute paths."""
full_path = str(root / "src") full_path = str(invocation_dir / "src")
assert resolve_collection_argument(root, full_path) == ( assert resolve_collection_argument(invocation_path, full_path) == (
py.path.local(os.path.abspath("src")), py.path.local(os.path.abspath("src")),
[], [],
) )
@ -195,10 +204,9 @@ class TestResolveCollectionArgument:
# ensure full paths given in the command-line without the drive letter resolve # ensure full paths given in the command-line without the drive letter resolve
# to the full path correctly (#7628) # to the full path correctly (#7628)
drive, full_path_without_drive = os.path.splitdrive(full_path) drive, full_path_without_drive = os.path.splitdrive(full_path)
assert resolve_collection_argument(root, full_path_without_drive) == ( assert resolve_collection_argument(
py.path.local(os.path.abspath("src")), invocation_path, full_path_without_drive
[], ) == (py.path.local(os.path.abspath("src")), [])
)
def test_module_full_path_without_drive(testdir): def test_module_full_path_without_drive(testdir):

View File

@ -18,6 +18,7 @@ import pytest
from _pytest._io.wcwidth import wcswidth from _pytest._io.wcwidth import wcswidth
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.pathlib import Path
from _pytest.pytester import Testdir from _pytest.pytester import Testdir
from _pytest.reports import BaseReport from _pytest.reports import BaseReport
from _pytest.reports import CollectReport from _pytest.reports import CollectReport
@ -2085,7 +2086,7 @@ def test_skip_reasons_folding() -> None:
ev3.longrepr = longrepr ev3.longrepr = longrepr
ev3.skipped = True ev3.skipped = True
values = _folded_skips(py.path.local(), [ev1, ev2, ev3]) values = _folded_skips(Path.cwd(), [ev1, ev2, ev3])
assert len(values) == 1 assert len(values) == 1
num, fspath, lineno_, reason = values[0] num, fspath, lineno_, reason = values[0]
assert num == 3 assert num == 3