From 49ac4afe6aa217ece0689ee1a37eb924c031a762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:16:34 +0200 Subject: [PATCH 1/3] ensure that `hashlib.` does not raise `AttributeError` --- Doc/whatsnew/3.15.rst | 11 +++++++ Lib/hashlib.py | 32 +++++++++++++++++-- ...-07-21-16-13-20.gh-issue-136929.obKZ2S.rst | 5 +++ 3 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-07-21-16-13-20.gh-issue-136929.obKZ2S.rst diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 4d4fb77ad4f030..8068977375177f 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -230,6 +230,17 @@ difflib (Contributed by Jiahao Li in :gh:`134580`.) +hashlib +------- + +* Ensure that hash functions guaranteed to be always *available* exist as + attributes of :mod:`hashlib` even if they will not work at runtime due to + missing backend implementations. For instance, ``hashlib.md5`` will no + longer raise :exc:`AttributeError` if OpenSSL is not available and Python + has been built without MD5 support. + (Contributed by Bénédikt Tran in :gh:`136929`.) + + http.client ----------- diff --git a/Lib/hashlib.py b/Lib/hashlib.py index a7db778b716537..bea0bf5bdbb2ae 100644 --- a/Lib/hashlib.py +++ b/Lib/hashlib.py @@ -261,16 +261,42 @@ def file_digest(fileobj, digest, /, *, _bufsize=2**18): return digestobj +__logging = None +__logger = None for __func_name in __always_supported: # try them all, some may not work due to the OpenSSL # version not supporting that algorithm. try: globals()[__func_name] = __get_hash(__func_name) except ValueError: - import logging - logging.exception('code for hash %s was not found.', __func_name) - + import logging as __logging + if __logger is None: + __logger = __logging.getLogger(__name__) + __logger.warning('hash algorithm %s will not be supported at runtime', + __func_name) + # The following code can be simplified in Python 3.19 + # once "string" is removed from the signature. + __code = f'''\ +def {__func_name}(data=__UNSET, *, usedforsecurity=True, string=__UNSET): + if data is __UNSET and string is not __UNSET: + import warnings + warnings.warn( + "the 'string' keyword parameter is deprecated since " + "Python 3.15 and slated for removal in Python 3.19; " + "use the 'data' keyword parameter or pass the data " + "to hash as a positional argument instead", + DeprecationWarning, stacklevel=2) + if data is not __UNSET and string is not __UNSET: + raise TypeError("'data' and 'string' are mutually exclusive " + "and support for 'string' keyword parameter " + "is slated for removal in a future version.") + raise ValueError("unsupported hash algorithm {__func_name}") +''' + exec(__code, {"__UNSET": object()}, __locals := {}) + globals()[__func_name] = __locals[__func_name] + del __code, __locals # Cleanup locals() del __always_supported, __func_name, __get_hash del __py_new, __hash_new, __get_openssl_constructor +del __logger, __logging diff --git a/Misc/NEWS.d/next/Library/2025-07-21-16-13-20.gh-issue-136929.obKZ2S.rst b/Misc/NEWS.d/next/Library/2025-07-21-16-13-20.gh-issue-136929.obKZ2S.rst new file mode 100644 index 00000000000000..31b8563f9d8523 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-21-16-13-20.gh-issue-136929.obKZ2S.rst @@ -0,0 +1,5 @@ +Ensure that hash functions guaranteed to be always *available* exist as +attributes of :mod:`hashlib` even if they will not work at runtime due to +missing backend implementations. For instance, ``hashlib.md5`` will no +longer raise :exc:`AttributeError` if OpenSSL is not available and Python +has been built without MD5 support. Patch by Bénédikt Tran. From 2720b6bb7b04257d38947c9f2f1cde1d97d60476 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:05:37 +0200 Subject: [PATCH 2/3] reduce diff --- Lib/hashlib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/hashlib.py b/Lib/hashlib.py index bea0bf5bdbb2ae..43200f11f08f56 100644 --- a/Lib/hashlib.py +++ b/Lib/hashlib.py @@ -261,8 +261,7 @@ def file_digest(fileobj, digest, /, *, _bufsize=2**18): return digestobj -__logging = None -__logger = None +__logger = __logging = None for __func_name in __always_supported: # try them all, some may not work due to the OpenSSL # version not supporting that algorithm. From c51d22faf06af53f276ad26ae8ec999437aa3069 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?B=C3=A9n=C3=A9dikt=20Tran?= <10796600+picnixz@users.noreply.github.com> Date: Fri, 25 Jul 2025 16:22:58 +0200 Subject: [PATCH 3/3] keep logging verbosity --- Lib/hashlib.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/Lib/hashlib.py b/Lib/hashlib.py index 43200f11f08f56..8e7083ba692348 100644 --- a/Lib/hashlib.py +++ b/Lib/hashlib.py @@ -261,18 +261,16 @@ def file_digest(fileobj, digest, /, *, _bufsize=2**18): return digestobj -__logger = __logging = None +__logging = None for __func_name in __always_supported: # try them all, some may not work due to the OpenSSL # version not supporting that algorithm. try: globals()[__func_name] = __get_hash(__func_name) - except ValueError: + except ValueError as __exc: import logging as __logging - if __logger is None: - __logger = __logging.getLogger(__name__) - __logger.warning('hash algorithm %s will not be supported at runtime', - __func_name) + __logging.error('hash algorithm %s will not be supported at runtime ' + '[reason: %s]', __func_name, __exc) # The following code can be simplified in Python 3.19 # once "string" is removed from the signature. __code = f'''\ @@ -293,9 +291,9 @@ def {__func_name}(data=__UNSET, *, usedforsecurity=True, string=__UNSET): ''' exec(__code, {"__UNSET": object()}, __locals := {}) globals()[__func_name] = __locals[__func_name] - del __code, __locals + del __exc, __code, __locals # Cleanup locals() del __always_supported, __func_name, __get_hash del __py_new, __hash_new, __get_openssl_constructor -del __logger, __logging +del __logging