Skip to content

Commit a124c42

Browse files
Switch to singleton
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 5af66c6 commit a124c42

File tree

8 files changed

+113
-96
lines changed

8 files changed

+113
-96
lines changed

Doc/library/os.path.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ the :mod:`glob` module.)
424424
In particular, :exc:`FileNotFoundError` is raised if *path* does not exist,
425425
or another :exc:`OSError` if it is otherwise inaccessible.
426426

427-
If *strict* is the string ``'allow_missing'``, errors other than
427+
If *strict* is :py:data:`os.path.ALLOW_MISSING`, errors other than
428428
:exc:`FileNotFoundError` are re-raised (as with ``strict=True``).
429429
Thus, the returned path will not contain any symbolic links, but the named
430430
file and some of its parent directories may be missing.
@@ -447,7 +447,13 @@ the :mod:`glob` module.)
447447
The *strict* parameter was added.
448448

449449
.. versionchanged:: next
450-
The ``'allow_missing'`` value for *strict* parameter was added.
450+
The :py:data:`~ntpath.ALLOW_MISSING` value for *strict* parameter was added.
451+
452+
.. data:: ALLOW_MISSING
453+
454+
Special value used for the *strict* argument in :func:`realpath`.
455+
456+
.. versionadded:: next
451457

452458
.. function:: relpath(path, start=os.curdir)
453459

Doc/whatsnew/3.15.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ os.path
116116
-------
117117

118118
* The *strict* parameter to :func:`os.path.realpath` accepts a new value,
119-
``'allow_missing'``.
119+
:data:`os.path.ALLOW_MISSING`.
120120
If used, errors other than :exc:`FileNotFoundError` will be re-raised;
121121
the resulting path can be missing but it will be free of symlinks.
122122
(Contributed by Petr Viktorin for :cve:`2025-4517`.)

Lib/genericpath.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
__all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime',
1010
'getsize', 'isdevdrive', 'isdir', 'isfile', 'isjunction', 'islink',
11-
'lexists', 'samefile', 'sameopenfile', 'samestat']
11+
'lexists', 'samefile', 'sameopenfile', 'samestat', 'ALLOW_MISSING']
1212

1313

1414
# Does a path exist?
@@ -189,3 +189,12 @@ def _check_arg_types(funcname, *args):
189189
f'os.PathLike object, not {s.__class__.__name__!r}') from None
190190
if hasstr and hasbytes:
191191
raise TypeError("Can't mix strings and bytes in path components") from None
192+
193+
# A singleton with a true boolean value.
194+
@object.__new__
195+
class ALLOW_MISSING:
196+
"""Special value for use in realpath()."""
197+
def __repr__(self):
198+
return 'os.path.ALLOW_MISSING'
199+
def __reduce__(self):
200+
return self.__class__.__name__

Lib/ntpath.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"abspath","curdir","pardir","sep","pathsep","defpath","altsep",
3030
"extsep","devnull","realpath","supports_unicode_filenames","relpath",
3131
"samefile", "sameopenfile", "samestat", "commonpath", "isjunction",
32-
"isdevdrive"]
32+
"isdevdrive", "ALLOW_MISSING"]
3333

3434
def _get_bothseps(path):
3535
if isinstance(path, bytes):
@@ -724,7 +724,7 @@ def realpath(path, *, strict=False):
724724
return '\\\\.\\NUL'
725725
had_prefix = path.startswith(prefix)
726726

727-
if strict == 'allow_missing':
727+
if strict is ALLOW_MISSING:
728728
ignored_error = FileNotFoundError
729729
strict = True
730730
elif strict:

Lib/posixpath.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"samefile","sameopenfile","samestat",
3737
"curdir","pardir","sep","pathsep","defpath","altsep","extsep",
3838
"devnull","realpath","supports_unicode_filenames","relpath",
39-
"commonpath", "isjunction","isdevdrive"]
39+
"commonpath", "isjunction","isdevdrive","ALLOW_MISSING"]
4040

4141

4242
def _get_sep(path):
@@ -402,7 +402,7 @@ def realpath(filename, *, strict=False):
402402
curdir = '.'
403403
pardir = '..'
404404
getcwd = os.getcwd
405-
if strict == 'allow_missing':
405+
if strict is ALLOW_MISSING:
406406
ignored_error = FileNotFoundError
407407
strict = True
408408
elif strict:

