diff --git a/Doc/library/curses.rst b/Doc/library/curses.rst index 0b13c559295f3c..fab54ca87efee9 100644 --- a/Doc/library/curses.rst +++ b/Doc/library/curses.rst @@ -770,7 +770,7 @@ the following methods and attributes: .. method:: window.attron(attr) - Add attribute *attr* from the "background" set applied to all writes to the + Add attribute *attr* to the "background" set applied to all writes to the current window. 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.14.rst b/Doc/whatsnew/3.14.rst index 80551048e8336b..aaa3487cb5ca8a 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -65,7 +65,7 @@ Summary -- release highlights .. This section singles out the most important changes in Python 3.14. Brevity is key. -Python 3.14 beta is the pre-release of the next version of the Python +Python 3.14 will be the latest stable release of the Python programming language, with a mix of changes to the language, the implementation and the standard library. @@ -635,7 +635,7 @@ Improved error messages misspellings may still result in regular syntax errors. (Contributed by Pablo Galindo in :gh:`132449`.) -* When unpacking assignment fails due to incorrect number of variables, the +* When an unpacking assignment fails due to an incorrect number of variables, the error message prints the received number of values in more cases than before. (Contributed by Tushar Sadhwani in :gh:`122239`.) @@ -763,7 +763,7 @@ ABI-compatible changes in the future. Complete the :pep:`587` :ref:`PyConfig C API ` by adding :c:func:`PyInitConfig_AddModule` which can be used to add a built-in extension -module; feature previously referred to as the “inittab”. +module; a feature previously referred to as the “inittab”. Add :c:func:`PyConfig_Get` and :c:func:`PyConfig_Set` functions to get and set the current runtime configuration. @@ -1051,7 +1051,7 @@ Concurrent safe warnings control The :class:`warnings.catch_warnings` context manager will now optionally use a context variable for warning filters. This is enabled by setting the :data:`~sys.flags.context_aware_warnings` flag, either with the ``-X`` -command-line option or an environment variable. This gives predicable +command-line option or an environment variable. This gives predictable warnings control when using :class:`~warnings.catch_warnings` combined with multiple threads or asynchronous tasks. The flag defaults to true for the free-threaded build and false for the GIL-enabled build. @@ -1152,7 +1152,7 @@ Other language changes unlike ``\Z``, which has subtly different behavior. (Contributed by Serhiy Storchaka in :gh:`133306`.) -* ``\B`` in :mod:`regular expression ` now matches empty input string. +* ``\B`` in :mod:`regular expression ` now matches the empty input string. Now it is always the opposite of ``\b``. (Contributed by Serhiy Storchaka in :gh:`124130`.) @@ -1221,7 +1221,7 @@ PEP 765: Disallow ``return``/``break``/``continue`` that exit a ``finally`` bloc --------------------------------------------------------------------------------- The compiler emits a :exc:`SyntaxWarning` when a :keyword:`return`, :keyword:`break` or -:keyword:`continue` statements appears where it exits a :keyword:`finally` block. +:keyword:`continue` statement appears where it exits a :keyword:`finally` block. This change is specified in :pep:`765`. @@ -1278,7 +1278,7 @@ ast (Contributed by Irit Katriel in :gh:`130139`.) * Add new ``--feature-version``, ``--optimize``, ``--show-empty`` options to - command-line interface. + the command-line interface. (Contributed by Semyon Moroz in :gh:`133367`.) @@ -2157,7 +2157,7 @@ unittest :meth:`~unittest.TestCase.assertNotStartsWith`, :meth:`~unittest.TestCase.assertEndsWith` and :meth:`~unittest.TestCase.assertNotEndsWith` check whether the Unicode - or byte string starts or ends with particular string(s). + or byte string starts or ends with particular strings. (Contributed by Serhiy Storchaka in :gh:`71339`.) @@ -2223,7 +2223,7 @@ webbrowser supported browsers on macOS. -zipinfo +zipfile ------- * Added :func:`ZipInfo._for_archive ` @@ -2231,7 +2231,7 @@ zipinfo as used by :func:`ZipFile.writestr `. (Contributed by Bénédikt Tran in :gh:`123424`.) -* :meth:`zipfile.ZipFile.writestr` now respect ``SOURCE_DATE_EPOCH`` that +* :meth:`zipfile.ZipFile.writestr` now respects ``SOURCE_DATE_EPOCH`` that distributions can set centrally and have build tools consume this in order to produce reproducible output. (Contributed by Jiahao Li in :gh:`91279`.) @@ -2379,7 +2379,7 @@ zlib * On Windows, `zlib-ng `__ is now used as the implementation of the :mod:`zlib` module in the default binaries. - There are no known incompatabilities between ``zlib-ng`` + There are no known incompatibilities between ``zlib-ng`` and the previously-used ``zlib`` implementation. This should result in better performance at all compression levels. @@ -2839,7 +2839,7 @@ Changes in the Python API rather than collecting generation 1. * Other calls to :func:`!gc.collect` are unchanged. -* The :func:`locale.nl_langinfo` function now sets temporarily the ``LC_CTYPE`` +* The :func:`locale.nl_langinfo` function now temporarily sets the ``LC_CTYPE`` locale in some cases. This temporary change affects other threads. (Contributed by Serhiy Storchaka in :gh:`69998`.) @@ -3019,8 +3019,8 @@ New features and get an attribute of the module. (Contributed by Victor Stinner in :gh:`128911`.) -* Add support for a new ``p`` format unit in :c:func:`Py_BuildValue` that allows to - take a C integer and produce a Python :class:`bool` object. (Contributed by +* Add support for a new ``p`` format unit in :c:func:`Py_BuildValue` that allows + taking a C integer and produces a Python :class:`bool` object. (Contributed by Pablo Galindo in :issue:`45325`.) * Add :c:func:`PyUnstable_Object_IsUniqueReferencedTemporary` to determine if an object 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/json/tool.py b/Lib/json/tool.py index 1967817add8abc..050c2fe2161e3e 100644 --- a/Lib/json/tool.py +++ b/Lib/json/tool.py @@ -46,7 +46,8 @@ def main(): 'to validate and pretty-print JSON objects.') parser = argparse.ArgumentParser(description=description, color=True) parser.add_argument('infile', nargs='?', - help='a JSON file to be validated or pretty-printed', + help='a JSON file to be validated or pretty-printed; ' + 'defaults to stdin', default='-') parser.add_argument('outfile', nargs='?', help='write the output of infile to outfile', diff --git a/Lib/test/test_dbm_sqlite3.py b/Lib/test/test_dbm_sqlite3.py index 9216da8a63f957..15826f51c54180 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 @@ -90,6 +92,49 @@ def test_readonly_iter(self): self.assertEqual([k for k in self.db], [b"key1", b"key2"]) +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_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_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/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/Modules/_cursesmodule.c b/Modules/_cursesmodule.c index a9c91fbcc89c18..232dbcace9ac57 100644 --- a/Modules/_cursesmodule.c +++ b/Modules/_cursesmodule.c @@ -1298,12 +1298,12 @@ _curses.window.attron attr: long / -Add attribute attr from the "background" set. +Add attribute attr to the "background" set. [clinic start generated code]*/ static PyObject * _curses_window_attron_impl(PyCursesWindowObject *self, long attr) -/*[clinic end generated code: output=7afea43b237fa870 input=5a88fba7b1524f32]*/ +/*[clinic end generated code: output=7afea43b237fa870 input=b57f824e1bf58326]*/ { int rtn = wattron(self->win, (attr_t)attr); return curses_window_check_err(self, rtn, "wattron", "attron"); diff --git a/Modules/_hashopenssl.c b/Modules/_hashopenssl.c index 9deafb5253056c..a6496d0f04f2d0 100644 --- a/Modules/_hashopenssl.c +++ b/Modules/_hashopenssl.c @@ -1939,20 +1939,30 @@ locked_HMAC_CTX_copy(HMAC_CTX *new_ctx_p, HMACobject *self) return 0; } -/* returning 0 means that an error occurred and an exception is set */ +#define BAD_DIGEST_SIZE 0 + +/* + * Return the digest size in bytes. + * + * On error, set an exception and return BAD_DIGEST_SIZE. + */ static unsigned int _hashlib_hmac_digest_size(HMACobject *self) { const EVP_MD *md = _hashlib_hmac_get_md(self); if (md == NULL) { - return 0; + return BAD_DIGEST_SIZE; } - unsigned int digest_size = EVP_MD_size(md); - assert(digest_size <= EVP_MAX_MD_SIZE); + int digest_size = EVP_MD_size(md); + /* digest_size < 0 iff EVP_MD context is NULL (which is impossible here) */ + assert(digest_size >= 0); + assert(digest_size <= (int)EVP_MAX_MD_SIZE); + /* digest_size == 0 means that the context is not entirely initialized */ if (digest_size == 0) { - notify_ssl_error_occurred("invalid digest size"); + raise_ssl_error(PyExc_ValueError, "missing digest size"); + return BAD_DIGEST_SIZE; } - return digest_size; + return (unsigned int)digest_size; } static int @@ -2053,24 +2063,38 @@ _hashlib_HMAC_update_impl(HMACobject *self, PyObject *msg) Py_RETURN_NONE; } -static int -_hmac_digest(HMACobject *self, unsigned char *buf, unsigned int len) +/* + * Extract the MAC value to 'buf' and return the digest size. + * + * The buffer 'buf' must have at least hashlib_openssl_HMAC_digest_size(self) + * bytes. Smaller buffers lead to undefined behaviors. + * + * On error, set an exception and return -1. + */ +static Py_ssize_t +_hmac_digest(HMACobject *self, unsigned char *buf) { + unsigned int digest_size = _hashlib_hmac_digest_size(self); + assert(digest_size <= EVP_MAX_MD_SIZE); + if (digest_size == BAD_DIGEST_SIZE) { + assert(PyErr_Occurred()); + return -1; + } HMAC_CTX *temp_ctx = py_openssl_wrapper_HMAC_CTX_new(); if (temp_ctx == NULL) { - return 0; + return -1; } if (locked_HMAC_CTX_copy(temp_ctx, self) < 0) { HMAC_CTX_free(temp_ctx); - return 0; + return -1; } - int r = HMAC_Final(temp_ctx, buf, &len); + int r = HMAC_Final(temp_ctx, buf, NULL); HMAC_CTX_free(temp_ctx); if (r == 0) { notify_ssl_error_occurred_in(Py_STRINGIFY(HMAC_Final)); - return 0; + return -1; } - return 1; + return digest_size; } /*[clinic input] @@ -2082,16 +2106,9 @@ static PyObject * _hashlib_HMAC_digest_impl(HMACobject *self) /*[clinic end generated code: output=1b1424355af7a41e input=bff07f74da318fb4]*/ { - unsigned char digest[EVP_MAX_MD_SIZE]; - unsigned int digest_size = _hashlib_hmac_digest_size(self); - if (digest_size == 0) { - return NULL; - } - int r = _hmac_digest(self, digest, digest_size); - if (r == 0) { - return NULL; - } - return PyBytes_FromStringAndSize((const char *)digest, digest_size); + unsigned char buf[EVP_MAX_MD_SIZE]; + Py_ssize_t n = _hmac_digest(self, buf); + return n < 0 ? NULL : PyBytes_FromStringAndSize((const char *)buf, n); } /*[clinic input] @@ -2109,24 +2126,17 @@ static PyObject * _hashlib_HMAC_hexdigest_impl(HMACobject *self) /*[clinic end generated code: output=80d825be1eaae6a7 input=5e48db83ab1a4d19]*/ { - unsigned char digest[EVP_MAX_MD_SIZE]; - unsigned int digest_size = _hashlib_hmac_digest_size(self); - if (digest_size == 0) { - return NULL; - } - int r = _hmac_digest(self, digest, digest_size); - if (r == 0) { - return NULL; - } - return _Py_strhex((const char *)digest, digest_size); + unsigned char buf[EVP_MAX_MD_SIZE]; + Py_ssize_t n = _hmac_digest(self, buf); + return n < 0 ? NULL : _Py_strhex((const char *)buf, n); } static PyObject * _hashlib_hmac_get_digest_size(PyObject *op, void *Py_UNUSED(closure)) { HMACobject *self = HMACobject_CAST(op); - unsigned int digest_size = _hashlib_hmac_digest_size(self); - return digest_size == 0 ? NULL : PyLong_FromLong(digest_size); + unsigned int size = _hashlib_hmac_digest_size(self); + return size == BAD_DIGEST_SIZE ? NULL : PyLong_FromLong(size); } static PyObject * diff --git a/Modules/clinic/_cursesmodule.c.h b/Modules/clinic/_cursesmodule.c.h index a8c32d6510604a..e6f9798cdf1249 100644 --- a/Modules/clinic/_cursesmodule.c.h +++ b/Modules/clinic/_cursesmodule.c.h @@ -301,7 +301,7 @@ PyDoc_STRVAR(_curses_window_attron__doc__, "attron($self, attr, /)\n" "--\n" "\n" -"Add attribute attr from the \"background\" set."); +"Add attribute attr to the \"background\" set."); #define _CURSES_WINDOW_ATTRON_METHODDEF \ {"attron", (PyCFunction)_curses_window_attron, METH_O, _curses_window_attron__doc__}, @@ -4450,4 +4450,4 @@ _curses_has_extended_color_support(PyObject *module, PyObject *Py_UNUSED(ignored #ifndef _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #define _CURSES_ASSUME_DEFAULT_COLORS_METHODDEF #endif /* !defined(_CURSES_ASSUME_DEFAULT_COLORS_METHODDEF) */ -/*[clinic end generated code: output=79ddaae4da3b80df input=a9049054013a1b77]*/ +/*[clinic end generated code: output=135246e29163510c input=a9049054013a1b77]*/ 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; }