Skip to content

Commit 3deeeb0

Browse files
Issue #21883: os.path.join() and os.path.relpath() now raise a TypeError with
more helpful error message for unsupported or mismatched types of arguments.
1 parent 385328b commit 3deeeb0

File tree

8 files changed

+137
-91
lines changed

8 files changed

+137
-91
lines changed

Lib/genericpath.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,16 @@ def _splitext(p, sep, altsep, extsep):
130130
filenameIndex += 1
131131

132132
return p, p[:0]
133+
134+
def _check_arg_types(funcname, *args):
135+
hasstr = hasbytes = False
136+
for s in args:
137+
if isinstance(s, str):
138+
hasstr = True
139+
elif isinstance(s, bytes):
140+
hasbytes = True
141+
else:
142+
raise TypeError('%s() argument must be str or bytes, not %r' %
143+
(funcname, s.__class__.__name__)) from None
144+
if hasstr and hasbytes:
145+
raise TypeError("Can't mix strings and bytes in path components") from None

Lib/macpath.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,24 @@ def isabs(s):
5050

5151

5252
def join(s, *p):
53-
colon = _get_colon(s)
54-
path = s
55-
for t in p:
56-
if (not path) or isabs(t):
57-
path = t
58-
continue
59-
if t[:1] == colon:
60-
t = t[1:]
61-
if colon not in path:
62-
path = colon + path
63-
if path[-1:] != colon:
64-
path = path + colon
65-
path = path + t
66-
return path
53+
try:
54+
colon = _get_colon(s)
55+
path = s
56+
for t in p:
57+
if (not path) or isabs(t):
58+
path = t
59+
continue
60+
if t[:1] == colon:
61+
t = t[1:]
62+
if colon not in path:
63+
path = colon + path
64+
if path[-1:] != colon:
65+
path = path + colon
66+
path = path + t
67+
return path
68+
except (TypeError, AttributeError, BytesWarning):
69+
genericpath._check_arg_types('join', s, *p)
70+
raise
6771

6872

6973
def split(s):

Lib/ntpath.py

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -80,32 +80,36 @@ def join(path, *paths):
8080
sep = '\\'
8181
seps = '\\/'
8282
colon = ':'
83-
result_drive, result_path = splitdrive(path)
84-
for p in paths:
85-
p_drive, p_path = splitdrive(p)
86-
if p_path and p_path[0] in seps:
87-
# Second path is absolute
88-
if p_drive or not result_drive:
89-
result_drive = p_drive
90-
result_path = p_path
91-
continue
92-
elif p_drive and p_drive != result_drive:
93-
if p_drive.lower() != result_drive.lower():
94-
# Different drives => ignore the first path entirely
95-
result_drive = p_drive
83+
try:
84+
result_drive, result_path = splitdrive(path)
85+
for p in paths:
86+
p_drive, p_path = splitdrive(p)
87+
if p_path and p_path[0] in seps:
88+
# Second path is absolute
89+
if p_drive or not result_drive:
90+
result_drive = p_drive
9691
result_path = p_path
9792
continue
98-
# Same drive in different case
99-
result_drive = p_drive
100-
# Second path is relative to the first
101-
if result_path and result_path[-1] not in seps:
102-
result_path = result_path + sep
103-
result_path = result_path + p_path
104-
## add separator between UNC and non-absolute path
105-
if (result_path and result_path[0] not in seps and
106-
result_drive and result_drive[-1:] != colon):
107-
return result_drive + sep + result_path
108-
return result_drive + result_path
93+
elif p_drive and p_drive != result_drive:
94+
if p_drive.lower() != result_drive.lower():
95+
# Different drives => ignore the first path entirely
96+
result_drive = p_drive
97+
result_path = p_path
98+
continue
99+
# Same drive in different case
100+
result_drive = p_drive
101+
# Second path is relative to the first
102+
if result_path and result_path[-1] not in seps:
103+
result_path = result_path + sep
104+
result_path = result_path + p_path
105+
## add separator between UNC and non-absolute path
106+
if (result_path and result_path[0] not in seps and
107+
result_drive and result_drive[-1:] != colon):
108+
return result_drive + sep + result_path
109+
return result_drive + result_path
110+
except (TypeError, AttributeError, BytesWarning):
111+
genericpath._check_arg_types('join', path, *paths)
112+
raise
109113

