Skip to content

Commit 2c2489e

Browse files
authored
added max_depth (#102)
* added max_depth * Added test * unused import * docstring * docs * factor out checks to make subclassing easier * unused imports
1 parent 34824b2 commit 2c2489e

File tree

7 files changed

+156
-22
lines changed

7 files changed

+156
-22
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [2.0.16] - 2017-11-11
8+
9+
### Added
10+
11+
- fs.parts
12+
13+
### Fixed
14+
15+
- Walk now yields Step named tuples as advertised
16+
17+
### Added
18+
19+
- Added max_depth parameter to fs.walk
20+
721
## [2.0.15] - 2017-11-05
822

923
### Changed

fs/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version, used in module and setup.py.
22
"""
3-
__version__ = "2.0.15"
3+
__version__ = "2.0.16"

fs/path.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"iteratepath",
3333
"join",
3434
"normpath",
35+
"parts",
3536
"recursepath",
3637
"relativefrom",
3738
"relpath",
@@ -256,6 +257,29 @@ def combine(path1, path2):
256257
return "{}/{}".format(path1.rstrip('/'), path2.lstrip('/'))
257258

258259

260+
def parts(path):
261+
"""Split a path in to its component parts.
262+
263+
Arguments:
264+
path (str): Path to split in to parts.
265+
266+
Returns:
267+
list: List of components
268+
269+
Example:
270+
>>> parts('/foo/bar/baz')
271+
['/', 'foo', 'bar', 'baz']
272+
273+
"""
274+
_path = normpath(path)
275+
components = _path.strip('/')
276+
277+
_parts = ['/' if _path.startswith('/') else './']
278+
if components:
279+
_parts += components.split('/')
280+
return _parts
281+
282+
259283
def split(path):
260284
"""Split a path into (head, tail) pair.
261285

fs/walk.py

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ class Walker(WalkerBase):
123123
be returned if the final component matches one of the patterns.
124124
exclude_dirs (list, optional): A list of patterns that will be used
125125
to filter out directories from the walk. e.g. ``['*.svn', '*.git']``.
126+
max_depth (int, optional): Maximum directory depth to walk.
126127
127128
"""
128129

@@ -131,7 +132,8 @@ def __init__(self,
131132
on_error=None,
132133
search="breadth",
133134
filter=None,
134-
exclude_dirs=None):
135+
exclude_dirs=None,
136+
max_depth=None):
135137
if search not in ('breadth', 'depth'):
136138
raise ValueError("search must be 'breadth' or 'depth'")
137139
self.ignore_errors = ignore_errors
@@ -153,6 +155,7 @@ def __init__(self,
153155
self.search = search
154156
self.filter = filter
155157
self.exclude_dirs = exclude_dirs
158+
self.max_depth = max_depth
156159
super(Walker, self).__init__()
157160

158161
@classmethod
@@ -165,6 +168,14 @@ def _raise_errors(cls, path, error):
165168
"""Callback to re-raise dir scan errors."""
166169
return False
167170

171+
@classmethod
172+
def _calculate_depth(cls, path):
173+
"""Calculate the 'depth' of a directory path (number of
174+
components).
175+
"""
176+
_path = path.strip('/')
177+
return _path.count('/') + 1 if _path else 0
178+
168179
@classmethod
169180
def bind(cls, fs):
170181
"""Bind a `Walker` instance to a given filesystem.
@@ -208,7 +219,8 @@ def __repr__(self):
208219
on_error=(self.on_error, None),
209220
search=(self.search, 'breadth'),
210221
filter=(self.filter, None),
211-
exclude_dirs=(self.exclude_dirs, None)
222+
exclude_dirs=(self.exclude_dirs, None),
223+
max_depth=(self.max_depth, None)
212224
)
213225

214226
def filter_files(self, fs, infos):
@@ -232,23 +244,53 @@ def filter_files(self, fs, infos):
232244
if _check_file(fs, info)
233245
]
234246

247+
def _check_open_dir(self, fs, path, info):
248+
"""Check if a directory should be considered in the walk.
249+
"""
250+
if (self.exclude_dirs is not None and
251+
fs.match(self.exclude_dirs, info.name)):
252+
return False
253+
return self.check_open_dir(fs, path, info)
235254

