1+ import itertools
12import os
3+ import sys
24from typing import Dict
35from typing import Iterable
46from typing import List
57from typing import Optional
8+ from typing import Sequence
69from typing import Tuple
710from typing import Union
811
912import iniconfig
10- import py
1113
1214from .exceptions import UsageError
1315from _pytest .compat import TYPE_CHECKING
1416from _pytest .outcomes import fail
17+ from _pytest .pathlib import absolutepath
18+ from _pytest .pathlib import commonpath
19+ from _pytest .pathlib import Path
1520
1621if 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
3237def 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
8186def 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
154174CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
155175
156176
157177def 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