Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 3822093

Browse files
Issue #10395: Added os.path.commonpath(). Implemented in posixpath and ntpath.
Based on patch by Rafik Draoui.
1 parent dd83bd2 commit 3822093

7 files changed

Lines changed: 255 additions & 5 deletions

File tree

Doc/library/os.path.rst

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,24 @@ the :mod:`glob` module.)
6666
empty string (``''``).
6767

6868

69+
.. function:: commonpath(paths)
70+
71+
Return the longest common sub-path of each pathname in the sequence
72+
*paths*. Raise ValueError if *paths* contains both absolute and relative
73+
pathnames, or if *paths* is empty. Unlike :func:`commonprefix`, this
74+
returns a valid path.
75+
76+
Availability: Unix, Windows
77+
78+
.. versionadded:: 3.5
79+
80+
6981
.. function:: commonprefix(list)
7082

71-
Return the longest path prefix (taken character-by-character) that is a prefix
72-
of all paths in *list*. If *list* is empty, return the empty string (``''``).
73-
Note that this may return invalid paths because it works a character at a time.
83+
Return the longest path prefix (taken character-by-character) that is a
84+
prefix of all paths in *list*. If *list* is empty, return the empty string
85+
(``''``). Note that this may return invalid paths because it works a
86+
character at a time. To obtain a valid path, see :func:`commonpath`.
7487

7588

7689
.. function:: dirname(path)

Doc/whatsnew/3.5.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,13 @@ os
370370
* :class:`os.stat_result` now has a :attr:`~os.stat_result.st_file_attributes`
371371
attribute on Windows. (Contributed by Ben Hoyt in :issue:`21719`.)
372372

373+
os.path
374+
-------
375+
376+
* New :func:`~os.path.commonpath` function that extracts common path prefix.
377+
Unlike the :func:`~os.path.commonprefix` function, it always returns a valid
378+
patch. (Contributed by Rafik Draoui and Serhiy Storchaka in :issue:`10395`.)
379+
373380
pickle
374381
------
375382

Lib/ntpath.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"ismount", "expanduser","expandvars","normpath","abspath",
1818
"splitunc","curdir","pardir","sep","pathsep","defpath","altsep",
1919
"extsep","devnull","realpath","supports_unicode_filenames","relpath",
20-
"samefile", "sameopenfile", "samestat",]
20+
"samefile", "sameopenfile", "samestat", "commonpath"]
2121

2222
# strings representing various path-related bits and pieces
2323
# These are primarily for export; internally, they are hardcoded.
@@ -589,6 +589,67 @@ def relpath(path, start=None):
589589
raise
590590

591591

592+
# Return the longest common sub-path of the sequence of paths given as input.
593+
# The function is case-insensitive and 'separator-insensitive', i.e. if the
594+
# only difference between two paths is the use of '\' versus '/' as separator,
595+
# they are deemed to be equal.
596+
#
597+
# However, the returned path will have the standard '\' separator (even if the
598+
# given paths had the alternative '/' separator) and will have the case of the
599+
# first path given in the sequence. Additionally, any trailing separator is
600+
# stripped from the returned path.
601+
602+
def commonpath(paths):
603+
"""Given a sequence of path names, returns the longest common sub-path."""
604+
605+
if not paths:
606+
raise ValueError('commonpath() arg is an empty sequence')
607+
608+
if isinstance(paths[0], bytes):
609+
sep = b'\\'
610+
altsep = b'/'
611+
curdir = b'.'
612+
else:
613+
sep = '\\'
614+
altsep = '/'
615+
curdir = '.'
616+
617+
try:
618+
drivesplits = [splitdrive(p.replace(altsep, sep).lower()) for p in paths]
619+
split_paths = [p.split(sep) for d, p in drivesplits]
620+
621+
try:
622+
isabs, = set(p[:1] == sep for d, p in drivesplits)
623+
except ValueError:
624+
raise ValueError("Can't mix absolute and relative paths") from None
625+
626+
# Check that all drive letters or UNC paths match. The check is made only
627+
# now otherwise type errors for mixing strings and bytes would not be
628+
# caught.
629+
if len(set(d for d, p in drivesplits)) != 1:
630+
raise ValueError("Paths don't have the same drive")
631+
632+
drive, path = splitdrive(paths[0].replace(altsep, sep))
633+
common = path.split(sep)
634+
common = [c for c in common if c and c != curdir]
635+
636+
split_paths = [[c for c in s if c and c != curdir] for s in split_paths]
637+
s1 = min(split_paths)
638+
s2 = max(split_paths)
639+
for i, c in enumerate(s1):
640+
if c != s2[i]:
641+
common = common[:i]
642+
break
643+
else:
644+
common = common[:len(s1)]
645+
646+
prefix = drive + sep if isabs else drive
647+
return prefix + sep.join(common)
648+
except (TypeError, AttributeError):
649+
genericpath._check_arg_types('commonpath', *paths)
650+
raise
651+
652+
592653
# determine if two files are in fact the same file
593654
try:
594655
# GetFinalPathNameByHandle is available starting with Windows 6.0.

