diff -r 470954641f3b Lib/genericpath.py --- a/Lib/genericpath.py Sun Jun 05 12:07:48 2016 +0000 +++ b/Lib/genericpath.py Sun Jun 05 10:41:18 2016 -0700 @@ -134,6 +134,7 @@ def _check_arg_types(funcname, *args): hasstr = hasbytes = False for s in args: + s = os.fspath(s) if isinstance(s, str): hasstr = True elif isinstance(s, bytes): diff -r 470954641f3b Lib/posixpath.py --- a/Lib/posixpath.py Sun Jun 05 12:07:48 2016 +0000 +++ b/Lib/posixpath.py Sun Jun 05 10:41:18 2016 -0700 @@ -49,7 +49,7 @@ def normcase(s): """Normalize case of pathname. Has no effect under Posix""" - if not isinstance(s, (bytes, str)): + if not isinstance(s, (bytes, str, os.PathLike)): raise TypeError("normcase() argument must be str or bytes, " "not '{}'".format(s.__class__.__name__)) return s @@ -60,6 +60,7 @@ def isabs(s): """Test whether a path is absolute""" + s = os.fspath(s) sep = _get_sep(s) return s.startswith(sep) @@ -73,12 +74,14 @@ If any component is an absolute path, all previous path components will be discarded. An empty last part will result in a path that ends with a separator.""" + a = os.fspath(a) sep = _get_sep(a) path = a try: if not p: path[:0] + sep #23780: Ensure compatible data type even if p is null. for b in p: + b = os.fspath(b) if b.startswith(sep): path = b elif not path or path.endswith(sep): @@ -99,6 +102,7 @@ def split(p): """Split a pathname. Returns tuple "(head, tail)" where "tail" is everything after the final slash. Either part may be empty.""" + p = os.fspath(p) sep = _get_sep(p) i = p.rfind(sep) + 1 head, tail = p[:i], p[i:] @@ -113,6 +117,7 @@ # It is always true that root + ext == p. def splitext(p): + p = os.fspath(p) if isinstance(p, bytes): sep = b'/' extsep = b'.' @@ -128,6 +133,7 @@ def splitdrive(p): """Split a pathname into drive and path. On Posix, drive is always empty.""" + p = os.fspath(p) return p[:0], p @@ -135,6 +141,7 @@ def basename(p): """Returns the final component of a pathname""" + p = os.fspath(p) sep = _get_sep(p) i = p.rfind(sep) + 1 return p[i:] @@ -144,6 +151,7 @@ def dirname(p): """Returns the directory component of a pathname""" + p = os.fspath(p) sep = _get_sep(p) i = p.rfind(sep) + 1 head = p[:i] @@ -179,6 +187,7 @@ def ismount(path): """Test whether a path is a mount point""" + path = os.fspath(path) try: s1 = os.lstat(path) except OSError: @@ -221,6 +230,7 @@ def expanduser(path): """Expand ~ and ~user constructions. If user or $HOME is unknown, do nothing.""" + path = os.fspath(path) if isinstance(path, bytes): tilde = b'~' else: @@ -267,6 +277,7 @@ """Expand shell variables of form $var and ${var}. Unknown variables are left unchanged.""" global _varprog, _varprogb + path = os.fspath(path) if isinstance(path, bytes): if b'$' not in path: return path @@ -317,6 +328,7 @@ def normpath(path): """Normalize path, eliminating double slashes, etc.""" + path = os.fspath(path) if isinstance(path, bytes): sep = b'/' empty = b'' @@ -354,6 +366,7 @@ def abspath(path): """Return an absolute path.""" + path = os.fspath(path) if not isabs(path): if isinstance(path, bytes): cwd = os.getcwdb() @@ -369,6 +382,7 @@ def realpath(filename): """Return the canonical path of the specified filename, eliminating any symbolic links encountered in the path.""" + filename = os.fspath(filename) path, ok = _joinrealpath(filename[:0], filename, {}) return abspath(path) @@ -429,6 +443,7 @@ def relpath(path, start=None): """Return a relative version of a path""" + path = os.fspath(path) if not path: raise ValueError("no path specified") @@ -471,6 +486,8 @@ if not paths: raise ValueError('commonpath() arg is an empty sequence') + paths = [os.fspath(path) for path in paths] + if isinstance(paths[0], bytes): sep = b'/' curdir = b'.' diff -r 470954641f3b Lib/test/test_genericpath.py --- a/Lib/test/test_genericpath.py Sun Jun 05 12:07:48 2016 +0000 +++ b/Lib/test/test_genericpath.py Sun Jun 05 10:41:18 2016 -0700 @@ -441,7 +441,7 @@ 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' + errmsg = 'expected str, bytes or os.PathLike object, not %s' with self.assertRaisesRegex(TypeError, errmsg % 'int'): self.pathmodule.join(42, 'str') with self.assertRaisesRegex(TypeError, errmsg % 'int'): @@ -462,7 +462,7 @@ 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' + errmsg = 'expected str, bytes or os.PathLike object, not %s' with self.assertRaisesRegex(TypeError, errmsg % 'int'): self.pathmodule.relpath(42, 'str') with self.assertRaisesRegex(TypeError, errmsg % 'int'): diff -r 470954641f3b Lib/test/test_posixpath.py --- a/Lib/test/test_posixpath.py Sun Jun 05 12:07:48 2016 +0000 +++ b/Lib/test/test_posixpath.py Sun Jun 05 10:41:18 2016 -0700 @@ -31,6 +31,14 @@ except OSError: pass +class PathLike: + """Object that returns an arbitrary object as its fspath.""" + def __init__(self, path): + self.path = path + + def __fspath__(self): + return self.path + class PosixPathTest(unittest.TestCase): def setUp(self): @@ -42,18 +50,34 @@ safe_rmdir(support.TESTFN + suffix) def test_join(self): - self.assertEqual(posixpath.join("/foo", "bar", "/bar", "baz"), - "/bar/baz") - self.assertEqual(posixpath.join("/foo", "bar", "baz"), "/foo/bar/baz") - self.assertEqual(posixpath.join("/foo/", "bar/", "baz/"), - "/foo/bar/baz/") + tests = [ + (("/foo", "bar", "/bar", "baz"), "/bar/baz"), + (("/foo", "bar", "baz"), "/foo/bar/baz"), + (("/foo/", "bar/", "baz/"), "/foo/bar/baz/"), + ] + for args, expected in tests: + with self.subTest(kind='str path', args=args): + self.assertEqual(posixpath.join(*args), expected) - self.assertEqual(posixpath.join(b"/foo", b"bar", b"/bar", b"baz"), - b"/bar/baz") - self.assertEqual(posixpath.join(b"/foo", b"bar", b"baz"), - b"/foo/bar/baz") - self.assertEqual(posixpath.join(b"/foo/", b"bar/", b"baz/"), - b"/foo/bar/baz/") + for args, expected in tests: + with self.subTest(kind='bytes path', args=args): + args = [bytes(arg, 'ascii') for arg in args] + expected = bytes(expected, 'ascii') + self.assertEqual(posixpath.join(*args), expected) + + for args, expected in tests: + with self.subTest(kind='str PathLike', args=args): + args = [PathLike(arg) for arg in args] + self.assertEqual(posixpath.join(*args), expected) + + for args, expected in tests: + with self.subTest(kind='bytes PathLike', args=args): + args = [PathLike(bytes(arg, 'ascii')) for arg in args] + expected = bytes(expected, 'ascii') + self.assertEqual(posixpath.join(*args), expected) + + self.assertRaises( + TypeError, posixpath.join, PathLike(None), PathLike('foo')) def test_split(self): self.assertEqual(posixpath.split("/foo/bar"), ("/foo", "bar")) @@ -68,6 +92,9 @@ self.assertEqual(posixpath.split(b"////foo"), (b"////", b"foo")) self.assertEqual(posixpath.split(b"//foo//bar"), (b"//foo", b"bar")) + self.assertEqual(posixpath.split(PathLike('/foo/bar')), + ('/foo', 'bar')) + def splitextTest(self, path, filename, ext): self.assertEqual(posixpath.splitext(path), (filename, ext)) self.assertEqual(posixpath.splitext("/" + path), ("/" + filename, ext)) @@ -110,6 +137,9 @@ self.splitextTest("........", "........", "") self.splitextTest("", "", "") + self.assertEqual(posixpath.splitext(PathLike('foo.bar')), + ('foo', '.bar')) + def test_isabs(self): self.assertIs(posixpath.isabs(""), False) self.assertIs(posixpath.isabs("/"), True) @@ -123,6 +153,8 @@ self.assertIs(posixpath.isabs(b"/foo/bar"), True) self.assertIs(posixpath.isabs(b"foo/bar"), False) + self.assertIs(posixpath.isabs(PathLike('/foo/bar')), True) + def test_basename(self): self.assertEqual(posixpath.basename("/foo/bar"), "bar") self.assertEqual(posixpath.basename("/"), "") @@ -136,6 +168,8 @@ self.assertEqual(posixpath.basename(b"////foo"), b"foo") self.assertEqual(posixpath.basename(b"//foo//bar"), b"bar") + self.assertEqual(posixpath.basename(PathLike("/foo/bar")), "bar") + def test_dirname(self): self.assertEqual(posixpath.dirname("/foo/bar"), "/foo") self.assertEqual(posixpath.dirname("/"), "/") @@ -149,9 +183,12 @@ self.assertEqual(posixpath.dirname(b"////foo"), b"////") self.assertEqual(posixpath.dirname(b"//foo//bar"), b"//foo") + self.assertEqual(posixpath.dirname(PathLike("/foo/bar")), "/foo") + def test_islink(self): self.assertIs(posixpath.islink(support.TESTFN + "1"), False) self.assertIs(posixpath.lexists(support.TESTFN + "2"), False) + f = open(support.TESTFN + "1", "wb") try: f.write(b"foo") @@ -170,6 +207,8 @@ def test_ismount(self): self.assertIs(posixpath.ismount("/"), True) + self.assertIs(posixpath.ismount(PathLike("/")), True) + with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) self.assertIs(posixpath.ismount(b"/"), True) @@ -190,6 +229,7 @@ try: os.symlink("/", ABSTFN) self.assertIs(posixpath.ismount(ABSTFN), False) + self.assertIs(posixpath.ismount(PathLike(ABSTFN)), False) finally: os.unlink(ABSTFN) @@ -208,6 +248,7 @@ try: os.lstat = fake_lstat self.assertIs(posixpath.ismount(ABSTFN), True) + self.assertIs(posixpath.ismount(PathLike(ABSTFN)), True) finally: os.lstat = save_lstat @@ -221,6 +262,8 @@ self.assertEqual(posixpath.expanduser("~"), "/") self.assertEqual(posixpath.expanduser("~/"), "/") self.assertEqual(posixpath.expanduser("~/foo"), "/foo") + self.assertEqual(posixpath.expanduser(PathLike("~/foo")), + "/foo") try: import pwd except ImportError: @@ -242,6 +285,10 @@ self.assertIsInstance(posixpath.expanduser("~foo/"), str) self.assertIsInstance(posixpath.expanduser(b"~root/"), bytes) self.assertIsInstance(posixpath.expanduser(b"~foo/"), bytes) + self.assertIsInstance(posixpath.expanduser(PathLike("~root/")), + str) + self.assertIsInstance(posixpath.expanduser(PathLike(b"~root/")), + bytes) with support.EnvironmentVarGuard() as env: # expanduser should fall back to using the password database @@ -271,6 +318,9 @@ self.assertEqual(posixpath.normpath(b"///..//./foo/.//bar"), b"/foo/bar") + self.assertEqual(posixpath.normpath(PathLike("///..//./foo/.//bar")), + "/foo/bar") + @skip_if_ABSTFN_contains_backslash def test_realpath_curdir(self): self.assertEqual(realpath('.'), os.getcwd()) @@ -281,6 +331,8 @@ self.assertEqual(realpath(b'./.'), os.getcwdb()) self.assertEqual(realpath(b'/'.join([b'.'] * 100)), os.getcwdb()) + self.assertEqual(realpath(PathLike('.')), os.getcwd()) + @skip_if_ABSTFN_contains_backslash def test_realpath_pardir(self): self.assertEqual(realpath('..'), dirname(os.getcwd())) @@ -291,6 +343,8 @@ self.assertEqual(realpath(b'../..'), dirname(dirname(os.getcwdb()))) self.assertEqual(realpath(b'/'.join([b'..'] * 100)), b'/') + self.assertEqual(realpath(PathLike('..')), dirname(os.getcwd())) + @unittest.skipUnless(hasattr(os, "symlink"), "Missing symlink implementation") @skip_if_ABSTFN_contains_backslash @@ -299,6 +353,7 @@ try: os.symlink(ABSTFN+"1", ABSTFN) self.assertEqual(realpath(ABSTFN), ABSTFN+"1") + self.assertEqual(realpath(PathLike(ABSTFN)), ABSTFN+"1") finally: support.unlink(ABSTFN) @@ -309,6 +364,7 @@ try: os.symlink(posixpath.relpath(ABSTFN+"1"), ABSTFN) self.assertEqual(realpath(ABSTFN), ABSTFN+"1") + self.assertEqual(realpath(PathLike(ABSTFN)), ABSTFN+"1") finally: support.unlink(ABSTFN) @@ -463,6 +519,8 @@ try: curdir = os.path.split(os.getcwd())[-1] self.assertRaises(ValueError, posixpath.relpath, "") + self.assertRaises(ValueError, posixpath.relpath, PathLike("")) + self.assertEqual(posixpath.relpath("a"), "a") self.assertEqual(posixpath.relpath(posixpath.abspath("a")), "a") self.assertEqual(posixpath.relpath("a/b"), "a/b") @@ -481,6 +539,7 @@ self.assertEqual(posixpath.relpath("/", "/"), '.') self.assertEqual(posixpath.relpath("/a", "/a"), '.') self.assertEqual(posixpath.relpath("/a/b", "/a/b"), '.') + self.assertEqual(posixpath.relpath(PathLike("/a/b"), "/a/b"), '.') finally: os.getcwd = real_getcwd @@ -489,6 +548,8 @@ try: curdir = os.path.split(os.getcwdb())[-1] self.assertRaises(ValueError, posixpath.relpath, b"") + self.assertRaises(ValueError, posixpath.relpath, PathLike(b"")) + self.assertEqual(posixpath.relpath(b"a"), b"a") self.assertEqual(posixpath.relpath(posixpath.abspath(b"a")), b"a") self.assertEqual(posixpath.relpath(b"a/b"), b"a/b") @@ -508,21 +569,35 @@ self.assertEqual(posixpath.relpath(b"/", b"/"), b'.') self.assertEqual(posixpath.relpath(b"/a", b"/a"), b'.') self.assertEqual(posixpath.relpath(b"/a/b", b"/a/b"), b'.') + self.assertEqual(posixpath.relpath(PathLike(b"/a/b"), + PathLike(b"/a/b")), b'.') self.assertRaises(TypeError, posixpath.relpath, b"bytes", "str") self.assertRaises(TypeError, posixpath.relpath, "str", b"bytes") + self.assertRaises(TypeError, posixpath.relpath, PathLike("str"), + PathLike(b"bytes")) finally: os.getcwdb = real_getcwdb def test_commonpath(self): def check(paths, expected): self.assertEqual(posixpath.commonpath(paths), expected) - self.assertEqual(posixpath.commonpath([os.fsencode(p) for p in paths]), + self.assertEqual(posixpath.commonpath([os.fsencode(p) + for p in paths]), + os.fsencode(expected)) + self.assertEqual(posixpath.commonpath([PathLike(p) for p in paths]), + expected) + self.assertEqual(posixpath.commonpath([PathLike(os.fsencode(p)) + for p in paths]), os.fsencode(expected)) def check_error(exc, paths): self.assertRaises(exc, posixpath.commonpath, paths) self.assertRaises(exc, posixpath.commonpath, [os.fsencode(p) for p in paths]) + self.assertRaises(exc, posixpath.commonpath, + [PathLike(p) for p in paths]) + self.assertRaises(exc, posixpath.commonpath, + [PathLike(os.fsencode(p)) for p in paths]) self.assertRaises(ValueError, posixpath.commonpath, []) check_error(ValueError, ['/usr', 'usr']) @@ -567,6 +642,8 @@ ['/usr/lib/', b'usr/lib/python3']) self.assertRaises(TypeError, posixpath.commonpath, ['usr/lib/', b'/usr/lib/python3']) + self.assertRaises(TypeError, posixpath.commonpath, + [PathLike('usr/lib/'), b'/usr/lib/python3']) class PosixCommonTest(test_genericpath.CommonTest, unittest.TestCase):