Skip to content

Commit b5b11c7

Browse files
committed
implement file io methods
1 parent 6dac0f6 commit b5b11c7

2 files changed

Lines changed: 280 additions & 69 deletions

File tree

python/runfiles/runfiles.py

Lines changed: 161 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
import posixpath
3030
import sys
3131
from collections import defaultdict
32-
from typing import Dict, List, Optional, Tuple, Union
32+
from typing import Dict, Generator, Iterable, List, Optional, Tuple, Union
3333

3434

3535
class _RepositoryMapping:
@@ -143,25 +143,25 @@ def is_empty(self) -> bool:
143143
)
144144

145145

146-
class Path(pathlib.PurePath):
146+
class Path(pathlib.Path):
147147
"""A pathlib-like path object for runfiles.
148148
149-
This class extends `pathlib.PurePath` and resolves paths
149+
This class extends `pathlib.Path` and resolves paths
150150
using the associated `Runfiles` instance when converted to a string.
151151
"""
152152

153-
# For Python < 3.12 compatibility when subclassing PurePath directly
154-
_flavour = getattr(type(pathlib.PurePath()), "_flavour", None)
153+
# For Python < 3.12 compatibility when subclassing Path directly
154+
_flavour = getattr(type(pathlib.Path()), "_flavour", None)
155155

156156
def __new__(
157157
cls,
158158
*args: Union[str, os.PathLike],
159159
runfiles: Optional["Runfiles"] = None,
160160
source_repo: Optional[str] = None,
161-
) -> "Path":
161+
) -> "Self":
162162
"""Private constructor. Use Runfiles.root() to create instances."""
163163
obj = super().__new__(cls, *args)
164-
# Type checkers might complain about adding attributes to PurePath,
164+
# Type checkers might complain about adding attributes to Path,
165165
# but this is standard for pathlib subclasses.
166166
obj._runfiles = runfiles # type: ignore
167167
obj._source_repo = source_repo # type: ignore
@@ -173,90 +173,205 @@ def __init__(
173173
runfiles: Optional["Runfiles"] = None,
174174
source_repo: Optional[str] = None,
175175
) -> None:
176-
pass
176+
# In Python 3.12+, pathlib was refactored and Path.__init__ now accepts
177+
# *args. Prior to 3.12, Path did not define __init__, so
178+
# super().__init__(*args) would fall through to object.__init__, which
179+
# raises a TypeError because it takes no arguments.
180+
if sys.version_info >= (3, 12):
181+
super().__init__(*args)
182+
else:
183+
super().__init__()
184+
185+
# We override resolve() and absolute() to ensure that in Python < 3.12,
186+
# where pathlib internally uses object.__new__ instead of our custom
187+
# __new__ or with_segments(), the runfiles state is preserved. We delegate
188+
# to self._as_path() because super().resolve() creates intermediate objects
189+
# that would otherwise crash during internal stat() calls.
190+
# override
191+
def resolve(self, strict: bool = False) -> "Self":
192+
return type(self)(
193+
self._as_path().resolve(strict=strict),
194+
runfiles=self._runfiles,
195+
source_repo=self._source_repo,
196+
)
197+
198+
# override
199+
def absolute(self) -> "Self":
200+
return type(self)(
201+
self._as_path().absolute(),
202+
runfiles=self._runfiles,
203+
source_repo=self._source_repo,
204+
)
177205

178-
def with_segments(self, *pathsegments: Union[str, os.PathLike]) -> "Path":
206+
# override
207+
def with_segments(self, *pathsegments: Union[str, os.PathLike]) -> "Self":
179208
"""Used by Python 3.12+ pathlib to create new path objects."""
180209
return type(self)(
181210
*pathsegments,
182-
runfiles=self._runfiles, # type: ignore
183-
source_repo=self._source_repo, # type: ignore
211+
runfiles=self._runfiles,
212+
source_repo=self._source_repo,
184213
)
185214

186215
# 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":
216+
# override
217+
def _make_child(self, args: Tuple[str, ...]) -> "Self":
196218
obj = super()._make_child(args) # type: ignore
197219
obj._runfiles = self._runfiles # type: ignore
198220
obj._source_repo = self._source_repo # type: ignore
199221
return obj
200222

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-
223+
# override
212224
@property
213-
def parents(self) -> Tuple["Path", ...]:
225+
def parents(self) -> Tuple["Self", ...]:
214226
return tuple(
215227
type(self)(
216228
p,
217-
runfiles=getattr(self, "_runfiles", None),
218-
source_repo=getattr(self, "_source_repo", None),
229+
runfiles=self._runfiles,
230+
source_repo=self._source_repo,
219231
)
220232
for p in super().parents
221233
)
222234

235+
# override
223236
@property
224-
def parent(self) -> "Path":
237+
def parent(self) -> "Self":
225238
return type(self)(
226239
super().parent,
227-
runfiles=getattr(self, "_runfiles", None),
228-
source_repo=getattr(self, "_source_repo", None),
240+
runfiles=self._runfiles,
241+
source_repo=self._source_repo,
229242
)
230243

