Skip to content

Commit d89c6b6

Browse files
authored
Merge pull request #2 from nexB/visit-symlinks
Modify walk(), is_file(), and is_dir() to have the option to follow symlinks
2 parents 3ae7ac3 + 94a95ae commit d89c6b6

8 files changed

Lines changed: 85 additions & 23 deletions

File tree

setup.cfg

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ license_file = apache-2.0.LICENSE
66
name = commoncode
77
author = nexB. Inc. and others
88
author_email = info@aboutcode.org
9-
description = commoncode
9+
description = commoncode
1010
long_description = file:README.rst
1111
url = https://github.com/nexB/commoncode
1212
classifiers =
@@ -16,7 +16,7 @@ classifiers =
1616
Programming Language :: Python :: 3
1717
Topic :: Software Development
1818
Topic :: Utilities
19-
keywords =
19+
keywords =
2020

2121
[options]
2222
package_dir=
@@ -33,7 +33,8 @@ install_requires =
3333
requests >= 2.7.0, < 3.0.0
3434
intbitset >= 2.3.0, < 3.0
3535
saneyaml
36-
setup_requires = setuptools_scm >= 4
36+
typing >=3.6, < 3.7
37+
setup_requires = setuptools_scm[toml] >= 4
3738

3839
[options.packages.find]
3940
where=src

src/commoncode/filetype.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,24 @@ def is_link(location):
4545
return location and os.path.islink(location)
4646

4747

48-
def is_file(location):
48+
def is_file(location, follow_symlinks=False):
4949
"""
5050
Return True if `location` is a file.
5151
"""
52-
return (location and os.path.isfile(location)
53-
and not is_link(location) and not is_broken_link(location))
52+
_is_file = location and os.path.isfile(location)
53+
if follow_symlinks:
54+
return _is_file
55+
return _is_file and not is_link(location) and not is_broken_link(location)
5456

5557

56-
def is_dir(location):
58+
def is_dir(location, follow_symlinks=False):
5759
"""
5860
Return True if `location` is a directory.
5961
"""
60-
return (location and os.path.isdir(location) and not is_file(location)
61-
and not is_link(location) and not is_broken_link(location))
62+
_is_dir = location and os.path.isdir(location) and not is_file(location)
63+
if follow_symlinks:
64+
return _is_dir
65+
return _is_dir and not is_link(location) and not is_broken_link(location)
6266

6367

6468
def is_regular(location):

src/commoncode/fileutils.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def prepare_path(pth):
197197
if not isinstance(pth, bytes):
198198
pth = fsencode(pth)
199199
return pth
200-
else:
200+
else:
201201
if not isinstance(pth, compat.unicode):
202202
return fsdecode(pth)
203203
return pth
@@ -365,16 +365,19 @@ def ignore_nothing(_):
365365
return False
366366

367367

368-
def walk(location, ignored=None):
368+
def walk(location, ignored=None, follow_symlinks=False):
369369
"""
370370
Walk location returning the same tuples as os.walk but with a different
371371
behavior:
372372
- always walk top-down, breadth-first.
373-
- always ignore and never follow symlinks,
373+
- always ignore and never follow symlinks (unless `follow_symlinks` is True),
374374
- always ignore special files (FIFOs, etc.)
375375
- optionally ignore files and directories by invoking the `ignored`
376376
callable on files and directories returning True if it should be ignored.
377377
- location is a directory or a file: for a file, the file is returned.
378+
379+
If `follow_symlinks` is True, then symlinks will not be ignored and be
380+
collected like regular files and directories
378381
"""
379382
if on_linux and py2:
380383
location = fsencode(location)
@@ -387,33 +390,38 @@ def walk(location, ignored=None):
387390
logger_debug('walk: ignored:', location, is_ignored)
388391
return
389392

390-
if filetype.is_file(location) :
393+
if filetype.is_file(location, follow_symlinks=follow_symlinks) :
391394
yield parent_directory(location), [], [file_name(location)]
392395

393-
elif filetype.is_dir(location):
396+
elif filetype.is_dir(location, follow_symlinks=follow_symlinks):
394397
dirs = []
395398
files = []
396399
# TODO: consider using scandir
397400
for name in os.listdir(location):
398401
loc = os.path.join(location, name)
399402
if filetype.is_special(loc) or (ignored and ignored(loc)):
400-
if TRACE:
401-
ign = ignored and ignored(loc)
402-
logger_debug('walk: ignored:', loc, ign)
403-
continue
403+
if (follow_symlinks
404+
and filetype.is_link(loc)
405+
and not filetype.is_broken_link(location)):
406+
pass
407+
else:
408+
if TRACE:
409+
ign = ignored and ignored(loc)
410+
logger_debug('walk: ignored:', loc, ign)
411+
continue
404412
# special files and symlinks are always ignored
405-
if filetype.is_dir(loc):
413+
if filetype.is_dir(loc, follow_symlinks=follow_symlinks):
406414
dirs.append(name)
407-
elif filetype.is_file(loc):
415+
elif filetype.is_file(loc, follow_symlinks=follow_symlinks):
408416
files.append(name)
409417
yield location, dirs, files
410418