236-
def check_open_dir(self, fs, info):
255+
def check_open_dir(self, fs, path, info):
237256
"""Check if a directory should be opened.
238257
239258
Override to exclude directories from the walk.
240259
241260
Arguments:
242261
fs (FS): A filesystem instance.
243-
info (Info): A resource info object.
262+
path (str): Path to directory.
263+
info (Info): A resource info object for the directory.
244264
245265
Returns:
246266
bool: `True` if the directory should be opened.
247267
248268
"""
249-
if self.exclude_dirs is None:
250-
return True
251-
return not fs.match(self.exclude_dirs, info.name)
269+
return True
270+
271+
def _check_scan_dir(self, fs, path, info, depth):
272+
"""Check if a directory contents should be scanned."""
273+
if self.max_depth is not None and depth >= self.max_depth:
274+
return False
275+
return self.check_scan_dir(fs, path, info)
276+
277+
def check_scan_dir(self, fs, path, info):
278+
"""Check if a directory should be scanned.
279+
280+
Override to omit scanning of certain directories. If a directory
281+
is omitted, it will appear in the walk but its files and
282+
sub-directories will not.
283+
284+
Arguments:
285+
fs (FS): A filesystem instance.
286+
path (str): Path to directory.
287+
info (Info): A resource info object for the directory.
288+
289+
Returns:
290+
bool: `True` if the directory should be scanned.
291+
292+
"""
293+
return True
252294

