]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-44626, GH-105476: Fix `ntpath.isabs()` handling of part-absolute paths (#113829)
authorBarney Gale <barney.gale@gmail.com>
Sat, 13 Jan 2024 07:36:05 +0000 (07:36 +0000)
committerGitHub <noreply@github.com>
Sat, 13 Jan 2024 07:36:05 +0000 (07:36 +0000)
On Windows, `os.path.isabs()` now returns `False` when given a path that
starts with exactly one (back)slash. This is more compatible with other
functions in `os.path`, and with Microsoft's own documentation.

Also adjust `pathlib.PureWindowsPath.is_absolute()` to call
`ntpath.isabs()`, which corrects its handling of partial UNC/device paths
like `//foo`.

Co-authored-by: Jon Foster <jon@jon-foster.co.uk>
Doc/library/os.path.rst
Doc/whatsnew/3.13.rst
Lib/ntpath.py
Lib/pathlib/_abc.py
Lib/test/test_ntpath.py
Lib/test/test_pathlib/test_pathlib.py
Lib/test/test_unittest/test_program.py
Lib/test/test_zoneinfo/test_zoneinfo.py
Misc/NEWS.d/next/Tests/2024-01-08-21-15-48.gh-issue-44626.DRq-PR.rst [new file with mode: 0644]

index 95933f56d50542e64b65657d3abf88504ccef714..3cab7a260df008401f6c231cfc5706510bc0b883 100644 (file)
@@ -239,12 +239,16 @@ the :mod:`glob` module.)
 .. function:: isabs(path)
 
    Return ``True`` if *path* is an absolute pathname.  On Unix, that means it
-   begins with a slash, on Windows that it begins with a (back)slash after chopping
-   off a potential drive letter.
+   begins with a slash, on Windows that it begins with two (back)slashes, or a
+   drive letter, colon, and (back)slash together.
 
    .. versionchanged:: 3.6
       Accepts a :term:`path-like object`.
 
+   .. versionchanged:: 3.13
+      On Windows, returns ``False`` if the given path starts with exactly one
+      (back)slash.
+
 
 .. function:: isfile(path)
 
index 59b9281e6d2b898fe4cd4be03c2883d24ad6bab1..05b9b87a63252f3326d54fdeec5e018f3c8bdd0c 100644 (file)
@@ -307,6 +307,13 @@ os
   :c:func:`!posix_spawn_file_actions_addclosefrom_np`.
   (Contributed by Jakub Kulik in :gh:`113117`.)
 
+os.path
+-------
+
+* On Windows, :func:`os.path.isabs` no longer considers paths starting with
+  exactly one (back)slash to be absolute.
+  (Contributed by Barney Gale and Jon Foster in :gh:`44626`.)
+
 pathlib
 -------
 
index 3061a4a5ef4c56a56148db9ace42b517215a1f76..aa0e018eb668c2e870b68194284de3f83cd5d0b9 100644 (file)
@@ -77,12 +77,6 @@ except ImportError:
         return s.replace('/', '\\').lower()
 
 
-# Return whether a path is absolute.
-# Trivial in Posix, harder on Windows.
-# For Windows it is absolute if it starts with a slash or backslash (current
-# volume), or if a pathname after the volume-letter-and-colon or UNC-resource
-# starts with a slash or backslash.
-
 def isabs(s):
     """Test whether a path is absolute"""
     s = os.fspath(s)
@@ -90,16 +84,15 @@ def isabs(s):
         sep = b'\\'
         altsep = b'/'
         colon_sep = b':\\'
+        double_sep = b'\\\\'
     else:
         sep = '\\'
         altsep = '/'
         colon_sep = ':\\'
+        double_sep = '\\\\'
     s = s[:3].replace(altsep, sep)
     # Absolute: UNC, device, and paths with a drive and root.
-    # LEGACY BUG: isabs("/x") should be false since the path has no drive.
-    if s.startswith(sep) or s.startswith(colon_sep, 1):
-        return True
-    return False
+    return s.startswith(colon_sep, 1) or s.startswith(double_sep)
 
 
 # Join two (or more) paths.
