1
1
import collections
2
+ import contextlib
2
3
import io
3
4
import os
4
5
import errno
@@ -1424,6 +1425,24 @@ def close(self):
1424
1425
'st_mode st_ino st_dev st_nlink st_uid st_gid st_size st_atime st_mtime st_ctime' )
1425
1426
1426
1427
1428
+ class DummyDirEntry :
1429
+ """
1430
+ Minimal os.DirEntry-like object. Returned from DummyPath.scandir().
1431
+ """
1432
+ __slots__ = ('name' , '_is_symlink' , '_is_dir' )
1433
+
1434
+ def __init__ (self , name , is_symlink , is_dir ):
1435
+ self .name = name
1436
+ self ._is_symlink = is_symlink
1437
+ self ._is_dir = is_dir
1438
+
1439
+ def is_symlink (self ):
1440
+ return self ._is_symlink
1441
+
1442
+ def is_dir (self , * , follow_symlinks = True ):
1443
+ return self ._is_dir and (follow_symlinks or not self ._is_symlink )
1444
+
1445
+
1427
1446
class DummyPath (PathBase ):
1428
1447
"""
1429
1448
Simple implementation of PathBase that keeps files and directories in
@@ -1491,14 +1510,25 @@ def open(self, mode='r', buffering=-1, encoding=None,
1491
1510
stream = io .TextIOWrapper (stream , encoding = encoding , errors = errors , newline = newline )
1492
1511
return stream
1493
1512
1494
- def iterdir (self ):
1495
- path = str (self .resolve ())
1496
- if path in self ._files :
1497
- raise NotADirectoryError (errno .ENOTDIR , "Not a directory" , path )
1498
- elif path in self ._directories :
1499
- return iter ([self / name for name in self ._directories [path ]])
1513
+ @contextlib .contextmanager
1514
+ def scandir (self ):
1515
+ path = self .resolve ()
1516
+ path_str = str (path )
1517
+ if path_str in self ._files :
1518
+ raise NotADirectoryError (errno .ENOTDIR , "Not a directory" , path_str )
1519
+ elif path_str in self ._directories :
1520
+ yield iter ([path .joinpath (name )._dir_entry for name in self ._directories [path_str ]])
1500
1521
else :
1501
- raise FileNotFoundError (errno .ENOENT , "File not found" , path )
1522
+ raise FileNotFoundError (errno .ENOENT , "File not found" , path_str )
1523
+
1524
+ @property
1525
+ def _dir_entry (self ):
1526
+ path_str = str (self )
1527
+ is_symlink = path_str in self ._symlinks
1528
+ is_directory = (path_str in self ._directories
1529
+ if not is_symlink
1530
+ else self ._symlinks [path_str ][1 ])
1531
+ return DummyDirEntry (self .name , is_symlink , is_directory )
1502
1532
1503
1533
def mkdir (self , mode = 0o777 , parents = False , exist_ok = False ):
1504
1534
path = str (self .parent .resolve () / self .name )
@@ -1602,7 +1632,7 @@ def setUp(self):
1602
1632
if self .can_symlink :
1603
1633
p .joinpath ('linkA' ).symlink_to ('fileA' )
1604
1634
p .joinpath ('brokenLink' ).symlink_to ('non-existing' )
1605
- p .joinpath ('linkB' ).symlink_to ('dirB' )
1635
+ p .joinpath ('linkB' ).symlink_to ('dirB' , target_is_directory = True )
1606
1636
p .joinpath ('dirA' , 'linkC' ).symlink_to (parser .join ('..' , 'dirB' ))
1607
1637
p .joinpath ('dirB' , 'linkD' ).symlink_to (parser .join ('..' , 'dirB' ))
1608
1638
p .joinpath ('brokenLinkLoop' ).symlink_to ('brokenLinkLoop' )
@@ -2187,6 +2217,23 @@ def test_iterdir_nodir(self):
2187
2217
self .assertIn (cm .exception .errno , (errno .ENOTDIR ,
2188
2218
errno .ENOENT , errno .EINVAL ))
2189
2219
2220
+ def test_scandir (self ):
2221
+ p = self .cls (self .base )
2222
+ with p .scandir () as entries :
2223
+ self .assertTrue (list (entries ))
2224
+ with p .scandir () as entries :
2225
+ for entry in entries :
2226
+ child = p / entry .name
2227
+ self .assertIsNotNone (entry )
2228
+ self .assertEqual (entry .name , child .name )
2229
+ self .assertEqual (entry .is_symlink (),
2230
+ child .is_symlink ())
2231
+ self .assertEqual (entry .is_dir (follow_symlinks = False ),
2232
+ child .is_dir (follow_symlinks = False ))
2233
+ if entry .name != 'brokenLinkLoop' :
2234
+ self .assertEqual (entry .is_dir (), child .is_dir ())
2235
+
2236
+
2190
2237
def test_glob_common (self ):
2191
2238
def _check (glob , expected ):
2192
2239
self .assertEqual (set (glob ), { P (self .base , q ) for q in expected })
@@ -3038,7 +3085,7 @@ class DummyPathWithSymlinks(DummyPath):
3038
3085
def readlink (self ):
3039
3086
path = str (self .parent .resolve () / self .name )
3040
3087
if path in self ._symlinks :
3041
- return self .with_segments (self ._symlinks [path ])
3088
+ return self .with_segments (self ._symlinks [path ][ 0 ] )
3042
3089
elif path in self ._files or path in self ._directories :
3043
3090
raise OSError (errno .EINVAL , "Not a symlink" , path )
3044
3091
else :
@@ -3050,7 +3097,7 @@ def symlink_to(self, target, target_is_directory=False):
3050
3097
if path in self ._symlinks :
3051
3098
raise FileExistsError (errno .EEXIST , "File exists" , path )
3052
3099
self ._directories [parent ].add (self .name )
3053
- self ._symlinks [path ] = str (target )
3100
+ self ._symlinks [path ] = str (target ), target_is_directory
3054
3101
3055
3102
3056
3103
class DummyPathWithSymlinksTest (DummyPathTest ):
0 commit comments