Skip to content

Commit 1b352a6

Browse files
authored
Merge pull request #124 from mrbean-bremen/iglob
Added support for glob.iglob and recursive flag
2 parents 69fe7cc + e795c65 commit 1b352a6

File tree

1 file changed

+195
-73
lines changed

1 file changed

+195
-73
lines changed

pyfakefs/fake_filesystem_glob.py

Lines changed: 195 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
Includes:
1818
FakeGlob: Uses a FakeFilesystem to provide a fake replacement for the
1919
glob module.
20+
Note: Code is taken form Python 3.5 and slightly adapted to work with older versions
21+
and use the fake os and os.path modules
2022
2123
Usage:
2224
>>> from pyfakefs import fake_filesystem
@@ -33,90 +35,210 @@
3335

3436
import fnmatch
3537
import glob
36-
import os
38+
import re
39+
import sys
3740

3841
from pyfakefs import fake_filesystem
3942

4043

4144
class FakeGlobModule(object):
42-
"""Uses a FakeFilesystem to provide a fake replacement for glob module."""
43-
44-
def __init__(self, filesystem):
45-
"""Construct fake glob module using the fake filesystem.
46-
47-
Args:
48-
filesystem: FakeFilesystem used to provide file system information
49-
"""
50-
self._glob_module = glob
51-
self._os_module = fake_filesystem.FakeOsModule(filesystem)
52-
self._path_module = self._os_module.path
53-
self._filesystem = filesystem
54-
55-
def glob(self, pathname): # pylint: disable-msg=C6409
56-
"""Return a list of paths matching a pathname pattern.
57-
58-
The pattern may contain shell-style wildcards a la fnmatch.
59-
60-
Args:
61-
pathname: the pattern with which to find a list of paths
62-
63-
Returns:
64-
List of strings matching the glob pattern.
65-
"""
66-
if not self.has_magic(pathname):
67-
if self._path_module.exists(pathname):
68-
return [pathname]
69-
else:
45+
"""Uses a FakeFilesystem to provide a fake replacement for glob module."""
46+
47+
def __init__(self, filesystem):
48+
"""Construct fake glob module using the fake filesystem.
49+
50+
Args:
51+
filesystem: FakeFilesystem used to provide file system information
52+
"""
53+
self._glob_module = glob
54+
self._os_module = fake_filesystem.FakeOsModule(filesystem)
55+
self._path_module = self._os_module.path
56+
self._filesystem = filesystem
57+
58+
def glob(self, pathname, recursive=None):
59+
"""Return a list of paths matching a pathname pattern.
60+
61+
The pattern may contain shell-style wildcards a la fnmatch.
62+
63+
Args:
64+
pathname: the pattern with which to find a list of paths
65+
recursive: if true, the pattern '**' will match any files and
66+
zero or more directories and subdirectories. (>= Python 3.5 only)
67+
68+
Returns:
69+
List of strings matching the glob pattern.
70+
"""
71+
return list(self.iglob(pathname, recursive=_recursive_from_arg(recursive)))
72+
73+
def iglob(self, pathname, recursive=None):
74+
"""Return an iterator which yields the paths matching a pathname pattern.
75+
76+
The pattern may contain shell-style wildcards a la fnmatch.
77+
78+
Args:
79+
pathname: the pattern with which to find a list of paths
80+
recursive: if true, the pattern '**' will match any files and
81+
zero or more directories and subdirectories. (>= Python 3.5 only)
82+
"""
83+
recursive = _recursive_from_arg(recursive)
84+
it = self._iglob(pathname, recursive)
85+
if recursive and _isrecursive(pathname):
86+
s = next(it) # skip empty string
87+
assert not s
88+
return it
89+
90+
def _iglob(self, pathname, recursive):
91+
dirname, basename = self._path_module.split(pathname)
92+
if not self.has_magic(pathname):
93+
if basename:
94+
if self._path_module.lexists(pathname):
95+
yield pathname
96+
else:
97+
# Patterns ending with a slash should match only directories
98+
if self._path_module.isdir(dirname):
99+
yield pathname
100+
return
101+
if not dirname:
102+
if recursive and _isrecursive(basename):
103+
for name in self.glob2(dirname, basename):
104+
yield name
105+
else:
106+
for name in self.glob1(dirname, basename):
107+
yield name
108+
return
109+
# `self._path_module.split()` returns the argument itself as a dirname if it is a
110+
# drive or UNC path. Prevent an infinite recursion if a drive or UNC path
111+
# contains magic characters (i.e. r'\\?\C:').
112+
if dirname != pathname and self.has_magic(dirname):
113+
dirs = self._iglob(dirname, recursive)
114+
else:
115+
dirs = [dirname]
116+
if self.has_magic(basename):
117+
if recursive and _isrecursive(basename):
118+
glob_in_dir = self.glob2
119+
else:
120+
glob_in_dir = self.glob1
121+
else:
122+
glob_in_dir = self.glob0
123+
for dirname in dirs:
124+
for name in glob_in_dir(dirname, basename):
125+
yield self._path_module.join(dirname, name)
126+
127+
# These 2 helper functions non-recursively glob inside a literal directory.
128+
# They return a list of basenames. `glob1` accepts a pattern while `glob0`
129+
# takes a literal basename (so it only has to check for its existence).
130+
def glob1(self, dirname, pattern):
131+
if not dirname:
132+
if sys.version_info >= (3,) and isinstance(pattern, bytes):
133+
dirname = bytes(self._os_module.curdir, 'ASCII')
134+
elif sys.version_info < (3,) and isinstance(pattern, unicode):
135+
dirname = unicode(self._os_module.curdir,
136+
sys.getfilesystemencoding() or sys.getdefaultencoding())
137+
else:
138+
dirname = self._os_module.curdir
139+
140+
try:
141+
names = self._os_module.listdir(dirname)
142+
except OSError:
143+
return []
144+
if not _ishidden(pattern):
145+
names = [x for x in names if not _ishidden(x)]
146+
return fnmatch.filter(names, pattern)
147+
148+
def glob0(self, dirname, basename):
149+
if not basename:
150+
# `self._path_module.split()` returns an empty basename for paths ending with a
151+
# directory separator. 'q*x/' should match only directories.
152+
if self._path_module.isdir(dirname):
153+
return [basename]
154+
else:
155+
if self._path_module.lexists(self._path_module.join(dirname, basename)):
156+
return [basename]
70157
return []
71158