411419
for dr in dirs:
412-
for tripple in walk(os.path.join(location, dr), ignored):
420+
for tripple in walk(os.path.join(location, dr), ignored, follow_symlinks=follow_symlinks):
413421
yield tripple
414422

415423

416-
def resource_iter(location, ignored=ignore_nothing, with_dirs=True):
424+
def resource_iter(location, ignored=ignore_nothing, with_dirs=True, follow_symlinks=False):
417425
"""
418426
Return an iterable of paths at `location` recursively.
419427
@@ -424,7 +432,7 @@ def resource_iter(location, ignored=ignore_nothing, with_dirs=True):
424432
"""
425433
if on_linux and py2:
426434
location = fsencode(location)
427-
for top, dirs, files in walk(location, ignored):
435+
for top, dirs, files in walk(location, ignored, follow_symlinks=follow_symlinks):
428436
if with_dirs:
429437
for d in dirs:
430438
yield os.path.join(top, d)

tests/commoncode/data/symlink/test

Whitespace-only changes.

tests/commoncode/data/symlink/walk/a

Whitespace-only changes.

tests/commoncode/data/symlink/walk/dir/b

Whitespace-only changes.

tests/commoncode/test_filetype.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,25 @@ def test_get_file_count(self):
209209
for test_file, count in tests:
210210
result = filetype.get_file_count(os.path.join(test_dir, test_file))
211211
assert count == result
212+
213+
214+
def SymlinkTest(FileBasedTesting):
215+
test_data_dir = os.path.join(os.path.dirname(__file__), 'data')
216+
217+
@skipIf(on_windows, 'os.symlink does not work on Windows')
218+
def test_is_file(self):
219+
test_file = self.get_test_loc('symlink/test', copy=True)
220+
temp_dir = fileutils.get_temp_dir()
221+
test_link = join(temp_dir, 'test-link')
222+
os.symlink(test_file, test_link)
223+
assert filetype.is_file(test_link, follow_symlinks=True)
224+
assert not filetype.is_file(test_link, follow_symlinks=False)
225+
226+
@skipIf(on_windows, 'os.symlink does not work on Windows')
227+
def test_is_dir(self):
228+
test_dir = self.get_test_loc('symlink', copy=True)
229+
temp_dir = fileutils.get_temp_dir()
230+
test_link = join(temp_dir, 'test-dir-link')
231+
os.symlink(test_dir, test_link)
232+
assert filetype.is_dir(test_link, follow_symlinks=True)
233+
assert not filetype.is_dir(test_link, follow_symlinks=False)

tests/commoncode/test_fileutils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,20 @@ def test_os_walk_can_walk_non_utf8_path_from_unicode_path(self):
384384
_dirpath, _dirnames, filenames = result
385385
assert 18 == len(filenames)
386386

387+
@skipIf(on_windows, 'os.symlink does not work on Windows')
388+
def test_walk_on_symlinks(self):
389+
test_dir = self.get_test_loc('symlink/walk', copy=True)
390+
temp_dir = fileutils.get_temp_dir()
391+
test_link = join(temp_dir, 'test-dir-link')
392+
os.symlink(test_dir, test_link)
393+
results = list(fileutils.walk(test_link, follow_symlinks=True))
394+
results = [(os.path.basename(top), dirs, files) for top, dirs, files in results]
395+
expected = [
396+
('test-dir-link', ['dir'], ['a']),
397+
('dir', [], ['b'])
398+
]
399+
assert expected == results
400+
387401

388402
class TestFileUtilsIter(FileBasedTesting):
389403
test_data_dir = os.path.join(os.path.dirname(__file__), 'data')
@@ -553,6 +567,19 @@ def test_resource_iter_can_walk_non_utf8_path_from_unicode_path(self):
553567
result = list(fileutils.resource_iter(test_dir, with_dirs=False))
554568
assert 18 == len(result)
555569

570+
def test_resource_iter_follow_symlinks(self):
571+
test_dir = self.get_test_loc('symlink/walk', copy=True)
572+
temp_dir = fileutils.get_temp_dir()
573+
test_link = join(temp_dir, 'test-dir-link')
574+
os.symlink(test_dir, test_link)
575+
result = [os.path.basename(f) for f in fileutils.resource_iter(test_dir, follow_symlinks=True)]
576+
expected = [
577+
'dir',
578+
'a',
579+
'b'
580+
]
581+
assert sorted(expected) == sorted(result)
582+
556583

557584
class TestBaseName(FileBasedTesting):
558585
test_data_dir = os.path.join(os.path.dirname(__file__), 'data')

0 commit comments

Comments
 (0)