mirror of https://github.com/python/cpython
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>
This commit is contained in:
parent
6c2b419fb9
commit
7e31d6dea2
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,19 +424,14 @@ 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:
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
def as_uri(self):
|
||||
"""Return the path as a URI."""
|
||||
if not self.is_absolute():
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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`.
|
Loading…
Reference in New Issue