72-
pathname = self._filesystem.NormalizePathSeparator(pathname)
73-
dirname, basename = self._path_module.split(pathname)
74-
75-
if not dirname:
76-
return self.glob1(self._path_module.curdir, basename)
77-
elif self.has_magic(dirname):
78-
path_list = self.glob(dirname)
79-
else:
80-
path_list = [dirname]
81-
82-
if not self.has_magic(basename):
83-
result = []
84-
for dirname in path_list:
85-
if basename or self._path_module.isdir(dirname):
86-
name = self._path_module.join(dirname, basename)
87-
if self._path_module.exists(name):
88-
result.append(name)
159+
# This helper function recursively yields relative pathnames inside a literal
160+
# directory.
161+
def glob2(self, dirname, pattern):
162+
assert _isrecursive(pattern)
163+
yield pattern[:0]
164+
for path_name in self._rlistdir(dirname):
165+
yield path_name
166+
167+
# Recursively yields relative pathnames inside a literal directory.
168+
def _rlistdir(self, dirname):
169+
if not dirname:
170+
if sys.version_info >= (3,) and isinstance(dirname, bytes):
171+
dirname = bytes(self._os_module.curdir, 'ASCII')
172+
elif sys.version_info < (3,) and isinstance(dirname, unicode):
173+
dirname = unicode(self._os_module.curdir,
174+
sys.getfilesystemencoding() or sys.getdefaultencoding())
175+
else:
176+
dirname = self._os_module.curdir
177+
178+
try:
179+
names = self._os_module.listdir(dirname)
180+
except self._os_module.error:
181+
return
182+
for x in names:
183+
if not _ishidden(x):
184+
yield x
185+
path = self._path_module.join(dirname, x) if dirname else x
186+
for y in self._rlistdir(path):
187+
yield self._path_module.join(x, y)
188+
189+
magic_check = re.compile('([*?[])')
190+
magic_check_bytes = re.compile(b'([*?[])')
191+
192+
def has_magic(self, s):
193+
if isinstance(s, bytes):
194+
match = self.magic_check_bytes.search(s)
195+
else:
196+
match = self.magic_check.search(s)
197+
return match is not None
198+
199+
def escape(self, pathname):
200+
"""Escape all special characters.
201+
"""
202+
# Escaping is done by wrapping any of "*?[" between square brackets.
203+
# Metacharacters do not work in the drive part and shouldn't be escaped.
204+
drive, pathname = self._path_module.splitdrive(pathname)
205+
if isinstance(pathname, bytes):
206+
pathname = self.magic_check_bytes.sub(br'[\1]', pathname)
207+
else:
208+
pathname = self.magic_check.sub(r'[\1]', pathname)
209+
return drive + pathname
210+
211+
def __getattr__(self, name):
212+
"""Forwards any non-faked calls to the standard glob module."""
213+
return getattr(self._glob_module, name)
214+
215+
216+
def _ishidden(path):
217+
return path[0] in ('.', b'.'[0])
218+
219+
220+
def _isrecursive(pattern):
221+
if isinstance(pattern, bytes):
222+
return pattern == b'**'
89223
else:
90-
result = []
91-
for dirname in path_list:
92-
sublist = self.glob1(dirname, basename)
93-
for name in sublist:
94-
result.append(self._path_module.join(dirname, name))
95-
96-
return result
97-
98-
def glob1(self, dirname, pattern): # pylint: disable-msg=C6409
99-
if not dirname:
100-
dirname = self._path_module.curdir
101-
try:
102-
names = self._os_module.listdir(dirname)
103-
except os.error:
104-
return []
105-
if pattern[0] != '.':
106-
names = filter(lambda x: x[0] != '.', names)
107-
return fnmatch.filter(names, pattern)
108-
109-
def __getattr__(self, name):
110-
"""Forwards any non-faked calls to the standard glob module."""
111-
return getattr(self._glob_module, name)
224+
return pattern == '**'
225+
226+
227+
def _recursive_from_arg(recursive):
228+
if sys.version_info >= (3, 5):
229+
if recursive is None:
230+
return False
231+
return recursive
232+
if recursive is not None:
233+
raise TypeError("glob() got an unexpected keyword argument 'recursive'")
112234

113235

114236
def _RunDoctest():
115-
# pylint: disable-msg=C6111,C6204,W0406
116-
import doctest
117-
from pyfakefs import fake_filesystem_glob
118-
return doctest.testmod(fake_filesystem_glob)
237+
# pylint: disable-msg=C6111,C6204,W0406
238+
import doctest
239+
from pyfakefs import fake_filesystem_glob
240+
return doctest.testmod(fake_filesystem_glob)
119241

120242

121243
if __name__ == '__main__':
122-
_RunDoctest()
244+
_RunDoctest()

0 commit comments

Comments
 (0)