From 3085c99e4768b6dc46a7a40c2e29216e12faeb84 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 25 Aug 2020 10:13:48 +0300 Subject: [PATCH 1/3] config: small doc improvements --- src/_pytest/config/__init__.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 5949c787b..ce88fc82f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -820,13 +820,13 @@ class Config: :param PytestPluginManager pluginmanager: :param InvocationParams invocation_params: - Object containing the parameters regarding the ``pytest.main`` + Object containing parameters regarding the :func:`pytest.main` invocation. """ @attr.s(frozen=True) class InvocationParams: - """Holds parameters passed during ``pytest.main()`` + """Holds parameters passed during :func:`pytest.main`. The object attributes are read-only. @@ -841,11 +841,20 @@ class Config: """ 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]]]) - """List of extra plugins, might be `None`.""" + """Extra plugins, might be `None`. + + :type: Optional[Sequence[Union[str, plugin]]] + """ 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__( self, @@ -867,6 +876,10 @@ class Config: """ self.invocation_params = invocation_params + """The parameters with which pytest was invoked. + + :type: InvocationParams + """ _a = FILE_OR_DIR self._parser = Parser( @@ -876,7 +889,7 @@ class Config: self.pluginmanager = pluginmanager """The plugin manager handles plugin registration and hook invocation. - :type: PytestPluginManager. + :type: PytestPluginManager """ self.trace = self.pluginmanager.trace.root.get("config") @@ -901,7 +914,12 @@ class Config: @property def invocation_dir(self) -> py.path.local: - """Backward compatibility.""" + """The directory from which pytest was invoked. + + Prefer to use :attr:`invocation_params.dir `. + + :type: py.path.local + """ return py.path.local(str(self.invocation_params.dir)) def add_cleanup(self, func: Callable[[], None]) -> None: From a346028006a7ca9d07788e837456b3ab3a1209eb Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 6 Aug 2020 14:37:19 +0300 Subject: [PATCH 2/3] config: add Config.{rootpath,inipath}, turn Config.{rootdir,inifile} to properties --- changelog/7685.improvement.rst | 3 +++ doc/en/customize.rst | 11 +++++--- src/_pytest/config/__init__.py | 47 +++++++++++++++++++++++++++++++--- 3 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 changelog/7685.improvement.rst diff --git a/changelog/7685.improvement.rst b/changelog/7685.improvement.rst new file mode 100644 index 000000000..597721624 --- /dev/null +++ b/changelog/7685.improvement.rst @@ -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. diff --git a/doc/en/customize.rst b/doc/en/customize.rst index e1f1b253b..9f7c365dc 100644 --- a/doc/en/customize.rst +++ b/doc/en/customize.rst @@ -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) 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`` - for historical reasons). +- :attr:`config.inipath <_pytest.config.Config.inipath>`: the determined ``configfile``, may be ``None`` + (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 addresses ("nodeids") and can be used also by plugins for storing diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ce88fc82f..6cda7da71 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -916,12 +916,53 @@ class Config: def invocation_dir(self) -> py.path.local: """The directory from which pytest was invoked. - Prefer to use :attr:`invocation_params.dir `. + Prefer to use :attr:`invocation_params.dir `, + which is a :class:`pathlib.Path`. :type: py.path.local """ return py.path.local(str(self.invocation_params.dir)) + @property + def rootpath(self) -> Path: + """The path to the :ref:`rootdir `. + + :type: pathlib.Path + + .. versionadded:: 6.1 + """ + return self._rootpath + + @property + def rootdir(self) -> py.path.local: + """The path to the :ref:`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 `. + + :type: Optional[pathlib.Path] + + .. versionadded:: 6.1 + """ + return self._inipath + + @property + def inifile(self) -> Optional[py.path.local]: + """The path to the :ref:`configfile `. + + 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: """Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" @@ -1032,8 +1073,8 @@ class Config: rootdir_cmd_arg=ns.rootdir or None, config=self, ) - self.rootdir = py.path.local(str(rootpath)) - self.inifile = py.path.local(str(inipath)) if inipath else None + self._rootpath = rootpath + self._inipath = inipath self.inicfg = inicfg self._parser.extra_info["rootdir"] = self.rootdir self._parser.extra_info["inifile"] = self.inifile From 62e249a1f934d1073c9a0167077e133c5e0f6270 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 6 Aug 2020 14:46:24 +0300 Subject: [PATCH 3/3] Replace some usages of config.{rootdir,inifile} with config.{rootpath,inipath} --- src/_pytest/cacheprovider.py | 6 +- src/_pytest/config/__init__.py | 31 +++++----- src/_pytest/fixtures.py | 12 +++- src/_pytest/logging.py | 2 +- src/_pytest/main.py | 27 +++++---- src/_pytest/nodes.py | 8 +-- src/_pytest/pathlib.py | 3 +- src/_pytest/terminal.py | 36 ++++++------ testing/test_main.py | 104 ++++++++++++++++++--------------- testing/test_terminal.py | 3 +- 10 files changed, 125 insertions(+), 107 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index bc26c26bc..ba27735d0 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -78,7 +78,7 @@ class Cache: @staticmethod 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: import warnings @@ -264,7 +264,7 @@ class LFPlugin: def get_last_failed_paths(self) -> Set[Path]: """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} 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 try: - displaypath = cachedir.relative_to(str(config.rootdir)) + displaypath = cachedir.relative_to(config.rootpath) except ValueError: displaypath = cachedir return "cachedir: {}".format(displaypath) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 6cda7da71..4b49a8467 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -47,6 +47,7 @@ from _pytest.compat import importlib_metadata from _pytest.compat import TYPE_CHECKING from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import bestrelpath from _pytest.pathlib import import_path from _pytest.pathlib import ImportMode from _pytest.pathlib import Path @@ -520,7 +521,7 @@ class PytestPluginManager(PluginManager): else: 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 # directories instead of requiring to specify confcutdir. clist = [] @@ -1036,9 +1037,9 @@ class Config: def cwd_relative_nodeid(self, nodeid: str) -> str: # nodeid's are relative to the rootpath, compute relative to cwd. - if self.invocation_dir != self.rootdir: - fullpath = self.rootdir.join(nodeid) - nodeid = self.invocation_dir.bestrelpath(fullpath) + if self.invocation_params.dir != self.rootpath: + fullpath = self.rootpath / nodeid + nodeid = bestrelpath(self.invocation_params.dir, fullpath) return nodeid @classmethod @@ -1076,8 +1077,8 @@ class Config: self._rootpath = rootpath self._inipath = inipath self.inicfg = inicfg - self._parser.extra_info["rootdir"] = self.rootdir - self._parser.extra_info["inifile"] = self.inifile + self._parser.extra_info["rootdir"] = str(self.rootpath) + self._parser.extra_info["inifile"] = str(self.inipath) self._parser.addini("addopts", "extra command line options", "args") self._parser.addini("minversion", "minimally required pytest version") self._parser.addini( @@ -1169,8 +1170,8 @@ class Config: self._validate_plugins() self._warn_about_skipped_plugins() - if self.known_args_namespace.confcutdir is None and self.inifile: - confcutdir = py.path.local(self.inifile).dirname + if self.known_args_namespace.confcutdir is None and self.inipath is not None: + confcutdir = str(self.inipath.parent) self.known_args_namespace.confcutdir = confcutdir try: self.hook.pytest_load_initial_conftests( @@ -1206,13 +1207,13 @@ class Config: if not isinstance(minver, str): 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__): raise pytest.UsageError( "%s: 'minversion' requires pytest-%s, actual pytest-%s'" - % (self.inifile, minver, pytest.__version__,) + % (self.inipath, minver, pytest.__version__,) ) def _validate_config_options(self) -> None: @@ -1277,10 +1278,10 @@ class Config: args, self.option, namespace=self.option ) if not args: - if self.invocation_dir == self.rootdir: + if self.invocation_params.dir == self.rootpath: args = self.getini("testpaths") if not args: - args = [str(self.invocation_dir)] + args = [str(self.invocation_params.dir)] self.args = args except PrintHelp: pass @@ -1383,10 +1384,10 @@ class Config: # if type == "pathlist": # TODO: This assert is probably not valid in all cases. - assert self.inifile is not None - dp = py.path.local(self.inifile).dirpath() + assert self.inipath is not None + dp = self.inipath.parent 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": return shlex.split(value) if isinstance(value, str) else value elif type == "linelist": diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d2a08d57b..bf77d09f1 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -50,6 +50,7 @@ from _pytest.deprecated import FILLFUNCARGS from _pytest.mark import ParameterSet from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME +from _pytest.pathlib import absolutepath if TYPE_CHECKING: from typing import Deque @@ -1443,7 +1444,7 @@ class FixtureManager: def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: nodeid = None try: - p = py.path.local(plugin.__file__) # type: ignore[attr-defined] + p = absolutepath(plugin.__file__) # type: ignore[attr-defined] except AttributeError: pass else: @@ -1452,8 +1453,13 @@ class FixtureManager: # Construct the base nodeid which is later used to check # what fixtures are visible for particular tests (as denoted # by their test id). - if p.basename.startswith("conftest.py"): - nodeid = p.dirpath().relto(self.config.rootdir) + if p.name.startswith("conftest.py"): + try: + nodeid = str(p.parent.relative_to(self.config.rootpath)) + except ValueError: + nodeid = "" + if nodeid == ".": + nodeid = "" if os.sep != nodes.SEP: nodeid = nodeid.replace(os.sep, nodes.SEP) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 95226e8cc..98386bacd 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -603,7 +603,7 @@ class LoggingPlugin: fpath = Path(fname) if not fpath.is_absolute(): - fpath = Path(str(self._config.rootdir), fpath) + fpath = self._config.rootpath / fpath if not fpath.parent.exists(): fpath.parent.mkdir(exist_ok=True, parents=True) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 4ab91f82c..4e35990ad 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -33,6 +33,7 @@ from _pytest.config.argparsing import Parser from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit from _pytest.pathlib import absolutepath +from _pytest.pathlib import bestrelpath from _pytest.pathlib import Path from _pytest.pathlib import visit from _pytest.reports import CollectReport @@ -425,11 +426,11 @@ class Failed(Exception): @attr.s -class _bestrelpath_cache(Dict[py.path.local, str]): - path = attr.ib(type=py.path.local) +class _bestrelpath_cache(Dict[Path, str]): + path = attr.ib(type=Path) - def __missing__(self, path: py.path.local) -> str: - r = self.path.bestrelpath(path) # type: str + def __missing__(self, path: Path) -> str: + r = bestrelpath(self.path, path) self[path] = r return r @@ -444,8 +445,8 @@ class Session(nodes.FSCollector): exitstatus = None # type: Union[int, ExitCode] def __init__(self, config: Config) -> None: - nodes.FSCollector.__init__( - self, config.rootdir, parent=None, config=config, session=self, nodeid="" + super().__init__( + config.rootdir, parent=None, config=config, session=self, nodeid="" ) self.testsfailed = 0 self.testscollected = 0 @@ -456,8 +457,8 @@ class Session(nodes.FSCollector): self._initialpaths = frozenset() # type: FrozenSet[py.path.local] self._bestrelpathcache = _bestrelpath_cache( - config.rootdir - ) # type: Dict[py.path.local, str] + config.rootpath + ) # type: Dict[Path, str] self.config.pluginmanager.register(self, name="session") @@ -475,7 +476,7 @@ class Session(nodes.FSCollector): 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. return self._bestrelpathcache[node_path] @@ -599,7 +600,9 @@ class Session(nodes.FSCollector): initialpaths = [] # type: List[py.path.local] for arg in args: 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)) initialpaths.append(fspath) @@ -817,7 +820,7 @@ def search_pypath(module_name: str) -> str: 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]]: """Parse path arguments optionally containing selection parts and return (fspath, names). @@ -844,7 +847,7 @@ def resolve_collection_argument( strpath, *parts = str(arg).split("::") if as_pypath: strpath = search_pypath(strpath) - fspath = Path(str(invocation_dir), strpath) + fspath = invocation_path / strpath fspath = absolutepath(fspath) if not fspath.exists(): msg = ( diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 5dde4c737..3665d8d5e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -31,6 +31,7 @@ from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords from _pytest.outcomes import fail +from _pytest.pathlib import absolutepath from _pytest.pathlib import Path 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 # this requires a lot of plumbing (#6428). try: - abspath = Path(os.getcwd()) != Path(str(self.config.invocation_dir)) + abspath = Path(os.getcwd()) != self.config.invocation_params.dir except OSError: abspath = True @@ -597,10 +598,7 @@ class Item(Node): @cached_property def location(self) -> Tuple[str, Optional[int], str]: location = self.reportinfo() - if isinstance(location[0], py.path.local): - fspath = location[0] - else: - fspath = py.path.local(location[0]) + fspath = absolutepath(str(location[0])) relfspath = self.session._node_location_to_relpath(fspath) assert type(location[2]) is str return (relfspath, location[1], location[2]) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 4a249c8fd..355281039 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -366,8 +366,7 @@ def make_numbered_dir_with_cleanup( raise e -def resolve_from_str(input: str, root: py.path.local) -> Path: - rootpath = Path(root) +def resolve_from_str(input: str, rootpath: Path) -> Path: input = expanduser(input) input = expandvars(input) if isabs(input): diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index af6843000..59d6aa97d 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -40,6 +40,9 @@ from _pytest.config import ExitCode from _pytest.config.argparsing import Parser from _pytest.nodes import Item 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 CollectReport from _pytest.reports import TestReport @@ -297,9 +300,9 @@ class WarningReport: if self.fslocation: if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2: filename, linenum = self.fslocation[:2] - relpath = py.path.local(filename).relto(config.invocation_dir) - if not relpath: - relpath = str(filename) + relpath = bestrelpath( + config.invocation_params.dir, absolutepath(filename) + ) return "{}:{}".format(relpath, linenum) else: return str(self.fslocation) @@ -319,11 +322,12 @@ class TerminalReporter: self._main_color = None # type: Optional[str] self._known_types = None # type: Optional[List[str]] self.startdir = config.invocation_dir + self.startpath = config.invocation_params.dir if file is None: file = sys.stdout self._tw = _pytest.config.create_terminal_writer(config, file) 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.hasmarkup = self._tw.hasmarkup self.isatty = file.isatty() @@ -385,19 +389,17 @@ class TerminalReporter: return char in self.reportchars def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None: - fspath = self.config.rootdir.join(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). + fspath = self.config.rootpath / nodeid.split("::")[0] if self.currentfspath is None or fspath != self.currentfspath: if self.currentfspath is not None and self._show_progress_info: self._write_progress_information_filling_space() self.currentfspath = fspath - relfspath = self.startdir.bestrelpath(fspath) + relfspath = bestrelpath(self.startpath, fspath) self._tw.line() self._tw.write(relfspath + " ") 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: self._tw.line() self.currentfspath = prefix @@ -709,14 +711,14 @@ class TerminalReporter: self.write_line(line) def pytest_report_header(self, config: Config) -> List[str]: - line = "rootdir: %s" % config.rootdir + line = "rootdir: %s" % config.rootpath - if config.inifile: - line += ", configfile: " + config.rootdir.bestrelpath(config.inifile) + if config.inipath: + line += ", configfile: " + bestrelpath(config.rootpath, config.inipath) testpaths = config.getini("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)) result = [line] @@ -860,7 +862,7 @@ class TerminalReporter: if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace( "\\", nodes.SEP ): - res += " <- " + self.startdir.bestrelpath(fspath) + res += " <- " + bestrelpath(self.startpath, fspath) else: res = "[location]" return res + " " @@ -1102,7 +1104,7 @@ class TerminalReporter: def show_skipped(lines: List[str]) -> None: 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: return verbose_word = skipped[0]._get_verbose_word(self.config) @@ -1230,7 +1232,7 @@ def _get_line_with_reprcrash_message( def _folded_skips( - startdir: py.path.local, skipped: Sequence[CollectReport], + startpath: Path, skipped: Sequence[CollectReport], ) -> List[Tuple[int, str, Optional[int], str]]: d = {} # type: Dict[Tuple[str, Optional[int], str], List[CollectReport]] for event in skipped: @@ -1239,7 +1241,7 @@ def _folded_skips( assert len(event.longrepr) == 3, (event, event.longrepr) fspath, lineno, reason = event.longrepr # For consistency, report all fspaths in relative form. - fspath = startdir.bestrelpath(py.path.local(fspath)) + fspath = bestrelpath(startpath, Path(fspath)) keywords = getattr(event, "keywords", {}) # Folding reports with global pytestmark variable. # This is a workaround, because for now we cannot identify the scope of a skip marker diff --git a/testing/test_main.py b/testing/test_main.py index 71eae16b0..5b45ec6b5 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -10,6 +10,7 @@ from _pytest.config import ExitCode from _pytest.config import UsageError from _pytest.main import resolve_collection_argument from _pytest.main import validate_basetemp +from _pytest.pathlib import Path from _pytest.pytester import Testdir @@ -108,73 +109,79 @@ def test_validate_basetemp_integration(testdir): class TestResolveCollectionArgument: @pytest.fixture - def root(self, testdir): + def invocation_dir(self, testdir: Testdir) -> py.path.local: testdir.syspathinsert(str(testdir.tmpdir / "src")) testdir.chdir() pkg = testdir.tmpdir.join("src/pkg").ensure_dir() - pkg.join("__init__.py").ensure(file=True) - pkg.join("test.py").ensure(file=True) + pkg.join("__init__.py").ensure() + pkg.join("test.py").ensure() 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.""" - assert resolve_collection_argument(root, "src/pkg/test.py") == ( - root / "src/pkg/test.py", + assert resolve_collection_argument(invocation_path, "src/pkg/test.py") == ( + invocation_dir / "src/pkg/test.py", [], ) - assert resolve_collection_argument(root, "src/pkg/test.py::") == ( - root / "src/pkg/test.py", + assert resolve_collection_argument(invocation_path, "src/pkg/test.py::") == ( + invocation_dir / "src/pkg/test.py", [""], ) - assert resolve_collection_argument(root, "src/pkg/test.py::foo::bar") == ( - root / "src/pkg/test.py", - ["foo", "bar"], - ) - assert resolve_collection_argument(root, "src/pkg/test.py::foo::bar::") == ( - root / "src/pkg/test.py", - ["foo", "bar", ""], - ) + assert resolve_collection_argument( + invocation_path, "src/pkg/test.py::foo::bar" + ) == (invocation_dir / "src/pkg/test.py", ["foo", "bar"]) + assert resolve_collection_argument( + invocation_path, "src/pkg/test.py::foo::bar::" + ) == (invocation_dir / "src/pkg/test.py", ["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.""" - assert resolve_collection_argument(root, "src/pkg") == (root / "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", + 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(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( - root, "pkg.test::foo::bar", as_pypath=True - ) == (root / "src/pkg/test.py", ["foo", "bar"],) - assert resolve_collection_argument(root, "pkg", as_pypath=True) == ( - root / "src/pkg", + invocation_path, "pkg.test", as_pypath=True + ) == (invocation_dir / "src/pkg/test.py", []) + assert resolve_collection_argument( + 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( 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.""" with pytest.raises( UsageError, match=re.escape("file or directory not found: foobar") ): - resolve_collection_argument(root, "foobar") + resolve_collection_argument(invocation_path, "foobar") with pytest.raises( UsageError, @@ -182,12 +189,14 @@ class TestResolveCollectionArgument: "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.""" - full_path = str(root / "src") - assert resolve_collection_argument(root, full_path) == ( + full_path = str(invocation_dir / "src") + assert resolve_collection_argument(invocation_path, full_path) == ( 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 # to the full path correctly (#7628) drive, full_path_without_drive = os.path.splitdrive(full_path) - assert resolve_collection_argument(root, full_path_without_drive) == ( - py.path.local(os.path.abspath("src")), - [], - ) + assert resolve_collection_argument( + invocation_path, full_path_without_drive + ) == (py.path.local(os.path.abspath("src")), []) def test_module_full_path_without_drive(testdir): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 0e26ae13c..57db1b9a5 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -18,6 +18,7 @@ import pytest from _pytest._io.wcwidth import wcswidth from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.pathlib import Path from _pytest.pytester import Testdir from _pytest.reports import BaseReport from _pytest.reports import CollectReport @@ -2085,7 +2086,7 @@ def test_skip_reasons_folding() -> None: ev3.longrepr = longrepr ev3.skipped = True - values = _folded_skips(py.path.local(), [ev1, ev2, ev3]) + values = _folded_skips(Path.cwd(), [ev1, ev2, ev3]) assert len(values) == 1 num, fspath, lineno_, reason = values[0] assert num == 3