|
25 | 25 | import collections.abc |
26 | 26 | import inspect |
27 | 27 | import os |
| 28 | +import pathlib |
28 | 29 | import posixpath |
29 | 30 | import sys |
30 | 31 | from collections import defaultdict |
31 | | -from typing import Dict, Optional, Tuple, Union |
| 32 | +from typing import Dict, List, Optional, Tuple, Union |
32 | 33 |
|
33 | 34 |
|
34 | 35 | class _RepositoryMapping: |
@@ -137,7 +138,127 @@ def is_empty(self) -> bool: |
137 | 138 | Returns: |
138 | 139 | True if there are no mappings, False otherwise |
139 | 140 | """ |
140 | | - return len(self._exact_mappings) == 0 and len(self._grouped_prefixed_mappings) == 0 |
| 141 | + return ( |
| 142 | + len(self._exact_mappings) == 0 and len(self._grouped_prefixed_mappings) == 0 |
| 143 | + ) |
| 144 | + |
| 145 | + |
| 146 | +class Path(pathlib.PurePath): |
| 147 | + """A pathlib-like path object for runfiles. |
| 148 | +
|
| 149 | + This class extends `pathlib.PurePath` and resolves paths |
| 150 | + using the associated `Runfiles` instance when converted to a string. |
| 151 | + """ |
| 152 | + |
| 153 | + # For Python < 3.12 compatibility when subclassing PurePath directly |
| 154 | + _flavour = getattr(type(pathlib.PurePath()), "_flavour", None) |
| 155 | + |
| 156 | + def __new__( |
| 157 | + cls, |
| 158 | + *args: Union[str, os.PathLike], |
| 159 | + runfiles: Optional["Runfiles"] = None, |
| 160 | + source_repo: Optional[str] = None, |
| 161 | + ) -> "Path": |
| 162 | + """Private constructor. Use Runfiles.root() to create instances.""" |
| 163 | + obj = super().__new__(cls, *args) |
| 164 | + # Type checkers might complain about adding attributes to PurePath, |
| 165 | + # but this is standard for pathlib subclasses. |
| 166 | + obj._runfiles = runfiles # type: ignore |
| 167 | + obj._source_repo = source_repo # type: ignore |
| 168 | + return obj |
| 169 | + |
| 170 | + def __init__( |
| 171 | + self, |
| 172 | + *args: Union[str, os.PathLike], |
| 173 | + runfiles: Optional["Runfiles"] = None, |
| 174 | + source_repo: Optional[str] = None, |
| 175 | + ) -> None: |
| 176 | + pass |
| 177 | + |
| 178 | + def with_segments(self, *pathsegments: Union[str, os.PathLike]) -> "Path": |
| 179 | + """Used by Python 3.12+ pathlib to create new path objects.""" |
| 180 | + return type(self)( |
| 181 | + *pathsegments, |
| 182 | + runfiles=self._runfiles, # type: ignore |
| 183 | + source_repo=self._source_repo, # type: ignore |
| 184 | + ) |
| 185 | + |
| 186 | + # For Python < 3.12 |
| 187 | + @classmethod |
| 188 | + def _from_parts(cls, args: Tuple[str, ...]) -> "Path": |
| 189 | + obj = super()._from_parts(args) # type: ignore |
| 190 | + # These will be set by the calling instance later, or we can't set them here |
| 191 | + # properly without context. Usually pathlib calls this from an instance |
| 192 | + # method like _make_child, which we also might need to override. |
| 193 | + return obj |
| 194 | + |
| 195 | + def _make_child(self, args: Tuple[str, ...]) -> "Path": |
| 196 | + obj = super()._make_child(args) # type: ignore |
| 197 | + obj._runfiles = self._runfiles # type: ignore |
| 198 | + obj._source_repo = self._source_repo # type: ignore |
| 199 | + return obj |
| 200 | + |
| 201 | + @classmethod |
| 202 | + def _from_parsed_parts(cls, drv: str, root: str, parts: List[str]) -> "Path": |
| 203 | + obj = super()._from_parsed_parts(drv, root, parts) # type: ignore |
| 204 | + return obj |
| 205 | + |
| 206 | + def _make_child_relpath(self, part: str) -> "Path": |
| 207 | + obj = super()._make_child_relpath(part) # type: ignore |
| 208 | + obj._runfiles = self._runfiles # type: ignore |
| 209 | + obj._source_repo = self._source_repo # type: ignore |
| 210 | + return obj |
| 211 | + |
| 212 | + @property |
| 213 | + def parents(self) -> Tuple["Path", ...]: |
| 214 | + return tuple( |
| 215 | + type(self)( |
| 216 | + p, |
| 217 | + runfiles=getattr(self, "_runfiles", None), |
| 218 | + source_repo=getattr(self, "_source_repo", None), |
| 219 | + ) |
| 220 | + for p in super().parents |
| 221 | + ) |
| 222 | + |
| 223 | + @property |
| 224 | + def parent(self) -> "Path": |
| 225 | + return type(self)( |
| 226 | + super().parent, |
| 227 | + runfiles=getattr(self, "_runfiles", None), |
| 228 | + source_repo=getattr(self, "_source_repo", None), |
| 229 | + ) |
| 230 | + |
| 231 | + def with_name(self, name: str) -> "Path": |
| 232 | + return type(self)( |
| 233 | + super().with_name(name), |
| 234 | + runfiles=getattr(self, "_runfiles", None), |
| 235 | + source_repo=getattr(self, "_source_repo", None), |
| 236 | + ) |
| 237 | + |
| 238 | + def with_suffix(self, suffix: str) -> "Path": |
| 239 | + return type(self)( |
| 240 | + super().with_suffix(suffix), |
| 241 | + runfiles=getattr(self, "_runfiles", None), |
| 242 | + source_repo=getattr(self, "_source_repo", None), |
| 243 | + ) |
| 244 | + |
| 245 | + def __repr__(self) -> str: |
| 246 | + return 'runfiles.Path({!r})'.format(super().__str__()) |
| 247 | + |
| 248 | + def __str__(self) -> str: |
| 249 | + path_posix = super().__str__().replace("\\", "/") |
| 250 | + if not path_posix or path_posix == ".": |
| 251 | + # pylint: disable=protected-access |
| 252 | + return self._runfiles._python_runfiles_root # type: ignore |
| 253 | + resolved = self._runfiles.Rlocation(path_posix, source_repo=self._source_repo) # type: ignore |
| 254 | + return resolved if resolved is not None else super().__str__() |
| 255 | + |
| 256 | + def __fspath__(self) -> str: |
| 257 | + return str(self) |
| 258 | + |
| 259 | + def runfiles_root(self) -> "Path": |
| 260 | + """Returns a Path object representing the runfiles root.""" |
| 261 | + return self._runfiles.root(source_repo=self._source_repo) # type: ignore |
141 | 262 |
|
142 | 263 |
|
143 | 264 | class _ManifestBased: |
@@ -254,6 +375,16 @@ def __init__(self, strategy: Union[_ManifestBased, _DirectoryBased]) -> None: |
254 | 375 | strategy.RlocationChecked("_repo_mapping") |
255 | 376 | ) |
256 | 377 |
|
| 378 | + def root(self, source_repo: Optional[str] = None) -> Path: |
| 379 | + """Returns a Path object representing the runfiles root. |
| 380 | +
|
| 381 | + The repository mapping used by the returned Path object is that of the |
| 382 | + caller of this method. |
| 383 | + """ |
| 384 | + if source_repo is None and not self._repo_mapping.is_empty(): |
| 385 | + source_repo = self.CurrentRepository(frame=2) |
| 386 | + return Path(runfiles=self, source_repo=source_repo) |
| 387 | + |
257 | 388 | def Rlocation(self, path: str, source_repo: Optional[str] = None) -> Optional[str]: |
258 | 389 | """Returns the runtime path of a runfile. |
259 | 390 |
|
@@ -325,9 +456,7 @@ def Rlocation(self, path: str, source_repo: Optional[str] = None) -> Optional[st |
325 | 456 |
|
326 | 457 | # Look up the target repository using the repository mapping |
327 | 458 | if target_canonical is not None: |
328 | | - return self._strategy.RlocationChecked( |
329 | | - target_canonical + "/" + remainder |
330 | | - ) |
| 459 | + return self._strategy.RlocationChecked(target_canonical + "/" + remainder) |
331 | 460 |
|
332 | 461 | # No mapping found - assume target_repo is already canonical or |
333 | 462 | # we're not using Bzlmod |
@@ -396,10 +525,9 @@ def CurrentRepository(self, frame: int = 1) -> str: |
396 | 525 | # TODO: This doesn't cover the case of a script being run from an |
397 | 526 | # external repository, which could be heuristically detected |
398 | 527 | # by parsing the script's path. |
399 | | - if ( |
400 | | - (sys.version_info.minor <= 10 or sys.platform == "win32") |
401 | | - and sys.path[0] != self._python_runfiles_root |
402 | | - ): |
| 528 | + if (sys.version_info.minor <= 10 or sys.platform == "win32") and sys.path[ |
| 529 | + 0 |
| 530 | + ] != self._python_runfiles_root: |
403 | 531 | return "" |
404 | 532 | raise ValueError( |
405 | 533 | "{} does not lie under the runfiles root {}".format( |
|
0 commit comments