Skip to content

config: start migrating Config.{rootdir,inifile} from py.path.local to pathlib #7685

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/7685.improvement.rst
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 8 additions & 3 deletions doc/en/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions src/_pytest/cacheprovider.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def clear_cache(cls, cachedir: Path) -> None:

@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
Expand Down Expand Up @@ -264,7 +264,7 @@ def __init__(self, config: Config) -> None:

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()}

Expand Down Expand Up @@ -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)
Expand Down
108 changes: 84 additions & 24 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
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
Expand Down Expand Up @@ -520,7 +521,7 @@ def _getconftestmodules(
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 = []
Expand Down Expand Up @@ -820,13 +821,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.

Expand All @@ -841,11 +842,20 @@ class InvocationParams:
"""

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,
Expand All @@ -867,6 +877,10 @@ def __init__(
"""

self.invocation_params = invocation_params
"""The parameters with which pytest was invoked.

:type: InvocationParams
"""

_a = FILE_OR_DIR
self._parser = Parser(
Expand All @@ -876,7 +890,7 @@ def __init__(
self.pluginmanager = pluginmanager
"""The plugin manager handles plugin registration and hook invocation.

:type: PytestPluginManager.
:type: PytestPluginManager
"""

self.trace = self.pluginmanager.trace.root.get("config")
Expand All @@ -901,9 +915,55 @@ def __init__(

@property
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))

@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:
"""Add a function to be called when the config object gets out of
use (usually coninciding with pytest_unconfigure)."""
Expand Down Expand Up @@ -977,9 +1037,9 @@ def notify_exception(

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
Expand Down Expand Up @@ -1014,11 +1074,11 @@ def _initini(self, args: Sequence[str]) -> None:
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
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(
Expand Down Expand Up @@ -1110,8 +1170,8 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None:
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(
Expand Down Expand Up @@ -1147,13 +1207,13 @@ def _checkversion(self) -> None:

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:
Expand Down Expand Up @@ -1218,10 +1278,10 @@ def parse(self, args: List[str], addopts: bool = True) -> None:
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
Expand Down Expand Up @@ -1324,10 +1384,10 @@ def _getini(self, name: str):
#
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":
Expand Down
12 changes: 9 additions & 3 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
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
Expand Down Expand Up @@ -1443,7 +1444,7 @@ def getfixtureinfo(
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:
Expand All @@ -1452,8 +1453,13 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
# 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)

Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ def set_log_path(self, fname: str) -> None:
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)
Expand Down
27 changes: 15 additions & 12 deletions src/_pytest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -456,8 +457,8 @@ def __init__(self, config: Config) -> None:
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")

Expand All @@ -475,7 +476,7 @@ def __repr__(self) -> str:
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]

Expand Down Expand Up @@ -599,7 +600,9 @@ def perform_collect( # noqa: F811
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)
Expand Down Expand Up @@ -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).

Expand All @@ -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 = (
Expand Down
Loading