Skip to content

Commit f381a5e

Browse files
committed
gh-74453: Deprecate os.path.commonprefix
1 parent 53fecbe commit f381a5e

File tree

11 files changed

+220
-91
lines changed

11 files changed

+220
-91
lines changed

Doc/library/os.path.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ the :mod:`glob` module.)
117117
>>> os.path.commonpath(['/usr/lib', '/usr/local/lib'])
118118
'/usr'
119119

120+
.. versionchanged:: 3.15
121+
Deprecated in favor of :func:`os.path.commonpath` for path prefixes and
122+
:func:`string.commonprefix` for string prefixes.
123+
120124
.. versionchanged:: 3.6
121125
Accepts a :term:`path-like object`.
122126

Doc/library/string.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,3 +990,13 @@ Helper functions
990990
or ``None``, runs of whitespace characters are replaced by a single space
991991
and leading and trailing whitespace are removed, otherwise *sep* is used to
992992
split and join the words.
993+
994+
.. function:: commonprefix(list, /)
995+
996+
Return the longest string prefix (taken character-by-character) that is a
997+
prefix of all string in *list*. If *list* is empty, return the empty string
998+
(``''``).
999+
1000+
.. versionadded:: 3.15
1001+
1002+
Moved to the :mod:`string` module from the :mod:`os.path` module.

Lib/genericpath.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,13 @@ def getctime(filename, /):
105105
# Return the longest prefix of all list elements.
106106
def commonprefix(m, /):
107107
"Given a list of pathnames, returns the longest common leading component"
108-
if not m: return ''
109-
# Some people pass in a list of pathname parts to operate in an OS-agnostic
110-
# fashion; don't try to translate in that case as that's an abuse of the
111-
# API and they are already doing what they need to be OS-agnostic and so
112-
# they most likely won't be using an os.PathLike object in the sublists.
113-
if not isinstance(m[0], (list, tuple)):
114-
m = tuple(map(os.fspath, m))
115-
s1 = min(m)
116-
s2 = max(m)
117-
for i, c in enumerate(s1):
118-
if c != s2[i]:
119-
return s1[:i]
120-
return s1
108+
import warnings
109+
warnings.warn('os.path.commonprefix() is deprecated. Use '
110+
'os.path.commonpath() or string.commonprefix() instead.',
111+
category=DeprecationWarning,
112+
stacklevel=2)
113+
import string
114+
return string.commonprefix(m)
121115

122116
# Are two stat buffers (obtained from stat, fstat or lstat)
123117
# describing the same file?

Lib/posixpath.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import os
2727
import sys
2828
import stat
29+
import string
2930
import genericpath
3031
from genericpath import *
3132

@@ -542,7 +543,7 @@ def relpath(path, start=None):
542543
start_list = start_tail.split(sep) if start_tail else []
543544
path_list = path_tail.split(sep) if path_tail else []
544545
# Work out how much of the filepath is shared by start and path.
545-
i = len(commonprefix([start_list, path_list]))
546+
i = len(string.commonprefix([start_list, path_list]))
546547

547548
rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
548549
if not rel_list:

