Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-amqp for openSUSE:Factory checked in at 2026-09-28 10:45:50 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-amqp (Old) and /work/SRC/openSUSE:Factory/.python-amqp.new.383539 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-amqp" Mon Sep 28 10:45:50 2026 rev:46 rq:1380793 version:5.4.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-amqp/python-amqp.changes 2025-06-13 18:46:24.681187743 +0200 +++ /work/SRC/openSUSE:Factory/.python-amqp.new.383539/python-amqp.changes 2026-09-28 10:46:27.582970360 +0200 @@ -1,0 +2,19 @@ +Sat Sep 26 12:44:20 UTC 2026 - Dirk Müller <[email protected]> + +- update to 5.4.0: + * Replace deprecated datetime.datetime.utcfromtimestamp (#448) + * Remove Python 3.8, 3.9 support + * Update CI workflow to exclude Python 3.9 (#454) + * Add null check for connection to prevent AttributeError on + drain_events (#457) + * Fix prevent channel revival during intentional close to fix + shutdown crash (#458) + * Add Python 3.14 support + * Support limiting tls version (#463) + * Decode AMQP field-table type l as signed 64-bit (#465) + * Treat basic_publish write timeouts as connection errors + (#464) + * Serialize frame_writer() writes with a lock (#466) + * Decode AMQP field-table type 's' as signed 16-bit (#469) + +------------------------------------------------------------------- Old: ---- amqp-5.3.1.tar.gz New: ---- amqp-5.4.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-amqp.spec ++++++ --- /var/tmp/diff_new_pack.Cpmmam/_old 2026-09-28 10:46:28.358002831 +0200 +++ /var/tmp/diff_new_pack.Cpmmam/_new 2026-09-28 10:46:28.360002915 +0200 @@ -1,7 +1,7 @@ # # spec file for package python-amqp # -# Copyright (c) 2025 SUSE LLC +# Copyright (c) 2026 SUSE LLC and contributors # # All modifications and additions to the file contributed by third parties # remain the property of their copyright owners, unless otherwise agreed @@ -18,7 +18,7 @@ %{?sle15_python_module_pythons} Name: python-amqp -Version: 5.3.1 +Version: 5.4.0 Release: 0 Summary: Low-level AMQP client for Python (fork of amqplib) License: LGPL-2.1-or-later ++++++ amqp-5.3.1.tar.gz -> amqp-5.4.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/Changelog new/amqp-5.4.0/Changelog --- old/amqp-5.3.1/Changelog 2024-11-12 20:55:18.000000000 +0100 +++ new/amqp-5.4.0/Changelog 2026-09-19 10:34:14.981160900 +0200 @@ -6,6 +6,26 @@ http://code.google.com/p/py-amqplib/source/browse/CHANGES +.. _version-5.4.0: + +5.4.0 +===== +:release-date: 2026-09-19 +:release-by: Asif Saif Uddin (@auvipy) + +- Replace deprecated datetime.datetime.utcfromtimestamp (#448) +- Remove Python 3.8, 3.9 support +- Update CI workflow to exclude Python 3.9 (#454) +- Add null check for connection to prevent AttributeError on drain_events (#457) +- Fix prevent channel revival during intentional close to fix shutdown crash (#458) +- Add Python 3.14 support +- Support limiting tls version (#463) +- Decode AMQP field-table type l as signed 64-bit (#465) +- Treat basic_publish write timeouts as connection errors (#464) +- Serialize frame_writer() writes with a lock (#466) +- Decode AMQP field-table type 's' as signed 16-bit (#469) + + .. _version-5.3.1: 5.3.1 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/PKG-INFO new/amqp-5.4.0/PKG-INFO --- old/amqp-5.3.1/PKG-INFO 2024-11-12 20:55:34.166940500 +0100 +++ new/amqp-5.4.0/PKG-INFO 2026-09-19 10:37:55.686862000 +0200 @@ -1,11 +1,11 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: amqp -Version: 5.3.1 +Version: 5.4.0 Summary: Low-level AMQP client for Python (fork of amqplib). Home-page: http://github.com/celery/py-amqp Author: Barry Pederson Author-email: [email protected] -Maintainer: Asif Saif Uddin, Matus Valo +Maintainer: Asif Saif Uddin License: BSD Keywords: amqp rabbitmq cloudamqp messaging Platform: any @@ -13,19 +13,34 @@ Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: OSI Approved :: BSD License Classifier: Intended Audience :: Developers Classifier: Operating System :: OS Independent -Requires-Python: >=3.6 +Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: vine<6.0.0,>=5.0.0 +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: home-page +Dynamic: keywords +Dynamic: license +Dynamic: license-file +Dynamic: maintainer +Dynamic: platform +Dynamic: requires-dist +Dynamic: requires-python +Dynamic: summary ===================================================================== Python AMQP 0.9.1 client library @@ -33,10 +48,11 @@ |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| -:Version: 5.3.1 +:Version: 5.4.0 :Web: https://amqp.readthedocs.io/ :Download: https://pypi.org/project/amqp/ :Source: http://github.com/celery/py-amqp/ +:DeepWiki: |deepwiki| :Keywords: amqp, rabbitmq About @@ -232,6 +248,11 @@ :alt: Support Python implementations. :target: https://pypi.org/project/amqp/ +.. |deepwiki| image:: https://devin.ai/assets/deepwiki-badge.png + :alt: Ask http://DeepWiki.com + :target: https://deepwiki.com/celery/py-amqp + :width: 125px + py-amqp as part of the Tidelift Subscription ============================================ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/README.rst new/amqp-5.4.0/README.rst --- old/amqp-5.3.1/README.rst 2024-11-12 20:55:18.000000000 +0100 +++ new/amqp-5.4.0/README.rst 2026-09-19 10:34:14.981480100 +0200 @@ -4,10 +4,11 @@ |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| -:Version: 5.3.1 +:Version: 5.4.0 :Web: https://amqp.readthedocs.io/ :Download: https://pypi.org/project/amqp/ :Source: http://github.com/celery/py-amqp/ +:DeepWiki: |deepwiki| :Keywords: amqp, rabbitmq About @@ -203,6 +204,11 @@ :alt: Support Python implementations. :target: https://pypi.org/project/amqp/ +.. |deepwiki| image:: https://devin.ai/assets/deepwiki-badge.png + :alt: Ask http://DeepWiki.com + :target: https://deepwiki.com/celery/py-amqp + :width: 125px + py-amqp as part of the Tidelift Subscription ============================================ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/amqp/__init__.py new/amqp-5.4.0/amqp/__init__.py --- old/amqp-5.3.1/amqp/__init__.py 2024-11-12 20:55:18.000000000 +0100 +++ new/amqp-5.4.0/amqp/__init__.py 2026-09-19 10:34:14.981763600 +0200 @@ -4,9 +4,9 @@ import re from collections import namedtuple -__version__ = '5.3.1' +__version__ = '5.4.0' __author__ = 'Barry Pederson' -__maintainer__ = 'Asif Saif Uddin, Matus Valo' +__maintainer__ = 'Asif Saif Uddin' __contact__ = '[email protected]' __homepage__ = 'http://github.com/celery/py-amqp' __docformat__ = 'restructuredtext' diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/amqp/abstract_channel.py new/amqp-5.4.0/amqp/abstract_channel.py --- old/amqp-5.3.1/amqp/abstract_channel.py 2022-12-11 13:02:13.000000000 +0100 +++ new/amqp-5.4.0/amqp/abstract_channel.py 2026-09-19 09:36:43.295559400 +0200 @@ -96,7 +96,10 @@ try: while not p.ready: - self.connection.drain_events(timeout=timeout) + conn = self.connection + if conn is None: + raise RecoverableConnectionError('connection already closed') + conn.drain_events(timeout=timeout) if p.value: args, kwargs = p.value diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/amqp/channel.py new/amqp-5.4.0/amqp/channel.py --- old/amqp-5.3.1/amqp/channel.py 2022-12-13 16:33:47.000000000 +0100 +++ new/amqp-5.4.0/amqp/channel.py 2026-09-19 09:36:43.295942500 +0200 @@ -11,7 +11,7 @@ from . import spec from .abstract_channel import AbstractChannel from .exceptions import (ChannelError, ConsumerCancelled, MessageNacked, - RecoverableChannelError, RecoverableConnectionError, + RecoverableConnectionError, error_for_code) from .protocol import queue_declare_ok_t @@ -288,7 +288,7 @@ is the ID of the method. """ self.send_method(spec.Channel.CloseOk) - if not self.connection.is_closing: + if not self.connection.is_closing and not self.is_closing: self._do_revive() raise error_for_code( reply_code, reply_text, (class_id, method_id), ChannelError, @@ -1798,8 +1798,11 @@ spec.Basic.Publish, argsig, (0, exchange, routing_key, mandatory, immediate), msg ) - except socket.timeout: - raise RecoverableChannelError('basic_publish: timed out') + except (socket.timeout, TimeoutError): + # A write timeout means the socket is dead. Channel errors do + # not trigger Kombu/ensure() reconnects, so this must be a + # connection error. See celery/py-amqp#452. + raise RecoverableConnectionError('basic_publish: timed out') basic_publish = _basic_publish diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/amqp/method_framing.py new/amqp-5.4.0/amqp/method_framing.py --- old/amqp-5.3.1/amqp/method_framing.py 2022-12-11 13:02:13.000000000 +0100 +++ new/amqp-5.4.0/amqp/method_framing.py 2026-09-19 09:36:43.296426500 +0200 @@ -1,6 +1,7 @@ """Convert between frames and higher-level AMQP methods.""" # Copyright (C) 2007-2008 Barry Pederson <[email protected]> +import threading from collections import defaultdict from struct import pack, pack_into, unpack_from @@ -108,7 +109,19 @@ buffer_store = Buffer(bytearray(connection.frame_max - 8)) + # The buffer is shared by every frame written on this connection and is + # handed to transport.write() as a live memoryview, so two threads + # publishing on the same connection would overwrite each other's frame + # before the socket has consumed it (#462). Looked up at connect time + # rather than import time so it picks up eventlet/gevent monkeypatching + # even when amqp was imported before the patch was applied. + buffer_lock = threading.Lock() + def write_frame(type_, channel, method_sig, args, content): + with buffer_lock: + return _write_frame(type_, channel, method_sig, args, content) + + def _write_frame(type_, channel, method_sig, args, content): chunk_size = connection.frame_max - 8 offset = 0 properties = None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/amqp/serialization.py new/amqp-5.4.0/amqp/serialization.py --- old/amqp-5.3.1/amqp/serialization.py 2022-12-11 13:02:13.000000000 +0100 +++ new/amqp-5.4.0/amqp/serialization.py 2026-09-19 09:36:43.296854700 +0200 @@ -7,6 +7,7 @@ import calendar from datetime import datetime +from datetime import timezone from decimal import Decimal from io import BytesIO from struct import pack, unpack_from @@ -43,12 +44,12 @@ val = buf[offset:offset + slen] offset += slen - # 's': short string + # 's': RabbitMQ 16-bit signed int (AMQP 0-9-1 errata; the spec's short + # string letter, which RabbitMQ and its clients never send in tables). + # Reading it as a string mangled pika/pamqp headers, celery/kombu#2354. elif ftype == 's': - slen, = unpack_from('>B', buf, offset) - offset += 1 - val = pstr_t(buf[offset:offset + slen]) - offset += slen + val, = unpack_from('>h', buf, offset) + offset += 2 # 'x': Bytes Array elif ftype == 'x': blen, = unpack_from('>I', buf, offset) @@ -79,13 +80,16 @@ elif ftype == 'i': val, = unpack_from('>I', buf, offset) offset += 4 - # 'L': long long int + # 'L': long long int (Qpid unsigned 64-bit uses this letter in some + # stacks; py-amqp historically wrote signed 64-bit values as 'L') elif ftype == 'L': val, = unpack_from('>q', buf, offset) offset += 8 - # 'l': long long unsigned int + # 'l': RabbitMQ 64-bit signed int (AMQP 0-9-1 long-long-int). + # Decoding as unsigned turned delayed-exchange x-delay=-1000 into + # 18446744073709550616. See celery/py-amqp#438. elif ftype == 'l': - val, = unpack_from('>Q', buf, offset) + val, = unpack_from('>q', buf, offset) offset += 8 # 'f': float elif ftype == 'f': @@ -132,7 +136,7 @@ elif ftype == 'T': val, = unpack_from('>Q', buf, offset) offset += 8 - val = datetime.utcfromtimestamp(val) + val = datetime.fromtimestamp(val, tz=timezone.utc).replace(tzinfo=None) # 'V': void elif ftype == 'V': val = None @@ -235,7 +239,8 @@ bitcount = bits = 0 val, = unpack_from('>Q', buf, offset) offset += 8 - val = datetime.utcfromtimestamp(val) + val = datetime.fromtimestamp(val, tz=timezone.utc).replace( + tzinfo=None) else: raise FrameSyntaxError(ILLEGAL_TABLE_TYPE.format(p)) append(val) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/amqp/transport.py new/amqp-5.4.0/amqp/transport.py --- old/amqp-5.3.1/amqp/transport.py 2024-06-08 11:58:20.000000000 +0200 +++ new/amqp-5.4.0/amqp/transport.py 2026-09-19 09:36:43.297068600 +0200 @@ -443,7 +443,8 @@ server_side=False, cert_reqs=None, ca_certs=None, do_handshake_on_connect=False, suppress_ragged_eofs=True, server_hostname=None, - ciphers=None, ssl_version=None): + ciphers=None, ssl_version=None, + tls_maximum_version=None, tls_minimum_version=None): """Socket wrap with SNI headers. stdlib :attr:`ssl.SSLContext.wrap_socket` method augmented with support @@ -505,6 +506,17 @@ Protocol of the SSL Context. The value is one of ``ssl.PROTOCOL_*`` constants. + + tls_maximum_version: + + The highest supported TLS version. The value is a member of + ``ssl.TLSVersion``. + + tls_minimum_version: + + The lowest supported TLS version. The value is a member of + ``ssl.TLSVersion``. + """ opts = { 'sock': sock, @@ -522,6 +534,10 @@ ) context = ssl.SSLContext(ssl_version) + if tls_maximum_version is not None: + context.maximum_version = tls_maximum_version + if tls_minimum_version is not None: + context.minimum_version = tls_minimum_version if certfile is not None: context.load_cert_chain(certfile, keyfile) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/amqp.egg-info/PKG-INFO new/amqp-5.4.0/amqp.egg-info/PKG-INFO --- old/amqp-5.3.1/amqp.egg-info/PKG-INFO 2024-11-12 20:55:34.000000000 +0100 +++ new/amqp-5.4.0/amqp.egg-info/PKG-INFO 2026-09-19 10:37:55.651678600 +0200 @@ -1,11 +1,11 @@ -Metadata-Version: 2.1 +Metadata-Version: 2.4 Name: amqp -Version: 5.3.1 +Version: 5.4.0 Summary: Low-level AMQP client for Python (fork of amqplib). Home-page: http://github.com/celery/py-amqp Author: Barry Pederson Author-email: [email protected] -Maintainer: Asif Saif Uddin, Matus Valo +Maintainer: Asif Saif Uddin License: BSD Keywords: amqp rabbitmq cloudamqp messaging Platform: any @@ -13,19 +13,34 @@ Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: License :: OSI Approved :: BSD License Classifier: Intended Audience :: Developers Classifier: Operating System :: OS Independent -Requires-Python: >=3.6 +Requires-Python: >=3.10 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: vine<6.0.0,>=5.0.0 +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: home-page +Dynamic: keywords +Dynamic: license +Dynamic: license-file +Dynamic: maintainer +Dynamic: platform +Dynamic: requires-dist +Dynamic: requires-python +Dynamic: summary ===================================================================== Python AMQP 0.9.1 client library @@ -33,10 +48,11 @@ |build-status| |coverage| |license| |wheel| |pyversion| |pyimp| -:Version: 5.3.1 +:Version: 5.4.0 :Web: https://amqp.readthedocs.io/ :Download: https://pypi.org/project/amqp/ :Source: http://github.com/celery/py-amqp/ +:DeepWiki: |deepwiki| :Keywords: amqp, rabbitmq About @@ -232,6 +248,11 @@ :alt: Support Python implementations. :target: https://pypi.org/project/amqp/ +.. |deepwiki| image:: https://devin.ai/assets/deepwiki-badge.png + :alt: Ask http://DeepWiki.com + :target: https://deepwiki.com/celery/py-amqp + :width: 125px + py-amqp as part of the Tidelift Subscription ============================================ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/docs/includes/introduction.txt new/amqp-5.4.0/docs/includes/introduction.txt --- old/amqp-5.3.1/docs/includes/introduction.txt 2024-11-12 20:55:18.000000000 +0100 +++ new/amqp-5.4.0/docs/includes/introduction.txt 2026-09-19 10:34:14.982070700 +0200 @@ -1,4 +1,4 @@ -:Version: 5.3.1 +:Version: 5.4.0 :Web: https://amqp.readthedocs.io/ :Download: https://pypi.org/project/amqp/ :Source: http://github.com/celery/py-amqp/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/setup.py new/amqp-5.4.0/setup.py --- old/amqp-5.3.1/setup.py 2022-12-11 13:02:13.000000000 +0100 +++ new/amqp-5.4.0/setup.py 2026-09-19 09:36:43.300408100 +0200 @@ -17,10 +17,11 @@ Programming Language :: Python Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 + Programming Language :: Python :: 3.13 + Programming Language :: Python :: 3.14 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy License :: OSI Approved :: BSD License @@ -132,7 +133,7 @@ platforms=['any'], license='BSD', classifiers=classifiers, - python_requires=">=3.6", + python_requires=">=3.10", install_requires=reqs('default.txt'), setup_requires=setup_requires, tests_require=reqs('test.txt'), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/t/integration/test_rmq.py new/amqp-5.4.0/t/integration/test_rmq.py --- old/amqp-5.3.1/t/integration/test_rmq.py 2022-12-11 13:02:13.000000000 +0100 +++ new/amqp-5.4.0/t/integration/test_rmq.py 2026-09-19 09:36:43.301426000 +0200 @@ -1,5 +1,6 @@ import os import ssl +from struct import pack from unittest.mock import ANY, Mock import pytest @@ -228,3 +229,27 @@ queue='py-amqp-unittest', ) assert msg is None + + @pytest.mark.flaky(reruns=5, reruns_delay=2) + def test_publish_get__short_int_header(self): + class ShortIntHeaderMessage(amqp.Message): + # application_headers={'key': 128} with the value typed 's', as + # pika/pamqp/Java publish it; py-amqp's own writer never emits 's'. + def _serialize_properties(self): + flags = pack('>H', 1 << 13) + table = b'\x03key' + b's' + pack('>h', 128) + return flags + pack('>I', len(table)) + table + + self.channel.queue_declare( + queue='py-amqp-unittest', durable=False, exclusive=True + ) + self.channel.basic_publish( + ShortIntHeaderMessage(b'Unittest'), routing_key='py-amqp-unittest' + ) + msg = self.channel.basic_get( + queue='py-amqp-unittest', + ) + assert msg.body == b'Unittest' + assert msg.properties == {'application_headers': {'key': 128}} + + self.channel.basic_ack(msg.delivery_tag) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/t/unit/test_abstract_channel.py new/amqp-5.4.0/t/unit/test_abstract_channel.py --- old/amqp-5.3.1/t/unit/test_abstract_channel.py 2022-12-11 13:02:13.000000000 +0100 +++ new/amqp-5.4.0/t/unit/test_abstract_channel.py 2026-09-19 09:36:43.301818000 +0200 @@ -83,6 +83,13 @@ self.c.wait([(50, 61)]) assert self.c._pending[(50, 61)] is prev + def test_wait__no_connection(self): + self.c.connection = None + with pytest.raises(RecoverableConnectionError, + match='connection already closed'): + self.c.wait((50, 61)) + assert (50, 61) not in self.c._pending + def test_dispatch_method__content_encoding(self): self.c.auto_decode = True self.method.args = None diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/t/unit/test_channel.py new/amqp-5.4.0/t/unit/test_channel.py --- old/amqp-5.3.1/t/unit/test_channel.py 2022-12-11 13:02:13.000000000 +0100 +++ new/amqp-5.4.0/t/unit/test_channel.py 2026-09-19 09:36:43.302006700 +0200 @@ -90,12 +90,22 @@ assert self.c.is_closing is False assert self.c.connection is None - def test_on_close(self): + @pytest.mark.parametrize( + "channel_is_closing, connection_is_closing, expect_error", + [(False, False, True), (True, False, False), (False, True, False)], + ) + def test_on_close(self, channel_is_closing, connection_is_closing, expect_error): self.c._do_revive = Mock(name='_do_revive') - with pytest.raises(NotFound): + self.c.is_closing = channel_is_closing + self.conn.is_closing = connection_is_closing + if expect_error: + with pytest.raises(NotFound): + self.c._on_close(404, 'text', 50, 61) + self.c._do_revive.assert_called_with() + else: self.c._on_close(404, 'text', 50, 61) + self.c._do_revive.assert_not_called() self.c.send_method.assert_called_with(spec.Channel.CloseOk) - self.c._do_revive.assert_called_with() def test_on_close_ok(self): self.c.collect = Mock(name='collect') @@ -392,6 +402,13 @@ (0, 'ex', 'rkey', False, False), 'msg', ) + @pytest.mark.parametrize('exc_type', [socket.timeout, TimeoutError]) + def test_basic_publish_timeout_is_connection_error(self, exc_type): + self.c.connection.transport.having_timeout = ContextMock() + self.c.send_method.side_effect = exc_type + with pytest.raises(RecoverableConnectionError, match='timed out'): + self.c._basic_publish('msg', 'ex', 'rkey') + def test_basic_publish_confirm(self): self.c._confirm_selected = False self.c.confirm_select = Mock(name='confirm_select') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/t/unit/test_method_framing.py new/amqp-5.4.0/t/unit/test_method_framing.py --- old/amqp-5.3.1/t/unit/test_method_framing.py 2022-12-11 13:02:13.000000000 +0100 +++ new/amqp-5.4.0/t/unit/test_method_framing.py 2026-09-19 09:36:43.302344600 +0200 @@ -1,3 +1,4 @@ +import threading from struct import pack from unittest.mock import Mock @@ -159,3 +160,50 @@ write_arg = self.write.call_args[0][0] assert isinstance(write_arg, memoryview) assert len(write_arg) > original_frame_max + + def test_write_frame__concurrent_writers_are_serialized(self): + """A second thread must not touch the shared buffer while a write + is in flight (#462).""" + in_write = threading.Event() + release_write = threading.Event() + seen = [] + + def blocking_write(view): + # Snapshot what the socket would send *after* it has consumed + # the view; another writer must not be able to change it. + in_write.set() + release_write.wait(2) + seen.append(bytes(view)) + + self.transport.write.side_effect = blocking_write + frame_a = 2, 1, spec.Basic.Publish, b'x' * 10, Message(body=b'A' * 40) + frame_b = 2, 1, spec.Basic.Publish, b'x' * 10, Message(body=b'B' * 40) + + writer_a = threading.Thread(target=self.g, args=frame_a) + writer_a.start() + assert in_write.wait(2) + + b_started = threading.Event() + + def write_b(): + b_started.set() + self.g(*frame_b) + + writer_b = threading.Thread(target=write_b) + writer_b.start() + try: + # Only check once B is actually running, so a slow scheduler + # can't make this pass on an unlocked implementation. + assert b_started.wait(2) + writer_b.join(0.2) + # B must not reach the transport while A's write is in flight. + assert writer_b.is_alive() + assert self.transport.write.call_count == 1 + finally: + release_write.set() + writer_a.join(2) + writer_b.join(2) + assert not writer_a.is_alive() and not writer_b.is_alive() + assert self.transport.write.call_count == 2 + assert b'A' * 40 in seen[0] and b'B' * 40 not in seen[0] + assert b'B' * 40 in seen[1] and b'A' * 40 not in seen[1] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/t/unit/test_serialization.py new/amqp-5.4.0/t/unit/test_serialization.py --- old/amqp-5.3.1/t/unit/test_serialization.py 2022-12-11 13:02:13.000000000 +0100 +++ new/amqp-5.4.0/t/unit/test_serialization.py 2026-09-19 09:36:43.302607000 +0200 @@ -23,7 +23,8 @@ class test_serialization: @pytest.mark.parametrize('descr,frame,expected,cast', [ - ('S', b's8thequick', 'thequick', None), + ('s', b's' + pack('>h', 128), 128, None), + ('s-neg', b's' + pack('>h', -1000), -1000, None), ('S', b'S\x00\x00\x00\x03\xc0\xc0\x00', b'\xc0\xc0\x00', None), ('x', b'x\x00\x00\x00\x09thequick\xffIGNORED', b'thequick\xff', None), ('b', b'b' + pack('>B', True), True, None), @@ -32,7 +33,8 @@ ('u', b'u' + pack('>H', 321), 321, None), ('i', b'i' + pack('>I', 1234), 1234, None), ('L', b'L' + pack('>q', -32451), -32451, None), - ('l', b'l' + pack('>Q', 32451), 32451, None), + ('l', b'l' + pack('>q', 32451), 32451, None), + ('l-neg', b'l' + pack('>q', -1000), -1000, None), ('f', b'f' + pack('>f', 33.3), 34.0, ceil), ]) def test_read_item(self, descr, frame, expected, cast): @@ -87,6 +89,11 @@ } assert loads(b'F', dumps(b'F', [table]), 0)[0][0] == table + def test_table__short_int(self): + # header table as encoded by pika/pamqp for {'key': 128} + frame = b'\x00\x00\x00\x07\x03keys\x00\x80' + assert loads(b'F', frame, 0)[0] == [{'key': 128}] + def test_table__unknown_type(self): table = { 'foo': object(), diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/amqp-5.3.1/t/unit/test_transport.py new/amqp-5.4.0/t/unit/test_transport.py --- old/amqp-5.3.1/t/unit/test_transport.py 2024-06-08 11:58:20.000000000 +0200 +++ new/amqp-5.4.0/t/unit/test_transport.py 2026-09-19 09:36:43.302727500 +0200 @@ -696,6 +696,26 @@ set_ciphers_method_mock.assert_called_with(sentinel.CIPHERS) + def test_wrap_socket_tls_minimum_version(self): + # testing _wrap_socket_sni() with parameter tls_minimum_version + with patch('ssl.SSLContext') as mock_ssl_context_class: + sock = Mock() + context = mock_ssl_context_class() + self.t._wrap_socket_sni( + sock, tls_minimum_version=ssl.TLSVersion.TLSv1_3 + ) + assert context.minimum_version == ssl.TLSVersion.TLSv1_3 + + def test_wrap_socket_tls_maximum_version(self): + # testing _wrap_socket_sni() with parameter tls_maximum_version + with patch('ssl.SSLContext') as mock_ssl_context_class: + sock = Mock() + context = mock_ssl_context_class() + self.t._wrap_socket_sni( + sock, tls_maximum_version=ssl.TLSVersion.TLSv1_3 + ) + assert context.maximum_version == ssl.TLSVersion.TLSv1_3 + def test_wrap_socket_sni_cert_reqs(self): with patch('ssl.SSLContext') as mock_ssl_context_class: sock = Mock()
