mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
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
8 changed files with 154 additions and 72 deletions
|
@ -326,6 +326,28 @@ the :mod:`glob` module.)
|
||||||
.. versionadded:: 3.12
|
.. 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)
|
.. function:: join(path, *paths)
|
||||||
|
|
||||||
Join one or more path segments intelligently. The return value is the
|
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`,
|
reserved under Windows, ``False`` otherwise. With :class:`PurePosixPath`,
|
||||||
``False`` is always returned.
|
``False`` is always returned.
|
||||||
|
|
||||||
>>> PureWindowsPath('nul').is_reserved()
|
.. versionchanged:: 3.13
|
||||||
True
|
Windows path names that contain a colon, or end with a dot or a space,
|
||||||
>>> PurePosixPath('nul').is_reserved()
|
are considered reserved. UNC paths may be reserved.
|
||||||
False
|
|
||||||
|
|
||||||
File system calls on reserved paths can fail mysteriously or have
|
|
||||||
unintended effects.
|
|
||||||
|
|
||||||
|
.. 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)
|
.. method:: PurePath.joinpath(*pathsegments)
|
||||||
|
|
||||||
|
|
|
@ -321,6 +321,9 @@ os
|
||||||
os.path
|
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
|
* On Windows, :func:`os.path.isabs` no longer considers paths starting with
|
||||||
exactly one (back)slash to be absolute.
|
exactly one (back)slash to be absolute.
|
||||||
(Contributed by Barney Gale and Jon Foster in :gh:`44626`.)
|
(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``
|
security and functionality bugs. This includes removal of the ``--cgi``
|
||||||
flag to the ``python -m http.server`` command line in 3.15.
|
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.
|
* :mod:`sys`: :func:`sys._enablelegacywindowsfsencoding` function.
|
||||||
Replace it with :envvar:`PYTHONLEGACYWINDOWSFSENCODING` environment variable.
|
Replace it with :envvar:`PYTHONLEGACYWINDOWSFSENCODING` environment variable.
|
||||||
(Contributed by Inada Naoki in :gh:`73427`.)
|
(Contributed by Inada Naoki in :gh:`73427`.)
|
||||||
|
@ -709,6 +718,12 @@ Pending Removal in Python 3.15
|
||||||
:func:`locale.getlocale()` instead.
|
:func:`locale.getlocale()` instead.
|
||||||
(Contributed by Hugo van Kemenade in :gh:`111187`.)
|
(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`:
|
* :class:`typing.NamedTuple`:
|
||||||
|
|
||||||
* The undocumented keyword argument syntax for creating NamedTuple classes
|
* The undocumented keyword argument syntax for creating NamedTuple classes
|
||||||
|
|
|
@ -26,8 +26,8 @@ from genericpath import *
|
||||||
__all__ = ["normcase","isabs","join","splitdrive","splitroot","split","splitext",
|
__all__ = ["normcase","isabs","join","splitdrive","splitroot","split","splitext",
|
||||||
"basename","dirname","commonprefix","getsize","getmtime",
|
"basename","dirname","commonprefix","getsize","getmtime",
|
||||||
"getatime","getctime", "islink","exists","lexists","isdir","isfile",
|
"getatime","getctime", "islink","exists","lexists","isdir","isfile",
|
||||||
"ismount", "expanduser","expandvars","normpath","abspath",
|
"ismount","isreserved","expanduser","expandvars","normpath",
|
||||||
"curdir","pardir","sep","pathsep","defpath","altsep",
|
"abspath","curdir","pardir","sep","pathsep","defpath","altsep",
|
||||||
"extsep","devnull","realpath","supports_unicode_filenames","relpath",
|
"extsep","devnull","realpath","supports_unicode_filenames","relpath",
|
||||||
"samefile", "sameopenfile", "samestat", "commonpath", "isjunction"]
|
"samefile", "sameopenfile", "samestat", "commonpath", "isjunction"]
|
||||||
|
|
||||||
|
@ -330,6 +330,42 @@ def ismount(path):
|
||||||
return False
|
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'.
|
# Expand paths beginning with '~' or '~user'.
|
||||||
# '~' means $HOME; '~user' means that user's home directory.
|
# '~' means $HOME; '~user' means that user's home directory.
|
||||||
# If the path doesn't begin with '~', or if the user or $HOME is unknown,
|
# 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):
|
class _PathParents(Sequence):
|
||||||
"""This object provides sequence-like access to the logical ancestors
|
"""This object provides sequence-like access to the logical ancestors
|
||||||
of a path. Don't try to construct it yourself."""
|
of a path. Don't try to construct it yourself."""
|
||||||
|
@ -433,19 +424,14 @@ class PurePath(_abc.PurePathBase):
|
||||||
def is_reserved(self):
|
def is_reserved(self):
|
||||||
"""Return True if the path contains one of the special names reserved
|
"""Return True if the path contains one of the special names reserved
|
||||||
by the system, if any."""
|
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
|
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):
|
def as_uri(self):
|
||||||
"""Return the path as a URI."""
|
"""Return the path as a URI."""
|
||||||
if not self.is_absolute():
|
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$"))
|
||||||
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):
|
def assertEqualCI(self, s1, s2):
|
||||||
"""Assert that two strings are equal ignoring case differences."""
|
"""Assert that two strings are equal ignoring case differences."""
|
||||||
self.assertEqual(s1.lower(), s2.lower())
|
self.assertEqual(s1.lower(), s2.lower())
|
||||||
|
|
|
@ -349,6 +349,12 @@ class PurePathTest(test_pathlib_abc.DummyPurePathTest):
|
||||||
with self.assertWarns(DeprecationWarning):
|
with self.assertWarns(DeprecationWarning):
|
||||||
p.is_relative_to('a', 'b')
|
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):
|
def test_match_empty(self):
|
||||||
P = self.cls
|
P = self.cls
|
||||||
self.assertRaises(ValueError, P('a').match, '')
|
self.assertRaises(ValueError, P('a').match, '')
|
||||||
|
@ -414,13 +420,6 @@ class PurePosixPathTest(PurePathTest):
|
||||||
self.assertTrue(P('//a').is_absolute())
|
self.assertTrue(P('//a').is_absolute())
|
||||||
self.assertTrue(P('//a/b').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):
|
def test_join(self):
|
||||||
P = self.cls
|
P = self.cls
|
||||||
p = P('//a')
|
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('./dd:s'), P('C:/a/b/dd:s'))
|
||||||
self.assertEqual(p / P('E:d:s'), P('E:d: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 PurePathSubclassTest(PurePathTest):
|
||||||
class cls(pathlib.PurePath):
|
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…
Add table
Add a link
Reference in a new issue