diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index b6507eb4d6fa2c..7ab603fd133b86 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1052,6 +1052,18 @@ call fails (for example because the path doesn't exist). Added return value, return the new Path instance. +.. method:: Path.absolute() + + Make the path absolute, without normalization or resolving symlinks. + Returns a new path object:: + + >>> p = Path('tests') + >>> p + PosixPath('tests') + >>> p.absolute() + PosixPath('/home/antoine/pathlib/tests') + + .. method:: Path.resolve(strict=False) Make the path absolute, resolving any symlinks. A new path object is @@ -1239,13 +1251,14 @@ Below is a table mapping various :mod:`os` functions to their corresponding Not all pairs of functions/methods below are equivalent. Some of them, despite having some overlapping use-cases, have different semantics. They - include :func:`os.path.abspath` and :meth:`Path.resolve`, + include :func:`os.path.abspath` and :meth:`Path.absolute`, :func:`os.path.relpath` and :meth:`PurePath.relative_to`. ==================================== ============================== :mod:`os` and :mod:`os.path` :mod:`pathlib` ==================================== ============================== -:func:`os.path.abspath` :meth:`Path.resolve` [#]_ +:func:`os.path.abspath` :meth:`Path.absolute` [#]_ +:func:`os.path.realpath` :meth:`Path.resolve` :func:`os.chmod` :meth:`Path.chmod` :func:`os.mkdir` :meth:`Path.mkdir` :func:`os.makedirs` :meth:`Path.mkdir` @@ -1278,5 +1291,5 @@ Below is a table mapping various :mod:`os` functions to their corresponding .. rubric:: Footnotes -.. [#] :func:`os.path.abspath` does not resolve symbolic links while :meth:`Path.resolve` does. +.. [#] :func:`os.path.abspath` normalizes the resulting path, which may change its meaning in the presence of symlinks, while :meth:`Path.absolute` does not. .. [#] :meth:`Path.relative_to` requires ``self`` to be the subpath of the argument, but :func:`os.path.relpath` does not. diff --git a/Lib/pathlib.py b/Lib/pathlib.py index d42ee4dc90b431..1603325180795f 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1043,24 +1043,19 @@ def rglob(self, pattern): yield p def absolute(self): - """Return an absolute version of this path. This function works - even if the path doesn't point to anything. + """Return an absolute version of this path by prepending the current + working directory. No normalization or symlink resolution is performed. - No normalization is done, i.e. all '.' and '..' will be kept along. Use resolve() to get the canonical path to a file. """ - # XXX untested yet! if self.is_absolute(): return self - # FIXME this must defer to the specific flavour (and, under Windows, - # use nt._getfullpathname()) return self._from_parts([self._accessor.getcwd()] + self._parts) def resolve(self, strict=False): """ Make the path absolute, resolving all symlinks on the way and also - normalizing it (for example turning slashes into backslashes under - Windows). + normalizing it. """ def check_eloop(e): diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 1bf21120a36ca1..3fbf1d1f7cd9d9 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1456,6 +1456,28 @@ def test_cwd(self): p = self.cls.cwd() self._test_cwd(p) + def test_absolute_common(self): + P = self.cls + + with mock.patch("pathlib._normal_accessor.getcwd") as getcwd: + getcwd.return_value = BASE + + # Simple relative paths. + self.assertEqual(str(P().absolute()), BASE) + self.assertEqual(str(P('.').absolute()), BASE) + self.assertEqual(str(P('a').absolute()), os.path.join(BASE, 'a')) + self.assertEqual(str(P('a', 'b', 'c').absolute()), os.path.join(BASE, 'a', 'b', 'c')) + + # Symlinks should not be resolved. + self.assertEqual(str(P('linkB', 'fileB').absolute()), os.path.join(BASE, 'linkB', 'fileB')) + self.assertEqual(str(P('brokenLink').absolute()), os.path.join(BASE, 'brokenLink')) + self.assertEqual(str(P('brokenLinkLoop').absolute()), os.path.join(BASE, 'brokenLinkLoop')) + + # '..' entries should be preserved and not normalised. + self.assertEqual(str(P('..').absolute()), os.path.join(BASE, '..')) + self.assertEqual(str(P('a', '..').absolute()), os.path.join(BASE, 'a', '..')) + self.assertEqual(str(P('..', 'b').absolute()), os.path.join(BASE, '..', 'b')) + def _test_home(self, p): q = self.cls(os.path.expanduser('~')) self.assertEqual(p, q) @@ -2463,6 +2485,17 @@ def test_glob_empty_pattern(self): class PosixPathTest(_BasePathTest, unittest.TestCase): cls = pathlib.PosixPath + def test_absolute(self): + P = self.cls + self.assertEqual(str(P('/').absolute()), '/') + self.assertEqual(str(P('/a').absolute()), '/a') + self.assertEqual(str(P('/a/b').absolute()), '/a/b') + + # '//'-prefixed absolute path (supported by POSIX). + self.assertEqual(str(P('//').absolute()), '//') + self.assertEqual(str(P('//a').absolute()), '//a') + self.assertEqual(str(P('//a/b').absolute()), '//a/b') + def _check_symlink_loop(self, *args, strict=True): path = self.cls(*args) with self.assertRaises(RuntimeError): @@ -2628,6 +2661,31 @@ def test_handling_bad_descriptor(self): class WindowsPathTest(_BasePathTest, unittest.TestCase): cls = pathlib.WindowsPath + def test_absolute(self): + P = self.cls + + # Simple absolute paths. + self.assertEqual(str(P('c:\\').absolute()), 'c:\\') + self.assertEqual(str(P('c:\\a').absolute()), 'c:\\a') + self.assertEqual(str(P('c:\\a\\b').absolute()), 'c:\\a\\b') + + # UNC absolute paths. + share = '\\\\server\\share\\' + self.assertEqual(str(P(share).absolute()), share) + self.assertEqual(str(P(share + 'a').absolute()), share + 'a') + self.assertEqual(str(P(share + 'a\\b').absolute()), share + 'a\\b') + + # UNC relative paths. + with mock.patch("pathlib._normal_accessor.getcwd") as getcwd: + getcwd.return_value = share + + self.assertEqual(str(P().absolute()), share) + self.assertEqual(str(P('.').absolute()), share) + self.assertEqual(str(P('a').absolute()), os.path.join(share, 'a')) + self.assertEqual(str(P('a', 'b', 'c').absolute()), + os.path.join(share, 'a', 'b', 'c')) + + def test_glob(self): P = self.cls p = P(BASE) diff --git a/Misc/NEWS.d/next/Library/2022-01-05-03-21-21.bpo-29688.W06bSH.rst b/Misc/NEWS.d/next/Library/2022-01-05-03-21-21.bpo-29688.W06bSH.rst new file mode 100644 index 00000000000000..1a202e59b075e7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-01-05-03-21-21.bpo-29688.W06bSH.rst @@ -0,0 +1 @@ +Document :meth:`pathlib.Path.absolute` (which has always existed).