Lib/string/__init__.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
"""
1616

1717
__all__ = ["ascii_letters", "ascii_lowercase", "ascii_uppercase", "capwords",
18-
"digits", "hexdigits", "octdigits", "printable", "punctuation",
19-
"whitespace", "Formatter", "Template"]
18+
"commonprefix", "digits", "hexdigits", "octdigits", "printable",
19+
"punctuation", "whitespace", "Formatter", "Template"]
2020

2121
import _string
2222

@@ -48,6 +48,26 @@ def capwords(s, sep=None):
4848
return (sep or ' ').join(map(str.capitalize, s.split(sep)))
4949

5050

51+
def commonprefix(m, /):
52+
"Given a list of strings, returns the longest common leading component"
53+
if not m: return ''
54+
# Note that previously this function was in the 'os.path' module, hence the
55+
# handling for paths. Maintain compatibility so users have a 1-to-1 drop-in.
56+
# Some people pass in a list of pathname parts to operate in an OS-agnostic
57+
# fashion; don't try to translate in that case as that's an abuse of the
58+
# API and they are already doing what they need to be OS-agnostic and so
59+
# they most likely won't be using an os.PathLike object in the sublists.
60+
if not isinstance(m[0], (list, tuple)):
61+
import os
62+
m = tuple(map(os.fspath, m))
63+
s1 = min(m)
64+
s2 = max(m)
65+
for i, c in enumerate(s1):
66+
if c != s2[i]:
67+
return s1[:i]
68+
return s1
69+
70+
5171
####################################################################
5272
_sentinel_dict = {}
5373

Lib/test/test_genericpath.py

Lines changed: 69 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -34,71 +34,72 @@ def test_no_argument(self):
3434
.format(self.pathmodule.__name__, attr))
3535

3636
def test_commonprefix(self):
37-
commonprefix = self.pathmodule.commonprefix
38-
self.assertEqual(
39-
commonprefix([]),
40-
""
41-
)
42-
self.assertEqual(
43-
commonprefix(["/home/swenson/spam", "/home/swen/spam"]),
44-
"/home/swen"
45-
)
46-
self.assertEqual(
47-
commonprefix(["/home/swen/spam", "/home/swen/eggs"]),
48-
"/home/swen/"
49-
)
50-
self.assertEqual(
51-
commonprefix(["/home/swen/spam", "/home/swen/spam"]),
52-
"/home/swen/spam"
53-
)
54-
self.assertEqual(
55-
commonprefix(["home:swenson:spam", "home:swen:spam"]),
56-
"home:swen"
57-
)
58-
self.assertEqual(
59-
commonprefix([":home:swen:spam", ":home:swen:eggs"]),
60-
":home:swen:"
61-
)
62-
self.assertEqual(
63-
commonprefix([":home:swen:spam", ":home:swen:spam"]),
64-
":home:swen:spam"
65-
)
66-
67-
self.assertEqual(
68-
commonprefix([b"/home/swenson/spam", b"/home/swen/spam"]),
69-
b"/home/swen"
70-
)
71-
self.assertEqual(
72-
commonprefix([b"/home/swen/spam", b"/home/swen/eggs"]),
73-
b"/home/swen/"
74-
)
75-
self.assertEqual(
76-
commonprefix([b"/home/swen/spam", b"/home/swen/spam"]),
77-
b"/home/swen/spam"
78-
)
79-
self.assertEqual(
80-
commonprefix([b"home:swenson:spam", b"home:swen:spam"]),
81-
b"home:swen"
82-
)
83-
self.assertEqual(
84-
commonprefix([b":home:swen:spam", b":home:swen:eggs"]),
85-
b":home:swen:"
86-
)
87-
self.assertEqual(
88-
commonprefix([b":home:swen:spam", b":home:swen:spam"]),
89-
b":home:swen:spam"
90-
)
91-
92-
testlist = ['', 'abc', 'Xbcd', 'Xb', 'XY', 'abcd',
93-
'aXc', 'abd', 'ab', 'aX', 'abcX']
94-
for s1 in testlist:
95-
for s2 in testlist:
96-
p = commonprefix([s1, s2])
97-
self.assertStartsWith(s1, p)
98-
self.assertStartsWith(s2, p)
99-
if s1 != s2:
100-
n = len(p)
101-
self.assertNotEqual(s1[n:n+1], s2[n:n+1])
37+
with warnings_helper.check_warnings((".*commonpath().*", DeprecationWarning)):
38+
commonprefix = self.pathmodule.commonprefix
39+
self.assertEqual(
40+
commonprefix([]),
41+
""
42+
)
43+
self.assertEqual(
44+
commonprefix(["/home/swenson/spam", "/home/swen/spam"]),
45+
"/home/swen"
46+
)
47+
self.assertEqual(
48+
commonprefix(["/home/swen/spam", "/home/swen/eggs"]),
49+
"/home/swen/"
50+
)
51+
self.assertEqual(
52+
commonprefix(["/home/swen/spam", "/home/swen/spam"]),
53+
"/home/swen/spam"
54+
)
55+
self.assertEqual(
56+
commonprefix(["home:swenson:spam", "home:swen:spam"]),
57+
"home:swen"
58+
)
59+
self.assertEqual(
60+
commonprefix([":home:swen:spam", ":home:swen:eggs"]),
61+
":home:swen:"
62+
)
63+
self.assertEqual(
64+
commonprefix([":home:swen:spam", ":home:swen:spam"]),
65+
":home:swen:spam"
66+
)
67+
68+
self.assertEqual(
69+
commonprefix([b"/home/swenson/spam", b"/home/swen/spam"]),
70+
b"/home/swen"
71+
)
72+
self.assertEqual(
73+
commonprefix([b"/home/swen/spam", b"/home/swen/eggs"]),
74+
b"/home/swen/"
75+
)
76+
self.assertEqual(
77+
commonprefix([b"/home/swen/spam", b"/home/swen/spam"]),
78+
b"/home/swen/spam"
79+
)
80+
self.assertEqual(
81+
commonprefix([b"home:swenson:spam", b"home:swen:spam"]),
82+
b"home:swen"
83+
)
84+
self.assertEqual(
85+
commonprefix([b":home:swen:spam", b":home:swen:eggs"]),
86+
b":home:swen:"
87+
)
88+
self.assertEqual(
89+
commonprefix([b":home:swen:spam", b":home:swen:spam"]),
90+
b":home:swen:spam"
91+
)
92+
93+
testlist = ['', 'abc', 'Xbcd', 'Xb', 'XY', 'abcd',
94+
'aXc', 'abd', 'ab', 'aX', 'abcX']
95+
for s1 in testlist:
96+
for s2 in testlist:
97+
p = commonprefix([s1, s2])
98+
self.assertStartsWith(s1, p)
99+
self.assertStartsWith(s2, p)
100+
if s1 != s2:
101+
n = len(p)
102+
self.assertNotEqual(s1[n:n+1], s2[n:n+1])
102103

103104
def test_getsize(self):
104105
filename = os_helper.TESTFN
@@ -606,8 +607,9 @@ def test_path_isdir(self):
606607
self.assertPathEqual(os.path.isdir)
607608

608609
def test_path_commonprefix(self):
609-
self.assertEqual(os.path.commonprefix([self.file_path, self.file_name]),
610-
self.file_name)
610+
with warnings_helper.check_warnings((".*commonpath().*", DeprecationWarning)):
611+
self.assertEqual(os.path.commonprefix([self.file_path, self.file_name]),
612+
self.file_name)
611613

612614
def test_path_getsize(self):
613615
self.assertPathEqual(os.path.getsize)

Lib/test/test_ntpath.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from ntpath import ALL_BUT_LAST, ALLOW_MISSING
1111
from test import support
1212
from test.support import os_helper
13+
from test.support import warnings_helper
1314
from test.support.os_helper import FakePath
1415
from test import test_genericpath
1516
from tempfile import TemporaryFile
@@ -298,12 +299,13 @@ def test_isabs(self):
298299
tester('ntpath.isabs("\\\\.\\C:")', 1)
299300

300301
def test_commonprefix(self):
301-
tester('ntpath.commonprefix(["/home/swenson/spam", "/home/swen/spam"])',
302-
"/home/swen")
303-
tester('ntpath.commonprefix(["\\home\\swen\\spam", "\\home\\swen\\eggs"])',
304-
"\\home\\swen\\")
305-
tester('ntpath.commonprefix(["/home/swen/spam", "/home/swen/spam"])',
306-
"/home/swen/spam")
302+
with warnings_helper.check_warnings((".*commonpath().*", DeprecationWarning)):
303+
tester('ntpath.commonprefix(["/home/swenson/spam", "/home/swen/spam"])',
304+
"/home/swen")
305+
tester('ntpath.commonprefix(["\\home\\swen\\spam", "\\home\\swen\\eggs"])',
306+
"\\home\\swen\\")
307+
tester('ntpath.commonprefix(["/home/swen/spam", "/home/swen/spam"])',
308+
"/home/swen/spam")
307309

308310
def test_join(self):
309311
tester('ntpath.join("")', '')

Lib/test/test_string/test_string.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from string import Template
44
import types
55
from test.support import cpython_only
6+
from test.support import os_helper
67
from test.support.import_helper import ensure_lazy_imports
78

89

@@ -40,6 +41,97 @@ def test_capwords(self):
4041
self.assertEqual(string.capwords('\taBc\tDeF\t'), 'Abc Def')
4142
self.assertEqual(string.capwords('\taBc\tDeF\t', '\t'), '\tAbc\tDef\t')
4243

44+
def test_commonprefix(self):
45+
self.assertEqual(
46+
string.commonprefix([]),
47+
""
48+
)
49+
self.assertEqual(
50+
string.commonprefix(["a", "b"]),
51+
""
52+
)
53+
self.assertEqual(
54+
string.commonprefix(["/home/swenson/spam", "/home/swen/spam"]),
55+
"/home/swen"
56+
)
57+
self.assertEqual(
58+
string.commonprefix(["/home/swen/spam", "/home/swen/eggs"]),
59+
"/home/swen/"
60+
)
61+
self.assertEqual(
62+
string.commonprefix(["/home/swen/spam", "/home/swen/spam"]),
63+
"/home/swen/spam"
64+
)
65+
self.assertEqual(
66+
string.commonprefix(["home:swenson:spam", "home:swen:spam"]),
67+
"home:swen"
68+
)
69+
self.assertEqual(
70+
string.commonprefix([":home:swen:spam", ":home:swen:eggs"]),
71+
":home:swen:"
72+
)
73+
self.assertEqual(
74+
string.commonprefix([":home:swen:spam", ":home:swen:spam"]),
75+
":home:swen:spam"
76+
)
77+
78+
self.assertEqual(
79+
string.commonprefix([b"/home/swenson/spam", b"/home/swen/spam"]),
80+
b"/home/swen"
81+
)
82+
self.assertEqual(
83+
string.commonprefix([b"/home/swen/spam", b"/home/swen/eggs"]),
84+
b"/home/swen/"
85+
)
86+
self.assertEqual(
87+
string.commonprefix([b"/home/swen/spam", b"/home/swen/spam"]),
88+
b"/home/swen/spam"
89+
)
90+
self.assertEqual(
91+
string.commonprefix([b"home:swenson:spam", b"home:swen:spam"]),
92+
b"home:swen"
93+
)
94+
self.assertEqual(
95+
string.commonprefix([b":home:swen:spam", b":home:swen:eggs"]),
96+
b":home:swen:"
97+
)
98+
self.assertEqual(
99+
string.commonprefix([b":home:swen:spam", b":home:swen:spam"]),
100+
b":home:swen:spam"
101+
)
102+
103+
testlist = ['', 'abc', 'Xbcd', 'Xb', 'XY', 'abcd',
104+
'aXc', 'abd', 'ab', 'aX', 'abcX']
105+
for s1 in testlist:
106+
for s2 in testlist:
107+
p = string.commonprefix([s1, s2])
108+
self.assertStartsWith(s1, p)
109+
self.assertStartsWith(s2, p)
110+
if s1 != s2:
111+
n = len(p)
112+
self.assertNotEqual(s1[n:n+1], s2[n:n+1])
113+
114+
def test_commonprefix_paths(self):
115+
# Test backwards-compatibility with os.path.commonprefix()
116+
# This function must handle PathLike objects.
117+
file_name = os_helper.TESTFN
118+
file_path = os_helper.FakePath(file_name)
119+
self.assertEqual(string.commonprefix([file_path, file_name]),
120+
file_name)
121+
122+
def test_commonprefix_sequence_of_str(self):
123+
# Test backwards-compatibility with os.path.commonprefix()
124+
# This function must handle lists and tuples of strings.
125+
for type_ in (tuple, list):
126+
seq1 = type_(["abc", "de", "fgh"])
127+
seq2 = type_(["abc", "def", "gh"])
128+
self.assertEqual(string.commonprefix([seq1, seq2]),
129+
type_(["abc"]))
130+
131+
seq1 = type_(["ab"])
132+
seq2 = type_(["ac"])
133+
self.assertEqual(string.commonprefix([seq1, seq2]), type_([]))
134+
43135
def test_basic_formatter(self):
44136
fmt = string.Formatter()
45137
self.assertEqual(fmt.format("foo"), "foo")

Lib/textwrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -432,7 +432,7 @@ def dedent(text):
432432
msg = f'expected str object, not {type(text).__qualname__!r}'
433433
raise TypeError(msg) from None
434434

435-
# Get length of leading whitespace, inspired by ``os.path.commonprefix()``.
435+
# Get length of leading whitespace, inspired by ``string.commonprefix()``.
436436
non_blank_lines = [l for l in lines if l and not l.isspace()]
437437
l1 = min(non_blank_lines, default='')
438438
l2 = max(non_blank_lines, default='')

0 commit comments

Comments
 (0)