Lib/posixpath.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"ismount", "expanduser","expandvars","normpath","abspath",
2323
"samefile","sameopenfile","samestat",
2424
"curdir","pardir","sep","pathsep","defpath","altsep","extsep",
25-
"devnull","realpath","supports_unicode_filenames","relpath"]
25+
"devnull","realpath","supports_unicode_filenames","relpath",
26+
"commonpath"]
2627

2728
# Strings representing various path-related bits and pieces.
2829
# These are primarily for export; internally, they are hardcoded.
@@ -455,3 +456,45 @@ def relpath(path, start=None):
455456
except (TypeError, AttributeError, BytesWarning, DeprecationWarning):
456457
genericpath._check_arg_types('relpath', path, start)
457458
raise
459+
460+
461+
# Return the longest common sub-path of the sequence of paths given as input.
462+
# The paths are not normalized before comparing them (this is the
463+
# responsibility of the caller). Any trailing separator is stripped from the
464+
# returned path.
465+
466+
def commonpath(paths):
467+
"""Given a sequence of path names, returns the longest common sub-path."""
468+
469+
if not paths:
470+
raise ValueError('commonpath() arg is an empty sequence')
471+
472+
if isinstance(paths[0], bytes):
473+
sep = b'/'
474+
curdir = b'.'
475+
else:
476+
sep = '/'
477+
curdir = '.'
478+
479+
try:
480+
split_paths = [path.split(sep) for path in paths]
481+
482+
try:
483+
isabs, = set(p[:1] == sep for p in paths)
484+
except ValueError:
485+
raise ValueError("Can't mix absolute and relative paths") from None
486+
487+
split_paths = [[c for c in s if c and c != curdir] for s in split_paths]
488+
s1 = min(split_paths)
489+
s2 = max(split_paths)
490+
common = s1
491+
for i, c in enumerate(s1):
492+
if c != s2[i]:
493+
common = s1[:i]
494+
break
495+
496+
prefix = sep if isabs else sep[:0]
497+
return prefix + sep.join(common)
498+
except (TypeError, AttributeError):
499+
genericpath._check_arg_types('commonpath', *paths)
500+
raise

Lib/test/test_ntpath.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,75 @@ def test_relpath(self):
330330
tester('ntpath.relpath("/a/b", "/a/b")', '.')
331331
tester('ntpath.relpath("c:/foo", "C:/FOO")', '.')
332332

333+
def test_commonpath(self):
334+
def check(paths, expected):
335+
tester(('ntpath.commonpath(%r)' % paths).replace('\\\\', '\\'),
336+
expected)
337+
def check_error(exc, paths):
338+
self.assertRaises(exc, ntpath.commonpath, paths)
339+
self.assertRaises(exc, ntpath.commonpath,
340+
[os.fsencode(p) for p in paths])
341+
342+
self.assertRaises(ValueError, ntpath.commonpath, [])
343+
check_error(ValueError, ['C:\\Program Files', 'Program Files'])
344+
check_error(ValueError, ['C:\\Program Files', 'C:Program Files'])
345+
check_error(ValueError, ['\\Program Files', 'Program Files'])
346+
check_error(ValueError, ['Program Files', 'C:\\Program Files'])
347+
check(['C:\\Program Files'], 'C:\\Program Files')
348+
check(['C:\\Program Files', 'C:\\Program Files'], 'C:\\Program Files')
349+
check(['C:\\Program Files\\', 'C:\\Program Files'],
350+
'C:\\Program Files')
351+
check(['C:\\Program Files\\', 'C:\\Program Files\\'],
352+
'C:\\Program Files')
353+
check(['C:\\\\Program Files', 'C:\\Program Files\\\\'],
354+
'C:\\Program Files')
355+
check(['C:\\.\\Program Files', 'C:\\Program Files\\.'],
356+
'C:\\Program Files')
357+
check(['C:\\', 'C:\\bin'], 'C:\\')
358+
check(['C:\\Program Files', 'C:\\bin'], 'C:\\')
359+
check(['C:\\Program Files', 'C:\\Program Files\\Bar'],
360+
'C:\\Program Files')
361+
check(['C:\\Program Files\\Foo', 'C:\\Program Files\\Bar'],
362+
'C:\\Program Files')
363+
check(['C:\\Program Files', 'C:\\Projects'], 'C:\\')
364+
check(['C:\\Program Files\\', 'C:\\Projects'], 'C:\\')
365+
366+
check(['C:\\Program Files\\Foo', 'C:/Program Files/Bar'],
367+
'C:\\Program Files')
368+
check(['C:\\Program Files\\Foo', 'c:/program files/bar'],
369+
'C:\\Program Files')
370+
check(['c:/program files/bar', 'C:\\Program Files\\Foo'],
371+
'c:\\program files')
372+
373+
check_error(ValueError, ['C:\\Program Files', 'D:\\Program Files'])
374+
375+
check(['spam'], 'spam')
376+
check(['spam', 'spam'], 'spam')
377+
check(['spam', 'alot'], '')
378+
check(['and\\jam', 'and\\spam'], 'and')
379+
check(['and\\\\jam', 'and\\spam\\\\'], 'and')
380+
check(['and\\.\\jam', '.\\and\\spam'], 'and')
381+
check(['and\\jam', 'and\\spam', 'alot'], '')
382+
check(['and\\jam', 'and\\spam', 'and'], 'and')
383+
check(['C:and\\jam', 'C:and\\spam'], 'C:and')
384+
385+
check([''], '')
386+
check(['', 'spam\\alot'], '')
387+
check_error(ValueError, ['', '\\spam\\alot'])
388+
389+
self.assertRaises(TypeError, ntpath.commonpath,
390+
[b'C:\\Program Files', 'C:\\Program Files\\Foo'])
391+
self.assertRaises(TypeError, ntpath.commonpath,
392+
[b'C:\\Program Files', 'Program Files\\Foo'])
393+
self.assertRaises(TypeError, ntpath.commonpath,
394+
[b'Program Files', 'C:\\Program Files\\Foo'])
395+
self.assertRaises(TypeError, ntpath.commonpath,
396+
['C:\\Program Files', b'C:\\Program Files\\Foo'])
397+
self.assertRaises(TypeError, ntpath.commonpath,
398+
['C:\\Program Files', b'Program Files\\Foo'])
399+
self.assertRaises(TypeError, ntpath.commonpath,
400+
['Program Files', b'C:\\Program Files\\Foo'])
401+
333402
def test_sameopenfile(self):
334403
with TemporaryFile() as tf1, TemporaryFile() as tf2:
335404
# Make sure the same file is really the same

