diff --git a/Doc/deprecations/c-api-pending-removal-in-3.19.rst b/Doc/deprecations/c-api-pending-removal-in-3.19.rst new file mode 100644 index 00000000000000..ac9dcb8b424a17 --- /dev/null +++ b/Doc/deprecations/c-api-pending-removal-in-3.19.rst @@ -0,0 +1,4 @@ +Pending removal in Python 3.19 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* :pep:`456` embedders support for the string hashing scheme definition. diff --git a/Doc/deprecations/index.rst b/Doc/deprecations/index.rst index c91c64a1092457..bb8bfb5c227c2d 100644 --- a/Doc/deprecations/index.rst +++ b/Doc/deprecations/index.rst @@ -20,8 +20,12 @@ C API deprecations .. include:: c-api-pending-removal-in-3.15.rst +.. include:: c-api-pending-removal-in-3.16.rst + .. include:: c-api-pending-removal-in-3.18.rst +.. include:: c-api-pending-removal-in-3.19.rst + .. include:: c-api-pending-removal-in-3.20.rst .. include:: c-api-pending-removal-in-future.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index feccc496fad0e0..fa3ba25a954e40 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -403,7 +403,7 @@ Improved error messages File "/home/pablogsal/github/python/main/lel.py", line 42, in print(container.area) ^^^^^^^^^^^^^^ - AttributeError: 'Container' object has no attribute 'area'. Did you mean: 'inner.area'? + AttributeError: 'Container' object has no attribute 'area'. Did you mean '.inner.area' instead of '.area'? Other language changes @@ -1723,6 +1723,16 @@ on Python 3.13 and older. Deprecated C APIs ----------------- +* Deprecate :pep:`456` support for providing an external definition + of the string hashing scheme. Removal is scheduled for Python 3.19. + + Previously, embedders could define :c:macro:`Py_HASH_ALGORITHM` to be + ``Py_HASH_EXTERNAL`` to indicate that the hashing scheme was provided + externally but this feature was undocumented, untested and most likely + unused. + + (Contributed by Bénédikt Tran in :gh:`141226`.) + * For unsigned integer formats in :c:func:`PyArg_ParseTuple`, accepting Python integers with value that is larger than the maximal value for the C type or less than the minimal value for the corresponding diff --git a/Lib/collections/__init__.py b/Lib/collections/__init__.py index 55ffc36ea5b0c7..2eee4c70955513 100644 --- a/Lib/collections/__init__.py +++ b/Lib/collections/__init__.py @@ -32,6 +32,8 @@ _sys.modules['collections.abc'] = _collections_abc abc = _collections_abc +lazy from copy import copy as _copy +lazy from heapq import nlargest as _nlargest from itertools import chain as _chain from itertools import repeat as _repeat from itertools import starmap as _starmap @@ -59,8 +61,6 @@ except ImportError: pass -heapq = None # Lazily imported - ################################################################################ ### OrderedDict @@ -634,12 +634,7 @@ def most_common(self, n=None): if n is None: return sorted(self.items(), key=_itemgetter(1), reverse=True) - # Lazy import to speedup Python startup time - global heapq - if heapq is None: - import heapq - - return heapq.nlargest(n, self.items(), key=_itemgetter(1)) + return _nlargest(n, self.items(), key=_itemgetter(1)) def elements(self): '''Iterator over elements repeating each as many times as its count. @@ -1249,11 +1244,10 @@ def __copy__(self): def copy(self): if self.__class__ is UserDict: return UserDict(self.data.copy()) - import copy data = self.data try: self.data = {} - c = copy.copy(self) + c = _copy(self) finally: self.data = data c.update(self) diff --git a/Lib/copy.py b/Lib/copy.py index 33dabb3395a7c0..6149301ad1389e 100644 --- a/Lib/copy.py +++ b/Lib/copy.py @@ -204,7 +204,17 @@ def _deepcopy_dict(x, memo, deepcopy=deepcopy): d[dict] = _deepcopy_dict def _deepcopy_frozendict(x, memo, deepcopy=deepcopy): - y = _deepcopy_dict(x, memo, deepcopy) + y = {} + for key, value in x.items(): + y[deepcopy(key, memo)] = deepcopy(value, memo) + + # We're not going to put the frozendict in the memo, but it's still + # important we check for it, in case the frozendict contains recursive + # mutable structures. + try: + return memo[id(x)] + except KeyError: + pass return frozendict(y) d[frozendict] = _deepcopy_frozendict diff --git a/Lib/pprint.py b/Lib/pprint.py index 92a2c543ac279c..e111bd59d4152c 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -235,6 +235,20 @@ def _pprint_dict(self, object, stream, indent, allowance, context, level): _dispatch[dict.__repr__] = _pprint_dict + def _pprint_frozendict(self, object, stream, indent, allowance, context, level): + write = stream.write + cls = object.__class__ + stream.write(cls.__name__ + '(') + length = len(object) + if length: + self._pprint_dict(object, stream, + indent + len(cls.__name__) + 1, + allowance + 1, + context, level) + write(')') + + _dispatch[frozendict.__repr__] = _pprint_frozendict + def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level): if not len(object): stream.write(repr(object)) @@ -623,12 +637,21 @@ def _safe_repr(self, object, context, maxlevels, level): else: return repr(object), True, False - if issubclass(typ, dict) and r is dict.__repr__: + if ((issubclass(typ, dict) and r is dict.__repr__) + or (issubclass(typ, frozendict) and r is frozendict.__repr__)): + is_frozendict = issubclass(typ, frozendict) if not object: - return "{}", True, False + if is_frozendict: + rep = f"{object.__class__.__name__}()" + else: + rep = "{}" + return rep, True, False objid = id(object) if maxlevels and level >= maxlevels: - return "{...}", False, objid in context + rep = "{...}" + if is_frozendict: + rep = f"{object.__class__.__name__}({rep})" + return rep, False, objid in context if objid in context: return _recursion(object), False, True context[objid] = 1 @@ -651,7 +674,10 @@ def _safe_repr(self, object, context, maxlevels, level): if krecur or vrecur: recursive = True del context[objid] - return "{%s}" % ", ".join(components), readable, recursive + rep = "{%s}" % ", ".join(components) + if is_frozendict: + rep = f"{object.__class__.__name__}({rep})" + return rep, readable, recursive if (issubclass(typ, list) and r is list.__repr__) or \ (issubclass(typ, tuple) and r is tuple.__repr__): diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index 0624b6a0257829..6ac4b19da3ea9c 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -2925,9 +2925,13 @@ def _test_recursive_collection_in_key(self, factory, minprotocol=0): self.assertIs(keys[0].attr, x) def test_recursive_frozendict_in_key(self): + if self.py_version < (3, 15): + self.skipTest('need frozendict') self._test_recursive_collection_in_key(frozendict, minprotocol=2) def test_recursive_frozendict_subclass_in_key(self): + if self.py_version < (3, 15): + self.skipTest('need frozendict') self._test_recursive_collection_in_key(MyFrozenDict) def _test_recursive_collection_in_value(self, factory, minprotocol=0): @@ -2942,9 +2946,13 @@ def _test_recursive_collection_in_value(self, factory, minprotocol=0): self.assertIs(x['key'][0], x) def test_recursive_frozendict_in_value(self): + if self.py_version < (3, 15): + self.skipTest('need frozendict') self._test_recursive_collection_in_value(frozendict, minprotocol=2) def test_recursive_frozendict_subclass_in_value(self): + if self.py_version < (3, 15): + self.skipTest('need frozendict') self._test_recursive_collection_in_value(MyFrozenDict) def test_recursive_inst_state(self): @@ -3437,6 +3445,8 @@ def test_newobj_generic(self): self.skipTest('int and str subclasses are not interoperable with Python 2') if (3, 0) <= self.py_version < (3, 4) and proto < 2 and C in (MyStr, MyUnicode): self.skipTest('str subclasses are not interoperable with Python < 3.4') + if self.py_version < (3, 15) and C == MyFrozenDict: + self.skipTest('frozendict is not available on Python < 3.15') B = C.__base__ x = C(C.sample) x.foo = 42 @@ -3458,6 +3468,8 @@ def test_newobj_proxies(self): with self.subTest(proto=proto, C=C): if self.py_version < (3, 4) and proto < 3 and C in (MyStr, MyUnicode): self.skipTest('str subclasses are not interoperable with Python < 3.4') + if self.py_version < (3, 15) and C == MyFrozenDict: + self.skipTest('frozendict is not available on Python < 3.15') B = C.__base__ x = C(C.sample) x.foo = 42 diff --git a/Lib/test/test_capi/test_dict.py b/Lib/test/test_capi/test_dict.py index e726e3d813d888..d3cc279cd3f955 100644 --- a/Lib/test/test_capi/test_dict.py +++ b/Lib/test/test_capi/test_dict.py @@ -26,6 +26,19 @@ def gen(): yield 'c' +class FrozenDictSubclass(frozendict): + pass + + +DICT_TYPES = (dict, DictSubclass, OrderedDict) +FROZENDICT_TYPES = (frozendict, FrozenDictSubclass) +ANYDICT_TYPES = DICT_TYPES + FROZENDICT_TYPES +MAPPING_TYPES = (UserDict,) +NOT_FROZENDICT_TYPES = DICT_TYPES + MAPPING_TYPES +NOT_ANYDICT_TYPES = MAPPING_TYPES +OTHER_TYPES = (lambda: [1], lambda: 42, object) # (list, int, object) + + class CAPITest(unittest.TestCase): def test_dict_check(self): @@ -406,6 +419,7 @@ def test_dict_next(self): # CRASHES dict_next(NULL, 0) def test_dict_update(self): + # Test PyDict_Update() update = _testlimitedcapi.dict_update for cls1 in dict, DictSubclass: for cls2 in dict, DictSubclass, UserDict: @@ -416,11 +430,13 @@ def test_dict_update(self): self.assertRaises(AttributeError, update, {}, []) self.assertRaises(AttributeError, update, {}, 42) self.assertRaises(SystemError, update, UserDict(), {}) + self.assertRaises(SystemError, update, frozendict(), {}) self.assertRaises(SystemError, update, 42, {}) self.assertRaises(SystemError, update, {}, NULL) self.assertRaises(SystemError, update, NULL, {}) def test_dict_merge(self): + # Test PyDict_Merge() merge = _testlimitedcapi.dict_merge for cls1 in dict, DictSubclass: for cls2 in dict, DictSubclass, UserDict: @@ -434,11 +450,13 @@ def test_dict_merge(self): self.assertRaises(AttributeError, merge, {}, [], 0) self.assertRaises(AttributeError, merge, {}, 42, 0) self.assertRaises(SystemError, merge, UserDict(), {}, 0) + self.assertRaises(SystemError, merge, frozendict(), {}, 0) self.assertRaises(SystemError, merge, 42, {}, 0) self.assertRaises(SystemError, merge, {}, NULL, 0) self.assertRaises(SystemError, merge, NULL, {}, 0) def test_dict_mergefromseq2(self): + # Test PyDict_MergeFromSeq2() mergefromseq2 = _testlimitedcapi.dict_mergefromseq2 for cls1 in dict, DictSubclass: for cls2 in list, iter: @@ -453,8 +471,8 @@ def test_dict_mergefromseq2(self): self.assertRaises(ValueError, mergefromseq2, {}, [(1, 2, 3)], 0) self.assertRaises(TypeError, mergefromseq2, {}, [1], 0) self.assertRaises(TypeError, mergefromseq2, {}, 42, 0) - # CRASHES mergefromseq2(UserDict(), [], 0) - # CRASHES mergefromseq2(42, [], 0) + self.assertRaises(SystemError, mergefromseq2, UserDict(), [], 0) + self.assertRaises(SystemError, mergefromseq2, 42, [], 0) # CRASHES mergefromseq2({}, NULL, 0) # CRASHES mergefromseq2(NULL, {}, 0) @@ -545,6 +563,61 @@ def test_dict_popstring(self): # CRASHES dict_popstring({}, NULL) # CRASHES dict_popstring({"a": 1}, NULL) + def test_frozendict_check(self): + # Test PyFrozenDict_Check() + check = _testcapi.frozendict_check + for dict_type in FROZENDICT_TYPES: + self.assertTrue(check(dict_type(x=1))) + for dict_type in NOT_FROZENDICT_TYPES + OTHER_TYPES: + self.assertFalse(check(dict_type())) + # CRASHES check(NULL) + + def test_frozendict_checkexact(self): + # Test PyFrozenDict_CheckExact() + check = _testcapi.frozendict_checkexact + for dict_type in FROZENDICT_TYPES: + self.assertEqual(check(dict_type(x=1)), dict_type == frozendict) + for dict_type in NOT_FROZENDICT_TYPES + OTHER_TYPES: + self.assertFalse(check(dict_type())) + # CRASHES check(NULL) + + def test_anydict_check(self): + # Test PyAnyDict_Check() + check = _testcapi.anydict_check + for dict_type in ANYDICT_TYPES: + self.assertTrue(check(dict_type({1: 2}))) + for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES: + self.assertFalse(check(test_type())) + # CRASHES check(NULL) + + def test_anydict_checkexact(self): + # Test PyAnyDict_CheckExact() + check = _testcapi.anydict_checkexact + for dict_type in ANYDICT_TYPES: + self.assertEqual(check(dict_type(x=1)), + dict_type in (dict, frozendict)) + for test_type in NOT_ANYDICT_TYPES + OTHER_TYPES: + self.assertFalse(check(test_type())) + # CRASHES check(NULL) + + def test_frozendict_new(self): + # Test PyFrozenDict_New() + frozendict_new = _testcapi.frozendict_new + + for dict_type in ANYDICT_TYPES: + dct = frozendict_new(dict_type({'x': 1})) + self.assertEqual(dct, frozendict(x=1)) + self.assertIs(type(dct), frozendict) + + dct = frozendict_new([('x', 1), ('y', 2)]) + self.assertEqual(dct, frozendict(x=1, y=2)) + self.assertIs(type(dct), frozendict) + + # PyFrozenDict_New(NULL) creates an empty dictionary + dct = frozendict_new(NULL) + self.assertEqual(dct, frozendict()) + self.assertIs(type(dct), frozendict) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_copy.py b/Lib/test/test_copy.py index 858e5e089d5aba..98f56b5ae87f96 100644 --- a/Lib/test/test_copy.py +++ b/Lib/test/test_copy.py @@ -432,6 +432,23 @@ def test_deepcopy_frozendict(self): self.assertIsNot(x, y) self.assertIsNot(x["foo"], y["foo"]) + # recursive frozendict + x = frozendict(foo=[]) + x['foo'].append(x) + y = copy.deepcopy(x) + self.assertEqual(y.keys(), x.keys()) + self.assertIsNot(x, y) + self.assertIsNot(x["foo"], y["foo"]) + self.assertIs(y['foo'][0], y) + + x = frozendict(foo=[]) + x['foo'].append(x) + x = x['foo'] + y = copy.deepcopy(x) + self.assertIsNot(x, y) + self.assertIsNot(x[0], y[0]) + self.assertIs(y[0]['foo'], y) + @support.skip_emscripten_stack_overflow() @support.skip_wasi_stack_overflow() def test_deepcopy_reflexive_dict(self): diff --git a/Lib/test/test_import/test_lazy_imports.py b/Lib/test/test_import/test_lazy_imports.py index a4c9c14ae2b5f8..dc185c070acc62 100644 --- a/Lib/test/test_import/test_lazy_imports.py +++ b/Lib/test/test_import/test_lazy_imports.py @@ -393,6 +393,15 @@ def test_dunder_lazy_import_used(self): import test.test_import.data.lazy_imports.dunder_lazy_import_used self.assertIn("test.test_import.data.lazy_imports.basic2", sys.modules) + def test_dunder_lazy_import_invalid_arguments(self): + """__lazy_import__ should reject invalid arguments.""" + for invalid_name in (b"", 123, None): + with self.assertRaises(TypeError): + __lazy_import__(invalid_name) + + with self.assertRaises(ValueError): + __lazy_import__("sys", level=-1) + def test_dunder_lazy_import_builtins(self): """__lazy_import__ should use module's __builtins__ for __import__.""" from test.test_import.data.lazy_imports import dunder_lazy_import_builtins diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 41c337ade7eca1..f3860a5d511989 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -67,6 +67,13 @@ class dict3(dict): def __repr__(self): return dict.__repr__(self) +class frozendict2(frozendict): + pass + +class frozendict3(frozendict): + def __repr__(self): + return frozendict.__repr__(self) + class dict_custom_repr(dict): def __repr__(self): return '*'*len(dict.__repr__(self)) @@ -254,18 +261,22 @@ def test_same_as_repr(self): set(), set2(), set3(), frozenset(), frozenset2(), frozenset3(), {}, dict2(), dict3(), + frozendict(), frozendict2(), frozendict3(), {}.keys(), {}.values(), {}.items(), MappingView({}), KeysView({}), ItemsView({}), ValuesView({}), self.assertTrue, pprint, -6, -6, -6-6j, -1.5, "x", b"x", bytearray(b"x"), (3,), [3], {3: 6}, - (1,2), [3,4], {5: 6}, + (1,2), [3,4], tuple2((1,2)), tuple3((1,2)), tuple3(range(100)), [3,4], list2([3,4]), list3([3,4]), list3(range(100)), set({7}), set2({7}), set3({7}), frozenset({8}), frozenset2({8}), frozenset3({8}), - dict2({5: 6}), dict3({5: 6}), + {5: 6}, dict2({5: 6}), dict3({5: 6}), + frozendict({5: 6}), frozendict2({5: 6}), frozendict3({5: 6}), {5: 6}.keys(), {5: 6}.values(), {5: 6}.items(), + frozendict({5: 6}).keys(), frozendict({5: 6}).values(), + frozendict({5: 6}).items(), MappingView({5: 6}), KeysView({5: 6}), ItemsView({5: 6}), ValuesView({5: 6}), range(10, -11, -1), @@ -330,20 +341,45 @@ def test_basic_line_wrap(self): for type in [dict, dict2]: self.assertEqual(pprint.pformat(type(o)), exp) + exp = """\ +frozendict({'RPM_cal': 0, + 'RPM_cal2': 48059, + 'Speed_cal': 0, + 'controldesk_runtime_us': 0, + 'main_code_runtime_us': 0, + 'read_io_runtime_us': 0, + 'write_io_runtime_us': 43690})""" + self.assertEqual(pprint.pformat(frozendict(o)), exp) + exp = """\ +frozendict2({'RPM_cal': 0, + 'RPM_cal2': 48059, + 'Speed_cal': 0, + 'controldesk_runtime_us': 0, + 'main_code_runtime_us': 0, + 'read_io_runtime_us': 0, + 'write_io_runtime_us': 43690})""" + self.assertEqual(pprint.pformat(frozendict2(o)), exp) + o = range(100) exp = 'dict_keys([%s])' % ',\n '.join(map(str, o)) keys = dict.fromkeys(o).keys() self.assertEqual(pprint.pformat(keys), exp) + keys = frozendict.fromkeys(o).keys() + self.assertEqual(pprint.pformat(keys), exp) o = range(100) exp = 'dict_values([%s])' % ',\n '.join(map(str, o)) values = {v: v for v in o}.values() self.assertEqual(pprint.pformat(values), exp) + values = frozendict({v: v for v in o}).values() + self.assertEqual(pprint.pformat(values), exp) o = range(100) exp = 'dict_items([%s])' % ',\n '.join("(%s, %s)" % (i, i) for i in o) items = {v: v for v in o}.items() self.assertEqual(pprint.pformat(items), exp) + items = frozendict({v: v for v in o}).items() + self.assertEqual(pprint.pformat(items), exp) o = range(100) exp = 'odict_keys([%s])' % ',\n '.join(map(str, o)) diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py index 7e9ba735b3ce66..dc795c6bd8a41f 100644 --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -57,6 +57,16 @@ CAN_GET_SELECTED_OPENSSL_SIGALG = ssl.OPENSSL_VERSION_INFO >= (3, 5) PY_SSL_DEFAULT_CIPHERS = sysconfig.get_config_var('PY_SSL_DEFAULT_CIPHERS') +HAS_KEYLOG = hasattr(ssl.SSLContext, 'keylog_filename') +requires_keylog = unittest.skipUnless( + HAS_KEYLOG, 'test requires OpenSSL 1.1.1 with keylog callback') +CAN_SET_KEYLOG = HAS_KEYLOG and os.name != "nt" +requires_keylog_setter = unittest.skipUnless( + CAN_SET_KEYLOG, + "cannot set 'keylog_filename' on Windows" +) + + PROTOCOL_TO_TLS_VERSION = {} for proto, ver in ( ("PROTOCOL_SSLv3", "SSLv3"), @@ -266,34 +276,69 @@ def utc_offset(): #NOTE: ignore issues like #1647654 ) -def test_wrap_socket(sock, *, - cert_reqs=ssl.CERT_NONE, ca_certs=None, - ciphers=None, ciphersuites=None, - min_version=None, max_version=None, - certfile=None, keyfile=None, - **kwargs): - if not kwargs.get("server_side"): - kwargs["server_hostname"] = SIGNED_CERTFILE_HOSTNAME - context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) - else: +def make_test_context( + *, + server_side=False, + check_hostname=None, + cert_reqs=ssl.CERT_NONE, + ca_certs=None, certfile=None, keyfile=None, + ciphers=None, ciphersuites=None, + min_version=None, max_version=None, +): + if server_side: context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) - if cert_reqs is not None: + else: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + + if check_hostname is None: if cert_reqs == ssl.CERT_NONE: context.check_hostname = False + else: + context.check_hostname = check_hostname + + if cert_reqs is not None: context.verify_mode = cert_reqs + if ca_certs is not None: context.load_verify_locations(ca_certs) if certfile is not None or keyfile is not None: context.load_cert_chain(certfile, keyfile) + if ciphers is not None: context.set_ciphers(ciphers) if ciphersuites is not None: context.set_ciphersuites(ciphersuites) + if min_version is not None: context.minimum_version = min_version if max_version is not None: context.maximum_version = max_version - return context.wrap_socket(sock, **kwargs) + + return context + + +def test_wrap_socket( + sock, + *, + server_side=False, + check_hostname=None, + cert_reqs=ssl.CERT_NONE, + ca_certs=None, certfile=None, keyfile=None, + ciphers=None, ciphersuites=None, + min_version=None, max_version=None, + **kwargs, +): + context = make_test_context( + server_side=server_side, + check_hostname=check_hostname, + cert_reqs=cert_reqs, + ca_certs=ca_certs, certfile=certfile, keyfile=keyfile, + ciphers=ciphers, ciphersuites=ciphersuites, + min_version=min_version, max_version=max_version, + ) + if not server_side: + kwargs.setdefault("server_hostname", SIGNED_CERTFILE_HOSTNAME) + return context.wrap_socket(sock, server_side=server_side, **kwargs) USE_SAME_TEST_CONTEXT = False @@ -1741,6 +1786,39 @@ def test_num_tickest(self): with self.assertRaises(ValueError): ctx.num_tickets = 1 + @support.cpython_only + def test_refcycle_msg_callback(self): + # See https://github.com/python/cpython/issues/142516. + ctx = make_test_context() + def msg_callback(*args, _=ctx, **kwargs): ... + ctx._msg_callback = msg_callback + + @support.cpython_only + @requires_keylog_setter + def test_refcycle_keylog_filename(self): + # See https://github.com/python/cpython/issues/142516. + self.addCleanup(os_helper.unlink, os_helper.TESTFN) + ctx = make_test_context() + class KeylogFilename(str): ... + ctx.keylog_filename = KeylogFilename(os_helper.TESTFN) + ctx.keylog_filename._ = ctx + + @support.cpython_only + @unittest.skipUnless(ssl.HAS_PSK, 'requires TLS-PSK') + def test_refcycle_psk_client_callback(self): + # See https://github.com/python/cpython/issues/142516. + ctx = make_test_context() + def psk_client_callback(*args, _=ctx, **kwargs): ... + ctx.set_psk_client_callback(psk_client_callback) + + @support.cpython_only + @unittest.skipUnless(ssl.HAS_PSK, 'requires TLS-PSK') + def test_refcycle_psk_server_callback(self): + # See https://github.com/python/cpython/issues/142516. + ctx = make_test_context(server_side=True) + def psk_server_callback(*args, _=ctx, **kwargs): ... + ctx.set_psk_server_callback(psk_server_callback) + class SSLErrorTests(unittest.TestCase): @@ -5174,10 +5252,6 @@ def test_internal_chain_server(self): self.assertEqual(res, b'\x02\n') -HAS_KEYLOG = hasattr(ssl.SSLContext, 'keylog_filename') -requires_keylog = unittest.skipUnless( - HAS_KEYLOG, 'test requires OpenSSL 1.1.1 with keylog callback') - class TestSSLDebug(unittest.TestCase): def keylog_lines(self, fname=os_helper.TESTFN): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-09-15-44-58.gh-issue-141226.KTb_3F.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-09-15-44-58.gh-issue-141226.KTb_3F.rst new file mode 100644 index 00000000000000..3f7ce7326187d4 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-09-15-44-58.gh-issue-141226.KTb_3F.rst @@ -0,0 +1,3 @@ +Deprecate :pep:`456` support for providing an external definition +of the string hashing scheme. Removal is scheduled for Python 3.19. +Patch by Bénédikt Tran. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-21-09-47-45.gh-issue-145058.e-RBw-.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-21-09-47-45.gh-issue-145058.e-RBw-.rst new file mode 100644 index 00000000000000..05eb296f96ec6d --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-21-09-47-45.gh-issue-145058.e-RBw-.rst @@ -0,0 +1,2 @@ +Fix a crash when :func:`!__lazy_import__` is passed a non-string argument, +by raising an :exc:`TypeError` instead. diff --git a/Misc/NEWS.d/next/Library/2026-01-11-13-03-32.gh-issue-142516.u7An-s.rst b/Misc/NEWS.d/next/Library/2026-01-11-13-03-32.gh-issue-142516.u7An-s.rst new file mode 100644 index 00000000000000..efa7c8a1f62692 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-01-11-13-03-32.gh-issue-142516.u7An-s.rst @@ -0,0 +1,2 @@ +:mod:`ssl`: fix reference leaks in :class:`ssl.SSLContext` objects. Patch by +Bénédikt Tran. diff --git a/Modules/_ssl.c b/Modules/_ssl.c index b0c0d8deeecd23..2eb31229a9bf3c 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -331,7 +331,7 @@ typedef struct { int post_handshake_auth; #endif PyObject *msg_cb; - PyObject *keylog_filename; + PyObject *keylog_filename; // can be anything accepted by Py_fopen() BIO *keylog_bio; /* Cached module state, also used in SSLSocket and SSLSession code. */ _sslmodulestate *state; @@ -361,7 +361,7 @@ typedef struct { PySSLContext *ctx; /* weakref to SSL context */ char shutdown_seen_zero; enum py_ssl_server_or_client socket_type; - PyObject *owner; /* Python level "owner" passed to servername callback */ + PyObject *owner; /* weakref to Python level "owner" passed to servername callback */ PyObject *server_hostname; } PySSLSocket; @@ -2436,6 +2436,17 @@ PySSL_traverse(PyObject *op, visitproc visit, void *arg) return 0; } +static int +PySSL_clear(PyObject *op) +{ + PySSLSocket *self = PySSLSocket_CAST(op); + Py_CLEAR(self->Socket); + Py_CLEAR(self->ctx); + Py_CLEAR(self->owner); + Py_CLEAR(self->server_hostname); + return 0; +} + static void PySSL_dealloc(PyObject *op) { @@ -2456,10 +2467,7 @@ PySSL_dealloc(PyObject *op) SSL_set_shutdown(self->ssl, SSL_SENT_SHUTDOWN | SSL_get_shutdown(self->ssl)); SSL_free(self->ssl); } - Py_XDECREF(self->Socket); - Py_XDECREF(self->ctx); - Py_XDECREF(self->server_hostname); - Py_XDECREF(self->owner); + (void)PySSL_clear(op); PyObject_GC_Del(self); Py_DECREF(tp); } @@ -3568,6 +3576,11 @@ context_traverse(PyObject *op, visitproc visit, void *arg) PySSLContext *self = PySSLContext_CAST(op); Py_VISIT(self->set_sni_cb); Py_VISIT(self->msg_cb); + Py_VISIT(self->keylog_filename); +#ifndef OPENSSL_NO_PSK + Py_VISIT(self->psk_client_callback); + Py_VISIT(self->psk_server_callback); +#endif Py_VISIT(Py_TYPE(self)); return 0; } diff --git a/Modules/_testcapi/dict.c b/Modules/_testcapi/dict.c index b7c73d7332bd4e..172591b03182ab 100644 --- a/Modules/_testcapi/dict.c +++ b/Modules/_testcapi/dict.c @@ -258,6 +258,43 @@ test_dict_iteration(PyObject* self, PyObject *Py_UNUSED(ignored)) } +static PyObject * +frozendict_check(PyObject *self, PyObject *obj) +{ + NULLABLE(obj); + return PyLong_FromLong(PyFrozenDict_Check(obj)); +} + +static PyObject * +frozendict_checkexact(PyObject *self, PyObject *obj) +{ + NULLABLE(obj); + return PyLong_FromLong(PyFrozenDict_CheckExact(obj)); +} + +static PyObject * +anydict_check(PyObject *self, PyObject *obj) +{ + NULLABLE(obj); + return PyLong_FromLong(PyAnyDict_Check(obj)); +} + +static PyObject * +anydict_checkexact(PyObject *self, PyObject *obj) +{ + NULLABLE(obj); + return PyLong_FromLong(PyAnyDict_CheckExact(obj)); +} + + +static PyObject * +frozendict_new(PyObject *self, PyObject *obj) +{ + NULLABLE(obj); + return PyFrozenDict_New(obj); +} + + static PyMethodDef test_methods[] = { {"dict_containsstring", dict_containsstring, METH_VARARGS}, {"dict_getitemref", dict_getitemref, METH_VARARGS}, @@ -269,6 +306,11 @@ static PyMethodDef test_methods[] = { {"dict_popstring", dict_popstring, METH_VARARGS}, {"dict_popstring_null", dict_popstring_null, METH_VARARGS}, {"test_dict_iteration", test_dict_iteration, METH_NOARGS}, + {"frozendict_check", frozendict_check, METH_O}, + {"frozendict_checkexact", frozendict_checkexact, METH_O}, + {"anydict_check", anydict_check, METH_O}, + {"anydict_checkexact", anydict_checkexact, METH_O}, + {"frozendict_new", frozendict_new, METH_O}, {NULL}, }; diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 8f960352fa4824..276e1df21a80d8 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -140,6 +140,7 @@ static PyObject* frozendict_new(PyTypeObject *type, PyObject *args, PyObject *kwds); static PyObject* dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds); static int dict_merge(PyObject *a, PyObject *b, int override); +static int dict_merge_from_seq2(PyObject *d, PyObject *seq2, int override); /*[clinic input] @@ -3818,16 +3819,16 @@ static int dict_update_arg(PyObject *self, PyObject *arg) { if (PyAnyDict_CheckExact(arg)) { - return PyDict_Merge(self, arg, 1); + return dict_merge(self, arg, 1); } int has_keys = PyObject_HasAttrWithError(arg, &_Py_ID(keys)); if (has_keys < 0) { return -1; } if (has_keys) { - return PyDict_Merge(self, arg, 1); + return dict_merge(self, arg, 1); } - return PyDict_MergeFromSeq2(self, arg, 1); + return dict_merge_from_seq2(self, arg, 1); } static int @@ -3846,7 +3847,7 @@ dict_update_common(PyObject *self, PyObject *args, PyObject *kwds, if (result == 0 && kwds != NULL) { if (PyArg_ValidateKeywordArguments(kwds)) - result = PyDict_Merge(self, kwds, 1); + result = dict_merge(self, kwds, 1); else result = -1; } @@ -3960,8 +3961,8 @@ merge_from_seq2_lock_held(PyObject *d, PyObject *seq2, int override) return Py_SAFE_DOWNCAST(i, Py_ssize_t, int); } -int -PyDict_MergeFromSeq2(PyObject *d, PyObject *seq2, int override) +static int +dict_merge_from_seq2(PyObject *d, PyObject *seq2, int override) { int res; Py_BEGIN_CRITICAL_SECTION(d); @@ -3971,6 +3972,19 @@ PyDict_MergeFromSeq2(PyObject *d, PyObject *seq2, int override) return res; } +int +PyDict_MergeFromSeq2(PyObject *d, PyObject *seq2, int override) +{ + assert(d != NULL); + assert(seq2 != NULL); + if (!PyDict_Check(d)) { + PyErr_BadInternalCall(); + return -1; + } + + return dict_merge_from_seq2(d, seq2, override); +} + static int dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override) { @@ -4070,23 +4084,14 @@ dict_dict_merge(PyDictObject *mp, PyDictObject *other, int override) static int dict_merge(PyObject *a, PyObject *b, int override) { - PyDictObject *mp, *other; - + assert(a != NULL); + assert(b != NULL); assert(0 <= override && override <= 2); - /* We accept for the argument either a concrete dictionary object, - * or an abstract "mapping" object. For the former, we can do - * things quite efficiently. For the latter, we only require that - * PyMapping_Keys() and PyObject_GetItem() be supported. - */ - if (a == NULL || !PyAnyDict_Check(a) || b == NULL) { - PyErr_BadInternalCall(); - return -1; - } - mp = (PyDictObject*)a; + PyDictObject *mp = _PyAnyDict_CAST(a); int res = 0; if (PyAnyDict_Check(b) && (Py_TYPE(b)->tp_iter == dict_iter)) { - other = (PyDictObject*)b; + PyDictObject *other = (PyDictObject*)b; int res; Py_BEGIN_CRITICAL_SECTION2(a, b); res = dict_dict_merge((PyDictObject *)a, other, override); @@ -4167,23 +4172,38 @@ dict_merge(PyObject *a, PyObject *b, int override) } } +static int +dict_merge_api(PyObject *a, PyObject *b, int override) +{ + /* We accept for the argument either a concrete dictionary object, + * or an abstract "mapping" object. For the former, we can do + * things quite efficiently. For the latter, we only require that + * PyMapping_Keys() and PyObject_GetItem() be supported. + */ + if (a == NULL || !PyDict_Check(a) || b == NULL) { + PyErr_BadInternalCall(); + return -1; + } + return dict_merge(a, b, override); +} + int PyDict_Update(PyObject *a, PyObject *b) { - return dict_merge(a, b, 1); + return dict_merge_api(a, b, 1); } int PyDict_Merge(PyObject *a, PyObject *b, int override) { /* XXX Deprecate override not in (0, 1). */ - return dict_merge(a, b, override != 0); + return dict_merge_api(a, b, override != 0); } int _PyDict_MergeEx(PyObject *a, PyObject *b, int override) { - return dict_merge(a, b, override); + return dict_merge_api(a, b, override); } /*[clinic input] diff --git a/Python/import.c b/Python/import.c index c20c55727d2f94..4c234a4a70437c 100644 --- a/Python/import.c +++ b/Python/import.c @@ -4468,6 +4468,17 @@ _PyImport_LazyImportModuleLevelObject(PyThreadState *tstate, PyObject *globals, PyObject *locals, PyObject *fromlist, int level) { + assert(name != NULL); + if (!PyUnicode_Check(name)) { + _PyErr_Format(tstate, PyExc_TypeError, + "module name must be a string, got %T", name); + return NULL; + } + if (level < 0) { + _PyErr_SetString(tstate, PyExc_ValueError, "level must be >= 0"); + return NULL; + } + PyObject *abs_name = get_abs_name(tstate, name, globals, level); if (abs_name == NULL) { return NULL; diff --git a/Python/pyhash.c b/Python/pyhash.c index 157312a936bbcc..1eb890794a7544 100644 --- a/Python/pyhash.c +++ b/Python/pyhash.c @@ -17,7 +17,7 @@ _Py_HashSecret_t _Py_HashSecret = {{0}}; #if Py_HASH_ALGORITHM == Py_HASH_EXTERNAL -extern PyHash_FuncDef PyHash_Func; +Py_DEPRECATED(3.15) extern PyHash_FuncDef PyHash_Func; #else static PyHash_FuncDef PyHash_Func; #endif