From 71cb9e8b64c4a89971955d3ce8ff393f283aeace Mon Sep 17 00:00:00 2001 From: Clay Dugo Date: Fri, 28 Aug 2026 19:37:42 -0400 Subject: [PATCH 1/4] gh-143768: Replace a dangling interpreter symlink when creating a venv (#150985) Co-authored-by: Brett Cannon --- Doc/library/venv.rst | 6 +++++ Lib/test/test_venv.py | 27 +++++++++++++++++++ Lib/venv/__init__.py | 2 ++ ...06-05-16-57-03.gh-issue-143768.RbLnkFx.rst | 3 +++ 4 files changed, 38 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst diff --git a/Doc/library/venv.rst b/Doc/library/venv.rst index fd9c9b9a19dd9b3..34b21aba17461b4 100644 --- a/Doc/library/venv.rst +++ b/Doc/library/venv.rst @@ -452,6 +452,12 @@ creation according to their needs, the :class:`EnvBuilder` class. On POSIX systems, if a specific executable ``python3.x`` was used, symlinks to ``python`` and ``python3`` will be created pointing to that executable, unless files with those names already exist. + On POSIX systems, a broken symlink at a destination path is removed + before the copy or symlink is created. + + .. versionchanged:: next + A broken symlink at a destination path is now removed and replaced. + Previously it was left in place, or it made the copy fail. .. method:: setup_scripts(context) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index b4ad1bf3f412948..2f30d3108021dc6 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -946,6 +946,33 @@ def test_failed_symlink(self): filepath_regex = r"'[A-Z]:\\\\(?:[^\\\\]+\\\\)*[^\\\\]+'" self.assertRegex(err, rf"Unable to symlink {filepath_regex} to {filepath_regex}") + @requireVenvCreate + @unittest.skipIf(os.name == 'nt', 'not relevant on Windows') + @unittest.skipUnless(can_symlink(), 'Needs symlinks') + def test_broken_symlink_in_existing_venv(self): + """ + Test creating a venv when a stale venv with broken symlinks exists. + """ + bindir = os.path.join(self.env_dir, self.bindir) + os.makedirs(bindir) + python = os.path.join(bindir, 'python3') + os.symlink('/path/to/deleted/env/bin/python3', python) + self.assertTrue(os.path.islink(python)) + self.assertFalse(os.path.exists(python)) + + builder = venv.EnvBuilder(with_pip=False, symlinks=True) + self.run_with_capture(builder.create, self.env_dir) + self.assertTrue(os.path.islink(python)) + self.assertTrue(os.path.exists(python)) + + rmtree(self.env_dir) + os.makedirs(bindir) + os.symlink('/path/to/deleted/env/bin/python3', python) + builder = venv.EnvBuilder(with_pip=False, symlinks=False) + self.run_with_capture(builder.create, self.env_dir) + self.assertFalse(os.path.islink(python)) + self.assertTrue(os.path.exists(python)) + @requireVenvCreate def test_multiprocessing(self): """ diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index 4c8e4e8efeaa724..38e1bfe0c5fdb9d 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -266,6 +266,8 @@ def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): switch to a different set of files instead.) """ assert os.name != 'nt' + if os.path.islink(dst) and not os.path.exists(dst): + os.unlink(dst) force_copy = not self.symlinks if not force_copy: try: diff --git a/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst b/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst new file mode 100644 index 000000000000000..1aacaf432ccfb25 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst @@ -0,0 +1,3 @@ +:mod:`venv`: Replace a dangling interpreter symlink in an existing +virtual environment instead of failing or silently leaving it broken. +Fix by Clay Dugo. From de2ea9aaefebd1b06e5302c1858d70e00cd63639 Mon Sep 17 00:00:00 2001 From: Victorien <65306057+Viicos@users.noreply.github.com> Date: Sat, 29 Aug 2026 04:56:34 +0200 Subject: [PATCH 2/4] Do not recurse into `Literal` arguments during type evaluation (#156534) Unlike other generic aliases, `Literal` arguments aren't type expressions (e.g. `Literal[1, 'a'].__args__ == (1, 'a')`. As such, there is no need to recurse into all the arguments as they are guaranteed to be returned unchanged. --- Lib/typing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/typing.py b/Lib/typing.py index 65e1d1ea6be5844..99c467a8af07d89 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -475,6 +475,9 @@ def _eval_type(t, globalns, localns, type_params, *, recursive_guard=frozenset() type_params=type_params, owner=owner, _recursive_guard=recursive_guard, format=format) if isinstance(t, (_GenericAlias, GenericAlias, Union)): + if isinstance(t, _LiteralGenericAlias): + # Unlike other generic aliases, Literal arguments aren't type expressions + return t if isinstance(t, GenericAlias): args = tuple( _make_forward_ref(arg, parent_fwdref=parent_fwdref) if isinstance(arg, str) else arg From 3e245faf34efc41c39bbe76071e6edb7a62692a8 Mon Sep 17 00:00:00 2001 From: Vyron Vasileiadis Date: Sat, 29 Aug 2026 06:08:53 +0300 Subject: [PATCH 3/4] gh-153772: Make abc isinstance() tolerate instances without __class__ (#154149) The built-in isinstance() reads an instance's __class__ with a lookup that suppresses AttributeError and falls back to the object's type, so isinstance(obj, int) returns False for an object whose __class__ access raises. ABCMeta.__instancecheck__ read __class__ directly instead, so isinstance(obj, Mapping) leaked that AttributeError. Fall back to type(instance) when __class__ is unavailable, in both the C and the pure-Python implementations, so the abstract base classes behave like the built-in isinstance(). Such objects are unusual, but they do turn up in the wild (for example some Qt widgets). --- Lib/_py_abc.py | 7 ++++++- Lib/test/test_abc.py | 19 +++++++++++++++++++ ...-07-19-17-05-00.gh-issue-153772.cobjTK.rst | 5 +++++ Modules/_abc.c | 8 ++++++-- 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-19-17-05-00.gh-issue-153772.cobjTK.rst diff --git a/Lib/_py_abc.py b/Lib/_py_abc.py index c870ae9048b4f13..2bfb9489a7acdcb 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/test/test_abc.py b/Lib/test/test_abc.py index 59a45a2eda07b00..814d7fff2f41351 100644 --- a/Lib/test/test_abc.py +++ b/Lib/test/test_abc.py @@ -380,6 +380,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/Misc/NEWS.d/next/Library/2026-07-19-17-05-00.gh-issue-153772.cobjTK.rst b/Misc/NEWS.d/next/Library/2026-07-19-17-05-00.gh-issue-153772.cobjTK.rst new file mode 100644 index 000000000000000..9393384151450d8 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-19-17-05-00.gh-issue-153772.cobjTK.rst @@ -0,0 +1,5 @@ +:func:`isinstance` checks against :mod:`collections.abc` classes such as +:class:`~collections.abc.Mapping` no longer raise :exc:`AttributeError` +when the instance has no ``__class__``. The abstract base class machinery +now falls back to the object's type in that case, matching the behaviour of +the built-in :func:`isinstance`. diff --git a/Modules/_abc.c b/Modules/_abc.c index 5826efbfecb6901..bca9066d340ee43 100644 --- a/Modules/_abc.c +++ b/Modules/_abc.c @@ -629,11 +629,15 @@ _abc__abc_instancecheck_impl(PyObject *module, PyObject *self, return NULL; } - subclass = PyObject_GetAttr(instance, &_Py_ID(__class__)); - if (subclass == NULL) { + if (PyObject_GetOptionalAttr(instance, &_Py_ID(__class__), &subclass) < 0) { Py_DECREF(impl); return NULL; } + if (subclass == NULL) { + /* Fall back to the type when the instance has no __class__, matching + the behaviour of the built-in isinstance() (gh-153772). */ + subclass = Py_NewRef((PyObject *)Py_TYPE(instance)); + } /* Inline the cache checking. */ int incache = _in_weak_set(impl, &impl->_abc_cache, subclass); if (incache < 0) { From d915492413869d616e269e7568c1ff08f02911cc Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Fri, 28 Aug 2026 23:37:45 -0400 Subject: [PATCH 4/4] gh-156537: Optimize codec.decode by inlining the per-character write in CJK decoders (gh-156538) --- Modules/cjkcodecs/cjkcodecs.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/cjkcodecs/cjkcodecs.h b/Modules/cjkcodecs/cjkcodecs.h index 9d86396f73b2b55..41e1287c8650701 100644 --- a/Modules/cjkcodecs/cjkcodecs.h +++ b/Modules/cjkcodecs/cjkcodecs.h @@ -155,8 +155,8 @@ get_module_state(PyObject *mod) #define OUTCHAR(c) \ do { \ - if (_PyUnicodeWriter_WriteChar(writer, (c)) < 0) \ - return MBERR_EXCEPTION; \ + if (_PyUnicodeWriter_WriteCharInline(writer, (c)) < 0) \ + return MBERR_EXCEPTION; \ } while (0) #define OUTCHAR2(c1, c2) \