110114

111115
# Split a path in a drive specification (a drive letter followed by a
@@ -558,27 +562,31 @@ def relpath(path, start=None):
558562
if not path:
559563
raise ValueError("no path specified")
560564

561-
start_abs = abspath(normpath(start))
562-
path_abs = abspath(normpath(path))
563-
start_drive, start_rest = splitdrive(start_abs)
564-
path_drive, path_rest = splitdrive(path_abs)
565-
if normcase(start_drive) != normcase(path_drive):
566-
raise ValueError("path is on mount %r, start on mount %r" % (
567-
path_drive, start_drive))
568-
569-
start_list = [x for x in start_rest.split(sep) if x]
570-
path_list = [x for x in path_rest.split(sep) if x]
571-
# Work out how much of the filepath is shared by start and path.
572-
i = 0
573-
for e1, e2 in zip(start_list, path_list):
574-
if normcase(e1) != normcase(e2):
575-
break
576-
i += 1
565+
try:
566+
start_abs = abspath(normpath(start))
567+
path_abs = abspath(normpath(path))
568+
start_drive, start_rest = splitdrive(start_abs)
569+
path_drive, path_rest = splitdrive(path_abs)
570+
if normcase(start_drive) != normcase(path_drive):
571+
raise ValueError("path is on mount %r, start on mount %r" % (
572+
path_drive, start_drive))
573+
574+
start_list = [x for x in start_rest.split(sep) if x]
575+
path_list = [x for x in path_rest.split(sep) if x]
576+
# Work out how much of the filepath is shared by start and path.
577+
i = 0
578+
for e1, e2 in zip(start_list, path_list):
579+
if normcase(e1) != normcase(e2):
580+
break
581+
i += 1
577582

578-
rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
579-
if not rel_list:
580-
return curdir
581-
return join(*rel_list)
583+
rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
584+
if not rel_list:
585+
return curdir
586+
return join(*rel_list)
587+
except (TypeError, ValueError, AttributeError, BytesWarning):
588+
genericpath._check_arg_types('relpath', path, start)
589+
raise
582590

583591

584592
# determine if two files are in fact the same file

Lib/posixpath.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,9 @@ def join(a, *p):
8282
path += b
8383
else:
8484
path += sep + b
85-
except (TypeError, AttributeError):
86-
for s in (a,) + p:
87-
if not isinstance(s, (str, bytes)):
88-
raise TypeError('join() argument must be str or bytes, not %r' %
89-
s.__class__.__name__) from None
90-
# Must have a mixture of text and binary data
91-
raise TypeError("Can't mix strings and bytes in path components") from None
85+
except (TypeError, AttributeError, BytesWarning):
86+
genericpath._check_arg_types('join', a, *p)
87+
raise
9288
return path
9389

9490

@@ -446,13 +442,16 @@ def relpath(path, start=None):
446442
if start is None:
447443
start = curdir
448444

449-
start_list = [x for x in abspath(start).split(sep) if x]
450-
path_list = [x for x in abspath(path).split(sep) if x]
451-
452-
# Work out how much of the filepath is shared by start and path.
453-
i = len(commonprefix([start_list, path_list]))
454-
455-
rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
456-
if not rel_list:
457-
return curdir
458-
return join(*rel_list)
445+
try:
446+
start_list = [x for x in abspath(start).split(sep) if x]
447+
path_list = [x for x in abspath(path).split(sep) if x]
448+
# Work out how much of the filepath is shared by start and path.
449+
i = len(commonprefix([start_list, path_list]))
450+
451+
rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
452+
if not rel_list:
453+
return curdir
454+
return join(*rel_list)
455+
except (TypeError, AttributeError, BytesWarning):
456+
genericpath._check_arg_types('relpath', path, start)
457+
raise

Lib/test/test_genericpath.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,39 @@ def test_nonascii_abspath(self):
434434
with support.temp_cwd(name):
435435
self.test_abspath()
436436

