44
55import os
66import posixpath
7- import stat
87from functools import cached_property
98from pathlib import Path
109
1110import attrs
11+ from upath import UPath
1212
1313from . import _typings as t
1414
1515__all__ = ('FileTree' ,)
1616
1717
18- @attrs .define
19- class UserDirEntry :
20- """Partial reimplementation of :class:`os.DirEntry`.
21-
22- :class:`os.DirEntry` can't be instantiated from Python, but this can.
23- """
24-
25- path : str = attrs .field (repr = False , converter = os .fspath )
26- name : str = attrs .field (init = False )
27- _stat : os .stat_result = attrs .field (init = False , repr = False , default = None )
28- _lstat : os .stat_result = attrs .field (init = False , repr = False , default = None )
29-
30- def __attrs_post_init__ (self ) -> None :
31- self .name = os .path .basename (self .path )
32-
33- def __fspath__ (self ) -> str :
34- return self .path
35-
36- def stat (self , * , follow_symlinks : bool = True ) -> os .stat_result :
37- """Return stat_result object for the entry; cached per entry."""
38- if follow_symlinks :
39- if self ._stat is None :
40- self ._stat = os .stat (self .path , follow_symlinks = True )
41- return self ._stat
42- else :
43- if self ._lstat is None :
44- self ._lstat = os .stat (self .path , follow_symlinks = False )
45- return self ._lstat
46-
47- def is_dir (self , * , follow_symlinks : bool = True ) -> bool :
48- """Return True if the entry is a directory; cached per entry."""
49- _stat = self .stat (follow_symlinks = follow_symlinks )
50- return stat .S_ISDIR (_stat .st_mode )
51-
52- def is_file (self , * , follow_symlinks : bool = True ) -> bool :
53- """Return True if the entry is a file; cached per entry."""
54- _stat = self .stat (follow_symlinks = follow_symlinks )
55- return stat .S_ISREG (_stat .st_mode )
56-
57- def is_symlink (self ) -> bool :
58- """Return True if the entry is a symlink; cached per entry."""
59- _stat = self .stat (follow_symlinks = False )
60- return stat .S_ISLNK (_stat .st_mode )
61-
62-
63- def as_direntry (obj : os .PathLike ) -> os .DirEntry | UserDirEntry :
64- """Convert PathLike into DirEntry-like object."""
65- if isinstance (obj , os .DirEntry ):
66- return obj
67- return UserDirEntry (obj )
68-
69-
70- @attrs .define
18+ @attrs .define (frozen = True )
7119class FileTree :
7220 """Represent a FileTree with cached metadata."""
7321
74- direntry : os .DirEntry | UserDirEntry = attrs .field (repr = False , converter = as_direntry )
75- parent : FileTree | None = attrs .field (repr = False , default = None )
76- is_dir : bool = attrs .field (default = False )
77- children : dict [str , FileTree ] = attrs .field (repr = False , factory = dict )
78- name : str = attrs .field (init = False )
22+ path_obj : UPath = attrs .field (repr = False , converter = UPath )
23+ is_dir : bool = attrs .field (repr = False , default = None )
24+ parent : FileTree | None = attrs .field (repr = False , default = None , eq = False )
25+ children : dict [str , FileTree ] = attrs .field (repr = False , factory = dict , eq = False )
7926
8027 def __attrs_post_init__ (self ):
81- self .name = self .direntry .name
82- self .children = {
83- name : attrs .evolve (child , parent = self ) for name , child in self .children .items ()
84- }
28+ if self .is_dir is None :
29+ object .__setattr__ (self , 'is_dir' , self .path_obj .is_dir ())
30+ object .__setattr__ (
31+ self ,
32+ 'children' ,
33+ {name : attrs .evolve (child , parent = self ) for name , child in self .children .items ()},
34+ )
8535
8636 @classmethod
87- def read_from_filesystem (
88- cls ,
89- direntry : os .PathLike ,
90- parent : FileTree | None = None ,
91- ) -> t .Self :
92- """Read a FileTree from the filesystem.
93-
94- Uses :func:`os.scandir` to walk the directory tree.
95- """
96- self = cls (direntry , parent = parent )
97- if self .direntry .is_dir ():
98- self .is_dir = True
99- self .children = {
100- entry .name : FileTree .read_from_filesystem (entry , parent = self )
101- for entry in os .scandir (self .direntry )
37+ def read_from_filesystem (cls , path_obj : os .PathLike ) -> t .Self :
38+ """Read a FileTree from the filesystem."""
39+ path_obj = UPath (path_obj )
40+ children = {}
41+ if is_dir := path_obj .is_dir ():
42+ children = {
43+ entry .name : FileTree .read_from_filesystem (entry ) for entry in path_obj .iterdir ()
10244 }
103- return self
45+ return cls (path_obj , is_dir = is_dir , children = children )
46+
47+ @property
48+ def name (self ) -> bool :
49+ """The name of the current FileTree node."""
50+ return self .path_obj .name
10451
10552 def __contains__ (self , relpath : os .PathLike ) -> bool :
10653 parts = Path (relpath ).parts
@@ -110,10 +57,7 @@ def __contains__(self, relpath: os.PathLike) -> bool:
11057 return child and (len (parts ) == 1 or posixpath .join (* parts [1 :]) in child )
11158
11259 def __fspath__ (self ):
113- return self .direntry .path
114-
115- def __hash__ (self ):
116- return hash (self .direntry .path )
60+ return self .path_obj .__fspath__ ()
11761
11862 def __truediv__ (self , relpath : str | os .PathLike ) -> t .Self :
11963 parts = Path (relpath ).parts
0 commit comments