From 6b89ecc8ad4695bab0cd414954ba8ac561f3b555 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sun, 31 Jul 2022 01:31:39 +0100 Subject: [PATCH 01/35] gh-88569: add `os.path.isreserved()` --- Doc/library/os.path.rst | 9 +++++ Doc/library/pathlib.rst | 1 + Lib/genericpath.py | 11 ++++- Lib/ntpath.py | 20 ++++++++++ Lib/pathlib.py | 24 +---------- Lib/posixpath.py | 4 +- Lib/test/test_genericpath.py | 10 +++++ Lib/test/test_ntpath.py | 40 +++++++++++++++++++ ...2-07-31-01-24-40.gh-issue-88569.eU0--b.rst | 3 ++ 9 files changed, 95 insertions(+), 27 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2022-07-31-01-24-40.gh-issue-88569.eU0--b.rst diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index 85989ef32d4911..cd7bb47694b732 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -295,6 +295,15 @@ the :mod:`glob` module.) Accepts a :term:`path-like object`. +.. function:: isreserved(path) + + Return ``True`` if *path* is a reserved pathname on the current system. On + Windows, reserved names include "NUL", "AUX" and "CON". On other platforms, + this function always returns ``False``. + + .. versionadded:: 3.12 + + .. function:: join(path, *paths) Join one or more path components intelligently. The return value is the diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 19944bd7bd0a89..ac30fa50f826c0 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1392,6 +1392,7 @@ Below is a table mapping various :mod:`os` functions to their corresponding :meth:`Path.owner`, :meth:`Path.group` :func:`os.path.isabs` :meth:`PurePath.is_absolute` +:func:`os.path.isreserved` :meth:`PurePath.is_reserved` :func:`os.path.join` :func:`PurePath.joinpath` :func:`os.path.basename` :data:`PurePath.name` :func:`os.path.dirname` :data:`PurePath.parent` diff --git a/Lib/genericpath.py b/Lib/genericpath.py index ce36451a3af01c..1eb07b1d2072ba 100644 --- a/Lib/genericpath.py +++ b/Lib/genericpath.py @@ -7,8 +7,8 @@ import stat __all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime', - 'getsize', 'isdir', 'isfile', 'samefile', 'sameopenfile', - 'samestat'] + 'getsize', 'isdir', 'isfile', 'isreserved', 'samefile', + 'sameopenfile', 'samestat'] # Does a path exist? @@ -45,6 +45,13 @@ def isdir(s): return stat.S_ISDIR(st.st_mode) +# Is a path reserved? The answer is "no" here, but it can be "yes" in ntpath. +def isreserved(path): + """Return true if the pathname is reserved by the system.""" + os.fspath(path) + return False + + def getsize(filename): """Return the size of a file, reported by os.stat().""" return os.stat(filename).st_size diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 959bcd09831186..1adc665b8dbed5 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -311,6 +311,26 @@ def ismount(path): return False +_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.""" + # 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. + path = os.fspath(path) + if not path[:2].lstrip(_get_bothseps(path)): + # UNC paths are never reserved + return False + name = os.fsdecode(basename(path)) + name = name.partition('.')[0].partition(':')[0].rstrip(' ') + return name.upper() in _reserved_names + + # 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.py b/Lib/pathlib.py index 2aee71742b23e4..e749c00b654e18 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -127,12 +127,6 @@ class _WindowsFlavour(_Flavour): is_supported = (os.name == 'nt') - reserved_names = ( - {'CON', 'PRN', 'AUX', 'NUL', 'CONIN$', 'CONOUT$'} | - {'COM%s' % c for c in '123456789\xb9\xb2\xb3'} | - {'LPT%s' % c for c in '123456789\xb9\xb2\xb3'} - ) - def splitroot(self, part, sep=sep): drv, rest = self.pathmod.splitdrive(part) if drv[:1] == sep or rest[:1] == sep: @@ -149,19 +143,6 @@ def casefold_parts(self, parts): def compile_pattern(self, pattern): return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch - def is_reserved(self, parts): - # 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 not parts: - return False - if parts[0].startswith('\\\\'): - # UNC paths are never reserved - return False - name = parts[-1].partition('.')[0].partition(':')[0].rstrip(' ') - return name.upper() in self.reserved_names - def make_uri(self, path): # Under Windows, file URIs use the UTF-8 encoding. drive = path.drive @@ -207,9 +188,6 @@ def casefold_parts(self, parts): def compile_pattern(self, pattern): return re.compile(fnmatch.translate(pattern)).fullmatch - def is_reserved(self, parts): - return False - def make_uri(self, path): # We represent the path using the local filesystem encoding, # for portability to other applications. @@ -750,7 +728,7 @@ def is_absolute(self): def is_reserved(self): """Return True if the path contains one of the special names reserved by the system, if any.""" - return self._flavour.is_reserved(self._parts) + return self._flavour.pathmod.isreserved(self) def match(self, path_pattern): """ diff --git a/Lib/posixpath.py b/Lib/posixpath.py index a7b2f2d64824fa..411f3b78ebf715 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -31,8 +31,8 @@ __all__ = ["normcase","isabs","join","splitdrive","split","splitext", "basename","dirname","commonprefix","getsize","getmtime", "getatime","getctime","islink","exists","lexists","isdir","isfile", - "ismount", "expanduser","expandvars","normpath","abspath", - "samefile","sameopenfile","samestat", + "ismount","isreserved","expanduser","expandvars","normpath", + "abspath","samefile","sameopenfile","samestat", "curdir","pardir","sep","pathsep","defpath","altsep","extsep", "devnull","realpath","supports_unicode_filenames","relpath", "commonpath"] diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py index 489044f8090d3b..fc2f4031ccf34c 100644 --- a/Lib/test/test_genericpath.py +++ b/Lib/test/test_genericpath.py @@ -215,6 +215,16 @@ def test_isfile(self): finally: os_helper.rmdir(filename) + def test_isreserved(self): + self.assertFalse(self.pathmodule.isreserved('')) + self.assertFalse(self.pathmodule.isreserved(b'')) + self.assertFalse(self.pathmodule.isreserved('/')) + self.assertFalse(self.pathmodule.isreserved(b'/')) + self.assertFalse(self.pathmodule.isreserved('hi')) + self.assertFalse(self.pathmodule.isreserved(b'hi')) + self.assertFalse(self.pathmodule.isreserved('NUL')) + self.assertFalse(self.pathmodule.isreserved(b'NUL')) + def test_samefile(self): file1 = os_helper.TESTFN file2 = os_helper.TESTFN + "2" diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index d51946322c8056..0c13b0853246f0 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -822,6 +822,46 @@ def test_ismount(self): 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('/foo/bar')) + # UNC paths are never reserved. + self.assertFalse(ntpath.isreserved('//my/share/nul/con/aux')) + # 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')) + # Only the last path component matters. + self.assertTrue(ntpath.isreserved('c:/baz/con/NUL')) + self.assertFalse(ntpath.isreserved('c:/NUL/con/baz')) + # Bytes are supported. + self.assertFalse(ntpath.isreserved(b'')) + self.assertFalse(ntpath.isreserved(b'/')) + self.assertFalse(ntpath.isreserved(b'/foo/bar')) + self.assertFalse(ntpath.isreserved(b'//my/share/nul/con/aux')) + 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/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 00000000000000..09a75de081943c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-07-31-01-24-40.gh-issue-88569.eU0--b.rst @@ -0,0 +1,3 @@ +Add :func:`os.path.isreserved`, which identifies reserved pathnames. On +Windows, reserved names include "NUL", "AUX" and "CON". On other platforms, +this function always returns ``False``. From 3e08e4abd45ca74e387da5fde0ad80c2f6509ff5 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sun, 31 Jul 2022 01:49:22 +0100 Subject: [PATCH 02/35] Fix tests --- Lib/test/test_genericpath.py | 10 ---------- Lib/test/test_posixpath.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py index fc2f4031ccf34c..489044f8090d3b 100644 --- a/Lib/test/test_genericpath.py +++ b/Lib/test/test_genericpath.py @@ -215,16 +215,6 @@ def test_isfile(self): finally: os_helper.rmdir(filename) - def test_isreserved(self): - self.assertFalse(self.pathmodule.isreserved('')) - self.assertFalse(self.pathmodule.isreserved(b'')) - self.assertFalse(self.pathmodule.isreserved('/')) - self.assertFalse(self.pathmodule.isreserved(b'/')) - self.assertFalse(self.pathmodule.isreserved('hi')) - self.assertFalse(self.pathmodule.isreserved(b'hi')) - self.assertFalse(self.pathmodule.isreserved('NUL')) - self.assertFalse(self.pathmodule.isreserved(b'NUL')) - def test_samefile(self): file1 = os_helper.TESTFN file2 = os_helper.TESTFN + "2" diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index c644f881e460fe..9d55b1027f1b45 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -242,6 +242,16 @@ def fake_lstat(path): finally: os.lstat = save_lstat + def test_isreserved(self): + self.assertFalse(posixpath.isreserved('')) + self.assertFalse(posixpath.isreserved(b'')) + self.assertFalse(posixpath.isreserved('/')) + self.assertFalse(posixpath.isreserved(b'/')) + self.assertFalse(posixpath.isreserved('hi')) + self.assertFalse(posixpath.isreserved(b'hi')) + self.assertFalse(posixpath.isreserved('NUL')) + self.assertFalse(posixpath.isreserved(b'NUL')) + def test_expanduser(self): self.assertEqual(posixpath.expanduser("foo"), "foo") self.assertEqual(posixpath.expanduser(b"foo"), b"foo") From 49ba439fdc24601932f2880a07f188576e2cac12 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sun, 31 Jul 2022 01:54:24 +0100 Subject: [PATCH 03/35] Remove implementation in `genericpath` --- Lib/genericpath.py | 11 ++--------- Lib/ntpath.py | 4 ++-- Lib/posixpath.py | 6 ++++++ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Lib/genericpath.py b/Lib/genericpath.py index 1eb07b1d2072ba..ce36451a3af01c 100644 --- a/Lib/genericpath.py +++ b/Lib/genericpath.py @@ -7,8 +7,8 @@ import stat __all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime', - 'getsize', 'isdir', 'isfile', 'isreserved', 'samefile', - 'sameopenfile', 'samestat'] + 'getsize', 'isdir', 'isfile', 'samefile', 'sameopenfile', + 'samestat'] # Does a path exist? @@ -45,13 +45,6 @@ def isdir(s): return stat.S_ISDIR(st.st_mode) -# Is a path reserved? The answer is "no" here, but it can be "yes" in ntpath. -def isreserved(path): - """Return true if the pathname is reserved by the system.""" - os.fspath(path) - return False - - def getsize(filename): """Return the size of a file, reported by os.stat().""" return os.stat(filename).st_size diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 1adc665b8dbed5..4a7b07a72c3cb6 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -27,8 +27,8 @@ __all__ = ["normcase","isabs","join","splitdrive","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"] diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 411f3b78ebf715..2a64770a51b4e5 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -216,6 +216,12 @@ def ismount(path): return False +def isreserved(path): + """Return true if the pathname is reserved by the system.""" + os.fspath(path) + 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, From 238b3e3494aa4d1c2d5082807a10a1d2d0738367 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 9 Aug 2022 20:21:13 +0100 Subject: [PATCH 04/35] Apply suggestions from code review Co-authored-by: Eryk Sun --- Lib/ntpath.py | 20 +++++++++++--------- Lib/test/test_ntpath.py | 9 ++++++--- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 4a7b07a72c3cb6..164f97dac41d6b 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -318,17 +318,19 @@ def ismount(path): ) def isreserved(path): """Return true if the pathname is reserved by the system.""" - # 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. + # Refer to "Naming Files, Paths, and Namespaces": + # https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file path = os.fspath(path) - if not path[:2].lstrip(_get_bothseps(path)): - # UNC paths are never reserved - return False name = os.fsdecode(basename(path)) - name = name.partition('.')[0].partition(':')[0].rstrip(' ') - return name.upper() in _reserved_names + # Trailing spaces and dots are reserved. + # File streams are reserved (e.g. "filename:stream[:type]"). + if name.rstrip('. ') != name or ':' in name: + return True + # DOS device names are reserved (e.g. "nul" or "nul .txt"). The rules + # are complicated and vary across Windows versions (e.g. "../nul" is + # reserved but not "foo/nul" if "foo" does not exist). On the side of + # caution, return True for names that may not be reserved. + return name.partition('.')[0].rstrip(' ').upper() in _reserved_names # Expand paths beginning with '~' or '~user'. diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 0c13b0853246f0..f0a9c12be9e929 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -826,8 +826,11 @@ def test_isreserved(self): self.assertFalse(ntpath.isreserved('')) self.assertFalse(ntpath.isreserved('/')) self.assertFalse(ntpath.isreserved('/foo/bar')) - # UNC paths are never reserved. - self.assertFalse(ntpath.isreserved('//my/share/nul/con/aux')) + # A name that ends with a space or dot is reserved. + self.assertTrue(ntpath.isreserved('foo.')) + self.assertTrue(ntpath.isreserved('foo ')) + # A name with a file stream is reserved. + self.assertTrue(ntpath.isreserved('foo:bar')) # Case-insensitive DOS-device names are reserved. self.assertTrue(ntpath.isreserved('nul')) self.assertTrue(ntpath.isreserved('aux')) @@ -859,7 +862,7 @@ def test_isreserved(self): self.assertFalse(ntpath.isreserved(b'')) self.assertFalse(ntpath.isreserved(b'/')) self.assertFalse(ntpath.isreserved(b'/foo/bar')) - self.assertFalse(ntpath.isreserved(b'//my/share/nul/con/aux')) + self.assertTrue(ntpath.isreserved(b'foo.')) self.assertTrue(ntpath.isreserved(b'nul')) def assertEqualCI(self, s1, s2): From f647502e5b90c6d0106d730e3927ce9557a7b442 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Wed, 10 Aug 2022 20:49:13 +0100 Subject: [PATCH 05/35] Apply suggestions from code review Co-authored-by: Eryk Sun --- Lib/ntpath.py | 4 +++- Lib/test/test_ntpath.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 164f97dac41d6b..6a0c01c1543009 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -323,8 +323,10 @@ def isreserved(path): path = os.fspath(path) name = os.fsdecode(basename(path)) # Trailing spaces and dots are reserved. + if name not in ('.', '..') and name.rstrip('. ') != name: + return True # File streams are reserved (e.g. "filename:stream[:type]"). - if name.rstrip('. ') != name or ':' in name: + if ':' in name: return True # DOS device names are reserved (e.g. "nul" or "nul .txt"). The rules # are complicated and vary across Windows versions (e.g. "../nul" is diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index f0a9c12be9e929..4b78ed3c6eb5b3 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -824,6 +824,8 @@ def test_ismount(self): 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. @@ -860,6 +862,8 @@ def test_isreserved(self): self.assertFalse(ntpath.isreserved('c:/NUL/con/baz')) # 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.')) From f07c7ad0d529601fa6fbeb0fe1232dac30d0322f Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Wed, 10 Aug 2022 20:52:46 +0100 Subject: [PATCH 06/35] Remove tests for pathlib.PurePath.is_reserved(). These have been moved to test_ntpath / test_posixpath. --- Lib/test/test_pathlib.py | 42 ---------------------------------------- 1 file changed, 42 deletions(-) diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index e14b0fca55360f..37ede4e0875931 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -752,13 +752,6 @@ def test_is_absolute(self): 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') @@ -1284,41 +1277,6 @@ def test_div(self): self.assertEqual(p / 'c:x/y', P('C:/a/b/x/y')) self.assertEqual(p / 'c:/x/y', P('C:/x/y')) - 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 PurePathTest(_BasePurePathTest, unittest.TestCase): cls = pathlib.PurePath From dc857c9f4b370d2d717b703504d371b92698ded8 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Fri, 12 Aug 2022 19:17:25 +0100 Subject: [PATCH 07/35] Speed up isreserved('.') and '..' --- Lib/ntpath.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 6a0c01c1543009..57dab64422fa4b 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -320,13 +320,15 @@ 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.fspath(path) name = os.fsdecode(basename(path)) + # '.' and '..' are not reserved. + if name == '.' or name == '..': + return False # Trailing spaces and dots are reserved. - if name not in ('.', '..') and name.rstrip('. ') != name: + elif name and name[-1] in '. ': return True # File streams are reserved (e.g. "filename:stream[:type]"). - if ':' in name: + elif ':' in name: return True # DOS device names are reserved (e.g. "nul" or "nul .txt"). The rules # are complicated and vary across Windows versions (e.g. "../nul" is From f0fd2c8b68ac7e7fcd35f895358eb2ab4feabe7f Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Fri, 12 Aug 2022 19:17:49 +0100 Subject: [PATCH 08/35] Note change to algorithm in pathlib docs. --- Doc/library/pathlib.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index ac30fa50f826c0..b7e41b4c76026f 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -517,6 +517,10 @@ Pure paths provide the following methods and properties: File system calls on reserved paths can fail mysteriously or have unintended effects. + .. versionchanged: 3.12 + Windows path names that contain a colon, or end with a dot or a space, + are considered reserved. UNC paths may be reserved. + .. method:: PurePath.joinpath(*other) From 3b51db8aab8718122c22f46c32dc2a165588eb5e Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Fri, 12 Aug 2022 20:16:03 +0100 Subject: [PATCH 09/35] Update Lib/ntpath.py Co-authored-by: Eryk Sun --- Lib/ntpath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 57dab64422fa4b..60e6037ccbf660 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -325,7 +325,7 @@ def isreserved(path): if name == '.' or name == '..': return False # Trailing spaces and dots are reserved. - elif name and name[-1] in '. ': + elif name.endswith((' ', '.')): return True # File streams are reserved (e.g. "filename:stream[:type]"). elif ':' in name: From 06cb428f4281ba3004adb3ad9ef7756622dd52ce Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sat, 13 Aug 2022 12:15:49 +0100 Subject: [PATCH 10/35] Update Doc/library/os.path.rst Co-authored-by: Eryk Sun --- Doc/library/os.path.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index cd7bb47694b732..56923640057ec1 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -298,8 +298,11 @@ the :mod:`glob` module.) .. function:: isreserved(path) Return ``True`` if *path* is a reserved pathname on the current system. On - Windows, reserved names include "NUL", "AUX" and "CON". On other platforms, - this function always returns ``False``. + Windows, reserved filenames include those that end with a space or dot; + those that contain colons, which may delimit file streams such as + "filename:streamname"; and DOS device names such as "NUL", "CON", "CONIN$", + "CONOUT$", "AUX", "PRN", "COM1", and "LPT1". On other platforms, this + function always returns ``False``. .. versionadded:: 3.12 From 79c0be481a2706eaef38e9aedb60178512d74b34 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Sat, 13 Aug 2022 17:28:15 +0100 Subject: [PATCH 11/35] Update Lib/posixpath.py Co-authored-by: Brett Cannon --- Lib/posixpath.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 2a64770a51b4e5..81b8fc7e0ef71d 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -218,7 +218,6 @@ def ismount(path): def isreserved(path): """Return true if the pathname is reserved by the system.""" - os.fspath(path) return False From 0a0db6a45e840f53d77fb96a37f1d589bd4c5320 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 16 Aug 2022 23:59:11 +0100 Subject: [PATCH 12/35] Restore `os.fspath()` call in `posixpath.isreserved()` --- Lib/posixpath.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 81b8fc7e0ef71d..73b2af5f81c087 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -218,6 +218,9 @@ def ismount(path): def isreserved(path): """Return true if the pathname is reserved by the system.""" + # For consistency with ntpath.isreserved(), ensure the argument is + # path-like. If it isn't, a TypeError exception is raised. + os.fspath(path) return False From 7145b86fc632deb3ebeaf7f1f0cffae02074806f Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 23 Aug 2022 02:44:22 +0100 Subject: [PATCH 13/35] Apply suggestions from code review Co-authored-by: Eryk Sun --- Doc/library/os.path.rst | 15 +++++++++------ Lib/ntpath.py | 42 ++++++++++++++++++++++++++--------------- Lib/test/test_ntpath.py | 17 +++++++++++++---- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index 56923640057ec1..77f8e465bb1c1a 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -297,12 +297,15 @@ the :mod:`glob` module.) .. 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, which may delimit file streams such as - "filename:streamname"; and DOS device names such as "NUL", "CON", "CONIN$", - "CONOUT$", "AUX", "PRN", "COM1", and "LPT1". On other platforms, this - function always returns ``False``. + 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". + + On POSIX platforms, this function always returns ``False``. .. versionadded:: 3.12 diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 60e6037ccbf660..0a5f6da65064f0 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -311,30 +311,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 - name = os.fsdecode(basename(path)) - # '.' and '..' are not reserved. - if name == '.' or name == '..': - return False - # Trailing spaces and dots are reserved. - elif name.endswith((' ', '.')): - return True - # File streams are reserved (e.g. "filename:stream[:type]"). - elif ':' in name: - return True - # DOS device names are reserved (e.g. "nul" or "nul .txt"). The rules - # are complicated and vary across Windows versions (e.g. "../nul" is - # reserved but not "foo/nul" if "foo" does not exist). On the side of - # caution, return True for names that may not be reserved. - return name.partition('.')[0].rstrip(' ').upper() in _reserved_names + path = os.fsdecode(splitdrive(path)[1]).rstrip(r'\/') + while path: + path, name = split(path) + if not name: + break + # '.' and '..' are not reserved. + if name == '.' or name == '..': + continue + # Trailing spaces and dots are reserved. + elif name.endswith((' ', '.')): + return True + # The wildcard characters, colon, and pipe (*?"<>:|) are reserved. + # Colon is reserved for file streams (e.g. "name:stream[:type]"). + elif _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. + elif name.partition('.')[0].rstrip(' ').upper() in _reserved_names: + return True + return False # Expand paths beginning with '~' or '~user'. diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 4b78ed3c6eb5b3..cc5b20f0da11d2 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -831,8 +831,16 @@ def test_isreserved(self): # A name that ends with a space or dot is reserved. self.assertTrue(ntpath.isreserved('foo.')) self.assertTrue(ntpath.isreserved('foo ')) - # A name with a file stream is reserved. + # 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('foobar')) 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')) @@ -857,9 +865,10 @@ def test_isreserved(self): # of a path component. self.assertFalse(ntpath.isreserved('bar.com9')) self.assertFalse(ntpath.isreserved('bar.lpt9')) - # Only the last path component matters. - self.assertTrue(ntpath.isreserved('c:/baz/con/NUL')) - self.assertFalse(ntpath.isreserved('c:/NUL/con/baz')) + # 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'.')) From 9f74b64f09d6f889097fa3ab24ef082dea0621c4 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 23 Aug 2022 04:06:49 +0100 Subject: [PATCH 14/35] posixpath.isreserved(): return True for paths with NUL characters --- Doc/library/os.path.rst | 3 ++- Doc/library/pathlib.rst | 14 +++++++++----- Lib/posixpath.py | 5 +---- Lib/test/test_posixpath.py | 19 +++++++++++++++++++ 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index 77f8e465bb1c1a..45dbc451970b7c 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -305,7 +305,8 @@ the :mod:`glob` module.) as well as DOS device names such as "NUL", "CON", "CONIN$", "CONOUT$", "AUX", "PRN", "COM1", and "LPT1". - On POSIX platforms, this function always returns ``False``. + On POSIX platforms, paths that contains NUL characters are reserved; all + other paths are unreserved. .. versionadded:: 3.12 diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index b7e41b4c76026f..88427af0793697 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -505,22 +505,26 @@ Pure paths provide the following methods and properties: .. method:: PurePath.is_reserved() - With :class:`PureWindowsPath`, return ``True`` if the path is considered - reserved under Windows, ``False`` otherwise. With :class:`PurePosixPath`, - ``False`` is always returned. + Return ``True`` if the path is considered reserved, ``False`` otherwise. >>> PureWindowsPath('nul').is_reserved() True - >>> PurePosixPath('nul').is_reserved() - False + >>> PurePosixPath('\x00').is_reserved() + True File system calls on reserved paths can fail mysteriously or have unintended effects. + .. seealso:: + :func:`os.path.isreserved`, which is used to implement this method. + .. versionchanged: 3.12 Windows path names that contain a colon, or end with a dot or a space, are considered reserved. UNC paths may be reserved. + .. versionchanged: 3.12 + POSIX path names that contain a NUL character are considered reserved. + .. method:: PurePath.joinpath(*other) diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 73b2af5f81c087..8d31e4fb656323 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -218,10 +218,7 @@ def ismount(path): def isreserved(path): """Return true if the pathname is reserved by the system.""" - # For consistency with ntpath.isreserved(), ensure the argument is - # path-like. If it isn't, a TypeError exception is raised. - os.fspath(path) - return False + return 0 in os.fsencode(path) # Expand paths beginning with '~' or '~user'. diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 9d55b1027f1b45..0bb009e0a90abd 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -243,15 +243,34 @@ def fake_lstat(path): os.lstat = save_lstat def test_isreserved(self): + # Regular ol' paths are unreserved. self.assertFalse(posixpath.isreserved('')) self.assertFalse(posixpath.isreserved(b'')) self.assertFalse(posixpath.isreserved('/')) self.assertFalse(posixpath.isreserved(b'/')) self.assertFalse(posixpath.isreserved('hi')) self.assertFalse(posixpath.isreserved(b'hi')) + + # '.' and '..' are unreserved. + self.assertFalse(posixpath.isreserved('.')) + self.assertFalse(posixpath.isreserved(b'.')) + self.assertFalse(posixpath.isreserved('..')) + self.assertFalse(posixpath.isreserved(b'..')) + + # Paths reserved on Windows aren't reserved here. self.assertFalse(posixpath.isreserved('NUL')) self.assertFalse(posixpath.isreserved(b'NUL')) + # Null bytes ARE reserved + self.assertTrue(posixpath.isreserved('\x00')) + self.assertTrue(posixpath.isreserved(b'\x00')) + self.assertTrue(posixpath.isreserved('ham/eggs\x00beans/tomato')) + self.assertTrue(posixpath.isreserved(b'ham/eggs\x00beans/tomato')) + + # Other special characters are not reserved. + self.assertFalse(posixpath.isreserved('\x01')) + self.assertFalse(posixpath.isreserved(b'\x01')) + def test_expanduser(self): self.assertEqual(posixpath.expanduser("foo"), "foo") self.assertEqual(posixpath.expanduser(b"foo"), b"foo") From 91b2bb326d77f646f85e0e5a994d673352c5e231 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 23 Aug 2022 04:08:43 +0100 Subject: [PATCH 15/35] ntpath.isreserved(): minor tweaks --- Lib/ntpath.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 0a5f6da65064f0..4cc9b04b3e0a75 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -326,16 +326,13 @@ 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(splitdrive(path)[1]).rstrip(r'\/') - while path: + path = os.fsdecode(path).rstrip(r'\/') + while True: path, name = split(path) if not name: - break - # '.' and '..' are not reserved. - if name == '.' or name == '..': - continue - # Trailing spaces and dots are reserved. - elif name.endswith((' ', '.')): + return False + # Trailing dots and spaces are reserved. + elif name.endswith(('.', ' ')) and name not in ('.', '..'): return True # The wildcard characters, colon, and pipe (*?"<>:|) are reserved. # Colon is reserved for file streams (e.g. "name:stream[:type]"). @@ -346,7 +343,6 @@ def isreserved(path): # caution, return True for names that may not be reserved. elif name.partition('.')[0].rstrip(' ').upper() in _reserved_names: return True - return False # Expand paths beginning with '~' or '~user'. From e6aff58a49dd87da63af5225a391b393e4cd8c18 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 23 Aug 2022 04:25:53 +0100 Subject: [PATCH 16/35] ntpath.isreserved(): restore initial splitdrive() call --- Lib/ntpath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 4cc9b04b3e0a75..859a98ad6f7c0f 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -326,7 +326,7 @@ 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(path).rstrip(r'\/') + path = os.fsdecode(splitdrive(path)[1]).rstrip(r'\/') while True: path, name = split(path) if not name: From e6a2c0ba8828c545c30c4fa4d36137604f72b4ca Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 23 Aug 2022 04:58:42 +0100 Subject: [PATCH 17/35] ntpath.isreserved(): avoid calling `splitdrive()` repeatedly. --- Lib/ntpath.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 859a98ad6f7c0f..1897d0c3269146 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -326,13 +326,11 @@ 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(splitdrive(path)[1]).rstrip(r'\/') - while True: - path, name = split(path) - if not name: - return False + path = splitdrive(path)[1] + path = os.fsdecode(path).replace(altsep, sep) + for name in path.split(sep): # Trailing dots and spaces are reserved. - elif name.endswith(('.', ' ')) and name not in ('.', '..'): + if name.endswith(('.', ' ')) and name not in ('.', '..'): return True # The wildcard characters, colon, and pipe (*?"<>:|) are reserved. # Colon is reserved for file streams (e.g. "name:stream[:type]"). @@ -343,6 +341,7 @@ def isreserved(path): # caution, return True for names that may not be reserved. elif name.partition('.')[0].rstrip(' ').upper() in _reserved_names: return True + return False # Expand paths beginning with '~' or '~user'. From 14dde1586fc46bf5daef118ad1e8984ce6fca139 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 23 Aug 2022 05:19:19 +0100 Subject: [PATCH 18/35] Update Lib/ntpath.py Co-authored-by: Eryk Sun --- Lib/ntpath.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 1897d0c3269146..46aeb0f6c88e30 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -328,7 +328,7 @@ def isreserved(path): # https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file path = splitdrive(path)[1] path = os.fsdecode(path).replace(altsep, sep) - for name in path.split(sep): + for name in reversed(path.split(sep)): # Trailing dots and spaces are reserved. if name.endswith(('.', ' ')) and name not in ('.', '..'): return True From fab274a3252c98efd8efdbd9d53175f503364443 Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 23 Aug 2022 06:26:14 +0100 Subject: [PATCH 19/35] Add `isreservedname()` for discussion. --- Lib/ntpath.py | 35 ++++++++++++++++++++--------------- Lib/posixpath.py | 9 ++++++++- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 46aeb0f6c88e30..1343ea93226f84 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -313,7 +313,7 @@ def ismount(path): _reserved_chars = frozenset( {chr(i) for i in range(32)} | - {'"', '*', ':', '<', '>', '?', '|'} + {'"', '*', ':', '<', '>', '?', '|', '/', '\\'} ) _reserved_names = frozenset( @@ -327,20 +327,25 @@ def isreserved(path): # Refer to "Naming Files, Paths, and Namespaces": # https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file path = splitdrive(path)[1] - path = os.fsdecode(path).replace(altsep, sep) - for name in reversed(path.split(sep)): - # Trailing dots and spaces are reserved. - if name.endswith(('.', ' ')) and name not in ('.', '..'): - return True - # The wildcard characters, colon, and pipe (*?"<>:|) are reserved. - # Colon is reserved for file streams (e.g. "name:stream[:type]"). - elif _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. - elif name.partition('.')[0].rstrip(' ').upper() in _reserved_names: - return True + path = os.fsdecode(path) + parts = path.replace(altsep, sep).split(sep) + return any(isreservedname(name) for name in reversed(parts)) + +def isreservedname(name): + """Return true if the filename is reserved by the system.""" + name = os.fsdecode(name) + # Trailing dots and spaces are reserved. + if name.endswith(('.', ' ')) and name not in ('.', '..'): + return True + # The wildcard characters, colon, and pipe (*?"<>:|) 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 diff --git a/Lib/posixpath.py b/Lib/posixpath.py index 8d31e4fb656323..e746474ecbe4d1 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -218,7 +218,14 @@ def ismount(path): def isreserved(path): """Return true if the pathname is reserved by the system.""" - return 0 in os.fsencode(path) + path = os.fsencode(path) + return 0x00 in path + + +def isreservedname(name): + """Return true if the filename is reserved by the system.""" + name = os.fsencode(name) + return 0x00 in name or 0x2f in name # Expand paths beginning with '~' or '~user'. From 936dcc861875f2ab0b03b449debb02205fcc60ac Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Tue, 23 Aug 2022 06:57:37 +0100 Subject: [PATCH 20/35] Apply suggestions from code review Co-authored-by: Eryk Sun --- Lib/ntpath.py | 6 ++---- Lib/posixpath.py | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 1343ea93226f84..5688d53438dd11 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -326,10 +326,8 @@ 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 = splitdrive(path)[1] - path = os.fsdecode(path) - parts = path.replace(altsep, sep).split(sep) - return any(isreservedname(name) for name in reversed(parts)) + path = os.fsdecode(splitdrive(path)[1]).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.""" diff --git a/Lib/posixpath.py b/Lib/posixpath.py index e746474ecbe4d1..f886e926c43bf9 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -219,13 +219,13 @@ def ismount(path): def isreserved(path): """Return true if the pathname is reserved by the system.""" path = os.fsencode(path) - return 0x00 in path + return b'\0' in path def isreservedname(name): """Return true if the filename is reserved by the system.""" name = os.fsencode(name) - return 0x00 in name or 0x2f in name + return b'\0' in name or b'/' in name # Expand paths beginning with '~' or '~user'. From 3fb127fbcc4d4eaf26fe833877ef862c40113c38 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 03:21:58 +0000 Subject: [PATCH 21/35] Undo posixpath changes --- Doc/library/os.path.rst | 3 +- Doc/library/pathlib.rst | 16 ++++------ Lib/posixpath.py | 16 ++-------- Lib/test/test_posixpath.py | 29 ------------------- ...2-07-31-01-24-40.gh-issue-88569.eU0--b.rst | 5 ++-- 5 files changed, 11 insertions(+), 58 deletions(-) diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index 45dbc451970b7c..885def4634db8b 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -305,8 +305,7 @@ the :mod:`glob` module.) as well as DOS device names such as "NUL", "CON", "CONIN$", "CONOUT$", "AUX", "PRN", "COM1", and "LPT1". - On POSIX platforms, paths that contains NUL characters are reserved; all - other paths are unreserved. + .. availability:: Windows. .. versionadded:: 3.12 diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 88427af0793697..1af2a0c263f4be 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -505,26 +505,23 @@ Pure paths provide the following methods and properties: .. method:: PurePath.is_reserved() - Return ``True`` if the path is considered reserved, ``False`` otherwise. + With :class:`PureWindowsPath`, return ``True`` if the path is considered + reserved under Windows, ``False`` otherwise. With :class:`PurePosixPath`, + ``False`` is always returned. + >>> PureWindowsPath('nul').is_reserved() True - >>> PurePosixPath('\x00').is_reserved() - True + >>> PurePosixPath('nul').is_reserved() + False File system calls on reserved paths can fail mysteriously or have unintended effects. - .. seealso:: - :func:`os.path.isreserved`, which is used to implement this method. - .. versionchanged: 3.12 Windows path names that contain a colon, or end with a dot or a space, are considered reserved. UNC paths may be reserved. - .. versionchanged: 3.12 - POSIX path names that contain a NUL character are considered reserved. - .. method:: PurePath.joinpath(*other) @@ -1400,7 +1397,6 @@ Below is a table mapping various :mod:`os` functions to their corresponding :meth:`Path.owner`, :meth:`Path.group` :func:`os.path.isabs` :meth:`PurePath.is_absolute` -:func:`os.path.isreserved` :meth:`PurePath.is_reserved` :func:`os.path.join` :func:`PurePath.joinpath` :func:`os.path.basename` :data:`PurePath.name` :func:`os.path.dirname` :data:`PurePath.parent` diff --git a/Lib/posixpath.py b/Lib/posixpath.py index f886e926c43bf9..a7b2f2d64824fa 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -31,8 +31,8 @@ __all__ = ["normcase","isabs","join","splitdrive","split","splitext", "basename","dirname","commonprefix","getsize","getmtime", "getatime","getctime","islink","exists","lexists","isdir","isfile", - "ismount","isreserved","expanduser","expandvars","normpath", - "abspath","samefile","sameopenfile","samestat", + "ismount", "expanduser","expandvars","normpath","abspath", + "samefile","sameopenfile","samestat", "curdir","pardir","sep","pathsep","defpath","altsep","extsep", "devnull","realpath","supports_unicode_filenames","relpath", "commonpath"] @@ -216,18 +216,6 @@ def ismount(path): return False -def isreserved(path): - """Return true if the pathname is reserved by the system.""" - path = os.fsencode(path) - return b'\0' in path - - -def isreservedname(name): - """Return true if the filename is reserved by the system.""" - name = os.fsencode(name) - return b'\0' in name or b'/' in name - - # 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/test/test_posixpath.py b/Lib/test/test_posixpath.py index 0bb009e0a90abd..c644f881e460fe 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -242,35 +242,6 @@ def fake_lstat(path): finally: os.lstat = save_lstat - def test_isreserved(self): - # Regular ol' paths are unreserved. - self.assertFalse(posixpath.isreserved('')) - self.assertFalse(posixpath.isreserved(b'')) - self.assertFalse(posixpath.isreserved('/')) - self.assertFalse(posixpath.isreserved(b'/')) - self.assertFalse(posixpath.isreserved('hi')) - self.assertFalse(posixpath.isreserved(b'hi')) - - # '.' and '..' are unreserved. - self.assertFalse(posixpath.isreserved('.')) - self.assertFalse(posixpath.isreserved(b'.')) - self.assertFalse(posixpath.isreserved('..')) - self.assertFalse(posixpath.isreserved(b'..')) - - # Paths reserved on Windows aren't reserved here. - self.assertFalse(posixpath.isreserved('NUL')) - self.assertFalse(posixpath.isreserved(b'NUL')) - - # Null bytes ARE reserved - self.assertTrue(posixpath.isreserved('\x00')) - self.assertTrue(posixpath.isreserved(b'\x00')) - self.assertTrue(posixpath.isreserved('ham/eggs\x00beans/tomato')) - self.assertTrue(posixpath.isreserved(b'ham/eggs\x00beans/tomato')) - - # Other special characters are not reserved. - self.assertFalse(posixpath.isreserved('\x01')) - self.assertFalse(posixpath.isreserved(b'\x01')) - def test_expanduser(self): self.assertEqual(posixpath.expanduser("foo"), "foo") self.assertEqual(posixpath.expanduser(b"foo"), b"foo") 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 index 09a75de081943c..228736ef230b97 100644 --- 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 @@ -1,3 +1,2 @@ -Add :func:`os.path.isreserved`, which identifies reserved pathnames. On -Windows, reserved names include "NUL", "AUX" and "CON". On other platforms, -this function always returns ``False``. +Add :func:`ntpath.isreserved`, which identifies reserved pathnames on +Windows; reserved names include "NUL", "AUX" and "CON". From c8ed711945d4a26183c8c52953297f03641a79ab Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 03:37:56 +0000 Subject: [PATCH 22/35] Update version numbers. --- Doc/library/os.path.rst | 2 +- Doc/library/pathlib.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index 0eb8829f29e98c..7108e0cbb92a79 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -334,7 +334,7 @@ the :mod:`glob` module.) .. availability:: Windows. - .. versionadded:: 3.12 + .. versionadded:: 3.13 .. function:: join(path, *paths) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index b145dde95304fb..f1518450db2534 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -549,7 +549,7 @@ Pure paths provide the following methods and properties: File system calls on reserved paths can fail mysteriously or have unintended effects. - .. versionchanged: 3.12 + .. 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. From c772b256738619eff8c8d2e1dc15ecbf5a7a57cf Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 03:39:02 +0000 Subject: [PATCH 23/35] Fix syntax, whitespace. --- Doc/library/pathlib.rst | 2 +- Lib/test/test_ntpath.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index f1518450db2534..0940fd49b44881 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -549,7 +549,7 @@ Pure paths provide the following methods and properties: File system calls on reserved paths can fail mysteriously or have unintended effects. - .. versionchanged: 3.13 + .. 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. diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 598ba75087c6aa..9d12780af61ee2 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -991,7 +991,7 @@ def test_isreserved(self): self.assertTrue(ntpath.isreserved('foobar')) 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')) From 3fbef574c9eb4c865fc69df99e2460d8838f2f31 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 03:42:47 +0000 Subject: [PATCH 24/35] Make `isreservedname()` private --- Lib/ntpath.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 5f5745b2de07fe..c119eba629773d 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -352,10 +352,10 @@ 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(splitdrive(path)[1]).replace(altsep, sep) - return any(isreservedname(name) for name in reversed(path.split(sep))) + path = os.fsdecode(splitroot(path)[2]).replace(altsep, sep) + return any(_isreservedname(name) for name in reversed(path.split(sep))) -def isreservedname(name): +def _isreservedname(name): """Return true if the filename is reserved by the system.""" name = os.fsdecode(name) # Trailing dots and spaces are reserved. From 4b34274efa49225b7bb81b658c5005354f4fe4bb Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Mon, 8 Jan 2024 03:43:32 +0000 Subject: [PATCH 25/35] Update Lib/ntpath.py Co-authored-by: Eryk Sun --- Lib/ntpath.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index c119eba629773d..3aa847b57e5f11 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -361,7 +361,8 @@ def _isreservedname(name): # Trailing dots and spaces are reserved. if name.endswith(('.', ' ')) and name not in ('.', '..'): return True - # The wildcard characters, colon, and pipe (*?"<>:|) are reserved. + # 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 From a8776770afb0b6bc2a6fe14e664483d4bad51f85 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 04:05:29 +0000 Subject: [PATCH 26/35] Tighten up `PurePath.is_reserved()` exception handling. --- Lib/pathlib/_abc.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 75a4a2efd0f5f6..30acba1873a65c 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -443,10 +443,9 @@ def is_absolute(self): def is_reserved(self): """Return True if the path contains one of the special names reserved by the system, if any.""" - try: + if hasattr(self.pathmod, 'isreserved'): return self.pathmod.isreserved(self) - except AttributeError: - return False + return False def match(self, path_pattern, *, case_sensitive=None): """ From 3721c8c3b6c91f394bd30e2196a045e798f2ae94 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 04:07:17 +0000 Subject: [PATCH 27/35] Use `str(self)` to support non-os.PathLike implementations. --- Lib/pathlib/_abc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 30acba1873a65c..816e9268eb688e 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -444,7 +444,7 @@ def is_reserved(self): """Return True if the path contains one of the special names reserved by the system, if any.""" if hasattr(self.pathmod, 'isreserved'): - return self.pathmod.isreserved(self) + return self.pathmod.isreserved(str(self)) return False def match(self, path_pattern, *, case_sensitive=None): From b905d2fae45617248ad8a2418ad7410cdda17aa6 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 17:27:08 +0000 Subject: [PATCH 28/35] Deprecate `pathlib.PurePath.is_reserved()` --- Doc/library/pathlib.rst | 2 ++ Lib/pathlib/__init__.py | 10 ++++++ Lib/pathlib/_abc.py | 7 ---- Lib/test/test_pathlib/test_pathlib.py | 50 +++++---------------------- 4 files changed, 20 insertions(+), 49 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 0940fd49b44881..08c3f1e2af61c2 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -553,6 +553,8 @@ Pure paths provide the following methods and properties: 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 + .. method:: PurePath.joinpath(*pathsegments) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index a432d45bfed3a9..612f3876d09b87 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -261,6 +261,16 @@ def is_relative_to(self, other, /, *_deprecated): other = self.with_segments(other, *_deprecated) return _abc.PurePathBase.is_relative_to(self, other) + def is_reserved(self): + """Return True if the path contains one of the special names reserved + by the system, if any.""" + msg = ("pathlib.PurePath.is_reserved() is deprecated and " + "scheduled for removal in Python 3.15") + 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.""" if not self.is_absolute(): diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 816e9268eb688e..f9d9b313d30cf9 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -440,13 +440,6 @@ def is_absolute(self): else: return self.pathmod.isabs(str(self)) - def is_reserved(self): - """Return True if the path contains one of the special names reserved - by the system, if any.""" - if hasattr(self.pathmod, 'isreserved'): - return self.pathmod.isreserved(str(self)) - return False - def match(self, path_pattern, *, case_sensitive=None): """ Return True if this path matches the given pattern. diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 2e3266ad4a95dc..d5bc6d6ed7c783 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -227,6 +227,12 @@ def test_is_relative_to_several_args(self): 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() + class PurePosixPathTest(PurePathTest): cls = pathlib.PurePosixPath @@ -287,13 +293,6 @@ def test_is_absolute(self): 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') @@ -951,41 +950,6 @@ def test_div(self): 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 may be reserved - self.assertIs(True, 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()) - # All path components matter. - self.assertIs(True, P('c:/baz/con/NUL').is_reserved()) - self.assertIs(True, P('c:/NUL/con/baz').is_reserved()) - class PurePathSubclassTest(PurePathTest): class cls(pathlib.PurePath): @@ -1020,6 +984,8 @@ def tempdir(self): def test_matches_pathbase_api(self): our_names = {name for name in dir(self.cls) if name[0] != '_'} + # is_reserved() is deprecated and PurePath/Path-only + our_names.remove('is_reserved') path_names = {name for name in dir(pathlib._abc.PathBase) if name[0] != '_'} self.assertEqual(our_names, path_names) for attr_name in our_names: From 2756ffb88dc5670544e642550b841dfed63d99d7 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 17:40:32 +0000 Subject: [PATCH 29/35] Add note about approximate and changing Windows rules; remove doctest. --- Doc/library/os.path.rst | 6 ++++++ Doc/library/pathlib.rst | 8 -------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index 7108e0cbb92a79..46bf144a5c7b7d 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -332,6 +332,12 @@ the :mod:`glob` module.) 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. It may be updated in future Python releases as changed rules + become more widely used. + .. availability:: Windows. .. versionadded:: 3.13 diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 08c3f1e2af61c2..720fd141497af8 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -541,14 +541,6 @@ 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. From c03c6726e2676a605be16f05c1c4dfde8d60fedb Mon Sep 17 00:00:00 2001 From: Barney Gale Date: Mon, 8 Jan 2024 17:51:10 +0000 Subject: [PATCH 30/35] Update Doc/library/os.path.rst Co-authored-by: Steve Dower --- Doc/library/os.path.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Doc/library/os.path.rst b/Doc/library/os.path.rst index 46bf144a5c7b7d..952800be16e897 100644 --- a/Doc/library/os.path.rst +++ b/Doc/library/os.path.rst @@ -335,8 +335,9 @@ the :mod:`glob` module.) .. note:: This function approximates rules for reserved paths on most Windows - systems. It may be updated in future Python releases as changed rules - become more widely used. + 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. From 4085ff537d50503292b3117ca19152fec2cced5f Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 17:51:26 +0000 Subject: [PATCH 31/35] Update what's new. --- Doc/whatsnew/3.13.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 3ab6d1ddc6ef21..561c4e8e9aec03 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -302,6 +302,13 @@ os :c:func:`!posix_spawn_file_actions_addclosefrom_np`. (Contributed by Jakub Kulik in :gh:`113117`.) +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`.) + pathlib ------- @@ -474,6 +481,11 @@ 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. + * :mod:`sys`: :func:`sys._enablelegacywindowsfsencoding` function. Replace it with :envvar:`PYTHONLEGACYWINDOWSFSENCODING` environment variable. (Contributed by Inada Naoki in :gh:`73427`.) @@ -685,6 +697,11 @@ 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. + * :class:`typing.NamedTuple`: * The undocumented keyword argument syntax for creating NamedTuple classes From b4b3d0bbb0310d5d033d0ec985a9ba5709dcc240 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 17:52:21 +0000 Subject: [PATCH 32/35] Mention deprecation in NEWS --- .../next/Library/2022-07-31-01-24-40.gh-issue-88569.eU0--b.rst | 2 ++ 1 file changed, 2 insertions(+) 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 index 228736ef230b97..c612d54deac9a2 100644 --- 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 @@ -1,2 +1,4 @@ Add :func:`ntpath.isreserved`, which identifies reserved pathnames on Windows; reserved names include "NUL", "AUX" and "CON". + +Deprecate :meth:`pathlib.PurePath.is_reserved`. From 9e2d21fe5cf156f4bf0079c22e948d3f1a3d9a2c Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 17:59:04 +0000 Subject: [PATCH 33/35] Address review feedback --- Doc/library/pathlib.rst | 3 ++- Lib/pathlib/__init__.py | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 720fd141497af8..c47bebf0cd8a77 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -546,7 +546,8 @@ Pure paths provide the following methods and properties: 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/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 612f3876d09b87..b1defac6e2e901 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -264,8 +264,9 @@ def is_relative_to(self, other, /, *_deprecated): def is_reserved(self): """Return True if the path contains one of the special names reserved by the system, if any.""" - msg = ("pathlib.PurePath.is_reserved() is deprecated and " - "scheduled for removal in Python 3.15") + 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) From 44c37cbc1be2751fd035c32b3510fe1590a4f703 Mon Sep 17 00:00:00 2001 From: barneygale Date: Mon, 8 Jan 2024 18:06:17 +0000 Subject: [PATCH 34/35] Point to `os.path.isreserved()` in whatsnew deprecation notices. --- Doc/whatsnew/3.13.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 561c4e8e9aec03..0632db2f5e3df4 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -484,7 +484,8 @@ Deprecated * :mod:`pathlib`: * :meth:`pathlib.PurePath.is_reserved` is deprecated and scheduled for - removal in Python 3.15. + 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. @@ -700,7 +701,8 @@ Pending Removal in Python 3.15 * :mod:`pathlib`: * :meth:`pathlib.PurePath.is_reserved` is deprecated and scheduled for - removal in Python 3.15. + removal in Python 3.15. Use :func:`os.path.isreserved` to detect reserved + paths on Windows. * :class:`typing.NamedTuple`: From e398c3fca3304a0e063b146fd83193a62388b9bc Mon Sep 17 00:00:00 2001 From: barneygale Date: Tue, 16 Jan 2024 00:43:04 +0000 Subject: [PATCH 35/35] Address review feedback --- Lib/ntpath.py | 1 - .../Library/2022-07-31-01-24-40.gh-issue-88569.eU0--b.rst | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 49946423b9388f..e7cbfe17ecb3c8 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -350,7 +350,6 @@ def isreserved(path): def _isreservedname(name): """Return true if the filename is reserved by the system.""" - name = os.fsdecode(name) # Trailing dots and spaces are reserved. if name.endswith(('.', ' ')) and name not in ('.', '..'): return True 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 index c612d54deac9a2..31dd985bb5c3b6 100644 --- 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 @@ -1,4 +1,4 @@ -Add :func:`ntpath.isreserved`, which identifies reserved pathnames on -Windows; reserved names include "NUL", "AUX" and "CON". +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`.