index 2fc087d13aee85f06419ccf151e5e3464cab09ff..d2a31ed643979afc2c1bc7997dcd4cefd35ff751 100644 (file)
@@ -1,5 +1,4 @@
 import functools
-import ntpath
 import posixpath
 from errno import ENOENT, ENOTDIR, EBADF, ELOOP, EINVAL
 from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
@@ -373,10 +372,7 @@ class PurePathBase:
     def is_absolute(self):
         """True if the path is absolute (has both a root and, if applicable,
         a drive)."""
-        if self.pathmod is ntpath:
-            # ntpath.isabs() is defective - see GH-44626.
-            return bool(self.drive and self.root)
-        elif self.pathmod is posixpath:
+        if self.pathmod is posixpath:
             # Optimization: work with raw paths on POSIX.
             for path in self._raw_paths:
                 if path.startswith('/'):
index bf990ed36fbcae0791a665d34e8e7f4fefe1a5ca..aefcb98f1c30eb04f66a4f0a494d5b49491f9c41 100644 (file)
@@ -227,10 +227,18 @@ class TestNtpath(NtpathTestCase):
         tester('ntpath.split("//conky/mountpoint/")', ('//conky/mountpoint/', ''))
 
     def test_isabs(self):
+        tester('ntpath.isabs("foo\\bar")', 0)
+        tester('ntpath.isabs("foo/bar")', 0)
         tester('ntpath.isabs("c:\\")', 1)
+        tester('ntpath.isabs("c:\\foo\\bar")', 1)
+        tester('ntpath.isabs("c:/foo/bar")', 1)
         tester('ntpath.isabs("\\\\conky\\mountpoint\\")', 1)
-        tester('ntpath.isabs("\\foo")', 1)
-        tester('ntpath.isabs("\\foo\\bar")', 1)
+
+        # gh-44626: paths with only a drive or root are not absolute.
+        tester('ntpath.isabs("\\foo\\bar")', 0)
+        tester('ntpath.isabs("/foo/bar")', 0)
+        tester('ntpath.isabs("c:foo\\bar")', 0)
+        tester('ntpath.isabs("c:foo/bar")', 0)
 
         # gh-96290: normal UNC paths and device paths without trailing backslashes
         tester('ntpath.isabs("\\\\conky\\mountpoint")', 1)
index 04e6280509ecfc94e988765b93e377adbb4a53a3..1b560adfc3b57af33f10504d75c534eb87cc6761 100644 (file)
@@ -1011,10 +1011,14 @@ class PureWindowsPathTest(PurePathTest):
         self.assertTrue(P('c:/a').is_absolute())
         self.assertTrue(P('c:/a/b/').is_absolute())
         # UNC paths are absolute by definition.
+        self.assertTrue(P('//').is_absolute())
+        self.assertTrue(P('//a').is_absolute())
         self.assertTrue(P('//a/b').is_absolute())
         self.assertTrue(P('//a/b/').is_absolute())
         self.assertTrue(P('//a/b/c').is_absolute())
         self.assertTrue(P('//a/b/c/d').is_absolute())
+        self.assertTrue(P('//?/UNC/').is_absolute())
+        self.assertTrue(P('//?/UNC/spam').is_absolute())
 
     def test_join(self):
         P = self.cls
index d8f5d3692a5088905a3d1a37a2c808239a33827b..7241cf59f73d4f425e89ba9e68950e5f0e973ad1 100644 (file)
@@ -459,8 +459,8 @@ class TestCommandLineArgs(unittest.TestCase):
 
     def testParseArgsAbsolutePathsThatCannotBeConverted(self):
         program = self.program
-        # even on Windows '/...' is considered absolute by os.path.abspath
-        argv = ['progname', '/foo/bar/baz.py', '/green/red.py']
+        drive = os.path.splitdrive(os.getcwd())[0]
+        argv = ['progname', f'{drive}/foo/bar/baz.py', f'{drive}/green/red.py']
         self._patch_isfile(argv)
 
         program.createTests = lambda: None
index 7b6b69d0109d885fd19f7cf7c3add2283df9958e..18eab5b33540c9e1ecc7f50ba37def2cb0756f23 100644 (file)
@@ -36,6 +36,7 @@ ZONEINFO_DATA_V1 = None
 TEMP_DIR = None
 DATA_DIR = pathlib.Path(__file__).parent / "data"
 ZONEINFO_JSON = DATA_DIR / "zoneinfo_data.json"