253295
def check_file(self, fs, info):
254296
"""Check if a filename should be included.
@@ -329,19 +371,22 @@ def _walk_breadth(self, fs, path, namespaces=None):
329371
queue = deque([path])
330372
push = queue.appendleft
331373
pop = queue.pop
374+
depth = self._calculate_depth(path)
332375

333376
while queue:
334377
dir_path = pop()
335378
dirs = []
336379
files = []
337380
for info in self._scan(fs, dir_path, namespaces=namespaces):
338381
if info.is_dir:
339-
if self.check_open_dir(fs, info):
382+
_depth = self._calculate_depth(dir_path) - depth + 1
383+
if self._check_open_dir(fs, dir_path, info):
340384
dirs.append(info)
341-
push(join(dir_path, info.name))
385+
if self._check_scan_dir(fs, dir_path, info, _depth):
386+
push(join(dir_path, info.name))
342387
else:
343388
files.append(info)
344-
yield (
389+
yield Step(
345390
dir_path,
346391
dirs,
347392
self.filter_files(fs, files)
@@ -353,8 +398,10 @@ def _walk_depth(self, fs, path, namespaces=None):
353398
# No recursion!
354399

355400
def scan(path):
401+
"""Perform scan."""
356402
return self._scan(fs, path, namespaces=namespaces)
357403

404+
depth = self._calculate_depth(path)
358405
stack = [(
359406
path, scan(path), [], []
360407
)]
@@ -365,20 +412,22 @@ def scan(path):
365412
try:
366413
info = next(iter_files)
367414
except StopIteration:
368-
yield (
415+
yield Step(
369416
dir_path,
370417
dirs,
371418
self.filter_files(fs, files)
372419
)
373420
del stack[-1]
374421
else:
375422
if info.is_dir:
376-
if self.check_open_dir(fs, info):
423+
_depth = self._calculate_depth(dir_path) - depth + 1
424+
if self._check_open_dir(fs, dir_path, info):
377425
dirs.append(info)
378-
_path = join(dir_path, info.name)
379-
push((
380-
_path, scan(_path), [], []
381-
))
426+
if self._check_scan_dir(fs, dir_path, info, _depth):
427+
_path = join(dir_path, info.name)
428+
push((
429+
_path, scan(_path), [], []
430+
))
382431
else:
383432
files.append(info)
384433

@@ -448,6 +497,7 @@ def walk(self,
448497
exclude_dirs (list): A list of patterns that will be used
449498
to filter out directories from the walk, e.g. ``['*.svn',
450499
'*.git']``.
500+
max_depth (int, optional): Maximum directory depth to walk.
451501
452502
Returns:
453503
~collections.Iterator: an iterator of ``(<path>, <dirs>, <files>)``
@@ -495,6 +545,7 @@ def files(self, path='/', **kwargs):
495545
exclude_dirs (list): A list of patterns that will be used
496546
to filter out directories from the walk, e.g. ``['*.svn',
497547
'*.git']``.
548+
max_depth (int, optional): Maximum directory depth to walk.
498549
499550
Returns:
500551
~collections.Iterable: An iterable of file paths (absolute
@@ -525,6 +576,7 @@ def dirs(self, path='/', **kwargs):
525576
exclude_dirs (list): A list of patterns that will be used
526577
to filter out directories from the walk, e.g. ``['*.svn',
527578
'*.git']``.
579+
max_depth (int, optional): Maximum directory depth to walk.
528580
529581
Returns:
530582
~collections.iterable: an iterable of directory paths
@@ -562,6 +614,7 @@ def info(self, path='/', namespaces=None, **kwargs):
562614
exclude_dirs (list): A list of patterns that will be used
563615
to filter out directories from the walk, e.g. ``['*.svn',
564616
'*.git']``.
617+
max_depth (int, optional): Maximum directory depth to walk.
565618
566619
Returns:
567620
~collections.Iterable: an iterable yielding tuples of

fs/zipfs.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from __future__ import unicode_literals
66

77
import zipfile
8-
import stat
98

109
from datetime import datetime
1110

tests/test_path.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,14 @@ def test_combine(self):
109109
self.assertEqual(combine('', 'bar'), 'bar')
110110
self.assertEqual(combine('foo', 'bar'), 'foo/bar')
111111

112+
def test_parts(self):
113+
self.assertEqual(parts('/'), ['/'])
114+
self.assertEqual(parts(''), ['./'])
115+
self.assertEqual(parts('/foo'), ['/', 'foo'])
116+
self.assertEqual(parts('/foo/bar'), ['/', 'foo', 'bar'])
117+
self.assertEqual(parts('/foo/bar/'), ['/', 'foo', 'bar'])
118+
self.assertEqual(parts('./foo/bar/'), ['./', 'foo', 'bar'])
119+
112120
def test_pathsplit(self):
113121
tests = [
114122
("a/b", ("a", "b")),

tests/test_walk.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,51 @@ def test_repr(self):
5353
repr(self.fs.walk)
5454

5555
def test_walk(self):
56-
walk = []
57-
for path, dirs, files in self.fs.walk():
58-
walk.append((
56+
_walk = []
57+
for step in self.fs.walk():
58+
self.assertIsInstance(step, walk.Step)
59+
path, dirs, files = step
60+
_walk.append((
5961
path,
6062
[info.name for info in dirs],
6163
[info.name for info in files]
6264
))
6365
expected = [(u'/', [u'foo1', u'foo2', u'foo3'], []), (u'/foo1', [u'bar1'], [u'top1.txt', u'top2.txt']), (u'/foo2', [u'bar2'], [u'top3.txt']), (u'/foo3', [], []), (u'/foo1/bar1', [], []), (u'/foo2/bar2', [u'bar3'], []), (u'/foo2/bar2/bar3', [], [u'test.txt'])]
64-
self.assertEqual(walk, expected)
66+
self.assertEqual(_walk, expected)
67+
68+
def test_walk_directory(self):
69+
_walk = []
70+
for step in self.fs.walk('foo2'):
71+
self.assertIsInstance(step, walk.Step)
72+
path, dirs, files = step
73+
_walk.append((
74+
path,
75+
[info.name for info in dirs],
76+
[info.name for info in files]
77+
))
78+
expected = [(u'/foo2', [u'bar2'], [u'top3.txt']), (u'/foo2/bar2', [u'bar3'], []), (u'/foo2/bar2/bar3', [], [u'test.txt'])]
79+
self.assertEqual(_walk, expected)
80+
81+
def test_walk_levels_1(self):
82+
results = list(self.fs.walk(max_depth=1))
83+
self.assertEqual(len(results), 1)
84+
dirs = sorted(info.name for info in results[0].dirs)
85+
self.assertEqual(dirs, ['foo1', 'foo2', 'foo3'])
86+
files = sorted(info.name for info in results[0].files)
87+
self.assertEqual(files, [])
88+
89+
def test_walk_levels_2(self):
90+
_walk = []
91+
for step in self.fs.walk(max_depth=2):
92+
self.assertIsInstance(step, walk.Step)
93+
path, dirs, files = step
94+
_walk.append((
95+
path,
96+
sorted(info.name for info in dirs),
97+
sorted(info.name for info in files)
98+
))
99+
expected = [(u'/', [u'foo1', u'foo2', u'foo3'], []), (u'/foo1', [u'bar1'], [u'top1.txt', u'top2.txt']), (u'/foo2', [u'bar2'], [u'top3.txt']), (u'/foo3', [], [])]
100+
self.assertEqual(_walk, expected)
65101

66102
def test_walk_files(self):
67103
files = list(self.fs.walk.files())

0 commit comments

Comments
 (0)