Lib/tarfile.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -781,7 +781,7 @@ def __init__(self, tarinfo, path):
781781
def _get_filtered_attrs(member, dest_path, for_data=True):
782782
new_attrs = {}
783783
name = member.name
784-
dest_path = os.path.realpath(dest_path, strict='allow_missing')
784+
dest_path = os.path.realpath(dest_path, strict=os.path.ALLOW_MISSING)
785785
# Strip leading / (tar's directory separator) from filenames.
786786
# Include os.sep (target OS directory separator) as well.
787787
if name.startswith(('/', os.sep)):
@@ -792,7 +792,7 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
792792
raise AbsolutePathError(member)
793793
# Ensure we stay in the destination
794794
target_path = os.path.realpath(os.path.join(dest_path, name),
795-
strict='allow_missing')
795+
strict=os.path.ALLOW_MISSING)
796796
if os.path.commonpath([target_path, dest_path]) != dest_path:
797797
raise OutsideDestinationError(member, target_path)
798798
# Limit permissions (no high bits, and go-w)
@@ -840,7 +840,8 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
840840
else:
841841
target_path = os.path.join(dest_path,
842842
member.linkname)
843-
target_path = os.path.realpath(target_path, strict='allow_missing')
843+
target_path = os.path.realpath(target_path,
844+
strict=os.path.ALLOW_MISSING)
844845
if os.path.commonpath([target_path, dest_path]) != dest_path:
845846
raise LinkOutsideDestinationError(member, target_path)
846847
return new_attrs

Lib/test/test_ntpath.py

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
import unittest
88
import warnings
9+
from ntpath import ALLOW_MISSING
910
from test.support import TestFailed, cpython_only, os_helper
1011
from test.support.os_helper import FakePath
1112
from test import test_genericpath
@@ -505,15 +506,15 @@ def test_realpath_curdir_strict(self):
505506

