From 96f331bc071259ac415d327513fdceee6376da10 Mon Sep 17 00:00:00 2001 From: Carlos Bernal Date: Thu, 23 Jul 2026 11:26:09 +0200 Subject: [PATCH 1/6] Fix invalid string-escape SyntaxWarnings via raw strings - protocol/http.py:438: use rb-string to fix '\.' escape SyntaxWarning (mirrors the raw-string sub already used on the line above, for the bytes path) - protocol/json.py: mark module docstring raw to silence '\s' SyntaxWarning - util/invregexp.py: mark docstring raw ('\d' escape in an example regex) These become SyntaxError in a future Python release. --- spyne/protocol/http.py | 2 +- spyne/protocol/json.py | 2 +- spyne/util/invregexp.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spyne/protocol/http.py b/spyne/protocol/http.py index b351d8169..79f5caf8b 100644 --- a/spyne/protocol/http.py +++ b/spyne/protocol/http.py @@ -435,7 +435,7 @@ def _compile_host_pattern(cls, pattern): pattern = _full_pattern_re.sub(r'(?P<\1>.*)', pattern) pattern_b = pattern.encode(cls.HOST_ENCODING) - pattern_b = _fragment_pattern_b_re.sub(b'(?P<\\1>[^\.]*)', pattern_b) + pattern_b = _fragment_pattern_b_re.sub(rb'(?P<\1>[^\.]*)', pattern_b) pattern_b = _full_pattern_b_re.sub(b'(?P<\\1>.*)', pattern_b) return re.compile(pattern), re.compile(pattern_b) diff --git a/spyne/protocol/json.py b/spyne/protocol/json.py index 16acb11f3..fa569a32c 100644 --- a/spyne/protocol/json.py +++ b/spyne/protocol/json.py @@ -17,7 +17,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 # -"""The ``spyne.protocol.json`` package contains the Json-related protocols. +r"""The ``spyne.protocol.json`` package contains the Json-related protocols. Currently, only :class:`spyne.protocol.json.JsonDocument` is supported. Initially released in 2.8.0-rc. diff --git a/spyne/util/invregexp.py b/spyne/util/invregexp.py index 1e7f1e52b..8d35a2662 100644 --- a/spyne/util/invregexp.py +++ b/spyne/util/invregexp.py @@ -256,7 +256,7 @@ def count(gen): def invregexp(regex): - """Call this routine as a generator to return all the strings that + r"""Call this routine as a generator to return all the strings that match the input regular expression. for s in invregexp("[A-Z]{3}\d{3}"): print s From c95b1d9c6401852e46479b51ffa09ff72d428606 Mon Sep 17 00:00:00 2001 From: Carlos Bernal Date: Thu, 23 Jul 2026 11:26:09 +0200 Subject: [PATCH 2/6] Hoist 'break' out of finally block (PEP 765, Python 3.14) protocol/cloth/to_cloth.py:770 had an unconditional 'break' inside a 'finally:' block, flagged as a SyntaxWarning on Python 3.14. The break runs unconditionally after the inner try/except StopIteration, so it can be hoisted out of the finally without changing happy-path semantics. --- spyne/protocol/cloth/to_cloth.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/spyne/protocol/cloth/to_cloth.py b/spyne/protocol/cloth/to_cloth.py index 6dd59cd77..1afaf6f20 100644 --- a/spyne/protocol/cloth/to_cloth.py +++ b/spyne/protocol/cloth/to_cloth.py @@ -764,10 +764,9 @@ def complex_to_cloth(self, ctx, cls, inst, cloth, parent, name=None, ret.throw(e) except StopIteration: pass - finally: - # cf below - if not (as_attr or as_data): - break + # cf below; hoisted out of finally for PEP 765 (Py 3.14) + if not (as_attr or as_data): + break else: # this is here so that attribute on complex model doesn't get # mixed with in-line attr inside complex model. if an element From 6fea579ae8f404b899ec970abbb8884a6fb12a32 Mon Sep 17 00:00:00 2001 From: Carlos Bernal Date: Thu, 23 Jul 2026 11:26:09 +0200 Subject: [PATCH 3/6] Handle cgi module removal in Python 3.13 (twisted multipart) server/twisted/http.py uses cgi.FieldStorage for multipart parsing. PR #713 covered cgi.parse_header but FieldStorage was untouched. - wrap 'import cgi' in try/except ImportError, falling back to the maintained 'legacy-cgi' PyPI package if installed - if neither is available and the multipart path is actually executed, raise a descriptive RuntimeError telling the user to install legacy-cgi Users who don't use multipart over Twisted pay nothing. A full rewrite of multipart parsing is out of scope. --- spyne/server/twisted/http.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/spyne/server/twisted/http.py b/spyne/server/twisted/http.py index fda93d152..54fca1e05 100644 --- a/spyne/server/twisted/http.py +++ b/spyne/server/twisted/http.py @@ -43,7 +43,13 @@ logger = logging.getLogger(__name__) import re -import cgi +try: + import cgi # removed in Python 3.13; pip install legacy-cgi to restore +except ImportError: # pragma: no cover + try: + import legacy_cgi as cgi + except ImportError: + cgi = None import gzip import shutil import threading @@ -485,6 +491,11 @@ def _get_file_info(ctx): shutil.copyfileobj(ifstr, content) content.seek(0) + if cgi is None: + raise RuntimeError( + "Multipart parsing requires the 'cgi' module which was removed in " + "Python 3.13. Install 'legacy-cgi' (pip install legacy-cgi) to " + "enable this code path.") img = cgi.FieldStorage( fp=content, headers=ctx.in_header_doc, From 039f0475e5827c2538cb64639f51a375a35bba13 Mon Sep 17 00:00:00 2001 From: Carlos Bernal Date: Thu, 23 Jul 2026 11:26:09 +0200 Subject: [PATCH 4/6] Replace deprecated unittest aliases removed in Python 3.12 Replaced assertEquals, assertNotEquals, assertRegexpMatches, assertRaisesRegexp and failUnlessEqual with their non-deprecated names in: - test/interop/test_django.py - test/multipython/model/test_complex.py - test/protocol/_test_dictdoc.py The shared _test_dictdoc base is why test_json/test_msgpack/test_yaml had 6 collective failures on 3.12+. --- spyne/test/interop/test_django.py | 4 ++-- spyne/test/multipython/model/test_complex.py | 6 +++--- spyne/test/protocol/_test_dictdoc.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spyne/test/interop/test_django.py b/spyne/test/interop/test_django.py index b5df8b511..cae494b80 100755 --- a/spyne/test/interop/test_django.py +++ b/spyne/test/interop/test_django.py @@ -281,13 +281,13 @@ class DjangoServiceTestCase(TestCase): def test_handle_does_not_exist(self): """Test if Django service handles `ObjectDoesNotExist` exceptions.""" client = DjangoTestClient('/api/', app) - with self.assertRaisesRegexp(Fault, 'Client.FieldContainerNotFound'): + with self.assertRaisesRegex(Fault, 'Client.FieldContainerNotFound'): client.service.raise_does_not_exist() def test_handle_validation_error(self): """Test if Django service handles `ValidationError` exceptions.""" client = DjangoTestClient('/api/', app) - with self.assertRaisesRegexp(Fault, 'Client.ValidationError'): + with self.assertRaisesRegex(Fault, 'Client.ValidationError'): client.service.raise_validation_error() diff --git a/spyne/test/multipython/model/test_complex.py b/spyne/test/multipython/model/test_complex.py index 9aa8d1cb2..0cd5b001f 100644 --- a/spyne/test/multipython/model/test_complex.py +++ b/spyne/test/multipython/model/test_complex.py @@ -140,7 +140,7 @@ class Attributes(ComplexModel.Attributes): Base2 = Base.customize(prop1=4) - self.assertNotEquals(Base.Attributes.prop1, Base2.Attributes.prop1) + self.assertNotEqual(Base.Attributes.prop1, Base2.Attributes.prop1) self.assertEqual(Base.Attributes.prop2, Base2.Attributes.prop2) class Derived(Base): @@ -156,7 +156,7 @@ class Attributes(Base.Attributes): self.assertEqual(Derived.Attributes.prop1, 3) self.assertEqual(Derived2.Attributes.prop1, 5) - self.assertNotEquals(Derived.Attributes.prop3, Derived2.Attributes.prop3) + self.assertNotEqual(Derived.Attributes.prop3, Derived2.Attributes.prop3) self.assertEqual(Derived.Attributes.prop4, Derived2.Attributes.prop4) Derived3 = Derived.customize(prop3=12) @@ -164,7 +164,7 @@ class Attributes(Base.Attributes): # changes made to bases propagate, unless overridden self.assertEqual(Derived.Attributes.prop1, Base.Attributes.prop1) - self.assertNotEquals(Derived2.Attributes.prop1, Base.Attributes.prop1) + self.assertNotEqual(Derived2.Attributes.prop1, Base.Attributes.prop1) self.assertEqual(Derived3.Attributes.prop1, Base.Attributes.prop1) def test_declare_order(self): diff --git a/spyne/test/protocol/_test_dictdoc.py b/spyne/test/protocol/_test_dictdoc.py index 7e4af3dfa..ba5e6b5cc 100644 --- a/spyne/test/protocol/_test_dictdoc.py +++ b/spyne/test/protocol/_test_dictdoc.py @@ -1234,7 +1234,7 @@ def some_call(p): print(ctx.out_document) d = convert_dict({"some_callResponse": {"some_callResult": inner}}) - self.assertEquals(ctx.out_document[0], d) + self.assertEqual(ctx.out_document[0], d) def test_validation_freq_parent(self): class C(ComplexModel): @@ -1308,7 +1308,7 @@ def some_call(sc): doc = [{"C": {"s1": "s1","s2": "s2"}}] ctx = _dry_me([SomeService], {"some_call": doc}) - self.assertEquals(ctx.out_document[0], convert_dict( + self.assertEqual(ctx.out_document[0], convert_dict( {'some_callResponse': {'some_callResult': {'C': {'s2': 's2'}}}}) ) From 8bb0aa5d8f05383641b686c796045bec86d25cd7 Mon Sep 17 00:00:00 2001 From: Carlos Bernal Date: Thu, 23 Jul 2026 11:26:09 +0200 Subject: [PATCH 5/6] Add Python 3.11-3.14 classifiers to setup.py Added the 3.11, 3.12, 3.13 and 3.14 classifiers without dropping older entries (can be decoupled to a follow-up release if preferred). --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 2f29fc2c9..1db47ed8d 100755 --- a/setup.py +++ b/setup.py @@ -275,6 +275,10 @@ def run_tests(self): '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 :: Jython', 'Programming Language :: Python :: Implementation :: PyPy', From 4dd2bb76428e71bc5a79f949872612d78133edb7 Mon Sep 17 00:00:00 2001 From: Carlos Bernal Date: Wed, 6 May 2026 17:03:52 +0200 Subject: [PATCH 6/6] Release 2.15.0 --- CHANGELOG.rst | 13 +++++++++++++ spyne/__init__.py | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3cf6f6663..925b075da 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,6 +2,19 @@ Changelog ========= +spyne-2.15.0 +------------ +* Python 3.11, 3.12, 3.13 and 3.14 support. +* Fixed ``SyntaxWarning`` for invalid string escapes in ``protocol/http.py``, + ``protocol/json.py`` and ``util/invregexp.py``. +* Fixed ``SyntaxWarning`` from PEP 765 (Python 3.14): ``break`` statement + hoisted out of a ``finally`` block in ``protocol/cloth/to_cloth.py``. +* ``server/twisted/http.py``: ``cgi`` module was removed in Python 3.13; + the multipart code path now falls back to the ``legacy-cgi`` PyPI package + and raises a descriptive error if it is needed but missing. +* Replaced deprecated ``unittest`` aliases (``assertEquals`` etc.) in the + test suite for Python 3.12+ compatibility. + spyne-2.14.0 ------------ * Python 3.10 support. diff --git a/spyne/__init__.py b/spyne/__init__.py index f7e01853b..797d08157 100644 --- a/spyne/__init__.py +++ b/spyne/__init__.py @@ -21,7 +21,7 @@ class LogicError(Exception): pass -__version__ = '2.15.0-alpha' +__version__ = '2.15.0' from pytz import utc as LOCAL_TZ from decimal import Decimal as D