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.
This commit is contained in:
Serhiy Storchaka 2014-10-04 14:58:43 +03:00
parent 385328bf76
commit 3deeeb0c39
8 changed files with 135 additions and 89 deletions

View File

@ -130,3 +130,16 @@ def _splitext(p, sep, altsep, extsep):
filenameIndex += 1 filenameIndex += 1
return p, p[:0] return p, p[:0]
def _check_arg_types(funcname, *args):
hasstr = hasbytes = False
for s in args:
if isinstance(s, str):
hasstr = True
elif isinstance(s, bytes):
hasbytes = True
else:
raise TypeError('%s() argument must be str or bytes, not %r' %
(funcname, s.__class__.__name__)) from None
if hasstr and hasbytes:
raise TypeError("Can't mix strings and bytes in path components") from None

View File

@ -50,20 +50,24 @@ def isabs(s):
def join(s, *p): def join(s, *p):
colon = _get_colon(s) try:
path = s colon = _get_colon(s)
for t in p: path = s
if (not path) or isabs(t): for t in p:
path = t if (not path) or isabs(t):
continue path = t
if t[:1] == colon: continue
t = t[1:] if t[:1] == colon:
if colon not in path: t = t[1:]
path = colon + path if colon not in path:
if path[-1:] != colon: path = colon + path
path = path + colon if path[-1:] != colon:
path = path + t path = path + colon
return path path = path + t
return path
except (TypeError, AttributeError, BytesWarning):
genericpath._check_arg_types('join', s, *p)
raise
def split(s): def split(s):

View File

@ -80,32 +80,36 @@ def join(path, *paths):
sep = '\\' sep = '\\'
seps = '\\/' seps = '\\/'
colon = ':' colon = ':'
result_drive, result_path = splitdrive(path) try:
for p in paths: result_drive, result_path = splitdrive(path)
p_drive, p_path = splitdrive(p) for p in paths:
if p_path and p_path[0] in seps: p_drive, p_path = splitdrive(p)
# Second path is absolute if p_path and p_path[0] in seps:
if p_drive or not result_drive: # Second path is absolute
result_drive = p_drive if p_drive or not result_drive:
result_path = p_path result_drive = p_drive
continue
elif p_drive and p_drive != result_drive:
if p_drive.lower() != result_drive.lower():
# Different drives => ignore the first path entirely
result_drive = p_drive
result_path = p_path result_path = p_path
continue continue
# Same drive in different case elif p_drive and p_drive != result_drive:
result_drive = p_drive if p_drive.lower() != result_drive.lower():
# Second path is relative to the first # Different drives => ignore the first path entirely
if result_path and result_path[-1] not in seps: result_drive = p_drive
result_path = result_path + sep result_path = p_path
result_path = result_path + p_path continue
## add separator between UNC and non-absolute path # Same drive in different case
if (result_path and result_path[0] not in seps and result_drive = p_drive
result_drive and result_drive[-1:] != colon): # Second path is relative to the first
return result_drive + sep + result_path if result_path and result_path[-1] not in seps:
return result_drive + result_path result_path = result_path + sep
result_path = result_path + p_path
## add separator between UNC and non-absolute path
if (result_path and result_path[0] not in seps and
result_drive and result_drive[-1:] != colon):
return result_drive + sep + result_path
return result_drive + result_path
except (TypeError, AttributeError, BytesWarning):
genericpath._check_arg_types('join', path, *paths)
raise
# Split a path in a drive specification (a drive letter followed by a # Split a path in a drive specification (a drive letter followed by a
@ -558,27 +562,31 @@ def relpath(path, start=None):
if not path: if not path:
raise ValueError("no path specified") raise ValueError("no path specified")
start_abs = abspath(normpath(start)) try:
path_abs = abspath(normpath(path)) start_abs = abspath(normpath(start))
start_drive, start_rest = splitdrive(start_abs) path_abs = abspath(normpath(path))
path_drive, path_rest = splitdrive(path_abs) start_drive, start_rest = splitdrive(start_abs)
if normcase(start_drive) != normcase(path_drive): path_drive, path_rest = splitdrive(path_abs)
raise ValueError("path is on mount %r, start on mount %r" % ( if normcase(start_drive) != normcase(path_drive):
path_drive, start_drive)) raise ValueError("path is on mount %r, start on mount %r" % (
path_drive, start_drive))
start_list = [x for x in start_rest.split(sep) if x] start_list = [x for x in start_rest.split(sep) if x]
path_list = [x for x in path_rest.split(sep) if x] path_list = [x for x in path_rest.split(sep) if x]
# Work out how much of the filepath is shared by start and path. # Work out how much of the filepath is shared by start and path.
i = 0 i = 0
for e1, e2 in zip(start_list, path_list): for e1, e2 in zip(start_list, path_list):
if normcase(e1) != normcase(e2): if normcase(e1) != normcase(e2):
break break
i += 1 i += 1
rel_list = [pardir] * (len(start_list)-i) + path_list[i:] rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
if not rel_list: if not rel_list:
return curdir return curdir
return join(*rel_list) return join(*rel_list)
except (TypeError, ValueError, AttributeError, BytesWarning):
genericpath._check_arg_types('relpath', path, start)
raise
# determine if two files are in fact the same file # determine if two files are in fact the same file

View File

