1
+ import itertools
1
2
import os
3
+ import sys
2
4
from typing import Dict
3
5
from typing import Iterable
4
6
from typing import List
5
7
from typing import Optional
8
+ from typing import Sequence
6
9
from typing import Tuple
7
10
from typing import Union
8
11
9
12
import iniconfig
10
- import py
11
13
12
14
from .exceptions import UsageError
13
15
from _pytest .compat import TYPE_CHECKING
14
16
from _pytest .outcomes import fail
17
+ from _pytest .pathlib import absolutepath
18
+ from _pytest .pathlib import commonpath
19
+ from _pytest .pathlib import Path
15
20
16
21
if TYPE_CHECKING :
17
22
from . import Config
18
23
19
24
20
- def _parse_ini_config (path : py . path . local ) -> iniconfig .IniConfig :
25
+ def _parse_ini_config (path : Path ) -> iniconfig .IniConfig :
21
26
"""Parse the given generic '.ini' file using legacy IniConfig parser, returning
22
27
the parsed object.
23
28
@@ -30,26 +35,26 @@ def _parse_ini_config(path: py.path.local) -> iniconfig.IniConfig:
30
35
31
36
32
37
def load_config_dict_from_file (
33
- filepath : py . path . local ,
38
+ filepath : Path ,
34
39
) -> Optional [Dict [str , Union [str , List [str ]]]]:
35
40
"""Load pytest configuration from the given file path, if supported.
36
41
37
42
Return None if the file does not contain valid pytest configuration.
38
43
"""
39
44
40
45
# Configuration from ini files are obtained from the [pytest] section, if present.
41
- if filepath .ext == ".ini" :
46
+ if filepath .suffix == ".ini" :
42
47
iniconfig = _parse_ini_config (filepath )
43
48
44
49
if "pytest" in iniconfig :
45
50
return dict (iniconfig ["pytest" ].items ())
46
51
else :
47
52
# "pytest.ini" files are always the source of configuration, even if empty.
48
- if filepath .basename == "pytest.ini" :
53
+ if filepath .name == "pytest.ini" :
49
54
return {}
50
55
51
56
# '.cfg' files are considered if they contain a "[tool:pytest]" section.
52
- elif filepath .ext == ".cfg" :
57
+ elif filepath .suffix == ".cfg" :
53
58
iniconfig = _parse_ini_config (filepath )
54
59
55
60
if "tool:pytest" in iniconfig .sections :
@@ -60,7 +65,7 @@ def load_config_dict_from_file(
60
65
fail (CFG_PYTEST_SECTION .format (filename = "setup.cfg" ), pytrace = False )
61
66
62
67
# '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
63
- elif filepath .ext == ".toml" :
68
+ elif filepath .suffix == ".toml" :
64
69
import toml
65
70
66
71
config = toml .load (str (filepath ))
@@ -79,9 +84,9 @@ def make_scalar(v: object) -> Union[str, List[str]]:
79
84
80
85
81
86
def locate_config (
82
- args : Iterable [Union [ str , py . path . local ]]
87
+ args : Iterable [Path ],
83
88
) -> 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 ]]],
85
90
]:
86
91
"""Search in the list of arguments for a valid ini-file for pytest,
87
92
and return a tuple of (rootdir, inifile, cfg-dict)."""
@@ -93,104 +98,121 @@ def locate_config(
93
98
]
94
99
args = [x for x in args if not str (x ).startswith ("-" )]
95
100
if not args :
96
- args = [py . path . local ()]
101
+ args = [Path . cwd ()]
97
102
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 ) ):
100
105
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 ():
103
108
ini_config = load_config_dict_from_file (p )
104
109
if ini_config is not None :
105
110
return base , p , ini_config
106
111
return None , None , {}
107
112
108
113
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 ]
111
116
for path in paths :
112
117
if not path .exists ():
113
118
continue
114
119
if common_ancestor is None :
115
120
common_ancestor = path
116
121
else :
117
- if path .relto ( common_ancestor ) or path == common_ancestor :
122
+ if common_ancestor in path .parents or path == common_ancestor :
118
123
continue
119
- elif common_ancestor .relto ( path ) :
124
+ elif path in common_ancestor .parents :
120
125
common_ancestor = path
121
126
else :
122
- shared = path . common ( common_ancestor )
127
+ shared = commonpath ( path , common_ancestor )
123
128
if shared is not None :
124
129
common_ancestor = shared
125
130
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
129
134
return common_ancestor
130
135
131
136
132
- def get_dirs_from_args (args : Iterable [str ]) -> List [py . path . local ]:
137
+ def get_dirs_from_args (args : Iterable [str ]) -> List [Path ]:
133
138
def is_option (x : str ) -> bool :
134
139
return x .startswith ("-" )
135
140
136
141
def get_file_part_from_node_id (x : str ) -> str :
137
142
return x .split ("::" )[0 ]
138
143
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 ():
141
146
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 ()
143
163
144
164
# These look like paths but may not exist
145
165
possible_paths = (
146
- py . path . local (get_file_part_from_node_id (arg ))
166
+ absolutepath (get_file_part_from_node_id (arg ))
147
167
for arg in args
148
168
if not is_option (arg )
149
169
)
150
170
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 )]
152
172
153
173
154
174
CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
155
175
156
176
157
177
def determine_setup (
158
178
inifile : Optional [str ],
159
- args : List [str ],
179
+ args : Sequence [str ],
160
180
rootdir_cmd_arg : Optional [str ] = None ,
161
181
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 ]]]]:
163
183
rootdir = None
164
184
dirs = get_dirs_from_args (args )
165
185
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 ]
168
188
inicfg = load_config_dict_from_file (inipath_ ) or {}
169
189
if rootdir_cmd_arg is None :
170
190
rootdir = get_common_ancestor (dirs )
171
191
else :
172
192
ancestor = get_common_ancestor (dirs )
173
193
rootdir , inipath , inicfg = locate_config ([ancestor ])
174
194
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 ():
177
199
rootdir = possible_rootdir
178
200
break
179
201
else :
180
202
if dirs != [ancestor ]:
181
203
rootdir , inipath , inicfg = locate_config (dirs )
182
204
if rootdir is None :
183
205
if config is not None :
184
- cwd = config .invocation_dir
206
+ cwd = config .invocation_params . dir
185
207
else :
186
- cwd = py . path . local ()
208
+ cwd = Path . cwd ()
187
209
rootdir = get_common_ancestor ([cwd , ancestor ])
188
210
is_fs_root = os .path .splitdrive (str (rootdir ))[1 ] == "/"
189
211
if is_fs_root :
190
212
rootdir = ancestor
191
213
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 ():
194
216
raise UsageError (
195
217
"Directory '{}' not found. Check your '--rootdir' option." .format (
196
218
rootdir
0 commit comments