506507
def test_realpath_curdir_missing_ok(self):
507508
expected = ntpath.normpath(os.getcwd())
508-
tester("ntpath.realpath('.', strict='allow_missing')",
509+
tester("ntpath.realpath('.', strict=ALLOW_MISSING)",
509510
expected)
510-
tester("ntpath.realpath('./.', strict='allow_missing')",
511+
tester("ntpath.realpath('./.', strict=ALLOW_MISSING)",
511512
expected)
512-
tester("ntpath.realpath('/'.join(['.'] * 100), strict='allow_missing')",
513+
tester("ntpath.realpath('/'.join(['.'] * 100), strict=ALLOW_MISSING)",
513514
expected)
514-
tester("ntpath.realpath('.\\.', strict='allow_missing')",
515+
tester("ntpath.realpath('.\\.', strict=ALLOW_MISSING)",
515516
expected)
516-
tester("ntpath.realpath('\\'.join(['.'] * 100), strict='allow_missing')",
517+
tester("ntpath.realpath('\\'.join(['.'] * 100), strict=ALLOW_MISSING)",
517518
expected)
518519

519520
def test_realpath_pardir(self):
@@ -542,20 +543,20 @@ def test_realpath_pardir_strict(self):
542543

543544
def test_realpath_pardir_missing_ok(self):
544545
expected = ntpath.normpath(os.getcwd())
545-
tester("ntpath.realpath('..', strict='allow_missing')",
546+
tester("ntpath.realpath('..', strict=ALLOW_MISSING)",
546547
ntpath.dirname(expected))
547-
tester("ntpath.realpath('../..', strict='allow_missing')",
548+
tester("ntpath.realpath('../..', strict=ALLOW_MISSING)",
548549
ntpath.dirname(ntpath.dirname(expected)))
549-
tester("ntpath.realpath('/'.join(['..'] * 50), strict='allow_missing')",
550+
tester("ntpath.realpath('/'.join(['..'] * 50), strict=ALLOW_MISSING)",
550551
ntpath.splitdrive(expected)[0] + '\\')
551-
tester("ntpath.realpath('..\\..', strict='allow_missing')",
552+
tester("ntpath.realpath('..\\..', strict=ALLOW_MISSING)",
552553
ntpath.dirname(ntpath.dirname(expected)))
553-
tester("ntpath.realpath('\\'.join(['..'] * 50), strict='allow_missing')",
554+
tester("ntpath.realpath('\\'.join(['..'] * 50), strict=ALLOW_MISSING)",
554555
ntpath.splitdrive(expected)[0] + '\\')
555556

556557
@os_helper.skip_unless_symlink
557558
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
558-
@_parameterize({}, {'strict': True}, {'strict': 'allow_missing'})
559+
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
559560
def test_realpath_basic(self, kwargs):
560561
ABSTFN = ntpath.abspath(os_helper.TESTFN)
561562
open(ABSTFN, "wb").close()
@@ -603,38 +604,38 @@ def test_realpath_invalid_paths(self):
603604
self.assertEqual(realpath(path, strict=False), path)
604605
# gh-106242: Embedded nulls should raise OSError (not ValueError)
605606
self.assertRaises(OSError, realpath, path, strict=True)
606-
self.assertRaises(OSError, realpath, path, strict='allow_missing')
607+
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
607608
path = ABSTFNb + b'\x00'
608609
self.assertEqual(realpath(path, strict=False), path)
609610
self.assertRaises(OSError, realpath, path, strict=True)
610-
self.assertRaises(OSError, realpath, path, strict='allow_missing')
611+
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
611612
path = ABSTFN + '\\nonexistent\\x\x00'
612613
self.assertEqual(realpath(path, strict=False), path)
613614
self.assertRaises(OSError, realpath, path, strict=True)
614-
self.assertRaises(OSError, realpath, path, strict='allow_missing')
615+
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
615616
path = ABSTFNb + b'\\nonexistent\\x\x00'
616617
self.assertEqual(realpath(path, strict=False), path)
617618
self.assertRaises(OSError, realpath, path, strict=True)
618-
self.assertRaises(OSError, realpath, path, strict='allow_missing')
619+
self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING)
619620
path = ABSTFN + '\x00\\..'
620621
self.assertEqual(realpath(path, strict=False), os.getcwd())
621622
self.assertEqual(realpath(path, strict=True), os.getcwd())
622-
self.assertEqual(realpath(path, strict='allow_missing'), os.getcwd())
623+
self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwd())
623624
path = ABSTFNb + b'\x00\\..'
624625
self.assertEqual(realpath(path, strict=False), os.getcwdb())
625626
self.assertEqual(realpath(path, strict=True), os.getcwdb())
626-
self.assertEqual(realpath(path, strict='allow_missing'), os.getcwdb())
627+
self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwdb())
627628
path = ABSTFN + '\\nonexistent\\x\x00\\..'
628629
self.assertEqual(realpath(path, strict=False), ABSTFN + '\\nonexistent')
629630
self.assertRaises(OSError, realpath, path, strict=True)
630-
self.assertEqual(realpath(path, strict='allow_missing'), ABSTFN + '\\nonexistent')
631+
self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + '\\nonexistent')
631632
path = ABSTFNb + b'\\nonexistent\\x\x00\\..'
632633
self.assertEqual(realpath(path, strict=False), ABSTFNb + b'\\nonexistent')
633634
self.assertRaises(OSError, realpath, path, strict=True)
634-
self.assertEqual(realpath(path, strict='allow_missing'), ABSTFNb + b'\\nonexistent')
635+
self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFNb + b'\\nonexistent')
635636

636637
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
637-
@_parameterize({}, {'strict': True}, {'strict': 'allow_missing'})
638+
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
638639
def test_realpath_invalid_unicode_paths(self, kwargs):
639640
realpath = ntpath.realpath
640641
ABSTFN = ntpath.abspath(os_helper.TESTFN)
@@ -654,7 +655,7 @@ def test_realpath_invalid_unicode_paths(self, kwargs):
654655

655656
@os_helper.skip_unless_symlink
656657
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
657-
@_parameterize({}, {'strict': True}, {'strict': 'allow_missing'})
658+
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
658659
def test_realpath_relative(self, kwargs):
659660
ABSTFN = ntpath.abspath(os_helper.TESTFN)
660661
open(ABSTFN, "wb").close()
@@ -815,7 +816,7 @@ def test_realpath_symlink_loops_strict(self):
815816
@os_helper.skip_unless_symlink
816817
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
817818
def test_realpath_symlink_loops_raise(self):
818-
# Symlink loops raise OSError in 'allow_missing' mode
819+
# Symlink loops raise OSError in ALLOW_MISSING mode
819820
ABSTFN = ntpath.abspath(os_helper.TESTFN)
820821
self.addCleanup(os_helper.unlink, ABSTFN)
821822
self.addCleanup(os_helper.unlink, ABSTFN + "1")
@@ -826,16 +827,16 @@ def test_realpath_symlink_loops_raise(self):
826827
self.addCleanup(os_helper.unlink, ABSTFN + "x")
827828

828829
os.symlink(ABSTFN, ABSTFN)
829-
self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict='allow_missing')
830+
self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict=ALLOW_MISSING)
830831

831832
os.symlink(ABSTFN + "1", ABSTFN + "2")
832833
os.symlink(ABSTFN + "2", ABSTFN + "1")
833834
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1",
834-
strict='allow_missing')
835+
strict=ALLOW_MISSING)
835836
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "2",
836-
strict='allow_missing')
837+
strict=ALLOW_MISSING)
837838
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\x",
838-
strict='allow_missing')
839+
strict=ALLOW_MISSING)
839840