@ -82,13 +82,9 @@ def join(a, *p):
path += b path += b
else: else:
path += sep + b path += sep + b
except (TypeError, AttributeError): except (TypeError, AttributeError, BytesWarning):
for s in (a,) + p: genericpath._check_arg_types('join', a, *p)
if not isinstance(s, (str, bytes)): raise
raise TypeError('join() argument must be str or bytes, not %r' %
s.__class__.__name__) from None
# Must have a mixture of text and binary data
raise TypeError("Can't mix strings and bytes in path components") from None
return path return path
@ -446,13 +442,16 @@ def relpath(path, start=None):
if start is None: if start is None:
start = curdir start = curdir
start_list = [x for x in abspath(start).split(sep) if x] try:
path_list = [x for x in abspath(path).split(sep) if x] start_list = [x for x in abspath(start).split(sep) if x]
path_list = [x for x in abspath(path).split(sep) if x]
# Work out how much of the filepath is shared by start and path.
i = len(commonprefix([start_list, path_list]))
# Work out how much of the filepath is shared by start and path. rel_list = [pardir] * (len(start_list)-i) + path_list[i:]
i = len(commonprefix([start_list, path_list])) if not rel_list:
return curdir
rel_list = [pardir] * (len(start_list)-i) + path_list[i:] return join(*rel_list)
if not rel_list: except (TypeError, AttributeError, BytesWarning):
return curdir genericpath._check_arg_types('relpath', path, start)
return join(*rel_list) raise

View File

@ -434,6 +434,39 @@ class CommonTest(GenericTest):
with support.temp_cwd(name): with support.temp_cwd(name):
self.test_abspath() self.test_abspath()
def test_join_errors(self):
# Check join() raises friendly TypeErrors.
with support.check_warnings(('', BytesWarning), quiet=True):
errmsg = "Can't mix strings and bytes in path components"
with self.assertRaisesRegex(TypeError, errmsg):
self.pathmodule.join(b'bytes', 'str')
with self.assertRaisesRegex(TypeError, errmsg):
self.pathmodule.join('str', b'bytes')
# regression, see #15377
errmsg = r'join\(\) argument must be str or bytes, not %r'
with self.assertRaisesRegex(TypeError, errmsg % 'int'):
self.pathmodule.join(42, 'str')
with self.assertRaisesRegex(TypeError, errmsg % 'int'):
self.pathmodule.join('str', 42)
with self.assertRaisesRegex(TypeError, errmsg % 'bytearray'):
self.pathmodule.join(bytearray(b'foo'), bytearray(b'bar'))
def test_relpath_errors(self):
# Check relpath() raises friendly TypeErrors.
with support.check_warnings(('', BytesWarning), quiet=True):
errmsg = "Can't mix strings and bytes in path components"
with self.assertRaisesRegex(TypeError, errmsg):
self.pathmodule.relpath(b'bytes', 'str')
with self.assertRaisesRegex(TypeError, errmsg):
self.pathmodule.relpath('str', b'bytes')
errmsg = r'relpath\(\) argument must be str or bytes, not %r'
with self.assertRaisesRegex(TypeError, errmsg % 'int'):
self.pathmodule.relpath(42, 'str')
with self.assertRaisesRegex(TypeError, errmsg % 'int'):
self.pathmodule.relpath('str', 42)
with self.assertRaisesRegex(TypeError, errmsg % 'bytearray'):
self.pathmodule.relpath(bytearray(b'foo'), bytearray(b'bar'))
if __name__=="__main__": if __name__=="__main__":
unittest.main() unittest.main()

View File

@ -142,6 +142,8 @@ class MacPathTestCase(unittest.TestCase):
class MacCommonTest(test_genericpath.CommonTest, unittest.TestCase): class MacCommonTest(test_genericpath.CommonTest, unittest.TestCase):
pathmodule = macpath pathmodule = macpath
test_relpath_errors = None
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -57,22 +57,6 @@ class PosixPathTest(unittest.TestCase):
self.assertEqual(posixpath.join(b"/foo/", b"bar/", b"baz/"), self.assertEqual(posixpath.join(b"/foo/", b"bar/", b"baz/"),
b"/foo/bar/baz/") b"/foo/bar/baz/")
def test_join_errors(self):
# Check posixpath.join raises friendly TypeErrors.
errmsg = "Can't mix strings and bytes in path components"
with self.assertRaisesRegex(TypeError, errmsg):
posixpath.join(b'bytes', 'str')
with self.assertRaisesRegex(TypeError, errmsg):
posixpath.join('str', b'bytes')
# regression, see #15377
errmsg = r'join\(\) argument must be str or bytes, not %r'
with self.assertRaisesRegex(TypeError, errmsg % 'NoneType'):
posixpath.join(None, 'str')
with self.assertRaisesRegex(TypeError, errmsg % 'NoneType'):
posixpath.join('str', None)
with self.assertRaisesRegex(TypeError, errmsg % 'bytearray'):
posixpath.join(bytearray(b'foo'), bytearray(b'bar'))
def test_split(self): def test_split(self):
self.assertEqual(posixpath.split("/foo/bar"), ("/foo", "bar")) self.assertEqual(posixpath.split("/foo/bar"), ("/foo", "bar"))
self.assertEqual(posixpath.split("/"), ("/", "")) self.assertEqual(posixpath.split("/"), ("/", ""))

View File

@ -162,6 +162,9 @@ Core and Builtins
Library Library
------- -------
- 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.
- Issue #22219: The zipfile module CLI now adds entries for directories - Issue #22219: The zipfile module CLI now adds entries for directories
(including empty directories) in ZIP file. (including empty directories) in ZIP file.