+DRIVE = os.path.splitdrive('x:')[0]
 
 # Useful constants
 ZERO = timedelta(0)
@@ -1679,8 +1680,8 @@ class TzPathTest(TzPathUserMixin, ZoneInfoTestBase):
         """Tests that the environment variable works with reset_tzpath."""
         new_paths = [
             ("", []),
-            ("/etc/zoneinfo", ["/etc/zoneinfo"]),
-            (f"/a/b/c{os.pathsep}/d/e/f", ["/a/b/c", "/d/e/f"]),
+            (f"{DRIVE}/etc/zoneinfo", [f"{DRIVE}/etc/zoneinfo"]),
+            (f"{DRIVE}/a/b/c{os.pathsep}{DRIVE}/d/e/f", [f"{DRIVE}/a/b/c", f"{DRIVE}/d/e/f"]),
         ]
 
         for new_path_var, expected_result in new_paths:
@@ -1694,22 +1695,22 @@ class TzPathTest(TzPathUserMixin, ZoneInfoTestBase):
         test_cases = [
             [("path/to/somewhere",), ()],
             [
-                ("/usr/share/zoneinfo", "path/to/somewhere",),
-                ("/usr/share/zoneinfo",),
+                (f"{DRIVE}/usr/share/zoneinfo", "path/to/somewhere",),
+                (f"{DRIVE}/usr/share/zoneinfo",),
             ],
             [("../relative/path",), ()],
             [
-                ("/usr/share/zoneinfo", "../relative/path",),
-                ("/usr/share/zoneinfo",),
+                (f"{DRIVE}/usr/share/zoneinfo", "../relative/path",),
+                (f"{DRIVE}/usr/share/zoneinfo",),
             ],
             [("path/to/somewhere", "../relative/path",), ()],
             [
                 (
-                    "/usr/share/zoneinfo",
+                    f"{DRIVE}/usr/share/zoneinfo",
                     "path/to/somewhere",
                     "../relative/path",
                 ),
-                ("/usr/share/zoneinfo",),
+                (f"{DRIVE}/usr/share/zoneinfo",),
             ],
         ]
 
@@ -1727,9 +1728,9 @@ class TzPathTest(TzPathUserMixin, ZoneInfoTestBase):
                     self.assertSequenceEqual(tzpath, expected_paths)
 
     def test_reset_tzpath_kwarg(self):
-        self.module.reset_tzpath(to=["/a/b/c"])
+        self.module.reset_tzpath(to=[f"{DRIVE}/a/b/c"])
 
-        self.assertSequenceEqual(self.module.TZPATH, ("/a/b/c",))
+        self.assertSequenceEqual(self.module.TZPATH, (f"{DRIVE}/a/b/c",))
 
     def test_reset_tzpath_relative_paths(self):
         bad_values = [
@@ -1758,8 +1759,8 @@ class TzPathTest(TzPathUserMixin, ZoneInfoTestBase):
                     self.module.reset_tzpath(bad_value)
 
     def test_tzpath_attribute(self):
-        tzpath_0 = ["/one", "/two"]
-        tzpath_1 = ["/three"]
+        tzpath_0 = [f"{DRIVE}/one", f"{DRIVE}/two"]
+        tzpath_1 = [f"{DRIVE}/three"]
 
         with self.tzpath_context(tzpath_0):
             query_0 = self.module.TZPATH
diff --git a/Misc/NEWS.d/next/Tests/2024-01-08-21-15-48.gh-issue-44626.DRq-PR.rst b/Misc/NEWS.d/next/Tests/2024-01-08-21-15-48.gh-issue-44626.DRq-PR.rst
new file mode 100644 (file)
index 0000000..3fa304b
--- /dev/null
@@ -0,0 +1,5 @@
+Fix :func:`os.path.isabs` incorrectly returning ``True`` when given a path
+that starts with exactly one (back)slash on Windows.
+
+Fix :meth:`pathlib.PureWindowsPath.is_absolute` incorrectly returning
+``False`` for some paths beginning with two (back)slashes.