840841
# Windows eliminates '..' components before resolving links;
841842
# realpath is not expected to raise if this removes the loop.
@@ -851,24 +852,24 @@ def test_realpath_symlink_loops_raise(self):
851852
self.assertRaises(
852853
OSError, ntpath.realpath,
853854
ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1",
854-
strict='allow_missing')
855+
strict=ALLOW_MISSING)
855856

856857
os.symlink(ntpath.basename(ABSTFN) + "a\\b", ABSTFN + "a")
857858
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a",
858-
strict='allow_missing')
859+
strict=ALLOW_MISSING)
859860

860861
os.symlink("..\\" + ntpath.basename(ntpath.dirname(ABSTFN))
861862
+ "\\" + ntpath.basename(ABSTFN) + "c", ABSTFN + "c")
862863
self.assertRaises(OSError, ntpath.realpath, ABSTFN + "c",
863-
strict='allow_missing')
864+
strict=ALLOW_MISSING)
864865

865866
# Test using relative path as well.
866867
self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN),
867-
strict='allow_missing')
868+
strict=ALLOW_MISSING)
868869

869870
@os_helper.skip_unless_symlink
870871
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
871-
@_parameterize({}, {'strict': True}, {'strict': 'allow_missing'})
872+
@_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING})
872873
def test_realpath_symlink_prefix(self, kwargs):
873874
ABSTFN = ntpath.abspath(os_helper.TESTFN)
874875
self.addCleanup(os_helper.unlink, ABSTFN + "3")
@@ -906,7 +907,7 @@ def test_realpath_nul(self):
906907
tester("ntpath.realpath('NUL')", r'\\.\NUL')
907908
tester("ntpath.realpath('NUL', strict=False)", r'\\.\NUL')
908909
tester("ntpath.realpath('NUL', strict=True)", r'\\.\NUL')
909-
tester("ntpath.realpath('NUL', strict='allow_missing')", r'\\.\NUL')
910+
tester("ntpath.realpath('NUL', strict=ALLOW_MISSING)", r'\\.\NUL')
910911

911912
@unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname')
912913
@unittest.skipUnless(HAVE_GETSHORTPATHNAME, 'need _getshortpathname')
@@ -930,7 +931,7 @@ def test_realpath_cwd(self):
930931

931932
self.assertPathEqual(test_file_long, ntpath.realpath(test_file_short))
932933

933-
for kwargs in {}, {'strict': True}, {'strict': 'allow_missing'}:
934+
for kwargs in {}, {'strict': True}, {'strict': ALLOW_MISSING}:
934935
with self.subTest(**kwargs):
935936
with os_helper.change_cwd(test_dir_long):
936937
self.assertPathEqual(

0 commit comments

Comments
 (0)