Lib/test/test_posixpath.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,60 @@ def test_relpath_bytes(self):
522522
finally:
523523
os.getcwdb = real_getcwdb
524524

525+
def test_commonpath(self):
526+
def check(paths, expected):
527+
self.assertEqual(posixpath.commonpath(paths), expected)
528+
self.assertEqual(posixpath.commonpath([os.fsencode(p) for p in paths]),
529+
os.fsencode(expected))
530+
def check_error(exc, paths):
531+
self.assertRaises(exc, posixpath.commonpath, paths)
532+
self.assertRaises(exc, posixpath.commonpath,
533+
[os.fsencode(p) for p in paths])
534+
535+
self.assertRaises(ValueError, posixpath.commonpath, [])
536+
check_error(ValueError, ['/usr', 'usr'])
537+
check_error(ValueError, ['usr', '/usr'])
538+
539+
check(['/usr/local'], '/usr/local')
540+
check(['/usr/local', '/usr/local'], '/usr/local')
541+
check(['/usr/local/', '/usr/local'], '/usr/local')
542+
check(['/usr/local/', '/usr/local/'], '/usr/local')
543+
check(['/usr//local', '//usr/local'], '/usr/local')
544+
check(['/usr/./local', '/./usr/local'], '/usr/local')
545+
check(['/', '/dev'], '/')
546+
check(['/usr', '/dev'], '/')
547+
check(['/usr/lib/', '/usr/lib/python3'], '/usr/lib')
548+
check(['/usr/lib/', '/usr/lib64/'], '/usr')
549+
550+
check(['/usr/lib', '/usr/lib64'], '/usr')
551+
check(['/usr/lib/', '/usr/lib64'], '/usr')
552+
553+
check(['spam'], 'spam')
554+
check(['spam', 'spam'], 'spam')
555+
check(['spam', 'alot'], '')
556+
check(['and/jam', 'and/spam'], 'and')
557+
check(['and//jam', 'and/spam//'], 'and')
558+
check(['and/./jam', './and/spam'], 'and')
559+
check(['and/jam', 'and/spam', 'alot'], '')
560+
check(['and/jam', 'and/spam', 'and'], 'and')
561+
562+
check([''], '')
563+
check(['', 'spam/alot'], '')
564+
check_error(ValueError, ['', '/spam/alot'])
565+
566+
self.assertRaises(TypeError, posixpath.commonpath,
567+
[b'/usr/lib/', '/usr/lib/python3'])
568+
self.assertRaises(TypeError, posixpath.commonpath,
569+
[b'/usr/lib/', 'usr/lib/python3'])
570+
self.assertRaises(TypeError, posixpath.commonpath,
571+
[b'usr/lib/', '/usr/lib/python3'])
572+
self.assertRaises(TypeError, posixpath.commonpath,
573+
['/usr/lib/', b'/usr/lib/python3'])
574+
self.assertRaises(TypeError, posixpath.commonpath,
575+
['/usr/lib/', b'usr/lib/python3'])
576+
self.assertRaises(TypeError, posixpath.commonpath,
577+
['usr/lib/', b'/usr/lib/python3'])
578+
525579

526580
class PosixCommonTest(test_genericpath.CommonTest, unittest.TestCase):
527581
pathmodule = posixpath

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ Core and Builtins
1313
Library
1414
-------
1515

16+
- Issue #10395: Added os.path.commonpath(). Implemented in posixpath and ntpath.
17+
Based on patch by Rafik Draoui.
18+
1619
- Issue #23611: Serializing more "lookupable" objects (such as unbound methods
1720
or nested classes) now are supported with pickle protocols < 4.
1821

0 commit comments

Comments
 (0)