2929import posixpath
3030import sys
3131from collections import defaultdict
32- from typing import Dict , List , Optional , Tuple , Union
32+ from typing import Dict , Generator , Iterable , List , Optional , Tuple , Union
33+
34+ if sys .version_info >= (3 , 11 ):
35+ from typing import Self
36+ elif sys .version_info >= (3 , 10 ):
37+ from typing import TypeAlias
38+
39+ Self : TypeAlias = "Path" # type: ignore
40+ else :
41+ from typing import Any as Self
3342
3443
3544class _RepositoryMapping :
@@ -143,25 +152,30 @@ def is_empty(self) -> bool:
143152 )
144153
145154
146- class Path (pathlib .PurePath ):
155+ class Path (pathlib .Path ):
147156 """A pathlib-like path object for runfiles.
148157
149- This class extends `pathlib.PurePath ` and resolves paths
158+ This class extends `pathlib.Path ` and resolves paths
150159 using the associated `Runfiles` instance when converted to a string.
151160 """
152161
153- # For Python < 3.12 compatibility when subclassing PurePath directly
154- _flavour = getattr (type (pathlib .PurePath ()), "_flavour" , None )
162+ # Mypy isn't smart enough to realize `self` in the methods
163+ # refers to our Path class instead of pathlib.Path
164+ _runfiles : Optional ["Runfiles" ]
165+ _source_repo : Optional [str ]
166+
167+ # For Python < 3.12 compatibility when subclassing Path directly
168+ _flavour = getattr (type (pathlib .Path ()), "_flavour" , None )
155169
156170 def __new__ (
157171 cls ,
158172 * args : Union [str , os .PathLike ],
159173 runfiles : Optional ["Runfiles" ] = None ,
160174 source_repo : Optional [str ] = None ,
161- ) -> "Path" :
175+ ) -> Self :
162176 """Private constructor. Use Runfiles.root() to create instances."""
163177 obj = super ().__new__ (cls , * args )
164- # Type checkers might complain about adding attributes to PurePath ,
178+ # Type checkers might complain about adding attributes to Path ,
165179 # but this is standard for pathlib subclasses.
166180 obj ._runfiles = runfiles # type: ignore
167181 obj ._source_repo = source_repo # type: ignore
@@ -173,90 +187,205 @@ def __init__(
173187 runfiles : Optional ["Runfiles" ] = None ,
174188 source_repo : Optional [str ] = None ,
175189 ) -> None :
176- pass
190+ # In Python 3.12+, pathlib was refactored and Path.__init__ now accepts
191+ # *args. Prior to 3.12, Path did not define __init__, so
192+ # super().__init__(*args) would fall through to object.__init__, which
193+ # raises a TypeError because it takes no arguments.
194+ if sys .version_info >= (3 , 12 ):
195+ super ().__init__ (* args )
196+ else :
197+ super ().__init__ ()
198+
199+ # We override resolve() and absolute() to ensure that in Python < 3.12,
200+ # where pathlib internally uses object.__new__ instead of our custom
201+ # __new__ or with_segments(), the runfiles state is preserved. We delegate
202+ # to self._as_path() because super().resolve() creates intermediate objects
203+ # that would otherwise crash during internal stat() calls.
204+ # override
205+ def resolve (self , strict : bool = False ) -> Self :
206+ return type (self )(
207+ self ._as_path ().resolve (strict = strict ),
208+ runfiles = self ._runfiles ,
209+ source_repo = self ._source_repo ,
210+ )
177211
178- def with_segments (self , * pathsegments : Union [str , os .PathLike ]) -> "Path" :
212+ # override
213+ def absolute (self ) -> Self :
214+ return type (self )(
215+ self ._as_path ().absolute (),
216+ runfiles = self ._runfiles ,
217+ source_repo = self ._source_repo ,
218+ )
219+
220+ # override
221+ def with_segments (self , * pathsegments : Union [str , os .PathLike ]) -> Self :
179222 """Used by Python 3.12+ pathlib to create new path objects."""
180223 return type (self )(
181224 * pathsegments ,
182- runfiles = self ._runfiles , # type: ignore
183- source_repo = self ._source_repo , # type: ignore
225+ runfiles = self ._runfiles ,
226+ source_repo = self ._source_repo ,
184227 )
185228
186229 # 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" :
230+ # override
231+ def _make_child (self , args : Tuple [str , ...]) -> Self :
196232 obj = super ()._make_child (args ) # type: ignore
197233 obj ._runfiles = self ._runfiles # type: ignore
198234 obj ._source_repo = self ._source_repo # type: ignore
199235 return obj
200236
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-
237+ # override
212238 @property
213- def parents (self ) -> Tuple ["Path" , ...]:
239+ def parents (self ) -> Tuple [Self , ...]:
214240 return tuple (
215241 type (self )(
216242 p ,
217- runfiles = getattr ( self , " _runfiles" , None ) ,
218- source_repo = getattr ( self , " _source_repo" , None ) ,
243+ runfiles = self . _runfiles ,
244+ source_repo = self . _source_repo ,
219245 )
220246 for p in super ().parents
221247 )
222248
249+ # override
223250 @property
224- def parent (self ) -> "Path" :
251+ def parent (self ) -> Self :
225252 return type (self )(
226253 super ().parent ,
227- runfiles = getattr ( self , " _runfiles" , None ) ,
228- source_repo = getattr ( self , " _source_repo" , None ) ,
254+ runfiles = self . _runfiles ,
255+ source_repo = self . _source_repo ,
229256 )
230257
231- def with_name (self , name : str ) -> "Path" :
258+ @property
259+ def runfile_path (self ) -> str :
260+ """Returns the runfiles-root relative path."""
261+ path_posix = super ().__str__ ().replace ("\\ " , "/" )
262+ if path_posix == "." :
263+ return ""
264+ return path_posix
265+
266+ # override
267+ def with_name (self , name : str ) -> Self :
232268 return type (self )(
233269 super ().with_name (name ),
234- runfiles = getattr ( self , " _runfiles" , None ) ,
235- source_repo = getattr ( self , " _source_repo" , None ) ,
270+ runfiles = self . _runfiles ,
271+ source_repo = self . _source_repo ,
236272 )
237273
238- def with_suffix (self , suffix : str ) -> "Path" :
274+ # override
275+ def with_suffix (self , suffix : str ) -> Self :
239276 return type (self )(
240277 super ().with_suffix (suffix ),
241- runfiles = getattr (self , "_runfiles" , None ),
242- source_repo = getattr (self , "_source_repo" , None ),
278+ runfiles = self ._runfiles ,
279+ source_repo = self ._source_repo ,
280+ )
281+
282+ def _as_path (self ) -> pathlib .Path :
283+ return pathlib .Path (str (self ))
284+
285+ # override
286+ def stat (self , * , follow_symlinks : bool = True ) -> os .stat_result :
287+ return self ._as_path ().stat (follow_symlinks = follow_symlinks )
288+
289+ # override
290+ def lstat (self ) -> os .stat_result :
291+ return self ._as_path ().lstat ()
292+
293+ # override
294+ def exists (self ) -> bool :
295+ return self ._as_path ().exists ()
296+
297+ # override
298+ def is_dir (self ) -> bool :
299+ return self ._as_path ().is_dir ()
300+
301+ # override
302+ def is_file (self ) -> bool :
303+ return self ._as_path ().is_file ()
304+
305+ # override
306+ def is_symlink (self ) -> bool :
307+ return self ._as_path ().is_symlink ()
308+
309+ # override
310+ def is_block_device (self ) -> bool :
311+ return self ._as_path ().is_block_device ()
312+
313+ # override
314+ def is_char_device (self ) -> bool :
315+ return self ._as_path ().is_char_device ()
316+
317+ # override
318+ def is_fifo (self ) -> bool :
319+ return self ._as_path ().is_fifo ()
320+
321+ # override
322+ def is_socket (self ) -> bool :
323+ return self ._as_path ().is_socket ()
324+
325+ # override
326+ def open (
327+ self ,
328+ mode : str = "r" ,
329+ buffering : int = - 1 ,
330+ encoding : Optional [str ] = None ,
331+ errors : Optional [str ] = None ,
332+ newline : Optional [str ] = None ,
333+ ):
334+ return self ._as_path ().open (
335+ mode = mode ,
336+ buffering = buffering ,
337+ encoding = encoding ,
338+ errors = errors ,
339+ newline = newline ,
243340 )
244341
342+ # override
343+ def read_bytes (self ) -> bytes :
344+ return self ._as_path ().read_bytes ()
345+
346+ # override
347+ def read_text (
348+ self , encoding : Optional [str ] = None , errors : Optional [str ] = None
349+ ) -> str :
350+ return self ._as_path ().read_text (encoding = encoding , errors = errors )
351+
352+ # override
353+ def iterdir (self ) -> Generator [Self , None , None ]:
354+ resolved = self ._as_path ()
355+ for p in resolved .iterdir ():
356+ yield self / p .name
357+
358+ # override
359+ def glob (self , pattern : str ) -> Generator [Self , None , None ]:
360+ resolved = self ._as_path ()
361+ for p in resolved .glob (pattern ):
362+ yield self / p .relative_to (resolved )
363+
364+ # override
365+ def rglob (self , pattern : str ) -> Generator [Self , None , None ]:
366+ resolved = self ._as_path ()
367+ for p in resolved .rglob (pattern ):
368+ yield self / p .relative_to (resolved )
369+
245370 def __repr__ (self ) -> str :
246- return ' runfiles.Path({!r})' .format (super (). __str__ () )
371+ return " runfiles.Path({!r})" .format (self . runfile_path )
247372
248373 def __str__ (self ) -> str :
249374 path_posix = super ().__str__ ().replace ("\\ " , "/" )
250375 if not path_posix or path_posix == "." :
251376 # pylint: disable=protected-access
252377 return self ._runfiles ._python_runfiles_root # type: ignore
253378 resolved = self ._runfiles .Rlocation (path_posix , source_repo = self ._source_repo ) # type: ignore
254- return resolved if resolved is not None else super ().__str__ ()
379+ if resolved is not None :
380+ return resolved
381+
382+ # pylint: disable=protected-access
383+ return posixpath .join (self ._runfiles ._python_runfiles_root , path_posix ) # type: ignore
255384
256385 def __fspath__ (self ) -> str :
257386 return str (self )
258387
259- def runfiles_root (self ) -> "Path" :
388+ def runfiles_root (self ) -> Self :
260389 """Returns a Path object representing the runfiles root."""
261390 return self ._runfiles .root (source_repo = self ._source_repo ) # type: ignore
262391
0 commit comments