437+
def test_join_errors(self):
438+
# Check join() raises friendly TypeErrors.
439+
with support.check_warnings(('', BytesWarning), quiet=True):
440+
errmsg = "Can't mix strings and bytes in path components"
441+
with self.assertRaisesRegex(TypeError, errmsg):
442+
self.pathmodule.join(b'bytes', 'str')
443+
with self.assertRaisesRegex(TypeError, errmsg):
444+
self.pathmodule.join('str', b'bytes')
445+
# regression, see #15377
446+
errmsg = r'join\(\) argument must be str or bytes, not %r'
447+
with self.assertRaisesRegex(TypeError, errmsg % 'int'):
448+
self.pathmodule.join(42, 'str')
449+
with self.assertRaisesRegex(TypeError, errmsg % 'int'):
450+
self.pathmodule.join('str', 42)
451+
with self.assertRaisesRegex(TypeError, errmsg % 'bytearray'):
452+
self.pathmodule.join(bytearray(b'foo'), bytearray(b'bar'))
453+
454+
def test_relpath_errors(self):
455+
# Check relpath() raises friendly TypeErrors.
456+
with support.check_warnings(('', BytesWarning), quiet=True):
457+
errmsg = "Can't mix strings and bytes in path components"
458+
with self.assertRaisesRegex(TypeError, errmsg):
459+
self.pathmodule.relpath(b'bytes', 'str')
460+
with self.assertRaisesRegex(TypeError, errmsg):
461+
self.pathmodule.relpath('str', b'bytes')
462+
errmsg = r'relpath\(\) argument must be str or bytes, not %r'
463+
with self.assertRaisesRegex(TypeError, errmsg % 'int'):
464+
self.pathmodule.relpath(42, 'str')
465+
with self.assertRaisesRegex(TypeError, errmsg % 'int'):
466+
self.pathmodule.relpath('str', 42)
467+
with self.assertRaisesRegex(TypeError, errmsg % 'bytearray'):
468+
self.pathmodule.relpath(bytearray(b'foo'), bytearray(b'bar'))
469+
437470

438471
if __name__=="__main__":
439472
unittest.main()

Lib/test/test_macpath.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ def test_normpath(self):
142142
class MacCommonTest(test_genericpath.CommonTest, unittest.TestCase):
143143
pathmodule = macpath
144144

145+
test_relpath_errors = None
146+
145147

146148
if __name__ == "__main__":
147149
unittest.main()

Lib/test/test_posixpath.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -57,22 +57,6 @@ def test_join(self):
5757
self.assertEqual(posixpath.join(b"/foo/", b"bar/", b"baz/"),
5858
b"/foo/bar/baz/")
5959

60-
def test_join_errors(self):
61-
# Check posixpath.join raises friendly TypeErrors.
62-
errmsg = "Can't mix strings and bytes in path components"
63-
with self.assertRaisesRegex(TypeError, errmsg):
64-
posixpath.join(b'bytes', 'str')
65-
with self.assertRaisesRegex(TypeError, errmsg):
66-
posixpath.join('str', b'bytes')
67-
# regression, see #15377
68-
errmsg = r'join\(\) argument must be str or bytes, not %r'
69-
with self.assertRaisesRegex(TypeError, errmsg % 'NoneType'):
70-
posixpath.join(None, 'str')
71-
with self.assertRaisesRegex(TypeError, errmsg % 'NoneType'):
72-
posixpath.join('str', None)
73-
with self.assertRaisesRegex(TypeError, errmsg % 'bytearray'):
74-
posixpath.join(bytearray(b'foo'), bytearray(b'bar'))
75-
7660
def test_split(self):
7761
self.assertEqual(posixpath.split("/foo/bar"), ("/foo", "bar"))
7862
self.assertEqual(posixpath.split("/"), ("/", ""))

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ Core and Builtins
162162
Library
163163
-------
164164

165+
- Issue #21883: os.path.join() and os.path.relpath() now raise a TypeError with
166+
more helpful error message for unsupported or mismatched types of arguments.
167+
165168
- Issue #22219: The zipfile module CLI now adds entries for directories
166169
(including empty directories) in ZIP file.
167170

0 commit comments

Comments
 (0)