231-
def with_name(self, name: str) -> "Path":
244+
@property
245+
def runfile_path(self) -> str:
246+
"""Returns the runfiles-root relative path."""
247+
path_posix = super().__str__().replace("\\", "/")
248+
if path_posix == ".":
249+
return ""
250+
return path_posix
251+
252+
# override
253+
def with_name(self, name: str) -> "Self":
232254
return type(self)(
233255
super().with_name(name),
234-
runfiles=getattr(self, "_runfiles", None),
235-
source_repo=getattr(self, "_source_repo", None),
256+
runfiles=self._runfiles,
257+
source_repo=self._source_repo,
236258
)
237259

238-
def with_suffix(self, suffix: str) -> "Path":
260+
# override
261+
def with_suffix(self, suffix: str) -> "Self":
239262
return type(self)(
240263
super().with_suffix(suffix),
241-
runfiles=getattr(self, "_runfiles", None),
242-
source_repo=getattr(self, "_source_repo", None),
264+
runfiles=self._runfiles,
265+
source_repo=self._source_repo,
243266
)
244267

268+
def _as_path(self) -> pathlib.Path:
269+
return pathlib.Path(str(self))
270+
271+
# override
272+
def stat(self, *, follow_symlinks: bool = True) -> os.stat_result:
273+
return self._as_path().stat(follow_symlinks=follow_symlinks)
274+
275+
# override
276+
def lstat(self) -> os.stat_result:
277+
return self._as_path().lstat()
278+
279+
# override
280+
def exists(self) -> bool:
281+
return self._as_path().exists()
282+
283+
# override
284+
def is_dir(self) -> bool:
285+
return self._as_path().is_dir()
286+
287+
# override
288+
def is_file(self) -> bool:
289+
return self._as_path().is_file()
290+
291+
# override
292+
def is_symlink(self) -> bool:
293+
return self._as_path().is_symlink()
294+
295+
# override
296+
def is_block_device(self) -> bool:
297+
return self._as_path().is_block_device()
298+
299+
# override
300+
def is_char_device(self) -> bool:
301+
return self._as_path().is_char_device()
302+
303+
# override
304+
def is_fifo(self) -> bool:
305+
return self._as_path().is_fifo()
306+
307+
# override
308+
def is_socket(self) -> bool:
309+
return self._as_path().is_socket()
310+
311+
# override
312+
def open(
313+
self,
314+
mode: str = "r",
315+
buffering: int = -1,
316+
encoding: Optional[str] = None,
317+
errors: Optional[str] = None,
318+
newline: Optional[str] = None,
319+
):
320+
return self._as_path().open(
321+
mode=mode,
322+
buffering=buffering,
323+
encoding=encoding,
324+
errors=errors,
325+
newline=newline,
326+
)
327+
328+
# override
329+
def read_bytes(self) -> bytes:
330+
return self._as_path().read_bytes()
331+
332+
# override
333+
def read_text(
334+
self, encoding: Optional[str] = None, errors: Optional[str] = None
335+
) -> str:
336+
return self._as_path().read_text(encoding=encoding, errors=errors)
337+
338+
# override
339+
def iterdir(self) -> Generator["Self", None, None]:
340+
resolved = self._as_path()
341+
for p in resolved.iterdir():
342+
yield self / p.name
343+
344+
# override
345+
def glob(self, pattern: str) -> Generator["Self", None, None]:
346+
resolved = self._as_path()
347+
for p in resolved.glob(pattern):
348+
yield self / p.relative_to(resolved)
349+
350+
# override
351+
def rglob(self, pattern: str) -> Generator["Self", None, None]:
352+
resolved = self._as_path()
353+
for p in resolved.rglob(pattern):
354+
yield self / p.relative_to(resolved)
355+
245356
def __repr__(self) -> str:
246-
return 'runfiles.Path({!r})'.format(super().__str__())
357+
return 'runfiles.Path({!r})'.format(self.runfile_path)
247358

248359
def __str__(self) -> str:
249360
path_posix = super().__str__().replace("\\", "/")
250361
if not path_posix or path_posix == ".":
251362
# pylint: disable=protected-access
252363
return self._runfiles._python_runfiles_root # type: ignore
253364
resolved = self._runfiles.Rlocation(path_posix, source_repo=self._source_repo) # type: ignore
254-
return resolved if resolved is not None else super().__str__()
365+
if resolved is not None:
366+
return resolved
367+
368+
# pylint: disable=protected-access
369+
return posixpath.join(self._runfiles._python_runfiles_root, path_posix) # type: ignore
255370

256371
def __fspath__(self) -> str:
257372
return str(self)
258373

259-
def runfiles_root(self) -> "Path":
374+
def runfiles_root(self) -> "Self":
260375
"""Returns a Path object representing the runfiles root."""
261376
return self._runfiles.root(source_repo=self._source_repo) # type: ignore
262377

0 commit comments

Comments
 (0)