@@ -666,23 +666,62 @@ def rmdir(path, **kwargs):
666666 _delete (SMBDirectoryIO , path , ** kwargs )
667667
668668
669- def scandir (path , search_pattern = "*" , ** kwargs ):
669+ class SMBScandirIterator :
670+ """Iterator over SMB directory entries with ``with``-driven close.
671+
672+ Iterable directly and usable as a context manager whose exit releases
673+ the SMB directory handle.
674+ """
675+
676+ __slots__ = ("_gen" ,)
677+
678+ def __init__ (self , gen : t .Generator [SMBDirEntry , None , None ]) -> None :
679+ self ._gen = gen
680+
681+ def __iter__ (self ) -> SMBScandirIterator :
682+ return self
683+
684+ def __next__ (self ) -> SMBDirEntry :
685+ return next (self ._gen )
686+
687+ def __enter__ (self ) -> SMBScandirIterator :
688+ return self
689+
690+ def __exit__ (self , exc_type , exc_val , exc_tb ) -> None :
691+ self .close ()
692+
693+ def close (self ) -> None :
694+ self ._gen .close ()
695+
696+
697+ def scandir (path : str , search_pattern : str = "*" , ** kwargs : t .Any ) -> SMBScandirIterator :
670698 """
671699 Return an iterator of DirEntry objects corresponding to the entries in the directory given by path. The entries are
672700 yielded in arbitrary order, and the special entries '.' and '..' are not included.
673701
702+ Mirrors stdlib ``os.scandir()``: the returned iterator also supports the context-manager protocol so callers can
703+ release the SMB directory handle deterministically:
704+
705+ with smbclient.scandir(path) as it:
706+ for entry in it:
707+ ...
708+
674709 Using scandir() instead of listdir() can significantly increase the performance of code that also needs file type
675710 or file attribute information, because DirEntry objects expose this information if the SMB server provides it when
676711 scanning a directory. All DirEntry methods may perform a SMB request, but is_dir(), is_file(), is_symlink() usually
677712 only require a one system call unless the file or directory is a reparse point which requires 2 calls. See the
678713 Python documentation for how DirEntry is set up and the methods and attributes that are available.
679714
680715 :param path: The path to a directory to scan.
681- :param search_pattern: THe search string to match against the names of directories or files. This pattern can use
716+ :param search_pattern: The search string to match against the names of directories or files. This pattern can use
682717 '*' as a wildcard for multiple chars and '?' as a wildcard for a single char. Does not support regex patterns.
683718 :param kwargs: Common SMB Session arguments for smbclient.
684- :return: An iterator of DirEntry objects in the directory.
719+ :return: A context-manager iterator of DirEntry objects in the directory.
685720 """
721+ return SMBScandirIterator (_scandir (path , search_pattern , ** kwargs ))
722+
723+
724+ def _scandir (path : str , search_pattern : str = "*" , ** kwargs : t .Any ) -> t .Generator [SMBDirEntry , None , None ]:
686725 connection_cache = kwargs .get ("connection_cache" , None )
687726 with SMBDirectoryIO (path , share_access = "rwd" , ** kwargs ) as fd :
688727 for raw_dir_info in fd .query_directory (search_pattern , FileInformationClass .FILE_ID_FULL_DIRECTORY_INFORMATION ):
@@ -1023,26 +1062,27 @@ def walk(top, topdown=True, onerror=None, follow_symlinks=False, **kwargs):
10231062 dirs = []
10241063 files = []
10251064 bottom_up_dirs = []
1026- while True :
1027- try :
1065+ with scandir_gen :
1066+ while True :
10281067 try :
1029- entry = next (scandir_gen )
1030- except StopIteration :
1031- break
1032- except OSError as err :
1033- if onerror is not None :
1034- onerror (err )
1035- return
1036-
1037- if not entry .is_dir ():
1038- files .append (entry .name )
1039- continue
1068+ try :
1069+ entry = next (scandir_gen )
1070+ except StopIteration :
1071+ break
1072+ except OSError as err :
1073+ if onerror is not None :
1074+ onerror (err )
1075+ return
1076+
1077+ if not entry .is_dir ():
1078+ files .append (entry .name )
1079+ continue
10401080
1041- dirs .append (entry .name )
1042- if not topdown and (follow_symlinks or not entry .is_symlink ()):
1043- # Add the directory to the bottom up list which is recursively walked below, we exclude symlink dirs if
1044- # follow_symlinks is False.
1045- bottom_up_dirs .append (entry .path )
1081+ dirs .append (entry .name )
1082+ if not topdown and (follow_symlinks or not entry .is_symlink ()):
1083+ # Add the directory to the bottom up list which is recursively walked below, we exclude symlink dirs
1084+ # if follow_symlinks is False.
1085+ bottom_up_dirs .append (entry .path )
10461086
10471087 walk_kwargs = {"topdown" : topdown , "onerror" : onerror , "follow_symlinks" : follow_symlinks }
10481088 walk_kwargs .update (kwargs )
0 commit comments