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()

Reply via email to