diff --git a/.github/workflows/tail-call.yml b/.github/workflows/tail-call.yml index e32cbf0aaa3c3e..57c92e193a9aec 100644 --- a/.github/workflows/tail-call.yml +++ b/.github/workflows/tail-call.yml @@ -114,8 +114,8 @@ jobs: find /usr/local/bin -lname '*/Library/Frameworks/Python.framework/*' -delete brew install llvm@${{ matrix.llvm }} export SDKROOT="$(xcrun --show-sdk-path)" - export PATH="/usr/local/opt/llvm/bin:$PATH" - export PATH="/opt/homebrew/opt/llvm/bin:$PATH" + export PATH="/usr/local/opt/llvm@${{ matrix.llvm }}/bin:$PATH" + export PATH="/opt/homebrew/opt/llvm@${{ matrix.llvm }}/bin:$PATH" CC=clang-20 ./configure --with-tail-call-interp make all --jobs 4 ./python.exe -m test --multiprocess 0 --timeout 4500 --verbose2 --verbose3 diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 9f198aebcb66b0..2a12a0ca8e960b 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -413,6 +413,9 @@ An :class:`IMAP4` instance has the following methods: the password. Will only work if the server ``CAPABILITY`` response includes the phrase ``AUTH=CRAM-MD5``. + .. versionchanged:: next + An :exc:`IMAP4.error` is raised if MD5 support is not available. + .. method:: IMAP4.logout() diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 7748c172e63b6d..54a7d0f3c57dad 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -239,6 +239,25 @@ Other language changes * Several error messages incorrectly using the term "argument" have been corrected. (Contributed by Stan Ulbrych in :gh:`133382`.) +* The interpreter now tries to provide a suggestion when + :func:`delattr` fails due to a missing attribute. + When an attribute name that closely resembles an existing attribute is used, + the interpreter will suggest the correct attribute name in the error message. + For example: + + .. doctest:: + + >>> class A: + ... pass + >>> a = A() + >>> a.abcde = 1 + >>> del a.abcdf # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + AttributeError: 'A' object has no attribute 'abcdf'. Did you mean: 'abcde'? + + (Contributed by Nikita Sobolev and Pranjal Prajapati in :gh:`136588`.) + * Unraisable exceptions are now highlighted with color by default. This can be controlled by :ref:`environment variables `. (Contributed by Peter Bierma in :gh:`134170`.) diff --git a/Lib/dbm/sqlite3.py b/Lib/dbm/sqlite3.py index b296a1bcd1bbfa..c8ee6f184b365b 100644 --- a/Lib/dbm/sqlite3.py +++ b/Lib/dbm/sqlite3.py @@ -60,18 +60,22 @@ def __init__(self, path, /, *, flag, mode): # We use the URI format when opening the database. uri = _normalize_uri(path) uri = f"{uri}?mode={flag}" + if flag == "ro": + # Add immutable=1 to allow read-only SQLite access even if wal/shm missing + uri += "&immutable=1" try: self._cx = sqlite3.connect(uri, autocommit=True, uri=True) except sqlite3.Error as exc: raise error(str(exc)) - # This is an optimization only; it's ok if it fails. - with suppress(sqlite3.OperationalError): - self._cx.execute("PRAGMA journal_mode = wal") + if flag != "ro": + # This is an optimization only; it's ok if it fails. + with suppress(sqlite3.OperationalError): + self._cx.execute("PRAGMA journal_mode = wal") - if flag == "rwc": - self._execute(BUILD_TABLE) + if flag == "rwc": + self._execute(BUILD_TABLE) def _execute(self, *args, **kwargs): if not self._cx: diff --git a/Lib/imaplib.py b/Lib/imaplib.py index 2c3925958d011b..362d6a2dcf2573 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -21,7 +21,7 @@ # GET/SETANNOTATION contributed by Tomas Lindroos June 2005. # IDLE contributed by Forest August 2024. -__version__ = "2.59" +__version__ = "2.60" import binascii, errno, random, re, socket, subprocess, sys, time, calendar from datetime import datetime, timezone, timedelta @@ -725,9 +725,17 @@ def login_cram_md5(self, user, password): def _CRAM_MD5_AUTH(self, challenge): """ Authobject to use with CRAM-MD5 authentication. """ import hmac - pwd = (self.password.encode('utf-8') if isinstance(self.password, str) - else self.password) - return self.user + " " + hmac.HMAC(pwd, challenge, 'md5').hexdigest() + + if isinstance(self.password, str): + password = self.password.encode('utf-8') + else: + password = self.password + + try: + authcode = hmac.HMAC(password, challenge, 'md5') + except ValueError: # HMAC-MD5 is not available + raise self.error("CRAM-MD5 authentication is not supported") + return f"{self.user} {authcode.hexdigest()}" def logout(self): diff --git a/Lib/smtplib.py b/Lib/smtplib.py index 84d6d858e7dec1..b71fee8777e866 100644 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -177,6 +177,15 @@ def _quote_periods(bindata): def _fix_eols(data): return re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data) + +try: + hmac.digest(b'', b'', 'md5') +except ValueError: + _have_cram_md5_support = False +else: + _have_cram_md5_support = True + + try: import ssl except ImportError: @@ -665,8 +674,11 @@ def auth_cram_md5(self, challenge=None): # CRAM-MD5 does not support initial-response. if challenge is None: return None - return self.user + " " + hmac.HMAC( - self.password.encode('ascii'), challenge, 'md5').hexdigest() + if not _have_cram_md5_support: + raise SMTPException("CRAM-MD5 is not supported") + password = self.password.encode('ascii') + authcode = hmac.HMAC(password, challenge, 'md5') + return f"{self.user} {authcode.hexdigest()}" def auth_plain(self, challenge=None): """ Authobject to use with PLAIN authentication. Requires self.user and @@ -718,9 +730,10 @@ def login(self, user, password, *, initial_response_ok=True): advertised_authlist = self.esmtp_features["auth"].split() # Authentication methods we can handle in our preferred order: - preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN'] - - # We try the supported authentications in our preferred order, if + if _have_cram_md5_support: + preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN'] + else: + preferred_auths = ['PLAIN', 'LOGIN'] # the server supports them. authlist = [auth for auth in preferred_auths if auth in advertised_authlist] diff --git a/Lib/test/test_dbm_sqlite3.py b/Lib/test/test_dbm_sqlite3.py index 9216da8a63f957..f367a98865d4aa 100644 --- a/Lib/test/test_dbm_sqlite3.py +++ b/Lib/test/test_dbm_sqlite3.py @@ -1,3 +1,5 @@ +import os +import stat import sys import unittest from contextlib import closing @@ -14,6 +16,11 @@ from dbm.sqlite3 import _normalize_uri +root_in_posix = False +if hasattr(os, 'geteuid'): + root_in_posix = (os.geteuid() == 0) + + class _SQLiteDbmTests(unittest.TestCase): def setUp(self): @@ -90,6 +97,50 @@ def test_readonly_iter(self): self.assertEqual([k for k in self.db], [b"key1", b"key2"]) +@unittest.skipIf(root_in_posix, "test is meanless with root privilege") +class ReadOnlyFilesystem(unittest.TestCase): + + def setUp(self): + self.test_dir = os_helper.TESTFN + self.addCleanup(os_helper.rmtree, self.test_dir) + os.mkdir(self.test_dir) + self.db_path = os.path.join(self.test_dir, "test.db") + + db = dbm_sqlite3.open(self.db_path, "c") + db[b"key"] = b"value" + db.close() + + def test_readonly_file_read(self): + os.chmod(self.db_path, stat.S_IREAD) + with dbm_sqlite3.open(self.db_path, "r") as db: + self.assertEqual(db[b"key"], b"value") + + def test_readonly_file_write(self): + os.chmod(self.db_path, stat.S_IREAD) + with dbm_sqlite3.open(self.db_path, "w") as db: + with self.assertRaises(dbm_sqlite3.error): + db[b"newkey"] = b"newvalue" + + def test_readonly_dir_read(self): + os.chmod(self.test_dir, stat.S_IREAD | stat.S_IEXEC) + with dbm_sqlite3.open(self.db_path, "r") as db: + self.assertEqual(db[b"key"], b"value") + + def test_readonly_dir_write(self): + os.chmod(self.test_dir, stat.S_IREAD | stat.S_IEXEC) + with dbm_sqlite3.open(self.db_path, "w") as db: + try: + db[b"newkey"] = b"newvalue" + modified = True # on Windows and macOS + except dbm_sqlite3.error: + modified = False + with dbm_sqlite3.open(self.db_path, "r") as db: + if modified: + self.assertEqual(db[b"newkey"], b"newvalue") + else: + self.assertNotIn(b"newkey", db) + + class ReadWrite(_SQLiteDbmTests): def setUp(self): diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index 7c9adf3049a131..4328909053465e 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -1580,6 +1580,19 @@ def test_ast_fini(self): """) assert_python_ok("-c", code) + def test_warnings_fini(self): + # See https://github.com/python/cpython/issues/137384 + code = textwrap.dedent(''' + import asyncio + from contextvars import ContextVar + + context_loop = ContextVar("context_loop", default=None) + loop = asyncio.new_event_loop() + context_loop.set(loop) + ''') + + assert_python_ok("-c", code) + def setUpModule(): global enabled, debug diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index a13ee58d650e1b..3507fc83b6a2ae 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -12,8 +12,7 @@ import socket from test.support import verbose, run_with_tz, run_with_locale, cpython_only -from test.support import hashlib_helper -from test.support import threading_helper +from test.support import hashlib_helper, threading_helper import unittest from unittest import mock from datetime import datetime, timezone, timedelta @@ -256,7 +255,20 @@ def cmd_IDLE(self, tag, args): self._send_tagged(tag, 'BAD', 'Expected DONE') -class NewIMAPTestsMixin(): +class AuthHandler_CRAM_MD5(SimpleIMAPHandler): + capabilities = 'LOGINDISABLED AUTH=CRAM-MD5' + def cmd_AUTHENTICATE(self, tag, args): + self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm' + 'VzdG9uLm1jaS5uZXQ=') + r = yield + if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT' + b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'): + self._send_tagged(tag, 'OK', 'CRAM-MD5 successful') + else: + self._send_tagged(tag, 'NO', 'No access') + + +class NewIMAPTestsMixin: client = None def _setup(self, imap_handler, connect=True): @@ -439,40 +451,26 @@ def cmd_AUTHENTICATE(self, tag, args): @hashlib_helper.requires_hashdigest('md5', openssl=True) def test_login_cram_md5_bytes(self): - class AuthHandler(SimpleIMAPHandler): - capabilities = 'LOGINDISABLED AUTH=CRAM-MD5' - def cmd_AUTHENTICATE(self, tag, args): - self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm' - 'VzdG9uLm1jaS5uZXQ=') - r = yield - if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT' - b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'): - self._send_tagged(tag, 'OK', 'CRAM-MD5 successful') - else: - self._send_tagged(tag, 'NO', 'No access') - client, _ = self._setup(AuthHandler) - self.assertTrue('AUTH=CRAM-MD5' in client.capabilities) + client, _ = self._setup(AuthHandler_CRAM_MD5) + self.assertIn('AUTH=CRAM-MD5', client.capabilities) ret, _ = client.login_cram_md5("tim", b"tanstaaftanstaaf") self.assertEqual(ret, "OK") @hashlib_helper.requires_hashdigest('md5', openssl=True) def test_login_cram_md5_plain_text(self): - class AuthHandler(SimpleIMAPHandler): - capabilities = 'LOGINDISABLED AUTH=CRAM-MD5' - def cmd_AUTHENTICATE(self, tag, args): - self._send_textline('+ PDE4OTYuNjk3MTcwOTUyQHBvc3RvZmZpY2Uucm' - 'VzdG9uLm1jaS5uZXQ=') - r = yield - if (r == b'dGltIGYxY2E2YmU0NjRiOWVmYT' - b'FjY2E2ZmZkNmNmMmQ5ZjMy\r\n'): - self._send_tagged(tag, 'OK', 'CRAM-MD5 successful') - else: - self._send_tagged(tag, 'NO', 'No access') - client, _ = self._setup(AuthHandler) - self.assertTrue('AUTH=CRAM-MD5' in client.capabilities) + client, _ = self._setup(AuthHandler_CRAM_MD5) + self.assertIn('AUTH=CRAM-MD5', client.capabilities) ret, _ = client.login_cram_md5("tim", "tanstaaftanstaaf") self.assertEqual(ret, "OK") + @hashlib_helper.block_algorithm("md5") + def test_login_cram_md5_blocked(self): + client, _ = self._setup(AuthHandler_CRAM_MD5) + self.assertIn('AUTH=CRAM-MD5', client.capabilities) + msg = re.escape("CRAM-MD5 authentication is not supported") + with self.assertRaisesRegex(imaplib.IMAP4.error, msg): + client.login_cram_md5("tim", b"tanstaaftanstaaf") + def test_aborted_authentication(self): class MyServer(SimpleIMAPHandler): def cmd_AUTHENTICATE(self, tag, args): diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py index 4c9fc14bd43f54..b8aac8c20202a2 100644 --- a/Lib/test/test_smtplib.py +++ b/Lib/test/test_smtplib.py @@ -17,6 +17,7 @@ import threading import unittest +import unittest.mock as mock from test import support, mock_socket from test.support import hashlib_helper from test.support import socket_helper @@ -926,11 +927,14 @@ def _auth_cram_md5(self, arg=None): except ValueError as e: self.push('535 Splitting response {!r} into user and password ' 'failed: {}'.format(logpass, e)) - return False - valid_hashed_pass = hmac.HMAC( - sim_auth[1].encode('ascii'), - self._decode_base64(sim_cram_md5_challenge).encode('ascii'), - 'md5').hexdigest() + return + pwd = sim_auth[1].encode('ascii') + msg = self._decode_base64(sim_cram_md5_challenge).encode('ascii') + try: + valid_hashed_pass = hmac.HMAC(pwd, msg, 'md5').hexdigest() + except ValueError: + self.push('504 CRAM-MD5 is not supported') + return self._authenticated(user, hashed_pass == valid_hashed_pass) # end AUTH related stuff. @@ -1181,6 +1185,39 @@ def testAUTH_CRAM_MD5(self): self.assertEqual(resp, (235, b'Authentication Succeeded')) smtp.close() + @hashlib_helper.block_algorithm('md5') + @mock.patch("smtplib._have_cram_md5_support", False) + def testAUTH_CRAM_MD5_blocked(self): + # CRAM-MD5 is the only "known" method by the server, + # but it is not supported by the client. In particular, + # no challenge will ever be sent. + self.serv.add_feature("AUTH CRAM-MD5") + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', + timeout=support.LOOPBACK_TIMEOUT) + self.addCleanup(smtp.close) + msg = re.escape("No suitable authentication method found.") + with self.assertRaisesRegex(smtplib.SMTPException, msg): + smtp.login(sim_auth[0], sim_auth[1]) + + @hashlib_helper.block_algorithm('md5') + @mock.patch("smtplib._have_cram_md5_support", False) + def testAUTH_CRAM_MD5_blocked_and_fallback(self): + # Test that PLAIN is tried after CRAM-MD5 failed + self.serv.add_feature("AUTH CRAM-MD5 PLAIN") + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', + timeout=support.LOOPBACK_TIMEOUT) + self.addCleanup(smtp.close) + with ( + mock.patch.object(smtp, "auth_cram_md5") as smtp_auth_cram_md5, + mock.patch.object( + smtp, "auth_plain", wraps=smtp.auth_plain + ) as smtp_auth_plain + ): + resp = smtp.login(sim_auth[0], sim_auth[1]) + smtp_auth_plain.assert_called_once() + smtp_auth_cram_md5.assert_not_called() + self.assertEqual(resp, (235, b'Authentication Succeeded')) + @hashlib_helper.requires_hashdigest('md5', openssl=True) def testAUTH_multiple(self): # Test that multiple authentication methods are tried. diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 046385478b5f19..bd3ecfd9a3863d 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -4064,11 +4064,13 @@ def test_dont_swallow_subexceptions_of_falsey_exceptiongroup(self): global_for_suggestions = None -class SuggestionFormattingTestBase: +class SuggestionFormattingTestMixin: + attr_function = getattr + def get_suggestion(self, obj, attr_name=None): if attr_name is not None: def callable(): - getattr(obj, attr_name) + self.attr_function(obj, attr_name) else: callable = obj @@ -4077,7 +4079,9 @@ def callable(): ) return result_lines[0] - def test_getattr_suggestions(self): + +class BaseSuggestionTests(SuggestionFormattingTestMixin): + def test_suggestions(self): class Substitution: noise = more_noise = a = bc = None blech = None @@ -4120,7 +4124,7 @@ class CaseChangeOverSubstitution: actual = self.get_suggestion(cls(), 'bluch') self.assertIn(suggestion, actual) - def test_getattr_suggestions_underscored(self): + def test_suggestions_underscored(self): class A: bluch = None @@ -4128,10 +4132,11 @@ class A: self.assertIn("'bluch'", self.get_suggestion(A(), '_luch')) self.assertIn("'bluch'", self.get_suggestion(A(), '_bluch')) + attr_function = self.attr_function class B: _bluch = None def method(self, name): - getattr(self, name) + attr_function(self, name) self.assertIn("'_bluch'", self.get_suggestion(B(), '_blach')) self.assertIn("'_bluch'", self.get_suggestion(B(), '_luch')) @@ -4141,20 +4146,21 @@ def method(self, name): self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, '_luch'))) self.assertIn("'_bluch'", self.get_suggestion(partial(B().method, 'bluch'))) - def test_getattr_suggestions_do_not_trigger_for_long_attributes(self): + + def test_do_not_trigger_for_long_attributes(self): class A: blech = None actual = self.get_suggestion(A(), 'somethingverywrong') self.assertNotIn("blech", actual) - def test_getattr_error_bad_suggestions_do_not_trigger_for_small_names(self): + def test_do_not_trigger_for_small_names(self): class MyClass: vvv = mom = w = id = pytho = None for name in ("b", "v", "m", "py"): with self.subTest(name=name): - actual = self.get_suggestion(MyClass, name) + actual = self.get_suggestion(MyClass(), name) self.assertNotIn("Did you mean", actual) self.assertNotIn("'vvv", actual) self.assertNotIn("'mom'", actual) @@ -4162,7 +4168,7 @@ class MyClass: self.assertNotIn("'w'", actual) self.assertNotIn("'pytho'", actual) - def test_getattr_suggestions_do_not_trigger_for_big_dicts(self): + def test_do_not_trigger_for_big_dicts(self): class A: blech = None # A class with a very big __dict__ will not be considered @@ -4173,7 +4179,16 @@ class A: actual = self.get_suggestion(A(), 'bluch') self.assertNotIn("blech", actual) - def test_getattr_suggestions_no_args(self): + def test_suggestions_for_same_name(self): + class A: + def __dir__(self): + return ['blech'] + actual = self.get_suggestion(A(), 'blech') + self.assertNotIn("Did you mean", actual) + + +class GetattrSuggestionTests(BaseSuggestionTests): + def test_suggestions_no_args(self): class A: blech = None def __getattr__(self, attr): @@ -4190,7 +4205,7 @@ def __getattr__(self, attr): actual = self.get_suggestion(A(), 'bluch') self.assertIn("blech", actual) - def test_getattr_suggestions_invalid_args(self): + def test_suggestions_invalid_args(self): class NonStringifyClass: __str__ = None __repr__ = None @@ -4214,13 +4229,12 @@ def __getattr__(self, attr): actual = self.get_suggestion(cls(), 'bluch') self.assertIn("blech", actual) - def test_getattr_suggestions_for_same_name(self): - class A: - def __dir__(self): - return ['blech'] - actual = self.get_suggestion(A(), 'blech') - self.assertNotIn("Did you mean", actual) +class DelattrSuggestionTests(BaseSuggestionTests): + attr_function = delattr + + +class SuggestionFormattingTestBase(SuggestionFormattingTestMixin): def test_attribute_error_with_failing_dict(self): class T: bluch = 1 @@ -4876,6 +4890,51 @@ class CPythonSuggestionFormattingTests( """ +class PurePythonGetattrSuggestionFormattingTests( + PurePythonExceptionFormattingMixin, + GetattrSuggestionTests, + unittest.TestCase, +): + """ + Same set of tests (for attribute access) as above using the pure Python + implementation of traceback printing in traceback.py. + """ + + +class PurePythonDelattrSuggestionFormattingTests( + PurePythonExceptionFormattingMixin, + DelattrSuggestionTests, + unittest.TestCase, +): + """ + Same set of tests (for attribute deletion) as above using the pure Python + implementation of traceback printing in traceback.py. + """ + + +@cpython_only +class CPythonGetattrSuggestionFormattingTests( + CAPIExceptionFormattingMixin, + GetattrSuggestionTests, + unittest.TestCase, +): + """ + Same set of tests (for attribute access) as above but with Python's + internal traceback printing. + """ + + +@cpython_only +class CPythonDelattrSuggestionFormattingTests( + CAPIExceptionFormattingMixin, + DelattrSuggestionTests, + unittest.TestCase, +): + """ + Same set of tests (for attribute deletion) as above but with Python's + internal traceback printing. + """ + class MiscTest(unittest.TestCase): def test_all(self): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-02-22-01-23-23.gh-issue-130425.x5SNQ8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-22-01-23-23.gh-issue-130425.x5SNQ8.rst new file mode 100644 index 00000000000000..a655cf2f2a765b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-02-22-01-23-23.gh-issue-130425.x5SNQ8.rst @@ -0,0 +1,2 @@ +Add ``"Did you mean: 'attr'?"`` suggestion when using ``del obj.attr`` if ``attr`` +does not exist. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-22-11-39-40.gh-issue-137384.j4b_in.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-22-11-39-40.gh-issue-137384.j4b_in.rst new file mode 100644 index 00000000000000..583d751a460eb6 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-22-11-39-40.gh-issue-137384.j4b_in.rst @@ -0,0 +1 @@ +Fix a crash when using the :mod:`warnings` module in a finalizer at shutdown. Patch by Kumar Aditya. diff --git a/Misc/NEWS.d/next/Library/2025-06-16-15-00-13.gh-issue-135386.lNrxLc.rst b/Misc/NEWS.d/next/Library/2025-06-16-15-00-13.gh-issue-135386.lNrxLc.rst new file mode 100644 index 00000000000000..dbf1f4525092c6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-06-16-15-00-13.gh-issue-135386.lNrxLc.rst @@ -0,0 +1,2 @@ +Fix opening a :mod:`dbm.sqlite3` database for reading from read-only file +or directory. diff --git a/Misc/NEWS.d/next/Library/2025-07-13-11-20-05.gh-issue-136134.xhh0Kq.rst b/Misc/NEWS.d/next/Library/2025-07-13-11-20-05.gh-issue-136134.xhh0Kq.rst new file mode 100644 index 00000000000000..619526ab12bee2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-13-11-20-05.gh-issue-136134.xhh0Kq.rst @@ -0,0 +1,3 @@ +:meth:`IMAP4.login_cram_md5 ` now raises an +:exc:`IMAP4.error ` if CRAM-MD5 authentication is not +supported. Patch by Bénédikt Tran. diff --git a/Misc/NEWS.d/next/Library/2025-07-13-13-31-22.gh-issue-136134.mh6VjS.rst b/Misc/NEWS.d/next/Library/2025-07-13-13-31-22.gh-issue-136134.mh6VjS.rst new file mode 100644 index 00000000000000..f0290be9ba1e05 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-13-13-31-22.gh-issue-136134.mh6VjS.rst @@ -0,0 +1,5 @@ +:meth:`!SMTP.auth_cram_md5` now raises an :exc:`~smtplib.SMTPException` +instead of a :exc:`ValueError` if Python has been built without MD5 support. +In particular, :class:`~smtplib.SMTP` clients will not attempt to use this +method even if the remote server is assumed to support it. Patch by Bénédikt +Tran. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 06e0c1b61cbcec..24188ffe7132d5 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -6983,6 +6983,7 @@ store_instance_attr_lock_held(PyObject *obj, PyDictValues *values, PyErr_Format(PyExc_AttributeError, "'%.100s' object has no attribute '%U'", Py_TYPE(obj)->tp_name, name); + (void)_PyObject_SetAttributeErrorContext(obj, name); return -1; } diff --git a/Python/pystate.c b/Python/pystate.c index 9091057f6f62cf..2465d8667472dc 100644 --- a/Python/pystate.c +++ b/Python/pystate.c @@ -805,7 +805,6 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) _Py_ClearExecutorDeletionList(interp); #endif _PyAST_Fini(interp); - _PyWarnings_Fini(interp); _PyAtExit_Fini(interp); // All Python types must be destroyed before the last GC collection. Python @@ -815,6 +814,10 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate) /* Last garbage collection on this interpreter */ _PyGC_CollectNoFail(tstate); _PyGC_Fini(interp); + + // Finalize warnings after last gc so that any finalizers can + // access warnings state + _PyWarnings_Fini(interp); struct _PyExecutorObject *cold = interp->cold_executor; if (cold != NULL) { interp->cold_executor = NULL;