mirror of
https://github.com/python/cpython.git
synced 2025-10-23 15:12:02 +00:00
GH-44626, GH-105476: Fix ntpath.isabs()
handling of part-absolute paths (#113829)
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>
This commit is contained in:
parent
dac1da2121
commit
e4ff131e01
9 changed files with 51 additions and 33 deletions
|
@ -239,12 +239,16 @@ the :mod:`glob` module.)
|
||||||
.. function:: isabs(path)
|
.. function:: isabs(path)
|
||||||
|
|
||||||
Return ``True`` if *path* is an absolute pathname. On Unix, that means it
|
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
|
begins with a slash, on Windows that it begins with two (back)slashes, or a
|
||||||
off a potential drive letter.
|
drive letter, colon, and (back)slash together.
|
||||||
|
|
||||||
.. versionchanged:: 3.6
|
.. versionchanged:: 3.6
|
||||||
Accepts a :term:`path-like object`.
|
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)
|
.. function:: isfile(path)
|
||||||
|
|
||||||
|
|
|
@ -307,6 +307,13 @@ os
|
||||||
:c:func:`!posix_spawn_file_actions_addclosefrom_np`.
|
:c:func:`!posix_spawn_file_actions_addclosefrom_np`.
|
||||||
(Contributed by Jakub Kulik in :gh:`113117`.)
|
(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
|
pathlib
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|
|
@ -77,12 +77,6 @@ except ImportError:
|
||||||
return s.replace('/', '\\').lower()
|
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):
|
def isabs(s):
|
||||||
"""Test whether a path is absolute"""
|
"""Test whether a path is absolute"""
|
||||||
s = os.fspath(s)
|
s = os.fspath(s)
|
||||||
|
@ -90,16 +84,15 @@ def isabs(s):
|
||||||
sep = b'\\'
|
sep = b'\\'
|
||||||
altsep = b'/'
|
altsep = b'/'
|
||||||
colon_sep = b':\\'
|
colon_sep = b':\\'
|
||||||
|
double_sep = b'\\\\'
|
||||||
else:
|
else:
|
||||||
sep = '\\'
|
sep = '\\'
|
||||||
altsep = '/'
|
altsep = '/'
|
||||||
colon_sep = ':\\'
|
colon_sep = ':\\'
|
||||||
|
double_sep = '\\\\'
|
||||||
s = s[:3].replace(altsep, sep)
|
s = s[:3].replace(altsep, sep)
|
||||||
# Absolute: UNC, device, and paths with a drive and root.
|
# Absolute: UNC, device, and paths with a drive and root.
|
||||||
# LEGACY BUG: isabs("/x") should be false since the path has no drive.
|
return s.startswith(colon_sep, 1) or s.startswith(double_sep)
|
||||||
if s.startswith(sep) or s.startswith(colon_sep, 1):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
# Join two (or more) paths.
|
# Join two (or more) paths.
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import functools
|
import functools
|
||||||
import ntpath
|
|
||||||
import posixpath
|
import posixpath
|
||||||
from errno import ENOENT, ENOTDIR, EBADF, ELOOP, EINVAL
|
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
|
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):
|
def is_absolute(self):
|
||||||
"""True if the path is absolute (has both a root and, if applicable,
|
"""True if the path is absolute (has both a root and, if applicable,
|
||||||
a drive)."""
|
a drive)."""
|
||||||
if self.pathmod is ntpath:
|
if self.pathmod is posixpath:
|
||||||
# ntpath.isabs() is defective - see GH-44626.
|
|
||||||
return bool(self.drive and self.root)
|
|
||||||
elif self.pathmod is posixpath:
|
|
||||||
# Optimization: work with raw paths on POSIX.
|
# Optimization: work with raw paths on POSIX.
|
||||||
for path in self._raw_paths:
|
for path in self._raw_paths:
|
||||||
if path.startswith('/'):
|
if path.startswith('/'):
|
||||||
|
|
|
@ -227,10 +227,18 @@ class TestNtpath(NtpathTestCase):
|
||||||
tester('ntpath.split("//conky/mountpoint/")', ('//conky/mountpoint/', ''))
|
tester('ntpath.split("//conky/mountpoint/")', ('//conky/mountpoint/', ''))
|
||||||
|
|
||||||
def test_isabs(self):
|
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:\\")', 1)
|
||||||
|
tester('ntpath.isabs("c:\\foo\\bar")', 1)
|
||||||
|
tester('ntpath.isabs("c:/foo/bar")', 1)
|
||||||
tester('ntpath.isabs("\\\\conky\\mountpoint\\")', 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
|
# gh-96290: normal UNC paths and device paths without trailing backslashes
|
||||||
tester('ntpath.isabs("\\\\conky\\mountpoint")', 1)
|
tester('ntpath.isabs("\\\\conky\\mountpoint")', 1)
|
||||||
|
|
|
@ -1011,10 +1011,14 @@ class PureWindowsPathTest(PurePathTest):
|
||||||
self.assertTrue(P('c:/a').is_absolute())
|
self.assertTrue(P('c:/a').is_absolute())
|
||||||
self.assertTrue(P('c:/a/b/').is_absolute())
|
self.assertTrue(P('c:/a/b/').is_absolute())
|
||||||
# UNC paths are absolute by definition.
|
# 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/').is_absolute())
|
self.assertTrue(P('//a/b/').is_absolute())
|
||||||
self.assertTrue(P('//a/b/c').is_absolute())
|
self.assertTrue(P('//a/b/c').is_absolute())
|
||||||
self.assertTrue(P('//a/b/c/d').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):
|
def test_join(self):
|
||||||
P = self.cls
|
P = self.cls
|
||||||
|
|
|
@ -459,8 +459,8 @@ class TestCommandLineArgs(unittest.TestCase):
|
||||||
|
|
||||||
def testParseArgsAbsolutePathsThatCannotBeConverted(self):
|
def testParseArgsAbsolutePathsThatCannotBeConverted(self):
|
||||||
program = self.program
|
program = self.program
|
||||||
# even on Windows '/...' is considered absolute by os.path.abspath
|
drive = os.path.splitdrive(os.getcwd())[0]
|
||||||
argv = ['progname', '/foo/bar/baz.py', '/green/red.py']
|
argv = ['progname', f'{drive}/foo/bar/baz.py', f'{drive}/green/red.py']
|
||||||
self._patch_isfile(argv)
|
self._patch_isfile(argv)
|
||||||
|
|
||||||
program.createTests = lambda: None
|
program.createTests = lambda: None
|
||||||
|
|
|
@ -36,6 +36,7 @@ ZONEINFO_DATA_V1 = None
|
||||||
TEMP_DIR = None
|
TEMP_DIR = None
|
||||||
DATA_DIR = pathlib.Path(__file__).parent / "data"
|
DATA_DIR = pathlib.Path(__file__).parent / "data"
|
||||||
ZONEINFO_JSON = DATA_DIR / "zoneinfo_data.json"
|
ZONEINFO_JSON = DATA_DIR / "zoneinfo_data.json"
|
||||||
|
DRIVE = os.path.splitdrive('x:')[0]
|
||||||
|
|
||||||
# Useful constants
|
# Useful constants
|
||||||
ZERO = timedelta(0)
|
ZERO = timedelta(0)
|
||||||
|
@ -1679,8 +1680,8 @@ class TzPathTest(TzPathUserMixin, ZoneInfoTestBase):
|
||||||
"""Tests that the environment variable works with reset_tzpath."""
|
"""Tests that the environment variable works with reset_tzpath."""
|
||||||
new_paths = [
|
new_paths = [
|
||||||
("", []),
|
("", []),
|
||||||
("/etc/zoneinfo", ["/etc/zoneinfo"]),
|
(f"{DRIVE}/etc/zoneinfo", [f"{DRIVE}/etc/zoneinfo"]),
|
||||||
(f"/a/b/c{os.pathsep}/d/e/f", ["/a/b/c", "/d/e/f"]),
|
(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:
|
for new_path_var, expected_result in new_paths:
|
||||||
|
@ -1694,22 +1695,22 @@ class TzPathTest(TzPathUserMixin, ZoneInfoTestBase):
|
||||||
test_cases = [
|
test_cases = [
|
||||||
[("path/to/somewhere",), ()],
|
[("path/to/somewhere",), ()],
|
||||||
[
|
[
|
||||||
("/usr/share/zoneinfo", "path/to/somewhere",),
|
(f"{DRIVE}/usr/share/zoneinfo", "path/to/somewhere",),
|
||||||
("/usr/share/zoneinfo",),
|
(f"{DRIVE}/usr/share/zoneinfo",),
|
||||||
],
|
],
|
||||||
[("../relative/path",), ()],
|
[("../relative/path",), ()],
|
||||||
[
|
[
|
||||||
("/usr/share/zoneinfo", "../relative/path",),
|
(f"{DRIVE}/usr/share/zoneinfo", "../relative/path",),
|
||||||
("/usr/share/zoneinfo",),
|
(f"{DRIVE}/usr/share/zoneinfo",),
|
||||||
],
|
],
|
||||||
[("path/to/somewhere", "../relative/path",), ()],
|
[("path/to/somewhere", "../relative/path",), ()],
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
"/usr/share/zoneinfo",
|
f"{DRIVE}/usr/share/zoneinfo",
|
||||||
"path/to/somewhere",
|
"path/to/somewhere",
|
||||||
"../relative/path",
|
"../relative/path",
|
||||||
),
|
),
|
||||||
("/usr/share/zoneinfo",),
|
(f"{DRIVE}/usr/share/zoneinfo",),
|
||||||
],
|
],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1727,9 +1728,9 @@ class TzPathTest(TzPathUserMixin, ZoneInfoTestBase):
|
||||||
self.assertSequenceEqual(tzpath, expected_paths)
|
self.assertSequenceEqual(tzpath, expected_paths)
|
||||||
|
|
||||||
def test_reset_tzpath_kwarg(self):
|
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):
|
def test_reset_tzpath_relative_paths(self):
|
||||||
bad_values = [
|
bad_values = [
|
||||||
|
@ -1758,8 +1759,8 @@ class TzPathTest(TzPathUserMixin, ZoneInfoTestBase):
|
||||||
self.module.reset_tzpath(bad_value)
|
self.module.reset_tzpath(bad_value)
|
||||||
|
|
||||||
def test_tzpath_attribute(self):
|
def test_tzpath_attribute(self):
|
||||||
tzpath_0 = ["/one", "/two"]
|
tzpath_0 = [f"{DRIVE}/one", f"{DRIVE}/two"]
|
||||||
tzpath_1 = ["/three"]
|
tzpath_1 = [f"{DRIVE}/three"]
|
||||||
|
|
||||||
with self.tzpath_context(tzpath_0):
|
with self.tzpath_context(tzpath_0):
|
||||||
query_0 = self.module.TZPATH
|
query_0 = self.module.TZPATH
|
||||||
|
|
|
@ -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.
|
Loading…
Add table
Add a link
Reference in a new issue