Skip to content

Commit 885d969

Browse files
authored
Merge pull request #7685 from bluetech/py-to-pathlib-2
config: start migrating Config.{rootdir,inifile} from py.path.local to pathlib
2 parents 0d0b798 + 62e249a commit 885d969

File tree

12 files changed

+196
-111
lines changed

12 files changed

+196
-111
lines changed

changelog/7685.improvement.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added two new attributes :attr:`rootpath <_pytest.config.Config.rootpath>` and :attr:`inipath <_pytest.config.Config.inipath>` to :class:`Config <_pytest.config.Config>`.
2+
These attributes are :class:`pathlib.Path` versions of the existing :attr:`rootdir <_pytest.config.Config.rootdir>` and :attr:`inifile <_pytest.config.Config.inifile>` attributes,
3+
and should be preferred over them when possible.

doc/en/customize.rst

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,15 @@ are never merged - the first match wins.
180180
The internal :class:`Config <_pytest.config.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
181181
will subsequently carry these attributes:
182182

183-
- ``config.rootdir``: the determined root directory, guaranteed to exist.
183+
- :attr:`config.rootpath <_pytest.config.Config.rootpath>`: the determined root directory, guaranteed to exist.
184184

185-
- ``config.inifile``: the determined ``configfile``, may be ``None`` (it is named ``inifile``
186-
for historical reasons).
185+
- :attr:`config.inipath <_pytest.config.Config.inipath>`: the determined ``configfile``, may be ``None``
186+
(it is named ``inipath`` for historical reasons).
187+
188+
.. versionadded:: 6.1
189+
The ``config.rootpath`` and ``config.inipath`` properties. They are :class:`pathlib.Path`
190+
versions of the older ``config.rootdir`` and ``config.inifile``, which have type
191+
``py.path.local``, and still exist for backward compatibility.
187192

188193
The ``rootdir`` is used as a reference directory for constructing test
189194
addresses ("nodeids") and can be used also by plugins for storing

src/_pytest/cacheprovider.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def clear_cache(cls, cachedir: Path) -> None:
7878

7979
@staticmethod
8080
def cache_dir_from_config(config: Config) -> Path:
81-
return resolve_from_str(config.getini("cache_dir"), config.rootdir)
81+
return resolve_from_str(config.getini("cache_dir"), config.rootpath)
8282

8383
def warn(self, fmt: str, **args: object) -> None:
8484
import warnings
@@ -264,7 +264,7 @@ def __init__(self, config: Config) -> None:
264264

265265
def get_last_failed_paths(self) -> Set[Path]:
266266
"""Return a set with all Paths()s of the previously failed nodeids."""
267-
rootpath = Path(str(self.config.rootdir))
267+
rootpath = self.config.rootpath
268268
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
269269
return {x for x in result if x.exists()}
270270

@@ -495,7 +495,7 @@ def pytest_report_header(config: Config) -> Optional[str]:
495495
# starting with .., ../.. if sensible
496496

497497
try:
498-
displaypath = cachedir.relative_to(str(config.rootdir))
498+
displaypath = cachedir.relative_to(config.rootpath)
499499
except ValueError:
500500
displaypath = cachedir
501501
return "cachedir: {}".format(displaypath)

src/_pytest/config/__init__.py

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
from _pytest.compat import TYPE_CHECKING
4848
from _pytest.outcomes import fail
4949
from _pytest.outcomes import Skipped
50+
from _pytest.pathlib import bestrelpath
5051
from _pytest.pathlib import import_path
5152
from _pytest.pathlib import ImportMode
5253
from _pytest.pathlib import Path
@@ -520,7 +521,7 @@ def _getconftestmodules(
520521
else:
521522
directory = path
522523

523-
# XXX these days we may rather want to use config.rootdir
524+
# XXX these days we may rather want to use config.rootpath
524525
# and allow users to opt into looking into the rootdir parent
525526
# directories instead of requiring to specify confcutdir.
526527
clist = []
@@ -820,13 +821,13 @@ class Config:
820821
:param PytestPluginManager pluginmanager:
821822
822823
:param InvocationParams invocation_params:
823-
Object containing the parameters regarding the ``pytest.main``
824+
Object containing parameters regarding the :func:`pytest.main`
824825
invocation.
825826
"""
826827

827828
@attr.s(frozen=True)
828829
class InvocationParams:
829-
"""Holds parameters passed during ``pytest.main()``
830+
"""Holds parameters passed during :func:`pytest.main`.
830831
831832
The object attributes are read-only.
832833
@@ -841,11 +842,20 @@ class InvocationParams:
841842
"""
842843

843844
args = attr.ib(type=Tuple[str, ...], converter=_args_converter)
844-
"""Tuple of command-line arguments as passed to ``pytest.main()``."""
845+
"""The command-line arguments as passed to :func:`pytest.main`.
846+
847+
:type: Tuple[str, ...]
848+
"""
845849
plugins = attr.ib(type=Optional[Sequence[Union[str, _PluggyPlugin]]])
846-
"""List of extra plugins, might be `None`."""
850+
"""Extra plugins, might be `None`.
851+
852+
:type: Optional[Sequence[Union[str, plugin]]]
853+
"""
847854
dir = attr.ib(type=Path)
848-
"""Directory from which ``pytest.main()`` was invoked."""
855+
"""The directory from which :func:`pytest.main` was invoked.
856+
857+
:type: pathlib.Path
858+
"""
849859

850860
def __init__(
851861
self,
@@ -867,6 +877,10 @@ def __init__(
867877
"""
868878

869879
self.invocation_params = invocation_params
880+
"""The parameters with which pytest was invoked.
881+
882+
:type: InvocationParams
883+
"""
870884

871885
_a = FILE_OR_DIR
872886
self._parser = Parser(
@@ -876,7 +890,7 @@ def __init__(
876890
self.pluginmanager = pluginmanager
877891
"""The plugin manager handles plugin registration and hook invocation.
878892
879-
:type: PytestPluginManager.
893+
:type: PytestPluginManager
880894
"""
881895

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

902916
@property
903917
def invocation_dir(self) -> py.path.local:
904-
"""Backward compatibility."""
918+
"""The directory from which pytest was invoked.
919+
920+
Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`,
921+
which is a :class:`pathlib.Path`.
922+
923+
:type: py.path.local
924+
"""
905925
return py.path.local(str(self.invocation_params.dir))
906926

927+
@property
928+
def rootpath(self) -> Path:
929+
"""The path to the :ref:`rootdir <rootdir>`.
930+
931+
:type: pathlib.Path
932+
933+
.. versionadded:: 6.1
934+
"""
935+
return self._rootpath
936+
937+
@property
938+
def rootdir(self) -> py.path.local:
939+
"""The path to the :ref:`rootdir <rootdir>`.
940+
941+
Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.
942+
943+
:type: py.path.local
944+
"""
945+
return py.path.local(str(self.rootpath))
946+
947+
@property
948+
def inipath(self) -> Optional[Path]:
949+
"""The path to the :ref:`configfile <configfiles>`.
950+
951+
:type: Optional[pathlib.Path]
952+
953+
.. versionadded:: 6.1
954+
"""
955+
return self._inipath
956+
957+
@property
958+
def inifile(self) -> Optional[py.path.local]:
959+
"""The path to the :ref:`configfile <configfiles>`.
960+
961+
Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
962+
963+
:type: Optional[py.path.local]
964+
"""
965+
return py.path.local(str(self.inipath)) if self.inipath else None
966+
907967
def add_cleanup(self, func: Callable[[], None]) -> None:
908968
"""Add a function to be called when the config object gets out of
909969
use (usually coninciding with pytest_unconfigure)."""
@@ -977,9 +1037,9 @@ def notify_exception(
9771037

9781038
def cwd_relative_nodeid(self, nodeid: str) -> str:
9791039
# nodeid's are relative to the rootpath, compute relative to cwd.
980-
if self.invocation_dir != self.rootdir:
981-
fullpath = self.rootdir.join(nodeid)
982-
nodeid = self.invocation_dir.bestrelpath(fullpath)
1040+
if self.invocation_params.dir != self.rootpath:
1041+
fullpath = self.rootpath / nodeid
1042+
nodeid = bestrelpath(self.invocation_params.dir, fullpath)
9831043
return nodeid
9841044

9851045
@classmethod
@@ -1014,11 +1074,11 @@ def _initini(self, args: Sequence[str]) -> None:
10141074
rootdir_cmd_arg=ns.rootdir or None,
10151075
config=self,
10161076
)
1017-
self.rootdir = py.path.local(str(rootpath))
1018-
self.inifile = py.path.local(str(inipath)) if inipath else None
1077+
self._rootpath = rootpath
1078+
self._inipath = inipath
10191079
self.inicfg = inicfg
1020-
self._parser.extra_info["rootdir"] = self.rootdir
1021-
self._parser.extra_info["inifile"] = self.inifile
1080+
self._parser.extra_info["rootdir"] = str(self.rootpath)
1081+
self._parser.extra_info["inifile"] = str(self.inipath)
10221082
self._parser.addini("addopts", "extra command line options", "args")
10231083
self._parser.addini("minversion", "minimally required pytest version")
10241084
self._parser.addini(
@@ -1110,8 +1170,8 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None:
11101170
self._validate_plugins()
11111171
self._warn_about_skipped_plugins()
11121172

1113-
if self.known_args_namespace.confcutdir is None and self.inifile:
1114-
confcutdir = py.path.local(self.inifile).dirname
1173+
if self.known_args_namespace.confcutdir is None and self.inipath is not None:
1174+
confcutdir = str(self.inipath.parent)
11151175
self.known_args_namespace.confcutdir = confcutdir
11161176
try:
11171177
self.hook.pytest_load_initial_conftests(
@@ -1147,13 +1207,13 @@ def _checkversion(self) -> None:
11471207

11481208
if not isinstance(minver, str):
11491209
raise pytest.UsageError(
1150-
"%s: 'minversion' must be a single value" % self.inifile
1210+
"%s: 'minversion' must be a single value" % self.inipath
11511211
)
11521212

11531213
if Version(minver) > Version(pytest.__version__):
11541214
raise pytest.UsageError(
11551215
"%s: 'minversion' requires pytest-%s, actual pytest-%s'"
1156-
% (self.inifile, minver, pytest.__version__,)
1216+
% (self.inipath, minver, pytest.__version__,)
11571217
)
11581218

11591219
def _validate_config_options(self) -> None:
@@ -1218,10 +1278,10 @@ def parse(self, args: List[str], addopts: bool = True) -> None:
12181278
args, self.option, namespace=self.option
12191279
)
12201280
if not args:
1221-
if self.invocation_dir == self.rootdir:
1281+
if self.invocation_params.dir == self.rootpath:
12221282
args = self.getini("testpaths")
12231283
if not args:
1224-
args = [str(self.invocation_dir)]
1284+
args = [str(self.invocation_params.dir)]
12251285
self.args = args
12261286
except PrintHelp:
12271287
pass
@@ -1324,10 +1384,10 @@ def _getini(self, name: str):
13241384
#
13251385
if type == "pathlist":
13261386
# TODO: This assert is probably not valid in all cases.
1327-
assert self.inifile is not None
1328-
dp = py.path.local(self.inifile).dirpath()
1387+
assert self.inipath is not None
1388+
dp = self.inipath.parent
13291389
input_values = shlex.split(value) if isinstance(value, str) else value
1330-
return [dp.join(x, abs=True) for x in input_values]
1390+
return [py.path.local(str(dp / x)) for x in input_values]
13311391
elif type == "args":
13321392
return shlex.split(value) if isinstance(value, str) else value
13331393
elif type == "linelist":

src/_pytest/fixtures.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
from _pytest.mark import ParameterSet
5151
from _pytest.outcomes import fail
5252
from _pytest.outcomes import TEST_OUTCOME
53+
from _pytest.pathlib import absolutepath
5354

5455
if TYPE_CHECKING:
5556
from typing import Deque
@@ -1443,7 +1444,7 @@ def getfixtureinfo(
14431444
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
14441445
nodeid = None
14451446
try:
1446-
p = py.path.local(plugin.__file__) # type: ignore[attr-defined]
1447+
p = absolutepath(plugin.__file__) # type: ignore[attr-defined]
14471448
except AttributeError:
14481449
pass
14491450
else:
@@ -1452,8 +1453,13 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
14521453
# Construct the base nodeid which is later used to check
14531454
# what fixtures are visible for particular tests (as denoted
14541455
# by their test id).
1455-
if p.basename.startswith("conftest.py"):
1456-
nodeid = p.dirpath().relto(self.config.rootdir)
1456+
if p.name.startswith("conftest.py"):
1457+
try:
1458+
nodeid = str(p.parent.relative_to(self.config.rootpath))
1459+
except ValueError:
1460+
nodeid = ""
1461+
if nodeid == ".":
1462+
nodeid = ""
14571463
if os.sep != nodes.SEP:
14581464
nodeid = nodeid.replace(os.sep, nodes.SEP)
14591465

src/_pytest/logging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,7 +603,7 @@ def set_log_path(self, fname: str) -> None:
603603
fpath = Path(fname)
604604

605605
if not fpath.is_absolute():
606-
fpath = Path(str(self._config.rootdir), fpath)
606+
fpath = self._config.rootpath / fpath
607607

608608
if not fpath.parent.exists():
609609
fpath.parent.mkdir(exist_ok=True, parents=True)

src/_pytest/main.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from _pytest.fixtures import FixtureManager
3434
from _pytest.outcomes import exit
3535
from _pytest.pathlib import absolutepath
36+
from _pytest.pathlib import bestrelpath
3637
from _pytest.pathlib import Path
3738
from _pytest.pathlib import visit
3839
from _pytest.reports import CollectReport
@@ -425,11 +426,11 @@ class Failed(Exception):
425426

426427

427428
@attr.s
428-
class _bestrelpath_cache(Dict[py.path.local, str]):
429-
path = attr.ib(type=py.path.local)
429+
class _bestrelpath_cache(Dict[Path, str]):
430+
path = attr.ib(type=Path)
430431

431-
def __missing__(self, path: py.path.local) -> str:
432-
r = self.path.bestrelpath(path) # type: str
432+
def __missing__(self, path: Path) -> str:
433+
r = bestrelpath(self.path, path)
433434
self[path] = r
434435
return r
435436

@@ -444,8 +445,8 @@ class Session(nodes.FSCollector):
444445
exitstatus = None # type: Union[int, ExitCode]
445446

446447
def __init__(self, config: Config) -> None:
447-
nodes.FSCollector.__init__(
448-
self, config.rootdir, parent=None, config=config, session=self, nodeid=""
448+
super().__init__(
449+
config.rootdir, parent=None, config=config, session=self, nodeid=""
449450
)
450451
self.testsfailed = 0
451452
self.testscollected = 0
@@ -456,8 +457,8 @@ def __init__(self, config: Config) -> None:
456457
self._initialpaths = frozenset() # type: FrozenSet[py.path.local]
457458

458459
self._bestrelpathcache = _bestrelpath_cache(
459-
config.rootdir
460-
) # type: Dict[py.path.local, str]
460+
config.rootpath
461+
) # type: Dict[Path, str]
461462

462463
self.config.pluginmanager.register(self, name="session")
463464

@@ -475,7 +476,7 @@ def __repr__(self) -> str:
475476
self.testscollected,
476477
)
477478

478-
def _node_location_to_relpath(self, node_path: py.path.local) -> str:
479+
def _node_location_to_relpath(self, node_path: Path) -> str:
479480
# bestrelpath is a quite slow function.
480481
return self._bestrelpathcache[node_path]
481482

@@ -599,7 +600,9 @@ def perform_collect( # noqa: F811
599600
initialpaths = [] # type: List[py.path.local]
600601
for arg in args:
601602
fspath, parts = resolve_collection_argument(
602-
self.config.invocation_dir, arg, as_pypath=self.config.option.pyargs
603+
self.config.invocation_params.dir,
604+
arg,
605+
as_pypath=self.config.option.pyargs,
603606
)
604607
self._initial_parts.append((fspath, parts))
605608
initialpaths.append(fspath)
@@ -817,7 +820,7 @@ def search_pypath(module_name: str) -> str:
817820

818821

819822
def resolve_collection_argument(
820-
invocation_dir: py.path.local, arg: str, *, as_pypath: bool = False
823+
invocation_path: Path, arg: str, *, as_pypath: bool = False
821824
) -> Tuple[py.path.local, List[str]]:
822825
"""Parse path arguments optionally containing selection parts and return (fspath, names).
823826
@@ -844,7 +847,7 @@ def resolve_collection_argument(
844847
strpath, *parts = str(arg).split("::")
845848
if as_pypath:
846849
strpath = search_pypath(strpath)
847-
fspath = Path(str(invocation_dir), strpath)
850+
fspath = invocation_path / strpath
848851
fspath = absolutepath(fspath)
849852
if not fspath.exists():
850853
msg = (

0 commit comments

Comments
 (0)