diff --git a/Lib/_py_abc.py b/Lib/_py_abc.py index c870ae9048b..2bfb9489a7a 100644 --- a/Lib/_py_abc.py +++ b/Lib/_py_abc.py @@ -92,7 +92,12 @@ def _abc_caches_clear(cls): def __instancecheck__(cls, instance): """Override for isinstance(instance, cls).""" # Inline the cache checking - subclass = instance.__class__ + try: + subclass = instance.__class__ + except AttributeError: + # Fall back to the type when the instance has no __class__, + # matching the behaviour of the built-in isinstance() (gh-153772). + subclass = type(instance) if subclass in cls._abc_cache: return True subtype = type(instance) diff --git a/Lib/functools.py b/Lib/functools.py index e8ccc2795d2..c5aaa15f3d5 100644 --- a/Lib/functools.py +++ b/Lib/functools.py @@ -19,7 +19,7 @@ # import weakref # Deferred to single_dispatch() from operator import itemgetter from reprlib import recursive_repr -from types import GenericAlias, MethodType, MappingProxyType, UnionType +from types import FunctionType, GenericAlias, MethodType, MappingProxyType, UnionType from _thread import RLock ################################################################################ @@ -170,7 +170,7 @@ def _lt_from_ge(self, other): return op_result return not op_result -_convert = { +_convert = frozendict({ '__lt__': [('__gt__', _gt_from_lt), ('__le__', _le_from_lt), ('__ge__', _ge_from_lt)], @@ -183,7 +183,7 @@ def _lt_from_ge(self, other): '__ge__': [('__le__', _le_from_ge), ('__gt__', _gt_from_ge), ('__lt__', _lt_from_ge)] -} +}) def total_ordering(cls): """Class decorator that fills in missing ordering methods""" @@ -232,7 +232,7 @@ def __ge__(self, other): ### reduce() sequence to a single item ################################################################################ -_initial_missing = object() +_initial_missing = sentinel('_initial_missing') def reduce(function, sequence, initial=_initial_missing): """ @@ -517,7 +517,7 @@ def _unwrap_partialmethod(func): ### LRU Cache function decorator ################################################################################ -_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) +_CacheInfo = namedtuple("CacheInfo", ("hits", "misses", "maxsize", "currsize")) def _make_key(args, kwds, typed, kwd_mark = (object(),), @@ -539,13 +539,15 @@ def _make_key(args, kwds, typed, # distinct call from f(y=2, x=1) which will be cached separately. key = args if kwds: + key = list(key) key += kwd_mark for item in kwds.items(): key += item + key = tuple(key) if typed: - key += tuple(type(v) for v in args) + key += tuple([type(v) for v in args]) if kwds: - key += tuple(type(v) for v in kwds.values()) + key += tuple([type(v) for v in kwds.values()]) elif len(key) == 1 and type(key[0]) in fasttypes: return key[0] return key @@ -580,12 +582,14 @@ def lru_cache(maxsize=128, typed=False): # Negative maxsize is treated as 0 if maxsize < 0: maxsize = 0 + elif callable(maxsize) and isinstance(typed, bool): # The user_function was passed in directly via the maxsize argument user_function, maxsize = maxsize, 128 wrapper = _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo) wrapper.cache_parameters = lambda : {'maxsize': maxsize, 'typed': typed} return update_wrapper(wrapper, user_function) + elif maxsize is not None: raise TypeError( 'Expected first argument to be an integer, a callable, or None') @@ -598,6 +602,9 @@ def decorating_function(user_function): return decorating_function def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo): + if not callable(user_function): + raise TypeError("the first argument must be callable") + # Constants shared by all lru cache instances: sentinel = object() # unique object used to signal cache misses make_key = _make_key # build a key from the function arguments @@ -617,6 +624,7 @@ def _lru_cache_wrapper(user_function, maxsize, typed, _CacheInfo): def wrapper(*args, **kwds): # No caching -- just a statistics update nonlocal misses + misses += 1 result = user_function(*args, **kwds) return result @@ -626,6 +634,7 @@ def wrapper(*args, **kwds): def wrapper(*args, **kwds): # Simple caching without ordering or size limit nonlocal hits, misses + key = make_key(args, kwds, typed) result = cache_get(key, sentinel) if result is not sentinel: @@ -641,7 +650,9 @@ def wrapper(*args, **kwds): def wrapper(*args, **kwds): # Size limited caching that tracks accesses by recency nonlocal root, hits, misses, full + key = make_key(args, kwds, typed) + with lock: link = cache_get(key) if link is not None: @@ -656,7 +667,9 @@ def wrapper(*args, **kwds): hits += 1 return result misses += 1 + result = user_function(*args, **kwds) + with lock: if key in cache: # Getting here means that this same key was added to the @@ -664,11 +677,13 @@ def wrapper(*args, **kwds): # update is already done, we need only return the # computed result and update the count of misses. pass + elif full: # Use the old root to store the new key and result. oldroot = root oldroot[KEY] = key oldroot[RESULT] = result + # Empty the oldest link and make it the new root. # Keep a reference to the old key and old result to # prevent their ref counts from going to zero during the @@ -677,22 +692,27 @@ def wrapper(*args, **kwds): # still adjusting the links. root = oldroot[NEXT] oldkey = root[KEY] - oldresult = root[RESULT] + oldresult = root[RESULT] # noqa: F841 root[KEY] = root[RESULT] = None + # Now update the cache dictionary. del cache[oldkey] + # Save the potentially reentrant cache[key] assignment # for last, after the root and links have been put in # a consistent state. cache[key] = oldroot + else: # Put result in a new link at the front of the queue. last = root[PREV] link = [last, root, key, result] last[NEXT] = root[PREV] = cache[key] = link + # Use the cache_len bound method instead of the len() function # which could potentially be wrapped in an lru_cache itself. full = (cache_len() >= maxsize) + return result def cache_info(): @@ -703,6 +723,7 @@ def cache_info(): def cache_clear(): """Clear the cache and cache statistics""" nonlocal hits, misses, full + with lock: cache.clear() root[:] = [root, root, None, None] @@ -995,7 +1016,8 @@ def wrapper(*args, **kw): class singledispatchmethod: """Single-dispatch generic method descriptor. - Supports wrapping existing descriptors. + Supports wrapping existing descriptors and handles non-descriptor + callables as instance methods. """ def __init__(self, func): @@ -1038,6 +1060,11 @@ def __init__(self, unbound, obj, cls): # Set instance attributes which cannot be handled in __getattr__() # because they conflict with type descriptors. func = unbound.func + + # Dispatch on the second argument if a generic method turns into + # a bound method on instance-level access. See GH-143535. + self._dispatch_arg_index = 1 if obj is None and isinstance(func, FunctionType) else 0 + try: self.__module__ = func.__module__ except AttributeError: @@ -1066,7 +1093,23 @@ def __call__(self, /, *args, **kwargs): 'singledispatchmethod method') raise TypeError(f'{funcname} requires at least ' '1 positional argument') - return self._dispatch(args[0].__class__).__get__(self._obj, self._cls)(*args, **kwargs) + method = self._dispatch(args[self._dispatch_arg_index].__class__) + + if hasattr(method, "__get__"): + # If the method is a descriptor, it might be necessary + # to drop the first argument before calling + # as it can be no longer expected after descriptor access. + skip_bound_arg = False + if isinstance(method, staticmethod): + skip_bound_arg = self._dispatch_arg_index == 1 + + method = method.__get__(self._obj, self._cls) + if isinstance(method, MethodType): + skip_bound_arg = self._dispatch_arg_index == 1 + + if skip_bound_arg: + return method(*args[1:], **kwargs) + return method(*args, **kwargs) def __getattr__(self, name): # Resolve these attributes lazily to speed up creation of diff --git a/Lib/genericpath.py b/Lib/genericpath.py index 9363f564aab..71ae1919083 100644 --- a/Lib/genericpath.py +++ b/Lib/genericpath.py @@ -8,7 +8,8 @@ __all__ = ['commonprefix', 'exists', 'getatime', 'getctime', 'getmtime', 'getsize', 'isdevdrive', 'isdir', 'isfile', 'isjunction', 'islink', - 'lexists', 'samefile', 'sameopenfile', 'samestat', 'ALLOW_MISSING'] + 'lexists', 'samefile', 'sameopenfile', 'samestat', + 'ALL_BUT_LAST', 'ALLOW_MISSING'] # Does a path exist? @@ -81,29 +82,38 @@ def isdevdrive(path): return False -def getsize(filename): +def getsize(filename, /): """Return the size of a file, reported by os.stat().""" return os.stat(filename).st_size -def getmtime(filename): +def getmtime(filename, /): """Return the last modification time of a file, reported by os.stat().""" return os.stat(filename).st_mtime -def getatime(filename): +def getatime(filename, /): """Return the last access time of a file, reported by os.stat().""" return os.stat(filename).st_atime -def getctime(filename): +def getctime(filename, /): """Return the metadata change time of a file, reported by os.stat().""" return os.stat(filename).st_ctime # Return the longest prefix of all list elements. -def commonprefix(m): +def commonprefix(m, /): "Given a list of pathnames, returns the longest common leading component" + import warnings + warnings.warn('os.path.commonprefix() is deprecated. Use ' + 'os.path.commonpath() for longest path prefix.', + category=DeprecationWarning, + stacklevel=2) + return _commonprefix(m) + +def _commonprefix(m, /): + "Internal implementation of commonprefix()" if not m: return '' # Some people pass in a list of pathname parts to operate in an OS-agnostic # fashion; don't try to translate in that case as that's an abuse of the @@ -120,14 +130,14 @@ def commonprefix(m): # Are two stat buffers (obtained from stat, fstat or lstat) # describing the same file? -def samestat(s1, s2): +def samestat(s1, s2, /): """Test whether two stat buffers reference the same file""" return (s1.st_ino == s2.st_ino and s1.st_dev == s2.st_dev) # Are two filenames really pointing to the same file? -def samefile(f1, f2): +def samefile(f1, f2, /): """Test whether two pathnames reference the same actual file or directory This is determined by the device number and i-node number and @@ -190,7 +200,17 @@ def _check_arg_types(funcname, *args): if hasstr and hasbytes: raise TypeError("Can't mix strings and bytes in path components") from None -# A singleton with a true boolean value. + +# Singletons with a true boolean value. + +@object.__new__ +class ALL_BUT_LAST: + """Special value for use in realpath().""" + def __repr__(self): + return 'os.path.ALL_BUT_LAST' + def __reduce__(self): + return self.__class__.__name__ + @object.__new__ class ALLOW_MISSING: """Special value for use in realpath().""" diff --git a/Lib/gettext.py b/Lib/gettext.py index 6c11ab2b1eb..2f77f0e849e 100644 --- a/Lib/gettext.py +++ b/Lib/gettext.py @@ -111,8 +111,9 @@ def _error(value): ('+', '-'), ('*', '/', '%'), ) -_binary_ops = {op: i for i, ops in enumerate(_binary_ops, 1) for op in ops} -_c2py_ops = {'||': 'or', '&&': 'and', '/': '//'} +_binary_ops = frozendict({op: i for i, ops in enumerate(_binary_ops, 1) + for op in ops}) +_c2py_ops = frozendict({'||': 'or', '&&': 'and', '/': '//'}) def _parse(tokens, priority=-1): diff --git a/Lib/mimetypes.py b/Lib/mimetypes.py index e7581d2ae0a..6d9278bccf9 100644 --- a/Lib/mimetypes.py +++ b/Lib/mimetypes.py @@ -244,7 +244,7 @@ def read(self, filename, strict=True): list of standard types, else to the list of non-standard types. """ - with open(filename, encoding='utf-8', errors='surrogateescape') as fp: + with open(filename, encoding='utf-8') as fp: self.readfp(fp, strict) def readfp(self, fp, strict=True): @@ -435,7 +435,7 @@ def init(files=None): def read_mime_types(file): try: - f = open(file, encoding='utf-8', errors='surrogateescape') + f = open(file, encoding='utf-8') except OSError: return None with f: @@ -477,6 +477,8 @@ def _default_mime_types(): types_map = _types_map_default = { '.js' : 'text/javascript', '.mjs' : 'text/javascript', + '.dcm' : 'application/dicom', + '.efi' : 'application/efi', '.epub' : 'application/epub+zip', '.gz' : 'application/gzip', '.json' : 'application/json', @@ -486,26 +488,35 @@ def _default_mime_types(): '.wiz' : 'application/msword', '.nq' : 'application/n-quads', '.nt' : 'application/n-triples', + '.cjs' : 'application/node', '.bin' : 'application/octet-stream', '.a' : 'application/octet-stream', - '.dll' : 'application/octet-stream', - '.exe' : 'application/octet-stream', '.o' : 'application/octet-stream', '.obj' : 'application/octet-stream', '.so' : 'application/octet-stream', '.oda' : 'application/oda', '.ogx' : 'application/ogg', '.pdf' : 'application/pdf', + '.ai' : 'application/pdf', '.p7c' : 'application/pkcs7-mime', '.ps' : 'application/postscript', - '.ai' : 'application/postscript', '.eps' : 'application/postscript', + '.rtf' : 'application/rtf', + '.sql' : 'application/sql', + '.texi' : 'application/texinfo', + '.texinfo': 'application/texinfo', + '.toml' : 'application/toml', '.trig' : 'application/trig', '.m3u' : 'application/vnd.apple.mpegurl', '.m3u8' : 'application/vnd.apple.mpegurl', + '.dll' : 'application/vnd.microsoft.portable-executable', + '.exe' : 'application/vnd.microsoft.portable-executable', + '.cab' : 'application/vnd.ms-cab-compressed', '.xls' : 'application/vnd.ms-excel', '.xlb' : 'application/vnd.ms-excel', '.eot' : 'application/vnd.ms-fontobject', + '.chm' : 'application/vnd.ms-htmlhelp', + '.thmx' : 'application/vnd.ms-officetheme', '.ppt' : 'application/vnd.ms-powerpoint', '.pot' : 'application/vnd.ms-powerpoint', '.ppa' : 'application/vnd.ms-powerpoint', @@ -519,6 +530,8 @@ def _default_mime_types(): '.xlsx' : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', '.docx' : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', '.rar' : 'application/vnd.rar', + '.sqlite3': 'application/vnd.sqlite3', + '.sqlite' : 'application/vnd.sqlite3', '.wasm' : 'application/wasm', '.7z' : 'application/x-7z-compressed', '.bcpio' : 'application/x-bcpio', @@ -548,8 +561,6 @@ def _default_mime_types(): '.tar' : 'application/x-tar', '.tcl' : 'application/x-tcl', '.tex' : 'application/x-tex', - '.texi' : 'application/x-texinfo', - '.texinfo': 'application/x-texinfo', '.roff' : 'application/x-troff', '.t' : 'application/x-troff', '.tr' : 'application/x-troff', @@ -587,11 +598,15 @@ def _default_mime_types(): '.aiff' : 'audio/x-aiff', '.ra' : 'audio/x-pn-realaudio', '.wav' : 'audio/vnd.wave', + '.weba' : 'audio/webm', + '.ttc' : 'font/collection', '.otf' : 'font/otf', '.ttf' : 'font/ttf', - '.weba' : 'audio/webm', '.woff' : 'font/woff', '.woff2' : 'font/woff2', + '.hjif' : 'haptics/hjif', + '.hmpg' : 'haptics/hmpg', + '.ivs' : 'haptics/ivs', '.avif' : 'image/avif', '.bmp' : 'image/bmp', '.emf' : 'image/emf', @@ -605,6 +620,7 @@ def _default_mime_types(): '.jpeg' : 'image/jpeg', '.jpm' : 'image/jpm', '.jpx' : 'image/jpx', + '.jxl' : 'image/jxl', '.heic' : 'image/heic', '.heif' : 'image/heif', '.png' : 'image/png', @@ -647,7 +663,6 @@ def _default_mime_types(): '.pl' : 'text/plain', '.srt' : 'text/plain', '.rtx' : 'text/richtext', - '.rtf' : 'text/rtf', '.tsv' : 'text/tab-separated-values', '.vtt' : 'text/vtt', '.py' : 'text/x-python', @@ -680,11 +695,9 @@ def _default_mime_types(): # Please sort these too common_types = _common_types_default = { - '.rtf' : 'application/rtf', '.apk' : 'application/vnd.android.package-archive', '.midi': 'audio/midi', '.mid' : 'audio/midi', - '.jpg' : 'image/jpg', '.pict': 'image/pict', '.pct' : 'image/pict', '.pic' : 'image/pict', @@ -699,7 +712,7 @@ def _parse_args(args): from argparse import ArgumentParser parser = ArgumentParser( - description='map filename extensions to MIME types', color=True + description='map filename extensions to MIME types', ) parser.add_argument( '-e', '--extension', diff --git a/Lib/ntpath.py b/Lib/ntpath.py index 36da5b7cb0f..b3c23f0abc2 100644 --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -29,7 +29,7 @@ "abspath","curdir","pardir","sep","pathsep","defpath","altsep", "extsep","devnull","realpath","supports_unicode_filenames","relpath", "samefile", "sameopenfile", "samestat", "commonpath", "isjunction", - "isdevdrive", "ALLOW_MISSING"] + "isdevdrive", "ALL_BUT_LAST", "ALLOW_MISSING"] def _get_bothseps(path): if isinstance(path, bytes): @@ -47,7 +47,7 @@ def _get_bothseps(path): LOCALE_NAME_INVARIANT as _LOCALE_NAME_INVARIANT, LCMAP_LOWERCASE as _LCMAP_LOWERCASE) - def normcase(s): + def normcase(s, /): """Normalize case of pathname. Makes all characters lowercase and all slashes into backslashes. @@ -66,7 +66,7 @@ def normcase(s): _LCMAP_LOWERCASE, s.replace('/', '\\')) except ImportError: - def normcase(s): + def normcase(s, /): """Normalize case of pathname. Makes all characters lowercase and all slashes into backslashes. @@ -77,7 +77,7 @@ def normcase(s): return s.replace('/', '\\').lower() -def isabs(s): +def isabs(s, /): """Test whether a path is absolute""" s = os.fspath(s) if isinstance(s, bytes): @@ -96,7 +96,7 @@ def isabs(s): # Join two (or more) paths. -def join(path, *paths): +def join(path, /, *paths): path = os.fspath(path) if isinstance(path, bytes): sep = b'\\' @@ -143,7 +143,7 @@ def join(path, *paths): # Split a path in a drive specification (a drive letter followed by a # colon) and the path specification. # It is always true that drivespec + pathspec == p -def splitdrive(p): +def splitdrive(p, /): """Split a pathname into drive/UNC sharepoint and relative path specifiers. Returns a 2-tuple (drive_or_unc, path); either part may be empty. @@ -171,7 +171,7 @@ def splitdrive(p): try: from nt import _path_splitroot_ex as splitroot except ImportError: - def splitroot(p): + def splitroot(p, /): """Split a pathname into drive, root and tail. The tail contains anything after the root.""" @@ -221,7 +221,7 @@ def splitroot(p): # join(head, tail) == p holds. # The resulting head won't end in '/' unless it is the root. -def split(p): +def split(p, /): """Split a pathname. Return tuple (head, tail) where tail is everything after the final @@ -242,7 +242,7 @@ def split(p): # pathname component; the root is everything before that. # It is always true that root + ext == p. -def splitext(p): +def splitext(p, /): p = os.fspath(p) if isinstance(p, bytes): return genericpath._splitext(p, b'\\', b'/', b'.') @@ -253,14 +253,14 @@ def splitext(p): # Return the tail (basename) part of a path. -def basename(p): +def basename(p, /): """Returns the final component of a pathname""" return split(p)[1] # Return the head (dirname) part of a path. -def dirname(p): +def dirname(p, /): """Returns the directory component of a pathname""" return split(p)[0] @@ -559,7 +559,7 @@ def abspath(path): from nt import _findfirstfile, _getfinalpathname, readlink as _nt_readlink except ImportError: # realpath is a no-op on systems without _getfinalpathname support. - def realpath(path, *, strict=False): + def realpath(path, /, *, strict=False): return abspath(path) else: def _readlink_deep(path, ignored_error=OSError): @@ -660,7 +660,7 @@ def _getfinalpathname_nonstrict(path, ignored_error=OSError): tail = join(name, tail) if tail else name return tail - def realpath(path, *, strict=False): + def realpath(path, /, *, strict=False): path = normpath(path) if isinstance(path, bytes): prefix = b'\\\\?\\' @@ -684,7 +684,8 @@ def realpath(path, *, strict=False): if strict is ALLOW_MISSING: ignored_error = FileNotFoundError - strict = True + elif strict is ALL_BUT_LAST: + ignored_error = FileNotFoundError elif strict: ignored_error = () else: @@ -704,6 +705,12 @@ def realpath(path, *, strict=False): raise OSError(str(ex)) from None path = normpath(path) except ignored_error as ex: + if strict is ALL_BUT_LAST: + dirname, basename = split(path) + if not basename: + dirname, basename = split(path) + if not isdir(dirname): + raise initial_winerror = ex.winerror path = _getfinalpathname_nonstrict(path, ignored_error=ignored_error) @@ -721,7 +728,7 @@ def realpath(path, *, strict=False): try: if _getfinalpathname(spath) == path: path = spath - except ValueError as ex: + except ValueError: # Unexpected, as an invalid path should not have gained a prefix # at any point, but we ignore this error just in case. pass diff --git a/Lib/optparse.py b/Lib/optparse.py index 38cf16d21ef..2928f8d7774 100644 --- a/Lib/optparse.py +++ b/Lib/optparse.py @@ -1,12 +1,5 @@ """A powerful, extensible, and easy-to-use option parser. -By Greg Ward - -Originally distributed as Optik. - -For support, use the optik-users@lists.sourceforge.net mailing list -(http://lists.sourceforge.net/lists/listinfo/optik-users). - Simple usage example: from optparse import OptionParser @@ -21,8 +14,6 @@ (options, args) = parser.parse_args() """ -__version__ = "1.5.3" - __all__ = ['Option', 'make_option', 'SUPPRESS_HELP', @@ -409,10 +400,12 @@ def _parse_num(val, type): def _parse_int(val): return _parse_num(val, int) -_builtin_cvt = { "int" : (_parse_int, _("integer")), - "long" : (_parse_int, _("integer")), - "float" : (float, _("floating-point")), - "complex" : (complex, _("complex")) } +_builtin_cvt = frozendict({ + "int": (_parse_int, _("integer")), + "long": (_parse_int, _("integer")), + "float": (float, _("floating-point")), + "complex": (complex, _("complex")), +}) def check_builtin(option, opt, value): (cvt, what) = _builtin_cvt[option.type] @@ -1374,7 +1367,7 @@ def parse_args(self, args=None, values=None): self.values = values try: - stop = self._process_args(largs, rargs, values) + self._process_args(largs, rargs, values) except (BadOptionError, OptionValueError) as err: self.error(str(err)) @@ -1669,3 +1662,12 @@ def _match_abbrev(s, wordmap): # which will become a factory function when there are many Option # classes. make_option = Option + + +def __getattr__(name): + if name == "__version__": + from warnings import _deprecated + + _deprecated("__version__", remove=(3, 20)) + return "1.5.3" # Do not change + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/Lib/posixpath.py b/Lib/posixpath.py index ad86cc06c01..8025b063397 100644 --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -36,7 +36,8 @@ "samefile","sameopenfile","samestat", "curdir","pardir","sep","pathsep","defpath","altsep","extsep", "devnull","realpath","supports_unicode_filenames","relpath", - "commonpath", "isjunction","isdevdrive","ALLOW_MISSING"] + "commonpath","isjunction","isdevdrive", + "ALL_BUT_LAST","ALLOW_MISSING"] def _get_sep(path): @@ -50,7 +51,7 @@ def _get_sep(path): # normalizations (such as optimizing '../' away) are not allowed # (another function should be defined to do that). -def normcase(s): +def normcase(s, /): """Normalize case of pathname. Has no effect under Posix""" return os.fspath(s) @@ -58,7 +59,7 @@ def normcase(s): # Return whether a path is absolute. # Trivial in Posix, harder on the Mac or MS-DOS. -def isabs(s): +def isabs(s, /): """Test whether a path is absolute""" s = os.fspath(s) sep = _get_sep(s) @@ -69,7 +70,7 @@ def isabs(s): # Ignore the previous parts if a part is absolute. # Insert a '/' unless the first part is empty or already ends in '/'. -def join(a, *p): +def join(a, /, *p): """Join two or more pathname components, inserting '/' as needed. If any component is an absolute path, all previous path components will be discarded. An empty last part will result in a path that @@ -97,7 +98,7 @@ def join(a, *p): # '/' in the path, head will be empty. # Trailing '/'es are stripped from head unless it is the root. -def split(p): +def split(p, /): """Split a pathname. Returns tuple "(head, tail)" where "tail" is everything after the final slash. Either part may be empty.""" p = os.fspath(p) @@ -114,7 +115,7 @@ def split(p): # pathname component; the root is everything before that. # It is always true that root + ext == p. -def splitext(p): +def splitext(p, /): p = os.fspath(p) if isinstance(p, bytes): sep = b'/' @@ -128,7 +129,7 @@ def splitext(p): # Split a pathname into a drive specification and the rest of the # path. Useful on DOS/Windows/NT; on Unix, the drive is always empty. -def splitdrive(p): +def splitdrive(p, /): """Split a pathname into drive and path. On Posix, drive is always empty.""" p = os.fspath(p) @@ -138,7 +139,7 @@ def splitdrive(p): try: from posix import _path_splitroot_ex as splitroot except ImportError: - def splitroot(p): + def splitroot(p, /): """Split a pathname into drive, root and tail. The tail contains anything after the root.""" @@ -163,7 +164,7 @@ def splitroot(p): # Return the tail (basename) part of a path, same as split(path)[1]. -def basename(p): +def basename(p, /): """Returns the final component of a pathname""" p = os.fspath(p) sep = _get_sep(p) @@ -173,7 +174,7 @@ def basename(p): # Return the head (dirname) part of a path, same as split(path)[0]. -def dirname(p): +def dirname(p, /): """Returns the directory component of a pathname""" p = os.fspath(p) sep = _get_sep(p) @@ -385,7 +386,7 @@ def abspath(path): # Return a canonical path (i.e. the absolute location of a file on the # filesystem). -def realpath(filename, *, strict=False): +def realpath(filename, /, *, strict=False): """Return the canonical path of the specified filename, eliminating any symbolic links encountered in the path.""" filename = os.fspath(filename) @@ -401,7 +402,8 @@ def realpath(filename, *, strict=False): getcwd = os.getcwd if strict is ALLOW_MISSING: ignored_error = FileNotFoundError - strict = True + elif strict is ALL_BUT_LAST: + ignored_error = FileNotFoundError elif strict: ignored_error = () else: @@ -415,7 +417,7 @@ def realpath(filename, *, strict=False): # indicates that a symlink target has been resolved, and that the original # symlink path can be retrieved by popping again. The [::-1] slice is a # very fast way of spelling list(reversed(...)). - rest = filename.split(sep)[::-1] + rest = filename.rstrip(sep).split(sep)[::-1] # Number of unprocessed parts in 'rest'. This can differ from len(rest) # later, because 'rest' might contain markers for unresolved symlinks. @@ -424,6 +426,7 @@ def realpath(filename, *, strict=False): # The resolved path, which is absolute throughout this function. # Note: getcwd() returns a normalized and symlink-free path. path = sep if filename.startswith(sep) else getcwd() + trailing_sep = filename.endswith(sep) # Mapping from symlink paths to *fully resolved* symlink targets. If a # symlink is encountered but not yet resolved, the value is None. This is @@ -456,7 +459,8 @@ def realpath(filename, *, strict=False): try: st_mode = lstat(newpath).st_mode if not stat.S_ISLNK(st_mode): - if strict and part_count and not stat.S_ISDIR(st_mode): + if (strict and (part_count or trailing_sep) + and not stat.S_ISDIR(st_mode)): raise OSError(errno.ENOTDIR, os.strerror(errno.ENOTDIR), newpath) path = newpath @@ -483,7 +487,8 @@ def realpath(filename, *, strict=False): continue target = readlink(newpath) except ignored_error: - pass + if strict is ALL_BUT_LAST and part_count: + raise else: # Resolve the symbolic link if target.startswith(sep): @@ -537,7 +542,7 @@ def relpath(path, start=None): start_list = start_tail.split(sep) if start_tail else [] path_list = path_tail.split(sep) if path_tail else [] # Work out how much of the filepath is shared by start and path. - i = len(commonprefix([start_list, path_list])) + i = len(genericpath._commonprefix([start_list, path_list])) rel_list = [pardir] * (len(start_list)-i) + path_list[i:] if not rel_list: diff --git a/Lib/stat.py b/Lib/stat.py index 81f694329bf..214c7917b5e 100644 --- a/Lib/stat.py +++ b/Lib/stat.py @@ -205,6 +205,21 @@ def filemode(mode): FILE_ATTRIBUTE_VIRTUAL = 65536 +# Linux STATX_ATTR constants for interpreting os.statx()'s +# "stx_attributes" and "stx_attributes_mask" members + +STATX_ATTR_COMPRESSED = 0x00000004 +STATX_ATTR_IMMUTABLE = 0x00000010 +STATX_ATTR_APPEND = 0x00000020 +STATX_ATTR_NODUMP = 0x00000040 +STATX_ATTR_ENCRYPTED = 0x00000800 +STATX_ATTR_AUTOMOUNT = 0x00001000 +STATX_ATTR_MOUNT_ROOT = 0x00002000 +STATX_ATTR_VERITY = 0x00100000 +STATX_ATTR_DAX = 0x00200000 +STATX_ATTR_WRITE_ATOMIC = 0x00400000 + + # If available, use C implementation try: from _stat import * diff --git a/Lib/string/__init__.py b/Lib/string/__init__.py index b2c369ef9d1..88f1ae1b9b0 100644 --- a/Lib/string/__init__.py +++ b/Lib/string/__init__.py @@ -198,16 +198,14 @@ def get_identifiers(self): ######################################################################## -# the Formatter class -# see PEP 3101 for details and purpose of this class - -# The hard parts are reused from the C implementation. They're exposed as "_" -# prefixed methods of str. - +# The Formatter class (PEP 3101). +# # The overall parser is implemented in _string.formatter_parser. -# The field name parser is implemented in _string.formatter_field_name_split +# The field name parser is implemented in _string.formatter_field_name_split. class Formatter: + """See PEP 3101 for details and purpose of this class.""" + def format(self, format_string, /, *args, **kwargs): return self.vformat(format_string, args, kwargs) @@ -271,22 +269,18 @@ def _vformat(self, format_string, args, kwargs, used_args, recursion_depth, return ''.join(result), auto_arg_index - def get_value(self, key, args, kwargs): if isinstance(key, int): return args[key] else: return kwargs[key] - def check_unused_args(self, used_args, args, kwargs): pass - def format_field(self, value, format_spec): return format(value, format_spec) - def convert_field(self, value, conversion): # do any conversion on the resulting object if conversion is None: @@ -299,28 +293,26 @@ def convert_field(self, value, conversion): return ascii(value) raise ValueError("Unknown conversion specifier {0!s}".format(conversion)) - - # returns an iterable that contains tuples of the form: - # (literal_text, field_name, format_spec, conversion) - # literal_text can be zero length - # field_name can be None, in which case there's no - # object to format and output - # if field_name is not None, it is looked up, formatted - # with format_spec and conversion and then used def parse(self, format_string): + """ + Return an iterable that contains tuples of the form + (literal_text, field_name, format_spec, conversion). + + *field_name* can be None, in which case there's no object + to format and output; otherwise, it is looked up and + formatted with *format_spec* and *conversion*. + """ return _string.formatter_parser(format_string) - - # given a field_name, find the object it references. - # field_name: the field being looked up, e.g. "0.name" - # or "lookup[3]" - # used_args: a set of which args have been used - # args, kwargs: as passed in to vformat def get_field(self, field_name, args, kwargs): - first, rest = _string.formatter_field_name_split(field_name) + """Find the object referenced by a given field name. + The field name *field_name* can be for instance "0.name" + or "lookup[3]". The *args* and *kwargs* arguments are + passed to get_value(). + """ + first, rest = _string.formatter_field_name_split(field_name) obj = self.get_value(first, args, kwargs) - # loop through the rest of the field_name, doing # getattr or getitem as needed for is_attr, i in rest: @@ -328,5 +320,4 @@ def get_field(self, field_name, args, kwargs): obj = getattr(obj, i) else: obj = obj[i] - return obj, first diff --git a/Lib/test/test_abc.py b/Lib/test/test_abc.py index 80ee9e0ba56..bdfad263db6 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -349,6 +349,25 @@ class C(str): pass self.assertIsSubclass(C, A) self.assertIsSubclass(C, (A,)) + def test_instancecheck_no_class(self): + # gh-153772: __instancecheck__ must fall back to type(instance) + # when the instance has no __class__, matching isinstance(). + class NoClass: + def __getattribute__(self, name): + if name == "__class__": + raise AttributeError(name) + return super().__getattribute__(name) + + class A(metaclass=abc_ABCMeta): + pass + + obj = NoClass() + # Must return False rather than propagating the AttributeError. + self.assertNotIsInstance(obj, A) + # Registering the actual type makes the fallback report a match. + A.register(NoClass) + self.assertIsInstance(obj, A) + def test_registration_edge_cases(self): class A(metaclass=abc_ABCMeta): pass diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py index c06140dbd58..9887fc74c23 100644 --- a/Lib/test/test_functools.py +++ b/Lib/test/test_functools.py @@ -438,7 +438,7 @@ def test_setstate_subclasses(self): self.assertIs(type(r[0]), tuple) @support.skip_if_sanitizer("thread sanitizer crashes in __tsan::FuncEntry", thread=True) - @support.skip_if_unlimited_stack_size + @support.skip_if_huge_c_stack() @support.skip_emscripten_stack_overflow() def test_recursive_pickle(self): with replaced_module('functools', self.module): @@ -565,7 +565,53 @@ def __repr__(self): g_partial = functools.partial(func, trigger, None, None, None, None, arg=None) self.assertEqual(repr(g_partial),"functools.partial(Function(old_function), EvilObject, None, None, None, None, arg=None)") + def test_str_subclass_error(self): + class BadStr(str): + def __eq__(self, other): + raise RuntimeError + def __hash__(self): + return str.__hash__(self) + + def f(**kwargs): + return kwargs + + p = functools.partial(f, poison="") + with self.assertRaises(RuntimeError): + result = p(**{BadStr("poison"): "new_value"}) + + def test_call_safety_against_reentrant_mutation(self): + def old_function(*args, **kwargs): + return "old_function", args, kwargs + + def new_function(*args, **kwargs): + return "new_function", args, kwargs + + g_partial = None + + class EvilKey(str): + armed = False + def __hash__(self): + if EvilKey.armed and g_partial is not None: + EvilKey.armed = False + new_args_tuple = ("new_arg",) + new_keywords_dict = {"new_keyword": None} + new_tuple_state = (new_function, new_args_tuple, new_keywords_dict, None) + g_partial.__setstate__(new_tuple_state) + gc.collect() + return str.__hash__(self) + + g_partial = functools.partial(old_function, "old_arg", old_keyword=None) + + kwargs = {EvilKey("evil_key"): None} + EvilKey.armed = True + + result = g_partial(**kwargs) + expected = ("old_function", ("old_arg",), {"old_keyword": None, "evil_key": None}) + self.assertEqual(result, expected) + result = g_partial() + expected = ("new_function", ("new_arg",), {"new_keyword": None}) + self.assertEqual(result, expected) @unittest.skipUnless(c_functools, 'requires the C _functools module') class TestPartialC(TestPartial, unittest.TestCase): @@ -2209,6 +2255,13 @@ def fib(n): with self.assertRaises(RecursionError): fib(support.exceeds_recursion_limit()) + def test_lru_checks_arg_is_callable(self): + with self.assertRaisesRegex( + TypeError, + "the first argument must be callable", + ): + self.module.lru_cache(1)('hello') + @py_functools.lru_cache() def py_cached_func(x, y): @@ -2843,7 +2896,7 @@ class Slot: @functools.singledispatchmethod @classmethod def go(cls, item, arg): - pass + return item - arg @go.register @classmethod @@ -2852,7 +2905,9 @@ def _(cls, item: int, arg): s = Slot() self.assertEqual(s.go(1, 1), 2) + self.assertEqual(s.go(1.5, 1), 0.5) self.assertEqual(Slot.go(1, 1), 2) + self.assertEqual(Slot.go(1.5, 1), 0.5) def test_staticmethod_slotted_class(self): class A: @@ -3052,6 +3107,57 @@ def static_func(arg: int) -> str: self.assertEqual(A.static_func.__name__, 'static_func') self.assertEqual(A().static_func.__name__, 'static_func') + def test_method_classlevel_calls(self): + """Regression test for GH-143535.""" + class C: + @functools.singledispatchmethod + def generic(self, x: object): + return "generic" + + @generic.register + def special1(self, x: int): + return "special1" + + @generic.register + @classmethod + def special2(self, x: float): + return "special2" + + @generic.register + @staticmethod + def special3(x: complex): + return "special3" + + def special4(self, x): + return "special4" + + class D1: + def __get__(self, _, owner): + return lambda inst, x: owner.special4(inst, x) + + generic.register(D1, D1()) + + def special5(self, x): + return "special5" + + class D2: + def __get__(self, inst, owner): + # Different instance bound to the returned method + # doesn't cause it to receive the original instance + # as a separate argument. + # To work around this, wrap the returned bound method + # with `functools.partial`. + return C().special5 + + generic.register(D2, D2()) + + self.assertEqual(C.generic(C(), "foo"), "generic") + self.assertEqual(C.generic(C(), 1), "special1") + self.assertEqual(C.generic(C(), 2.0), "special2") + self.assertEqual(C.generic(C(), 3j), "special3") + self.assertEqual(C.generic(C(), C.D1()), "special4") + self.assertEqual(C.generic(C(), C.D2()), "special5") + def test_method_repr(self): class Callable: def __call__(self, *args): @@ -3290,6 +3396,21 @@ def t(self, *args, **kwargs): with self.assertRaisesRegex(TypeError, msg): A().t(a=1) + def test_positional_only_argument(self): + @functools.singledispatch + def f(arg, /, extra): + return "base" + @f.register + def f_int(arg: int, /, extra: str): + return "int" + @f.register + def f_str(arg: str, /, extra: int): + return "str" + + self.assertEqual(f(None, "extra"), "base") + self.assertEqual(f(1, "extra"), "int") + self.assertEqual(f("s", "extra"), "str") + def test_union(self): @functools.singledispatch def f(arg): @@ -3538,6 +3659,37 @@ def _(item: int, arg: bytes) -> str: self.assertEqual(str(Signature.from_callable(A.static_func)), '(item, arg: int) -> str') + def test_method_non_descriptor(self): + class Callable: + def __init__(self, value): + self.value = value + def __call__(self, arg): + return self.value, arg + + class A: + t = functools.singledispatchmethod(Callable('general')) + t.register(int, Callable('special')) + + @functools.singledispatchmethod + def u(self, arg): + return 'general', arg + u.register(int, Callable('special')) + + v = functools.singledispatchmethod(Callable('general')) + @v.register(int) + def _(self, arg): + return 'special', arg + + a = A() + self.assertEqual(a.t(0), ('special', 0)) + self.assertEqual(a.t(2.5), ('general', 2.5)) + self.assertEqual(A.t(0), ('special', 0)) + self.assertEqual(A.t(2.5), ('general', 2.5)) + self.assertEqual(a.u(0), ('special', 0)) + self.assertEqual(a.u(2.5), ('general', 2.5)) + self.assertEqual(a.v(0), ('special', 0)) + self.assertEqual(a.v(2.5), ('general', 2.5)) + class CachedCostItem: _cost = 1 diff --git a/Lib/test/test_genericpath.py b/Lib/test/test_genericpath.py index 1a44cedcd36..10d3f409d88 100644 --- a/Lib/test/test_genericpath.py +++ b/Lib/test/test_genericpath.py @@ -2,8 +2,10 @@ Tests common to genericpath, ntpath and posixpath """ +import copy import genericpath import os +import pickle import sys import unittest import warnings @@ -32,6 +34,10 @@ def test_no_argument(self): .format(self.pathmodule.__name__, attr)) def test_commonprefix(self): + with warnings_helper.check_warnings((".*commonpath().*", DeprecationWarning)): + self.do_test_commonprefix() + + def do_test_commonprefix(self): commonprefix = self.pathmodule.commonprefix self.assertEqual( commonprefix([]), @@ -320,6 +326,21 @@ def test_sameopenfile(self): fd2 = fp2.fileno() self.assertTrue(self.pathmodule.sameopenfile(fd1, fd2)) + def test_realpath_mode_values(self): + for name in 'ALL_BUT_LAST', 'ALLOW_MISSING': + with self.subTest(name): + mode = getattr(self.pathmodule, name) + self.assertEqual(repr(mode), 'os.path.' + name) + self.assertEqual(str(mode), 'os.path.' + name) + self.assertTrue(mode) + self.assertIs(copy.copy(mode), mode) + self.assertIs(copy.deepcopy(mode), mode) + for proto in range(pickle.HIGHEST_PROTOCOL+1): + with self.subTest(protocol=proto): + pickled = pickle.dumps(mode, proto) + unpickled = pickle.loads(pickled) + self.assertIs(unpickled, mode) + class TestGenericTest(GenericTest, unittest.TestCase): # Issue 16852: GenericTest can't inherit from unittest.TestCase @@ -589,8 +610,9 @@ def test_path_isdir(self): self.assertPathEqual(os.path.isdir) def test_path_commonprefix(self): - self.assertEqual(os.path.commonprefix([self.file_path, self.file_name]), - self.file_name) + with warnings_helper.check_warnings((".*commonpath().*", DeprecationWarning)): + self.assertEqual(os.path.commonprefix([self.file_path, self.file_name]), + self.file_name) def test_path_getsize(self): self.assertPathEqual(os.path.getsize) diff --git a/Lib/test/test_mimetypes.py b/Lib/test/test_mimetypes.py index a7d1d724ff0..f0851431a27 100644 --- a/Lib/test/test_mimetypes.py +++ b/Lib/test/test_mimetypes.py @@ -67,50 +67,9 @@ def test_read_mime_types(self): with unittest.mock.patch.object(mimetypes, 'open', return_value=fp) as mock_open: mime_dict = mimetypes.read_mime_types(filename) - mock_open.assert_called_with(filename, encoding='utf-8', - errors='surrogateescape') + mock_open.assert_called_with(filename, encoding='utf-8') eq(mime_dict[".Français"], "application/no-mans-land") - def test_read_mime_types_invalid_utf8_comment(self): - with os_helper.temp_dir() as directory: - data = (b"# non-UTF-8 comment: \x83\n" - b"x-application/x-unittest pyunit\n") - file = os.path.join(directory, "sample.mimetype") - with open(file, "wb") as f: - f.write(data) - - mime_dict = mimetypes.read_mime_types(file) - self.assertEqual( - mime_dict[".pyunit"], "x-application/x-unittest") - - db = mimetypes.MimeTypes() - db.read(file) - self.assertEqual( - db.guess_file_type("sample.pyunit")[0], - "x-application/x-unittest") - - mimetypes.init(files=[file]) - self.assertEqual( - mimetypes.guess_file_type("sample.pyunit")[0], - "x-application/x-unittest") - - def test_read_mime_types_invalid_utf8_type(self): - # A non-UTF-8 byte in a type or extension (not only in a comment) is - # preserved via surrogateescape, so the mapping is not corrupted. - with os_helper.temp_dir() as directory: - data = (b"x-application/x-unittest pyunit\n" - b"application/bad\x83 badext\x83\n") - file = os.path.join(directory, "sample.mimetype") - with open(file, "wb") as f: - f.write(data) - - bad_type = b"application/bad\x83".decode("utf-8", "surrogateescape") - bad_ext = b".badext\x83".decode("utf-8", "surrogateescape") - - mime_dict = mimetypes.read_mime_types(file) - self.assertEqual(mime_dict[".pyunit"], "x-application/x-unittest") - self.assertEqual(mime_dict[bad_ext], bad_type) - def test_init_reinitializes(self): # Issue 4936: make sure an init starts clean # First, put some poison into the types table @@ -135,9 +94,15 @@ def check_extensions(): for mime_type, ext in ( ("application/epub+zip", ".epub"), ("application/octet-stream", ".bin"), + ("application/dicom", ".dcm"), ("application/gzip", ".gz"), ("application/ogg", ".ogx"), + ("application/pdf", ".pdf"), ("application/postscript", ".ps"), + ("application/rtf", ".rtf"), + ("application/sql", ".sql"), + ("application/texinfo", ".texi"), + ("application/toml", ".toml"), ("application/vnd.apple.mpegurl", ".m3u"), ("application/vnd.ms-excel", ".xls"), ("application/vnd.ms-fontobject", ".eot"), @@ -150,11 +115,11 @@ def check_extensions(): ("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ".xlsx"), ("application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".docx"), ("application/vnd.rar", ".rar"), + ("application/vnd.sqlite3", ".sqlite3"), ("application/x-7z-compressed", ".7z"), ("application/x-debian-package", ".deb"), ("application/x-httpd-php", ".php"), ("application/x-rpm", ".rpm"), - ("application/x-texinfo", ".texi"), ("application/x-troff", ".roff"), ("application/xml", ".xsl"), ("application/yaml", ".yaml"), @@ -176,6 +141,7 @@ def check_extensions(): ("image/jp2", ".jp2"), ("image/jpeg", ".jpg"), ("image/jpm", ".jpm"), + ("image/jxl", ".jxl"), ("image/t38", ".t38"), ("image/tiff", ".tiff"), ("image/tiff-fx", ".tfx"), @@ -187,7 +153,6 @@ def check_extensions(): ("model/stl", ".stl"), ("text/html", ".html"), ("text/plain", ".txt"), - ("text/rtf", ".rtf"), ("text/x-rst", ".rst"), ("video/matroska", ".mkv"), ("video/matroska-3d", ".mk3d"), @@ -349,13 +314,12 @@ def test_non_standard_types(self): eq = self.assertEqual # First try strict eq(self.db.guess_file_type('foo.xul', strict=True), (None, None)) - eq(self.db.guess_extension('image/jpg', strict=True), None) # And then non-strict eq(self.db.guess_file_type('foo.xul', strict=False), ('text/xul', None)) eq(self.db.guess_file_type('foo.XUL', strict=False), ('text/xul', None)) eq(self.db.guess_file_type('foo.invalid', strict=False), (None, None)) - eq(self.db.guess_extension('image/jpg', strict=False), '.jpg') - eq(self.db.guess_extension('image/JPG', strict=False), '.jpg') + eq(self.db.guess_extension('image/jpeg', strict=False), '.jpg') + eq(self.db.guess_extension('image/JPEG', strict=False), '.jpg') def test_filename_with_url_delimiters(self): # bpo-38449: URL delimiters cases should be handled also. @@ -416,8 +380,8 @@ def test_guess_all_types(self): self.assertTrue(set(all) >= {'.bat', '.c', '.h', '.ksh', '.pl', '.txt'}) self.assertEqual(len(set(all)), len(all)) # no duplicates # And now non-strict - all = self.db.guess_all_extensions('image/jpg', strict=False) - self.assertEqual(all, ['.jpg']) + all = self.db.guess_all_extensions('image/jpeg', strict=False) + self.assertEqual(all, ['.jpg', '.jpe', '.jpeg']) # And now for no hits all = self.db.guess_all_extensions('image/jpg', strict=True) self.assertEqual(all, []) @@ -475,9 +439,7 @@ def test_keywords_args_api(self): self.assertEqual(self.db.guess_type( url="scheme:foo.html", strict=True), ("text/html", None)) self.assertEqual(self.db.guess_all_extensions( - type='image/jpg', strict=True), []) - self.assertEqual(self.db.guess_extension( - type='image/jpg', strict=False), '.jpg') + type='image/jpeg', strict=True), ['.jpg', '.jpe', '.jpeg']) @unittest.skipUnless(sys.platform.startswith("win"), "Windows only") @@ -539,15 +501,15 @@ def test_parse_args(self): args, help_text = mimetypes._parse_args("--invalid") self.assertTrue(help_text.startswith("usage: ")) - args, _ = mimetypes._parse_args(shlex.split("-l -e image/jpg")) + args, _ = mimetypes._parse_args(shlex.split("-l -e image/jpeg")) self.assertTrue(args.extension) self.assertTrue(args.lenient) - self.assertEqual(args.type, ["image/jpg"]) + self.assertEqual(args.type, ["image/jpeg"]) - args, _ = mimetypes._parse_args(shlex.split("-e image/jpg")) + args, _ = mimetypes._parse_args(shlex.split("-e image/jpeg")) self.assertTrue(args.extension) self.assertFalse(args.lenient) - self.assertEqual(args.type, ["image/jpg"]) + self.assertEqual(args.type, ["image/jpeg"]) args, _ = mimetypes._parse_args(shlex.split("-l foo.webp")) self.assertFalse(args.extension) @@ -578,7 +540,6 @@ def test_multiple_inputs_error(self): def test_invocation(self): for command, expected in [ - ("-l -e image/jpg", ".jpg"), ("-e image/jpeg", ".jpg"), ("-l foo.webp", "type: image/webp encoding: None"), ]: diff --git a/Lib/test/test_ntpath.py b/Lib/test/test_ntpath.py index 9270f325706..a3728b58335 100644 --- a/Lib/test/test_ntpath.py +++ b/Lib/test/test_ntpath.py @@ -1,3 +1,4 @@ +import errno import inspect import ntpath import os @@ -6,9 +7,10 @@ import sys import unittest import warnings +from ntpath import ALL_BUT_LAST, ALLOW_MISSING from test import support from test.support import os_helper -from ntpath import ALLOW_MISSING +from test.support import warnings_helper from test.support.os_helper import FakePath from test import test_genericpath from tempfile import TemporaryFile @@ -297,6 +299,10 @@ def test_isabs(self): tester('ntpath.isabs("\\\\.\\C:")', 1) def test_commonprefix(self): + with warnings_helper.check_warnings((".*commonpath().*", DeprecationWarning)): + self.do_test_commonprefix() + + def do_test_commonprefix(self): tester('ntpath.commonprefix(["/home/swenson/spam", "/home/swen/spam"])', "/home/swen") tester('ntpath.commonprefix(["\\home\\swen\\spam", "\\home\\swen\\eggs"])', @@ -587,59 +593,63 @@ def test_realpath_invalid_paths(self): # gh-106242: Embedded nulls and non-strict fallback to abspath self.assertEqual(realpath(path, strict=False), path) # gh-106242: Embedded nulls should raise OSError (not ValueError) + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) path = ABSTFNb + b'\x00' self.assertEqual(realpath(path, strict=False), path) + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) path = ABSTFN + '\\nonexistent\\x\x00' self.assertEqual(realpath(path, strict=False), path) + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) path = ABSTFNb + b'\\nonexistent\\x\x00' self.assertEqual(realpath(path, strict=False), path) + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) path = ABSTFN + '\x00\\..' self.assertEqual(realpath(path, strict=False), os.getcwd()) + self.assertEqual(realpath(path, strict=ALL_BUT_LAST), os.getcwd()) self.assertEqual(realpath(path, strict=True), os.getcwd()) self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwd()) path = ABSTFNb + b'\x00\\..' self.assertEqual(realpath(path, strict=False), os.getcwdb()) + self.assertEqual(realpath(path, strict=ALL_BUT_LAST), os.getcwdb()) self.assertEqual(realpath(path, strict=True), os.getcwdb()) self.assertEqual(realpath(path, strict=ALLOW_MISSING), os.getcwdb()) path = ABSTFN + '\\nonexistent\\x\x00\\..' self.assertEqual(realpath(path, strict=False), ABSTFN + '\\nonexistent') + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, path, strict=True) self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + '\\nonexistent') path = ABSTFNb + b'\\nonexistent\\x\x00\\..' self.assertEqual(realpath(path, strict=False), ABSTFNb + b'\\nonexistent') + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, path, strict=True) self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFNb + b'\\nonexistent') @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') - @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_invalid_unicode_paths(self, kwargs): realpath = ntpath.realpath ABSTFN = ntpath.abspath(os_helper.TESTFN) ABSTFNb = os.fsencode(ABSTFN) path = ABSTFNb + b'\xff' self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) - self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) path = ABSTFNb + b'\\nonexistent\\\xff' self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) - self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) path = ABSTFNb + b'\xff\\..' self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) - self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) path = ABSTFNb + b'\\nonexistent\\\xff\\..' self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) - self.assertRaises(UnicodeDecodeError, realpath, path, **kwargs) @os_helper.skip_unless_symlink @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') - @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_relative(self, kwargs): ABSTFN = ntpath.abspath(os_helper.TESTFN) open(ABSTFN, "wb").close() @@ -766,34 +776,53 @@ def test_realpath_symlink_loops_strict(self): self.addCleanup(os_helper.unlink, ABSTFN + "a") os.symlink(ABSTFN, ABSTFN) + self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict=ALL_BUT_LAST) self.assertRaises(OSError, ntpath.realpath, ABSTFN, strict=True) os.symlink(ABSTFN + "1", ABSTFN + "2") os.symlink(ABSTFN + "2", ABSTFN + "1") + self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1", strict=ALL_BUT_LAST) self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1", strict=True) + self.assertRaises(OSError, ntpath.realpath, ABSTFN + "2", strict=ALL_BUT_LAST) self.assertRaises(OSError, ntpath.realpath, ABSTFN + "2", strict=True) + self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\x", strict=ALL_BUT_LAST) self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\x", strict=True) # Windows eliminates '..' components before resolving links, so the # following call is not expected to raise. + self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..", strict=ALL_BUT_LAST), + ntpath.dirname(ABSTFN)) self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..", strict=True), ntpath.dirname(ABSTFN)) + self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..\\x", strict=ALL_BUT_LAST), + ntpath.dirname(ABSTFN) + "\\x") self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\..\\x", strict=True) os.symlink(ABSTFN + "x", ABSTFN + "y") + self.assertPathEqual(ntpath.realpath(ABSTFN + "1\\..\\" + + ntpath.basename(ABSTFN) + "y", + strict=ALL_BUT_LAST), + ABSTFN + "x") self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "y", strict=True) + self.assertRaises(OSError, ntpath.realpath, + ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1", + strict=ALL_BUT_LAST) self.assertRaises(OSError, ntpath.realpath, ABSTFN + "1\\..\\" + ntpath.basename(ABSTFN) + "1", strict=True) os.symlink(ntpath.basename(ABSTFN) + "a\\b", ABSTFN + "a") + self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a", strict=ALL_BUT_LAST) self.assertRaises(OSError, ntpath.realpath, ABSTFN + "a", strict=True) os.symlink("..\\" + ntpath.basename(ntpath.dirname(ABSTFN)) + "\\" + ntpath.basename(ABSTFN) + "c", ABSTFN + "c") + self.assertRaises(OSError, ntpath.realpath, ABSTFN + "c", strict=ALL_BUT_LAST) self.assertRaises(OSError, ntpath.realpath, ABSTFN + "c", strict=True) # Test using relative path as well. + self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN), + strict=ALL_BUT_LAST) self.assertRaises(OSError, ntpath.realpath, ntpath.basename(ABSTFN), strict=True) @@ -853,7 +882,7 @@ def test_realpath_symlink_loops_raise(self): @os_helper.skip_unless_symlink @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') - @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_symlink_prefix(self, kwargs): ABSTFN = ntpath.abspath(os_helper.TESTFN) self.addCleanup(os_helper.unlink, ABSTFN + "3") @@ -891,6 +920,7 @@ def test_realpath_nul(self): tester("ntpath.realpath('NUL')", r'\\.\NUL') tester("ntpath.realpath('NUL', strict=False)", r'\\.\NUL') tester("ntpath.realpath('NUL', strict=True)", r'\\.\NUL') + tester("ntpath.realpath('NUL', strict=ALL_BUT_LAST)", r'\\.\NUL') tester("ntpath.realpath('NUL', strict=ALLOW_MISSING)", r'\\.\NUL') @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') @@ -915,7 +945,7 @@ def test_realpath_cwd(self): self.assertPathEqual(test_file_long, ntpath.realpath(test_file_short)) - for kwargs in {}, {'strict': True}, {'strict': ALLOW_MISSING}: + for kwargs in {}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}: with self.subTest(**kwargs): with os_helper.change_cwd(test_dir_long): self.assertPathEqual( @@ -975,6 +1005,93 @@ def test_realpath_permission(self): self.assertPathEqual(test_file, ntpath.realpath(test_file_short)) + @os_helper.skip_unless_symlink + @unittest.skipUnless(HAVE_GETFINALPATHNAME, 'need _getfinalpathname') + def test_realpath_mode(self): + realpath = ntpath.realpath + ABSTFN = ntpath.abspath(os_helper.TESTFN) + self.addCleanup(os_helper.rmdir, ABSTFN) + self.addCleanup(os_helper.rmdir, ABSTFN + "/dir") + self.addCleanup(os_helper.unlink, ABSTFN + "/file") + self.addCleanup(os_helper.unlink, ABSTFN + "/dir/file2") + self.addCleanup(os_helper.unlink, ABSTFN + "/link") + self.addCleanup(os_helper.unlink, ABSTFN + "/link2") + self.addCleanup(os_helper.unlink, ABSTFN + "/broken") + self.addCleanup(os_helper.unlink, ABSTFN + "/cycle") + + os.mkdir(ABSTFN) + os.mkdir(ABSTFN + "\\dir") + open(ABSTFN + "\\file", "wb").close() + open(ABSTFN + "\\dir\\file2", "wb").close() + os.symlink("file", ABSTFN + "\\link") + os.symlink("dir", ABSTFN + "\\link2") + os.symlink("nonexistent", ABSTFN + "\\broken") + os.symlink("cycle", ABSTFN + "\\cycle") + def check(path, modes, expected, errno=None): + path = path.replace('/', '\\') + if isinstance(expected, str): + assert errno is None + expected = expected.replace('/', os.sep) + for mode in modes: + with self.subTest(mode=mode): + self.assertEqual(realpath(path, strict=mode), + ABSTFN + expected) + else: + for mode in modes: + with self.subTest(mode=mode): + with self.assertRaises(expected) as cm: + realpath(path, strict=mode) + if errno is not None: + self.assertEqual(cm.exception.errno, errno) + + self.enterContext(os_helper.change_cwd(ABSTFN)) + all_modes = [False, ALLOW_MISSING, ALL_BUT_LAST, True] + check("file", all_modes, "/file") + check("file/", all_modes, "/file") + check("file/file2", [False, ALLOW_MISSING], "/file/file2") + check("file/file2", [ALL_BUT_LAST, True], FileNotFoundError) + check("file/.", all_modes, "/file") + check("file/../link2", all_modes, "/dir") + + check("dir", all_modes, "/dir") + check("dir/", all_modes, "/dir") + check("dir/file2", all_modes, "/dir/file2") + + check("link", all_modes, "/file") + check("link/", all_modes, "/file") + check("link/file2", [False, ALLOW_MISSING], "/file/file2") + check("link/file2", [ALL_BUT_LAST, True], FileNotFoundError) + check("link/.", all_modes, "/file") + check("link/../link", all_modes, "/file") + + check("link2", all_modes, "/dir") + check("link2/", all_modes, "/dir") + check("link2/file2", all_modes, "/dir/file2") + + check("nonexistent", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent") + check("nonexistent", [True], FileNotFoundError) + check("nonexistent/", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent") + check("nonexistent/", [True], FileNotFoundError) + check("nonexistent/file", [False, ALLOW_MISSING], "/nonexistent/file") + check("nonexistent/file", [ALL_BUT_LAST, True], FileNotFoundError) + check("nonexistent/../link", all_modes, "/file") + + check("broken", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent") + check("broken", [True], FileNotFoundError) + check("broken/", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent") + check("broken/", [True], FileNotFoundError) + check("broken/file", [False, ALLOW_MISSING], "/nonexistent/file") + check("broken/file", [ALL_BUT_LAST, True], FileNotFoundError) + check("broken/../link", all_modes, "/file") + + check("cycle", [False], "/cycle") + check("cycle", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.EINVAL) + check("cycle/", [False], "/cycle") + check("cycle/", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.EINVAL) + check("cycle/file", [False], "/cycle/file") + check("cycle/file", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.EINVAL) + check("cycle/../link", all_modes, "/file") + def test_expandvars(self): with os_helper.EnvironmentVarGuard() as env: env.clear() diff --git a/Lib/test/test_optparse.py b/Lib/test/test_optparse.py index e476e472780..fc8ef9520b3 100644 --- a/Lib/test/test_optparse.py +++ b/Lib/test/test_optparse.py @@ -1666,6 +1666,16 @@ def test_translations(self): self.assertMsgidsEqual(optparse) +class TestModule(unittest.TestCase): + def test_deprecated__version__(self): + with self.assertWarnsRegex( + DeprecationWarning, + "'__version__' is deprecated and slated for removal in Python 3.20", + ) as cm: + getattr(optparse, "__version__") + self.assertEqual(cm.filename, __file__) + + if __name__ == '__main__': # To regenerate translation snapshots if len(sys.argv) > 1 and sys.argv[1] == '--snapshot-update': diff --git a/Lib/test/test_posixpath.py b/Lib/test/test_posixpath.py index 21f06712548..a9975b75f7c 100644 --- a/Lib/test/test_posixpath.py +++ b/Lib/test/test_posixpath.py @@ -1,3 +1,4 @@ +import errno import inspect import os import posixpath @@ -5,7 +6,7 @@ import sys import unittest from functools import partial -from posixpath import realpath, abspath, dirname, basename, ALLOW_MISSING +from posixpath import realpath, abspath, dirname, basename, ALL_BUT_LAST, ALLOW_MISSING from test import support from test import test_genericpath from test.support import import_helper @@ -448,7 +449,7 @@ def test_normpath(self): self.assertEqual(result, expected) @skip_if_ABSTFN_contains_backslash - @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_curdir(self, kwargs): self.assertEqual(realpath('.', **kwargs), os.getcwd()) self.assertEqual(realpath('./.', **kwargs), os.getcwd()) @@ -459,7 +460,7 @@ def test_realpath_curdir(self, kwargs): self.assertEqual(realpath(b'/'.join([b'.'] * 100), **kwargs), os.getcwdb()) @skip_if_ABSTFN_contains_backslash - @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_pardir(self, kwargs): self.assertEqual(realpath('..', **kwargs), dirname(os.getcwd())) self.assertEqual(realpath('../..', **kwargs), dirname(dirname(os.getcwd()))) @@ -495,35 +496,43 @@ def test_realpath_strict(self): def test_realpath_invalid_paths(self): path = '/\x00' self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(ValueError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) path = b'/\x00' self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(ValueError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) path = '/nonexistent/x\x00' self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) path = b'/nonexistent/x\x00' self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) path = '/\x00/..' self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(ValueError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) path = b'/\x00/..' self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(ValueError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(ValueError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) path = '/nonexistent/x\x00/..' self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) path = b'/nonexistent/x\x00/..' self.assertRaises(ValueError, realpath, path, strict=False) + self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertRaises(ValueError, realpath, path, strict=ALLOW_MISSING) @@ -534,6 +543,7 @@ def test_realpath_invalid_paths(self): self.assertEqual(realpath(path, strict=ALLOW_MISSING), path) else: self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) + self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(UnicodeEncodeError, realpath, path, strict=True) self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) path = '/nonexistent/\udfff' @@ -543,6 +553,7 @@ def test_realpath_invalid_paths(self): else: self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) + self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(FileNotFoundError, realpath, path, strict=True) path = '/\udfff/..' if sys.platform == 'win32': @@ -551,6 +562,7 @@ def test_realpath_invalid_paths(self): self.assertEqual(realpath(path, strict=ALLOW_MISSING), '/') else: self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) + self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(UnicodeEncodeError, realpath, path, strict=True) self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) path = '/nonexistent/\udfff/..' @@ -560,6 +572,7 @@ def test_realpath_invalid_paths(self): else: self.assertRaises(UnicodeEncodeError, realpath, path, strict=False) self.assertRaises(UnicodeEncodeError, realpath, path, strict=ALLOW_MISSING) + self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(FileNotFoundError, realpath, path, strict=True) path = b'/\xff' @@ -570,9 +583,11 @@ def test_realpath_invalid_paths(self): else: self.assertEqual(realpath(path, strict=False), path) if support.is_wasi: + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) else: + self.assertEqual(realpath(path, strict=ALL_BUT_LAST), path) self.assertRaises(FileNotFoundError, realpath, path, strict=True) self.assertEqual(realpath(path, strict=ALLOW_MISSING), path) path = b'/nonexistent/\xff' @@ -582,14 +597,16 @@ def test_realpath_invalid_paths(self): else: self.assertEqual(realpath(path, strict=False), path) if support.is_wasi: + self.assertRaises(OSError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(OSError, realpath, path, strict=True) self.assertRaises(OSError, realpath, path, strict=ALLOW_MISSING) else: + self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST) self.assertRaises(FileNotFoundError, realpath, path, strict=True) @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash - @_parameterize({}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_relative(self, kwargs): try: os.symlink(posixpath.relpath(ABSTFN+"1"), ABSTFN) @@ -599,12 +616,14 @@ def test_realpath_relative(self, kwargs): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash - @_parameterize({}, {'strict': ALLOW_MISSING}) - def test_realpath_missing_pardir(self, kwargs): + def test_realpath_missing_pardir(self): try: os.symlink(TESTFN + "1", TESTFN) - self.assertEqual( - realpath("nonexistent/../" + TESTFN, **kwargs), ABSTFN + "1") + path = "nonexistent/../" + TESTFN + self.assertEqual(realpath(path), ABSTFN + "1") + self.assertEqual(realpath(path, strict=ALLOW_MISSING), ABSTFN + "1") + self.assertRaises(FileNotFoundError, realpath, path, strict=ALL_BUT_LAST) + self.assertRaises(FileNotFoundError, realpath, path, strict=True) finally: os_helper.unlink(TESTFN) @@ -651,7 +670,7 @@ def test_realpath_symlink_loops(self): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash - @_parameterize({'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_symlink_loops_strict(self, kwargs): # Bug #43757, raise OSError if we get into an infinite symlink loop in # the strict modes. @@ -693,7 +712,7 @@ def test_realpath_symlink_loops_strict(self, kwargs): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash - @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_repeated_indirect_symlinks(self, kwargs): # Issue #6975. try: @@ -708,7 +727,7 @@ def test_realpath_repeated_indirect_symlinks(self, kwargs): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash - @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_deep_recursion(self, kwargs): depth = 10 try: @@ -728,7 +747,7 @@ def test_realpath_deep_recursion(self, kwargs): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash - @_parameterize({}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_resolve_parents(self, kwargs): # We also need to resolve any symlinks in the parents of a relative # path passed to realpath. E.g.: current working directory is @@ -749,7 +768,7 @@ def test_realpath_resolve_parents(self, kwargs): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash - @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_resolve_before_normalizing(self, kwargs): # Bug #990669: Symbolic links should be resolved before we # normalize the path. E.g.: if we have directories 'a', 'k' and 'y' @@ -778,7 +797,7 @@ def test_realpath_resolve_before_normalizing(self, kwargs): @os_helper.skip_unless_symlink @skip_if_ABSTFN_contains_backslash - @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_resolve_first(self, kwargs): # Bug #1213894: The first component of the path, if not absolute, # must be resolved too. @@ -816,7 +835,7 @@ def test_realpath_unreadable_symlink(self): @skip_if_ABSTFN_contains_backslash @unittest.skipIf(os.chmod not in os.supports_follow_symlinks, "Can't set symlink permissions") @unittest.skipIf(sys.platform != "darwin", "only macOS requires read permission to readlink()") - @_parameterize({'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_realpath_unreadable_symlink_strict(self, kwargs): try: os.symlink(ABSTFN+"1", ABSTFN) @@ -842,6 +861,7 @@ def test_realpath_unreadable_directory(self): os.chmod(ABSTFN, 0o000) self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN) self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN) + self.assertEqual(realpath(ABSTFN, strict=ALL_BUT_LAST), ABSTFN) self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN) try: @@ -855,6 +875,8 @@ def test_realpath_unreadable_directory(self): ABSTFN + '/k') self.assertRaises(PermissionError, realpath, ABSTFN + '/k', strict=True) + self.assertRaises(PermissionError, realpath, ABSTFN + '/k', + strict=ALL_BUT_LAST) self.assertRaises(PermissionError, realpath, ABSTFN + '/k', strict=ALLOW_MISSING) @@ -862,6 +884,8 @@ def test_realpath_unreadable_directory(self): ABSTFN + '/missing') self.assertRaises(PermissionError, realpath, ABSTFN + '/missing', strict=True) + self.assertRaises(PermissionError, realpath, ABSTFN + '/missing', + strict=ALL_BUT_LAST) self.assertRaises(PermissionError, realpath, ABSTFN + '/missing', strict=ALLOW_MISSING) finally: @@ -875,25 +899,30 @@ def test_realpath_nonterminal_file(self): with open(ABSTFN, 'w') as f: f.write('test_posixpath wuz ere') self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN) + self.assertEqual(realpath(ABSTFN, strict=ALL_BUT_LAST), ABSTFN) self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN) self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN) self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALLOW_MISSING) self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=ALLOW_MISSING) self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN)) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=ALLOW_MISSING) self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "/subdir") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=ALLOW_MISSING) @@ -908,25 +937,30 @@ def test_realpath_nonterminal_symlink_to_file(self): f.write('test_posixpath wuz ere') os.symlink(ABSTFN + "1", ABSTFN) self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "1") + self.assertEqual(realpath(ABSTFN, strict=ALL_BUT_LAST), ABSTFN + "1") self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "1") self.assertEqual(realpath(ABSTFN, strict=ALLOW_MISSING), ABSTFN + "1") self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "1") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALLOW_MISSING) self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "1") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=ALLOW_MISSING) self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN)) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=ALLOW_MISSING) self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "1/subdir") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=ALLOW_MISSING) @@ -943,25 +977,30 @@ def test_realpath_nonterminal_symlink_to_symlinks_to_file(self): os.symlink(ABSTFN + "2", ABSTFN + "1") os.symlink(ABSTFN + "1", ABSTFN) self.assertEqual(realpath(ABSTFN, strict=False), ABSTFN + "2") + self.assertEqual(realpath(ABSTFN, strict=ALL_BUT_LAST), ABSTFN + "2") self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2") self.assertEqual(realpath(ABSTFN, strict=True), ABSTFN + "2") self.assertEqual(realpath(ABSTFN + "/", strict=False), ABSTFN + "2") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/", strict=ALLOW_MISSING) self.assertEqual(realpath(ABSTFN + "/.", strict=False), ABSTFN + "2") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/.", strict=ALLOW_MISSING) self.assertEqual(realpath(ABSTFN + "/..", strict=False), dirname(ABSTFN)) + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/..", strict=ALLOW_MISSING) self.assertEqual(realpath(ABSTFN + "/subdir", strict=False), ABSTFN + "2/subdir") + self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=ALL_BUT_LAST) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=True) self.assertRaises(NotADirectoryError, realpath, ABSTFN + "/subdir", strict=ALLOW_MISSING) @@ -970,6 +1009,98 @@ def test_realpath_nonterminal_symlink_to_symlinks_to_file(self): os_helper.unlink(ABSTFN + "1") os_helper.unlink(ABSTFN + "2") + @os_helper.skip_unless_symlink + def test_realpath_mode(self): + self.addCleanup(os_helper.rmdir, ABSTFN) + self.addCleanup(os_helper.rmdir, ABSTFN + "/dir") + self.addCleanup(os_helper.unlink, ABSTFN + "/file") + self.addCleanup(os_helper.unlink, ABSTFN + "/dir/file2") + self.addCleanup(os_helper.unlink, ABSTFN + "/link") + self.addCleanup(os_helper.unlink, ABSTFN + "/link2") + self.addCleanup(os_helper.unlink, ABSTFN + "/broken") + self.addCleanup(os_helper.unlink, ABSTFN + "/cycle") + + os.mkdir(ABSTFN) + os.mkdir(ABSTFN + "/dir") + open(ABSTFN + "/file", "wb").close() + open(ABSTFN + "/dir/file2", "wb").close() + os.symlink("file", ABSTFN + "/link") + os.symlink("dir", ABSTFN + "/link2") + os.symlink("nonexistent", ABSTFN + "/broken") + os.symlink("cycle", ABSTFN + "/cycle") + def check(path, modes, expected, errno=None): + if isinstance(expected, str): + assert errno is None + expected = expected.replace('/', os.sep) + for mode in modes: + with self.subTest(mode=mode): + self.assertEqual(realpath(path, strict=mode).replace('/', os.sep), + ABSTFN.replace('/', os.sep) + expected) + else: + for mode in modes: + with self.subTest(mode=mode): + with self.assertRaises(expected) as cm: + realpath(path, strict=mode) + if errno is not None: + self.assertEqual(cm.exception.errno, errno) + + self.enterContext(os_helper.change_cwd(ABSTFN)) + all_modes = [False, ALLOW_MISSING, ALL_BUT_LAST, True] + check("file", all_modes, "/file") + check("file/", [False], "/file") + check("file/", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError) + check("file/file2", [False], "/file/file2") + check("file/file2", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError) + check("file/.", [False], "/file") + check("file/.", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError) + check("file/../link2", [False], "/dir") + check("file/../link2", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError) + + check("dir", all_modes, "/dir") + check("dir/", all_modes, "/dir") + check("dir/file2", all_modes, "/dir/file2") + + check("link", all_modes, "/file") + check("link/", [False], "/file") + check("link/", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError) + check("link/file2", [False], "/file/file2") + check("link/file2", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError) + check("link/.", [False], "/file") + check("link/.", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError) + check("link/../link", [False], "/file") + check("link/../link", [ALLOW_MISSING, ALL_BUT_LAST, True], NotADirectoryError) + + check("link2", all_modes, "/dir") + check("link2/", all_modes, "/dir") + check("link2/file2", all_modes, "/dir/file2") + + check("nonexistent", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent") + check("nonexistent", [True], FileNotFoundError) + check("nonexistent/", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent") + check("nonexistent/", [True], FileNotFoundError) + check("nonexistent/file", [False, ALLOW_MISSING], "/nonexistent/file") + check("nonexistent/file", [ALL_BUT_LAST, True], FileNotFoundError) + check("nonexistent/../link", [False, ALLOW_MISSING], "/file") + check("nonexistent/../link", [ALL_BUT_LAST, True], FileNotFoundError) + + check("broken", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent") + check("broken", [True], FileNotFoundError) + check("broken/", [False, ALLOW_MISSING, ALL_BUT_LAST], "/nonexistent") + check("broken/", [True], FileNotFoundError) + check("broken/file", [False, ALLOW_MISSING], "/nonexistent/file") + check("broken/file", [ALL_BUT_LAST, True], FileNotFoundError) + check("broken/../link", [False, ALLOW_MISSING], "/file") + check("broken/../link", [ALL_BUT_LAST, True], FileNotFoundError) + + check("cycle", [False], "/cycle") + check("cycle", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.ELOOP) + check("cycle/", [False], "/cycle") + check("cycle/", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.ELOOP) + check("cycle/file", [False], "/cycle/file") + check("cycle/file", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.ELOOP) + check("cycle/../link", [False], "/file") + check("cycle/../link", [ALLOW_MISSING, ALL_BUT_LAST, True], OSError, errno.ELOOP) + def test_relpath(self): (real_getcwd, os.getcwd) = (os.getcwd, lambda: r"/home/user/bar") try: @@ -1152,7 +1283,7 @@ def test_path_normpath(self): def test_path_abspath(self): self.assertPathEqual(self.path.abspath) - @_parameterize({}, {'strict': True}, {'strict': ALLOW_MISSING}) + @_parameterize({}, {'strict': True}, {'strict': ALL_BUT_LAST}, {'strict': ALLOW_MISSING}) def test_path_realpath(self, kwargs): self.assertPathEqual(self.path.realpath) diff --git a/Lib/test/test_string/_support.py b/Lib/test/test_string/_support.py index cfead782b7d..e1d7f6f6500 100644 --- a/Lib/test/test_string/_support.py +++ b/Lib/test/test_string/_support.py @@ -1,4 +1,3 @@ -import unittest from string.templatelib import Interpolation diff --git a/crates/codegen/src/compile.rs b/crates/codegen/src/compile.rs index 1f290d442bb..7ad29321ce6 100644 --- a/crates/codegen/src/compile.rs +++ b/crates/codegen/src/compile.rs @@ -5321,10 +5321,10 @@ impl<'warnings> Compiler<'warnings> { // Count annotations let parameters_iter = parameters - .args + .posonlyargs .iter() .map(|x| &x.parameter) - .chain(parameters.posonlyargs.iter().map(|x| &x.parameter)) + .chain(parameters.args.iter().map(|x| &x.parameter)) .chain(parameters.vararg.as_deref()) .chain(parameters.kwonlyargs.iter().map(|x| &x.parameter)) .chain(parameters.kwarg.as_deref()); @@ -5336,10 +5336,10 @@ impl<'warnings> Compiler<'warnings> { // Compile annotations inside the annotation scope let parameters_iter = parameters - .args + .posonlyargs .iter() .map(|x| &x.parameter) - .chain(parameters.posonlyargs.iter().map(|x| &x.parameter)) + .chain(parameters.args.iter().map(|x| &x.parameter)) .chain(parameters.vararg.as_deref()) .chain(parameters.kwonlyargs.iter().map(|x| &x.parameter)) .chain(parameters.kwarg.as_deref());