summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBarney Gale <barney.gale@gmail.com>2024-01-26 18:14:24 (GMT)
committerGitHub <noreply@github.com>2024-01-26 18:14:24 (GMT)
commit7e31d6dea276ac91402aefb023c58d239dfd9246 (patch)
tree3a32adf17e7fa06baa399363f57a079d4f631f24
parent6c2b419fb91c8d7daa769d39f73768114b5eb45a (diff)
downloadcpython-7e31d6dea276ac91402aefb023c58d239dfd9246.zip
cpython-7e31d6dea276ac91402aefb023c58d239dfd9246.tar.gz
cpython-7e31d6dea276ac91402aefb023c58d239dfd9246.tar.bz2
gh-88569: add `ntpath.isreserved()` (#95486)
Add `ntpath.isreserved()`, which identifies reserved pathnames such as "NUL", "AUX" and "CON". Deprecate `pathlib.PurePath.is_reserved()`. --------- Co-authored-by: Eryk Sun <eryksun@gmail.com> Co-authored-by: Brett Cannon <brett@python.org> Co-authored-by: Steve Dower <steve.dower@microsoft.com>
-rw-r--r--Doc/library/os.path.rst22
-rw-r--r--Doc/library/pathlib.rst13
-rw-r--r--Doc/whatsnew/3.13.rst15
-rw-r--r--Lib/ntpath.py40
-rw-r--r--Lib/pathlib/__init__.py28
-rw-r--r--Lib/test/test_ntpath.py56
-rw-r--r--Lib/test/test_pathlib/test_pathlib.py48
-rw-r--r--Misc/NEWS.d/next/Library/2022-07-31-01-24-40.gh-issue-88569.eU0--b.rst4
8 files changed, 154 insertions, 72 deletions
diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst
index 3cab7a2..34bc76b 100644
--- a/Doc/library/os.path.rst
+++ b/Doc/library/os.path.rst
@@ -326,6 +326,28 @@ the :mod:`glob` module.)
.. versionadded:: 3.12
+.. function:: isreserved(path)
+
+ Return ``True`` if *path* is a reserved pathname on the current system.
+
+ On Windows, reserved filenames include those that end with a space or dot;
+ those that contain colons (i.e. file streams such as "name:stream"),
+ wildcard characters (i.e. ``'*?"<>'``), pipe, or ASCII control characters;
+ as well as DOS device names such as "NUL", "CON", "CONIN$", "CONOUT$",
+ "AUX", "PRN", "COM1", and "LPT1".
+
+ .. note::
+
+ This function approximates rules for reserved paths on most Windows
+ systems. These rules change over time in various Windows releases.
+ This function may be updated in future Python releases as changes to
+ the rules become broadly available.
+
+ .. availability:: Windows.
+
+ .. versionadded:: 3.13
+
+
.. function:: join(path, *paths)
Join one or more path segments intelligently. The return value is the
diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst
index 2f4ff4e..f1aba79 100644
--- a/Doc/library/pathlib.rst
+++ b/Doc/library/pathlib.rst
@@ -535,14 +535,13 @@ Pure paths provide the following methods and properties:
reserved under Windows, ``False`` otherwise. With :class:`PurePosixPath`,
``False`` is always returned.
- >>> PureWindowsPath('nul').is_reserved()
- True
- >>> PurePosixPath('nul').is_reserved()
- False
-
- File system calls on reserved paths can fail mysteriously or have
- unintended effects.
+ .. versionchanged:: 3.13
+ Windows path names that contain a colon, or end with a dot or a space,
+ are considered reserved. UNC paths may be reserved.
+ .. deprecated-removed:: 3.13 3.15
+ This method is deprecated; use :func:`os.path.isreserved` to detect
+ reserved paths on Windows.
.. method:: PurePath.joinpath(*pathsegments)
diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst
index 8c2bb05..985e34b 100644
--- a/Doc/whatsnew/3.13.rst
+++ b/Doc/whatsnew/3.13.rst
@@ -321,6 +321,9 @@ os
os.path
-------
+* Add :func:`os.path.isreserved` to check if a path is reserved on the current
+ system. This function is only available on Windows.
+ (Contributed by Barney Gale in :gh:`88569`.)
* 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`.)
@@ -498,6 +501,12 @@ Deprecated
security and functionality bugs. This includes removal of the ``--cgi``
flag to the ``python -m http.server`` command line in 3.15.
+* :mod:`pathlib`:
+
+ * :meth:`pathlib.PurePath.is_reserved` is deprecated and scheduled for
+ removal in Python 3.15. Use :func:`os.path.isreserved` to detect reserved
+ paths on Windows.
+
* :mod:`sys`: :func:`sys._enablelegacywindowsfsencoding` function.
Replace it with :envvar:`PYTHONLEGACYWINDOWSFSENCODING` environment variable.
(Contributed by Inada Naoki in :gh:`73427`.)
@@ -709,6 +718,12 @@ Pending Removal in Python 3.15
:func:`locale.getlocale()` instead.
(Contributed by Hugo van Kemenade in :gh:`111187`.)
+* :mod:`pathlib`:
+
+ * :meth:`pathlib.PurePath.is_reserved` is deprecated and scheduled for
+ removal in Python 3.15. Use :func:`os.path.isreserved` to detect reserved
+ paths on Windows.
+
* :class:`typing.NamedTuple`:
* The undocumented keyword argument syntax for creating NamedTuple classes
diff --git a/Lib/ntpath.py b/Lib/ntpath.py
index aa0e018..e7cbfe1 100644
--- a/Lib/ntpath.py
+++ b/Lib/ntpath.py
@@ -26,8 +26,8 @@ from genericpath import *
__all__ = ["normcase","isabs","join","splitdrive","splitroot","split","splitext",
"basename","dirname","commonprefix","getsize","getmtime",
"getatime","getctime", "islink","exists","lexists","isdir","isfile",
- "ismount", "expanduser","expandvars","normpath","abspath",
- "curdir","pardir","sep","pathsep","defpath","altsep",
+ "ismount","isreserved","expanduser","expandvars","normpath",
+ "abspath","curdir","pardir","sep","pathsep","defpath","altsep",
"extsep","devnull","realpath","supports_unicode_filenames","relpath",
"samefile", "sameopenfile", "samestat", "commonpath", "isjunction"]
@@ -330,6 +330,42 @@ def ismount(path):
return False
+_reserved_chars = frozenset(
+ {chr(i) for i in range(32)} |
+ {'"', '*', ':', '<', '>', '?', '|', '/', '\\'}
+)
+
+_reserved_names = frozenset(
+ {'CON', 'PRN', 'AUX', 'NUL', 'CONIN$', 'CONOUT$'} |
+ {f'COM{c}' for c in '123456789\xb9\xb2\xb3'} |
+ {f'LPT{c}' for c in '123456789\xb9\xb2\xb3'}
+)
+
+def isreserved(path):
+ """Return true if the pathname is reserved by the system."""
+ # Refer to "Naming Files, Paths, and Namespaces":
+ # https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
+ path = os.fsdecode(splitroot(path)[2]).replace(altsep, sep)
+ return any(_isreservedname(name) for name in reversed(path.split(sep)))
+
+def _isreservedname(name):
+ """Return true if the filename is reserved by the system."""
+ # Trailing dots and spaces are reserved.
+ if name.endswith(('.', ' ')) and name not in ('.', '..'):
+ return True
+ # Wildcards, separators, colon, and pipe (*?"<>/\:|) are reserved.
+ # ASCII control characters (0-31) are reserved.
+ # Colon is reserved for file streams (e.g. "name:stream[:type]").
+ if _reserved_chars.intersection(name):
+ return True
+ # DOS device names are reserved (e.g. "nul" or "nul .txt"). The rules
+ # are complex and vary across Windows versions. On the side of
+ # caution, return True for names that may not be reserved.
+ if name.partition('.')[0].rstrip(' ').upper() in _reserved_names:
+ return True
+ return False
+
+
# Expand paths beginning with '~' or '~user'.
# '~' means $HOME; '~user' means that user's home directory.
# If the path doesn't begin with '~', or if the user or $HOME is unknown,
diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py
index eee82ef..cc159ed 100644
--- a/Lib/pathlib/__init__.py
+++ b/Lib/pathlib/__init__.py
@@ -33,15 +33,6 @@ __all__ = [
]
-# Reference for Windows paths can be found at
-# https://learn.microsoft.com/en-gb/windows/win32/fileio/naming-a-file .
-_WIN_RESERVED_NAMES = frozenset(
- {'CON', 'PRN', 'AUX', 'NUL', 'CONIN$', 'CONOUT$'} |
- {f'COM{c}' for c in '123456789\xb9\xb2\xb3'} |
- {f'LPT{c}' for c in '123456789\xb9\xb2\xb3'}
-)
-
-
class _PathParents(Sequence):
"""This object provides sequence-like access to the logical ancestors
of a path. Don't try to construct it yourself."""
@@ -433,18 +424,13 @@ class PurePath(_abc.PurePathBase):
def is_reserved(self):
"""Return True if the path contains one of the special names reserved
by the system, if any."""
- if self.pathmod is not ntpath or not self.name:
- return False
-
- # NOTE: the rules for reserved names seem somewhat complicated
- # (e.g. r"..\NUL" is reserved but not r"foo\NUL" if "foo" does not
- # exist). We err on the side of caution and return True for paths
- # which are not considered reserved by Windows.
- if self.drive.startswith('\\\\'):
- # UNC paths are never reserved.
- return False
- name = self.name.partition('.')[0].partition(':')[0].rstrip(' ')
- return name.upper() in _WIN_RESERVED_NAMES
+ msg = ("pathlib.PurePath.is_reserved() is deprecated and scheduled "
+ "for removal in Python 3.15. Use os.path.isreserved() to "
+ "detect reserved paths on Windows.")
+ warnings.warn(msg, DeprecationWarning, stacklevel=2)
+ if self.pathmod is ntpath:
+ return self.pathmod.isreserved(self)
+ return False
def as_uri(self):
"""Return the path as a URI."""
diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py
index aefcb98..9cb03e3 100644
--- a/Lib/test/test_ntpath.py
+++ b/Lib/test/test_ntpath.py
@@ -981,6 +981,62 @@ class TestNtpath(NtpathTestCase):
self.assertTrue(ntpath.ismount(b"\\\\localhost\\c$"))
self.assertTrue(ntpath.ismount(b"\\\\localhost\\c$\\"))
+ def test_isreserved(self):
+ self.assertFalse(ntpath.isreserved(''))
+ self.assertFalse(ntpath.isreserved('.'))
+ self.assertFalse(ntpath.isreserved('..'))
+ self.assertFalse(ntpath.isreserved('/'))
+ self.assertFalse(ntpath.isreserved('/foo/bar'))
+ # A name that ends with a space or dot is reserved.
+ self.assertTrue(ntpath.isreserved('foo.'))
+ self.assertTrue(ntpath.isreserved('foo '))
+ # ASCII control characters are reserved.
+ self.assertTrue(ntpath.isreserved('\foo'))
+ # Wildcard characters, colon, and pipe are reserved.
+ self.assertTrue(ntpath.isreserved('foo*bar'))
+ self.assertTrue(ntpath.isreserved('foo?bar'))
+ self.assertTrue(ntpath.isreserved('foo"bar'))
+ self.assertTrue(ntpath.isreserved('foo<bar'))
+ self.assertTrue(ntpath.isreserved('foo>bar'))
+ self.assertTrue(ntpath.isreserved('foo:bar'))
+ self.assertTrue(ntpath.isreserved('foo|bar'))
+ # Case-insensitive DOS-device names are reserved.
+ self.assertTrue(ntpath.isreserved('nul'))
+ self.assertTrue(ntpath.isreserved('aux'))
+ self.assertTrue(ntpath.isreserved('prn'))
+ self.assertTrue(ntpath.isreserved('con'))
+ self.assertTrue(ntpath.isreserved('conin$'))
+ self.assertTrue(ntpath.isreserved('conout$'))
+ # COM/LPT + 1-9 or + superscript 1-3 are reserved.
+ self.assertTrue(ntpath.isreserved('COM1'))
+ self.assertTrue(ntpath.isreserved('LPT9'))
+ self.assertTrue(ntpath.isreserved('com\xb9'))
+ self.assertTrue(ntpath.isreserved('com\xb2'))
+ self.assertTrue(ntpath.isreserved('lpt\xb3'))
+ # DOS-device name matching ignores characters after a dot or
+ # a colon and also ignores trailing spaces.
+ self.assertTrue(ntpath.isreserved('NUL.txt'))
+ self.assertTrue(ntpath.isreserved('PRN '))
+ self.assertTrue(ntpath.isreserved('AUX .txt'))
+ self.assertTrue(ntpath.isreserved('COM1:bar'))
+ self.assertTrue(ntpath.isreserved('LPT9 :bar'))
+ # DOS-device names are only matched at the beginning
+ # of a path component.
+ self.assertFalse(ntpath.isreserved('bar.com9'))
+ self.assertFalse(ntpath.isreserved('bar.lpt9'))
+ # The entire path is checked, except for the drive.
+ self.assertTrue(ntpath.isreserved('c:/bar/baz/NUL'))
+ self.assertTrue(ntpath.isreserved('c:/NUL/bar/baz'))
+ self.assertFalse(ntpath.isreserved('//./NUL'))
+ # Bytes are supported.
+ self.assertFalse(ntpath.isreserved(b''))
+ self.assertFalse(ntpath.isreserved(b'.'))
+ self.assertFalse(ntpath.isreserved(b'..'))
+ self.assertFalse(ntpath.isreserved(b'/'))
+ self.assertFalse(ntpath.isreserved(b'/foo/bar'))
+ self.assertTrue(ntpath.isreserved(b'foo.'))
+ self.assertTrue(ntpath.isreserved(b'nul'))
+
def assertEqualCI(self, s1, s2):
"""Assert that two strings are equal ignoring case differences."""
self.assertEqual(s1.lower(), s2.lower())
diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py
index bdbe923..2da3afdd 100644
--- a/Lib/test/test_pathlib/test_pathlib.py
+++ b/Lib/test/test_pathlib/test_pathlib.py
@@ -349,6 +349,12 @@ class PurePathTest(test_pathlib_abc.DummyPurePathTest):
with self.assertWarns(DeprecationWarning):
p.is_relative_to('a', 'b')
+ def test_is_reserved_deprecated(self):
+ P = self.cls
+ p = P('a/b')
+ with self.assertWarns(DeprecationWarning):
+ p.is_reserved()
+
def test_match_empty(self):
P = self.cls
self.assertRaises(ValueError, P('a').match, '')
@@ -414,13 +420,6 @@ class PurePosixPathTest(PurePathTest):
self.assertTrue(P('//a').is_absolute())
self.assertTrue(P('//a/b').is_absolute())
- def test_is_reserved(self):
- P = self.cls
- self.assertIs(False, P('').is_reserved())
- self.assertIs(False, P('/').is_reserved())
- self.assertIs(False, P('/foo/bar').is_reserved())
- self.assertIs(False, P('/dev/con/PRN/NUL').is_reserved())
-
def test_join(self):
P = self.cls
p = P('//a')
@@ -1082,41 +1081,6 @@ class PureWindowsPathTest(PurePathTest):
self.assertEqual(p / P('./dd:s'), P('C:/a/b/dd:s'))
self.assertEqual(p / P('E:d:s'), P('E:d:s'))
- def test_is_reserved(self):
- P = self.cls
- self.assertIs(False, P('').is_reserved())
- self.assertIs(False, P('/').is_reserved())
- self.assertIs(False, P('/foo/bar').is_reserved())
- # UNC paths are never reserved.
- self.assertIs(False, P('//my/share/nul/con/aux').is_reserved())
- # Case-insensitive DOS-device names are reserved.
- self.assertIs(True, P('nul').is_reserved())
- self.assertIs(True, P('aux').is_reserved())
- self.assertIs(True, P('prn').is_reserved())
- self.assertIs(True, P('con').is_reserved())
- self.assertIs(True, P('conin$').is_reserved())
- self.assertIs(True, P('conout$').is_reserved())
- # COM/LPT + 1-9 or + superscript 1-3 are reserved.
- self.assertIs(True, P('COM1').is_reserved())
- self.assertIs(True, P('LPT9').is_reserved())
- self.assertIs(True, P('com\xb9').is_reserved())
- self.assertIs(True, P('com\xb2').is_reserved())
- self.assertIs(True, P('lpt\xb3').is_reserved())
- # DOS-device name mataching ignores characters after a dot or
- # a colon and also ignores trailing spaces.
- self.assertIs(True, P('NUL.txt').is_reserved())
- self.assertIs(True, P('PRN ').is_reserved())
- self.assertIs(True, P('AUX .txt').is_reserved())
- self.assertIs(True, P('COM1:bar').is_reserved())
- self.assertIs(True, P('LPT9 :bar').is_reserved())
- # DOS-device names are only matched at the beginning
- # of a path component.
- self.assertIs(False, P('bar.com9').is_reserved())
- self.assertIs(False, P('bar.lpt9').is_reserved())
- # Only the last path component matters.
- self.assertIs(True, P('c:/baz/con/NUL').is_reserved())
- self.assertIs(False, P('c:/NUL/con/baz').is_reserved())
-
class PurePathSubclassTest(PurePathTest):
class cls(pathlib.PurePath):
diff --git a/Misc/NEWS.d/next/Library/2022-07-31-01-24-40.gh-issue-88569.eU0--b.rst b/Misc/NEWS.d/next/Library/2022-07-31-01-24-40.gh-issue-88569.eU0--b.rst
new file mode 100644
index 0000000..31dd985
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-07-31-01-24-40.gh-issue-88569.eU0--b.rst
@@ -0,0 +1,4 @@
+Add :func:`os.path.isreserved`, which identifies reserved pathnames such
+as "NUL", "AUX" and "CON". This function is only available on Windows.
+
+Deprecate :meth:`pathlib.PurePath.is_reserved`.