Skip to content

Commit 70f3ad1

Browse files
committed
config/findpaths: convert from py.path.local to pathlib
1 parent 9e55288 commit 70f3ad1

File tree

4 files changed

+227
-176
lines changed

4 files changed

+227
-176
lines changed

src/_pytest/config/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1006,12 +1006,15 @@ def _initini(self, args: Sequence[str]) -> None:
10061006
ns, unknown_args = self._parser.parse_known_and_unknown_args(
10071007
args, namespace=copy.copy(self.option)
10081008
)
1009-
self.rootdir, self.inifile, self.inicfg = determine_setup(
1009+
rootpath, inipath, inicfg = determine_setup(
10101010
ns.inifilename,
10111011
ns.file_or_dir + unknown_args,
10121012
rootdir_cmd_arg=ns.rootdir or None,
10131013
config=self,
10141014
)
1015+
self.rootdir = py.path.local(str(rootpath))
1016+
self.inifile = py.path.local(str(inipath)) if inipath else None
1017+
self.inicfg = inicfg
10151018
self._parser.extra_info["rootdir"] = self.rootdir
10161019
self._parser.extra_info["inifile"] = self.inifile
10171020
self._parser.addini("addopts", "extra command line options", "args")

src/_pytest/config/findpaths.py

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
1+
import itertools
12
import os
3+
import sys
24
from typing import Dict
35
from typing import Iterable
46
from typing import List
57
from typing import Optional
8+
from typing import Sequence
69
from typing import Tuple
710
from typing import Union
811

912
import iniconfig
10-
import py
1113

1214
from .exceptions import UsageError
1315
from _pytest.compat import TYPE_CHECKING
1416
from _pytest.outcomes import fail
17+
from _pytest.pathlib import absolutepath
18+
from _pytest.pathlib import commonpath
19+
from _pytest.pathlib import Path
1520

1621
if TYPE_CHECKING:
1722
from . import Config
1823

1924

20-
def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig:
25+
def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
2126
"""Parse the given generic '.ini' file using legacy IniConfig parser, returning
2227
the parsed object.
2328
@@ -30,26 +35,26 @@ def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig:
3035

3136

3237
def load_config_dict_from_file(
33-
filepath: py.path.local,
38+
filepath: Path,
3439
) -> Optional[Dict[str, Union[str, List[str]]]]:
3540
"""Load pytest configuration from the given file path, if supported.
3641
3742
Return None if the file does not contain valid pytest configuration.
3843
"""
3944

4045
# Configuration from ini files are obtained from the [pytest] section, if present.
41-
if filepath.ext == ".ini":
46+
if filepath.suffix == ".ini":
4247
iniconfig = _parse_ini_config(filepath)
4348

4449
if "pytest" in iniconfig:
4550
return dict(iniconfig["pytest"].items())
4651
else:
4752
# "pytest.ini" files are always the source of configuration, even if empty.
48-
if filepath.basename == "pytest.ini":
53+
if filepath.name == "pytest.ini":
4954
return {}
5055

5156
# '.cfg' files are considered if they contain a "[tool:pytest]" section.
52-
elif filepath.ext == ".cfg":
57+
elif filepath.suffix == ".cfg":
5358
iniconfig = _parse_ini_config(filepath)
5459

5560
if "tool:pytest" in iniconfig.sections:
@@ -60,7 +65,7 @@ def load_config_dict_from_file(
6065
fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
6166

6267
# '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
63-
elif filepath.ext == ".toml":
68+
elif filepath.suffix == ".toml":
6469
import toml
6570

6671
config = toml.load(str(filepath))
@@ -79,9 +84,9 @@ def make_scalar(v: object) -> Union[str, List[str]]:
7984

8085

8186
def locate_config(
82-
args: Iterable[Union[str, py.path.local]]
87+
args: Iterable[Path],
8388
) -> Tuple[
84-
Optional[py.path.local], Optional[py.path.local], Dict[str, Union[str, List[str]]],
89+
Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]],
8590
]:
8691
"""Search in the list of arguments for a valid ini-file for pytest,
8792
and return a tuple of (rootdir, inifile, cfg-dict)."""
@@ -93,104 +98,121 @@ def locate_config(
9398
]
9499
args = [x for x in args if not str(x).startswith("-")]
95100
if not args:
96-
args = [py.path.local()]
101+
args = [Path.cwd()]
97102
for arg in args:
98-
arg = py.path.local(arg)
99-
for base in arg.parts(reverse=True):
103+
argpath = absolutepath(arg)
104+
for base in itertools.chain((argpath,), reversed(argpath.parents)):
100105
for config_name in config_names:
101-
p = base.join(config_name)
102-
if p.isfile():
106+
p = base / config_name
107+
if p.is_file():
103108
ini_config = load_config_dict_from_file(p)
104109
if ini_config is not None:
105110
return base, p, ini_config
106111
return None, None, {}
107112

108113

109-
def get_common_ancestor(paths: Iterable[py.path.local]) -> py.path.local:
110-
common_ancestor = None # type: Optional[py.path.local]
114+
def get_common_ancestor(paths: Iterable[Path]) -> Path:
115+
common_ancestor = None # type: Optional[Path]
111116
for path in paths:
112117
if not path.exists():
113118
continue
114119
if common_ancestor is None:
115120
common_ancestor = path
116121
else:
117-
if path.relto(common_ancestor) or path == common_ancestor:
122+
if common_ancestor in path.parents or path == common_ancestor:
118123
continue
119-
elif common_ancestor.relto(path):
124+
elif path in common_ancestor.parents:
120125
common_ancestor = path
121126
else:
122-
shared = path.common(common_ancestor)
127+
shared = commonpath(path, common_ancestor)
123128
if shared is not None:
124129
common_ancestor = shared
125130
if common_ancestor is None:
126-
common_ancestor = py.path.local()
127-
elif common_ancestor.isfile():
128-
common_ancestor = common_ancestor.dirpath()
131+
common_ancestor = Path.cwd()
132+
elif common_ancestor.is_file():
133+
common_ancestor = common_ancestor.parent
129134
return common_ancestor
130135

131136

132-
def get_dirs_from_args(args: Iterable[str]) -> List[py.path.local]:
137+
def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
133138
def is_option(x: str) -> bool:
134139
return x.startswith("-")
135140

136141
def get_file_part_from_node_id(x: str) -> str:
137142
return x.split("::")[0]
138143

139-
def get_dir_from_path(path: py.path.local) -> py.path.local:
140-
if path.isdir():
144+
def get_dir_from_path(path: Path) -> Path:
145+
if path.is_dir():
141146
return path
142-
return py.path.local(path.dirname)
147+
return path.parent
148+
149+
if sys.version_info < (3, 8):
150+
151+
def safe_exists(path: Path) -> bool:
152+
# On Python<3.8, this can throw on paths that contain characters
153+
# unrepresentable at the OS level.
154+
try:
155+
return path.exists()
156+
except OSError:
157+
return False
158+
159+
else:
160+
161+
def safe_exists(path: Path) -> bool:
162+
return path.exists()
143163

144164
# These look like paths but may not exist
145165
possible_paths = (
146-
py.path.local(get_file_part_from_node_id(arg))
166+
absolutepath(get_file_part_from_node_id(arg))
147167
for arg in args
148168
if not is_option(arg)
149169
)
150170

151-
return [get_dir_from_path(path) for path in possible_paths if path.exists()]
171+
return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
152172

153173

154174
CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
155175

156176

157177
def determine_setup(
158178
inifile: Optional[str],
159-
args: List[str],
179+
args: Sequence[str],
160180
rootdir_cmd_arg: Optional[str] = None,
161181
config: Optional["Config"] = None,
162-
) -> Tuple[py.path.local, Optional[py.path.local], Dict[str, Union[str, List[str]]]]:
182+
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
163183
rootdir = None
164184
dirs = get_dirs_from_args(args)
165185
if inifile:
166-
inipath_ = py.path.local(inifile)
167-
inipath = inipath_ # type: Optional[py.path.local]
186+
inipath_ = absolutepath(inifile)
187+
inipath = inipath_ # type: Optional[Path]
168188
inicfg = load_config_dict_from_file(inipath_) or {}
169189
if rootdir_cmd_arg is None:
170190
rootdir = get_common_ancestor(dirs)
171191
else:
172192
ancestor = get_common_ancestor(dirs)
173193
rootdir, inipath, inicfg = locate_config([ancestor])
174194
if rootdir is None and rootdir_cmd_arg is None:
175-
for possible_rootdir in ancestor.parts(reverse=True):
176-
if possible_rootdir.join("setup.py").exists():
195+
for possible_rootdir in itertools.chain(
196+
(ancestor,), reversed(ancestor.parents)
197+
):
198+
if (possible_rootdir / "setup.py").is_file():
177199
rootdir = possible_rootdir
178200
break
179201
else:
180202
if dirs != [ancestor]:
181203
rootdir, inipath, inicfg = locate_config(dirs)
182204
if rootdir is None:
183205
if config is not None:
184-
cwd = config.invocation_dir
206+
cwd = config.invocation_params.dir
185207
else:
186-
cwd = py.path.local()
208+
cwd = Path.cwd()
187209
rootdir = get_common_ancestor([cwd, ancestor])
188210
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
189211
if is_fs_root:
190212
rootdir = ancestor
191213
if rootdir_cmd_arg:
192-
rootdir = py.path.local(os.path.expandvars(rootdir_cmd_arg))
193-
if not rootdir.isdir():
214+
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
215+
if not rootdir.is_dir():
194216
raise UsageError(
195217
"Directory '{}' not found. Check your '--rootdir' option.".format(
196218
rootdir

0 commit comments

Comments
 (0)