Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/config/findpaths.py: 53%
135 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1import os
2import sys
3from pathlib import Path
4from typing import Dict
5from typing import Iterable
6from typing import List
7from typing import Optional
8from typing import Sequence
9from typing import Tuple
10from typing import TYPE_CHECKING
11from typing import Union
13import iniconfig
15from .exceptions import UsageError
16from _pytest.outcomes import fail
17from _pytest.pathlib import absolutepath
18from _pytest.pathlib import commonpath
20if TYPE_CHECKING:
21 from . import Config
24def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
25 """Parse the given generic '.ini' file using legacy IniConfig parser, returning
26 the parsed object.
28 Raise UsageError if the file cannot be parsed.
29 """
30 try:
31 return iniconfig.IniConfig(str(path))
32 except iniconfig.ParseError as exc:
33 raise UsageError(str(exc)) from exc
36def load_config_dict_from_file(
37 filepath: Path,
38) -> Optional[Dict[str, Union[str, List[str]]]]:
39 """Load pytest configuration from the given file path, if supported.
41 Return None if the file does not contain valid pytest configuration.
42 """
44 # Configuration from ini files are obtained from the [pytest] section, if present.
45 if filepath.suffix == ".ini":
46 iniconfig = _parse_ini_config(filepath)
48 if "pytest" in iniconfig:
49 return dict(iniconfig["pytest"].items())
50 else:
51 # "pytest.ini" files are always the source of configuration, even if empty.
52 if filepath.name == "pytest.ini":
53 return {}
55 # '.cfg' files are considered if they contain a "[tool:pytest]" section.
56 elif filepath.suffix == ".cfg":
57 iniconfig = _parse_ini_config(filepath)
59 if "tool:pytest" in iniconfig.sections:
60 return dict(iniconfig["tool:pytest"].items())
61 elif "pytest" in iniconfig.sections:
62 # If a setup.cfg contains a "[pytest]" section, we raise a failure to indicate users that
63 # plain "[pytest]" sections in setup.cfg files is no longer supported (#3086).
64 fail(CFG_PYTEST_SECTION.format(filename="setup.cfg"), pytrace=False)
66 # '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
67 elif filepath.suffix == ".toml":
68 if sys.version_info >= (3, 11):
69 import tomllib
70 else:
71 import tomli as tomllib
73 toml_text = filepath.read_text(encoding="utf-8")
74 try:
75 config = tomllib.loads(toml_text)
76 except tomllib.TOMLDecodeError as exc:
77 raise UsageError(f"{filepath}: {exc}") from exc
79 result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
80 if result is not None:
81 # TOML supports richer data types than ini files (strings, arrays, floats, ints, etc),
82 # however we need to convert all scalar values to str for compatibility with the rest
83 # of the configuration system, which expects strings only.
84 def make_scalar(v: object) -> Union[str, List[str]]:
85 return v if isinstance(v, list) else str(v)
87 return {k: make_scalar(v) for k, v in result.items()}
89 return None
92def locate_config(
93 args: Iterable[Path],
94) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
95 """Search in the list of arguments for a valid ini-file for pytest,
96 and return a tuple of (rootdir, inifile, cfg-dict)."""
97 config_names = [
98 "pytest.ini",
99 ".pytest.ini",
100 "pyproject.toml",
101 "tox.ini",
102 "setup.cfg",
103 ]
104 args = [x for x in args if not str(x).startswith("-")]
105 if not args:
106 args = [Path.cwd()]
107 for arg in args:
108 argpath = absolutepath(arg)
109 for base in (argpath, *argpath.parents):
110 for config_name in config_names:
111 p = base / config_name
112 if p.is_file():
113 ini_config = load_config_dict_from_file(p)
114 if ini_config is not None:
115 return base, p, ini_config
116 return None, None, {}
119def get_common_ancestor(paths: Iterable[Path]) -> Path:
120 common_ancestor: Optional[Path] = None
121 for path in paths:
122 if not path.exists():
123 continue
124 if common_ancestor is None:
125 common_ancestor = path
126 else:
127 if common_ancestor in path.parents or path == common_ancestor:
128 continue
129 elif path in common_ancestor.parents:
130 common_ancestor = path
131 else:
132 shared = commonpath(path, common_ancestor)
133 if shared is not None:
134 common_ancestor = shared
135 if common_ancestor is None:
136 common_ancestor = Path.cwd()
137 elif common_ancestor.is_file():
138 common_ancestor = common_ancestor.parent
139 return common_ancestor
142def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
143 def is_option(x: str) -> bool:
144 return x.startswith("-")
146 def get_file_part_from_node_id(x: str) -> str:
147 return x.split("::")[0]
149 def get_dir_from_path(path: Path) -> Path:
150 if path.is_dir():
151 return path
152 return path.parent
154 def safe_exists(path: Path) -> bool:
155 # This can throw on paths that contain characters unrepresentable at the OS level,
156 # or with invalid syntax on Windows (https://bugs.python.org/issue35306)
157 try:
158 return path.exists()
159 except OSError:
160 return False
162 # These look like paths but may not exist
163 possible_paths = (
164 absolutepath(get_file_part_from_node_id(arg))
165 for arg in args
166 if not is_option(arg)
167 )
169 return [get_dir_from_path(path) for path in possible_paths if safe_exists(path)]
172CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead."
175def determine_setup(
176 inifile: Optional[str],
177 args: Sequence[str],
178 rootdir_cmd_arg: Optional[str] = None,
179 config: Optional["Config"] = None,
180) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
181 rootdir = None
182 dirs = get_dirs_from_args(args)
183 if inifile:
184 inipath_ = absolutepath(inifile)
185 inipath: Optional[Path] = inipath_
186 inicfg = load_config_dict_from_file(inipath_) or {}
187 if rootdir_cmd_arg is None:
188 rootdir = inipath_.parent
189 else:
190 ancestor = get_common_ancestor(dirs)
191 rootdir, inipath, inicfg = locate_config([ancestor])
192 if rootdir is None and rootdir_cmd_arg is None:
193 for possible_rootdir in (ancestor, *ancestor.parents):
194 if (possible_rootdir / "setup.py").is_file():
195 rootdir = possible_rootdir
196 break
197 else:
198 if dirs != [ancestor]:
199 rootdir, inipath, inicfg = locate_config(dirs)
200 if rootdir is None:
201 if config is not None:
202 cwd = config.invocation_params.dir
203 else:
204 cwd = Path.cwd()
205 rootdir = get_common_ancestor([cwd, ancestor])
206 if is_fs_root(rootdir):
207 rootdir = ancestor
208 if rootdir_cmd_arg:
209 rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
210 if not rootdir.is_dir():
211 raise UsageError(
212 "Directory '{}' not found. Check your '--rootdir' option.".format(
213 rootdir
214 )
215 )
216 assert rootdir is not None
217 return rootdir, inipath, inicfg or {}
220def is_fs_root(p: Path) -> bool:
221 r"""
222 Return True if the given path is pointing to the root of the
223 file system ("/" on Unix and "C:\\" on Windows for example).
224 """
225 return os.path.splitdrive(str(p))[1] == os.sep