Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-SQLAlchemy-Utils for
openSUSE:Factory checked in at 2026-10-27 16:48:43
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-SQLAlchemy-Utils (Old)
and /work/SRC/openSUSE:Factory/.python-SQLAlchemy-Utils.new.2252680 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-SQLAlchemy-Utils"
Tue Oct 27 16:48:43 2026 rev:45 rq:1392850 version:0.43.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-SQLAlchemy-Utils/python-SQLAlchemy-Utils.changes
2026-03-27 06:47:14.876383680 +0100
+++
/work/SRC/openSUSE:Factory/.python-SQLAlchemy-Utils.new.2252680/python-SQLAlchemy-Utils.changes
2026-10-27 16:49:18.022337492 +0100
@@ -1,0 +2,20 @@
+Mon Oct 26 22:01:23 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.43.0:
+ * Add support for SQLAlchemy 2.1.
+ * Reimplement generic_relationship as a hybrid property built
+ only on public SQLAlchemy APIs, instead of private attribute
+ internals. Assigning to a generic relationship now sets the
+ discriminator and id attributes through regular attribute
+ instrumentation, so re-assigning it on a persistent object is
+ flushed to the database.
+ * Support querying generic relationships through aliased
+ models, including composite keys and renamed columns.
+ * Drop support for sqlalchemy 1.4.
+ * Drop support for Python 3.9.
+ * Support Python 3.14.
+ * Fix ChoiceType returning the raw scalar instead of a Choice
+ for falsy codes such as 0 or the empty string. (#813) NULL
+ values continue to return None.
+
+-------------------------------------------------------------------
Old:
----
sqlalchemy_utils-0.42.1.tar.gz
New:
----
sqlalchemy_utils-0.43.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-SQLAlchemy-Utils.spec ++++++
--- /var/tmp/diff_new_pack.QNLCF1/_old 2026-10-27 16:49:19.327392185 +0100
+++ /var/tmp/diff_new_pack.QNLCF1/_new 2026-10-27 16:49:19.331392353 +0100
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-SQLAlchemy-Utils
-Version: 0.42.1
+Version: 0.43.0
Release: 0
Summary: Various utility functions for SQLAlchemy
License: BSD-3-Clause
@@ -27,9 +27,8 @@
BuildRequires: %{python_module Babel >= 1.3}
BuildRequires: %{python_module Jinja2 >= 2.3}
BuildRequires: %{python_module Pygments >= 1.2}
-BuildRequires: %{python_module SQLAlchemy >= 1.0}
+BuildRequires: %{python_module SQLAlchemy >= 2.0}
BuildRequires: %{python_module arrow >= 0.3.4}
-BuildRequires: %{python_module backports.zoneinfo if %python-base < 3.9}
BuildRequires: %{python_module colour >= 0.0.4}
BuildRequires: %{python_module cryptography >= 0.6}
BuildRequires: %{python_module docutils >= 0.10}
@@ -43,14 +42,14 @@
BuildRequires: %{python_module psycopg2 >= 2.5.1}
BuildRequires: %{python_module psycopg2cffi >= 2.8.1}
BuildRequires: %{python_module pyodbc}
-BuildRequires: %{python_module pytest >= 2.7.1}
+BuildRequires: %{python_module pytest >= 9.0}
BuildRequires: %{python_module python-dateutil >= 2.6}
BuildRequires: %{python_module pytz >= 2014.2}
-BuildRequires: %{python_module setuptools}
+BuildRequires: %{python_module setuptools >= 77.0.0}
BuildRequires: %{python_module wheel}
BuildRequires: fdupes
BuildRequires: python-rpm-macros
-Requires: python-SQLAlchemy >= 1.0
+Requires: python-SQLAlchemy >= 2.0
Recommends: python-Babel >= 1.3
Recommends: python-arrow >= 0.3.4
Recommends: python-colour >= 0.0.4
@@ -62,9 +61,6 @@
Recommends: python-phonenumbers >= 5.9.2
Suggests: python-pendulum >= 2.0.5
BuildArch: noarch
-%if 0%{?python_version_nodots} < 39
-Requires: python-backports.zoneinfo
-%endif
%python_subpackages
%description
++++++ sqlalchemy_utils-0.42.1.tar.gz -> sqlalchemy_utils-0.43.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/CHANGES.rst
new/sqlalchemy_utils-0.43.0/CHANGES.rst
--- old/sqlalchemy_utils-0.42.1/CHANGES.rst 2025-12-13 04:12:25.000000000
+0100
+++ new/sqlalchemy_utils-0.43.0/CHANGES.rst 2026-10-09 17:35:51.229952300
+0200
@@ -3,6 +3,21 @@
Here you can see the full list of changes between each SQLAlchemy-Utils
release.
+0.43.0 (2026-10-09)
+^^^^^^^^^^^^^^^^^^^
+
+- Add support for SQLAlchemy 2.1.
+- Reimplement ``generic_relationship`` as a hybrid property built only on
public SQLAlchemy APIs, instead of private attribute internals.
+
+ Assigning to a generic relationship now sets the discriminator and id
attributes through regular attribute instrumentation, so re-assigning it on a
persistent object is flushed to the database.
+- Support querying generic relationships through aliased models, including
composite keys and renamed columns.
+- Drop support for sqlalchemy 1.4.
+- Drop support for Python 3.9.
+- Support Python 3.14.
+- Fix ``ChoiceType`` returning the raw scalar instead of a ``Choice`` for
falsy codes such as ``0`` or the empty string. (#813)
+
+ NULL values continue to return ``None``.
+
0.42.1 (2025-12-12)
^^^^^^^^^^^^^^^^^^^
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/PKG-INFO
new/sqlalchemy_utils-0.43.0/PKG-INFO
--- old/sqlalchemy_utils-0.42.1/PKG-INFO 2025-12-13 04:12:59.770267500
+0100
+++ new/sqlalchemy_utils-0.43.0/PKG-INFO 2026-10-09 17:46:12.943159800
+0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: SQLAlchemy-Utils
-Version: 0.42.1
+Version: 0.43.0
Summary: Various utility functions for SQLAlchemy.
Author: Ryan Leckey, Janne Vanhala, Vesa Uimonen
Author-email: Konsta Vesterinen <[email protected]>
@@ -10,19 +10,19 @@
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
-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: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Requires-Python: >=3.9
+Requires-Python: >=3.10
Description-Content-Type: text/x-rst
License-File: LICENSE
-Requires-Dist: SQLAlchemy>=1.4
+Requires-Dist: SQLAlchemy>=2.0
Provides-Extra: test
-Requires-Dist: pytest==7.4.4; extra == "test"
+Requires-Dist: pytest<10,>=9.0; extra == "test"
Requires-Dist: Pygments>=1.2; extra == "test"
Requires-Dist: Jinja2>=2.3; extra == "test"
Requires-Dist: docutils>=0.10; extra == "test"
@@ -34,8 +34,6 @@
Requires-Dist: pytz>=2014.2; extra == "test"
Requires-Dist: python-dateutil>=2.6; extra == "test"
Requires-Dist: pymysql; extra == "test"
-Requires-Dist: flake8>=2.4.0; extra == "test"
-Requires-Dist: isort>=4.2.2; extra == "test"
Requires-Dist: pyodbc; extra == "test"
Provides-Extra: babel
Requires-Dist: Babel>=1.3; extra == "babel"
@@ -58,7 +56,7 @@
Provides-Extra: encrypted
Requires-Dist: cryptography>=0.6; extra == "encrypted"
Provides-Extra: test-all
-Requires-Dist: pytest==7.4.4; extra == "test-all"
+Requires-Dist: pytest<10,>=9.0; extra == "test-all"
Requires-Dist: Pygments>=1.2; extra == "test-all"
Requires-Dist: Jinja2>=2.3; extra == "test-all"
Requires-Dist: docutils>=0.10; extra == "test-all"
@@ -70,8 +68,6 @@
Requires-Dist: pytz>=2014.2; extra == "test-all"
Requires-Dist: python-dateutil>=2.6; extra == "test-all"
Requires-Dist: pymysql; extra == "test-all"
-Requires-Dist: flake8>=2.4.0; extra == "test-all"
-Requires-Dist: isort>=4.2.2; extra == "test-all"
Requires-Dist: pyodbc; extra == "test-all"
Requires-Dist: Babel>=1.3; extra == "test-all"
Requires-Dist: arrow>=0.3.4; extra == "test-all"
@@ -88,8 +84,6 @@
SQLAlchemy-Utils
================
-|Build| |Version Status| |Downloads|
-
Various utility functions, new data types and helpers for SQLAlchemy.
@@ -99,11 +93,3 @@
- `Documentation <https://sqlalchemy-utils.readthedocs.io/>`_
- `Issue Tracker <https://github.com/kvesteri/sqlalchemy-utils/issues>`_
- `Code <https://github.com/kvesteri/sqlalchemy-utils/>`_
-
-.. |Build| image::
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml/badge.svg?branch=master
- :target:
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml
- :alt: Tests
-.. |Version Status| image:: https://img.shields.io/pypi/v/SQLAlchemy-Utils.svg
- :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
-.. |Downloads| image:: https://img.shields.io/pypi/dm/SQLAlchemy-Utils.svg
- :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/README.rst
new/sqlalchemy_utils-0.43.0/README.rst
--- old/sqlalchemy_utils-0.42.1/README.rst 2024-03-21 22:26:25.000000000
+0100
+++ new/sqlalchemy_utils-0.43.0/README.rst 2026-08-06 17:05:53.343547800
+0200
@@ -1,8 +1,6 @@
SQLAlchemy-Utils
================
-|Build| |Version Status| |Downloads|
-
Various utility functions, new data types and helpers for SQLAlchemy.
@@ -12,11 +10,3 @@
- `Documentation <https://sqlalchemy-utils.readthedocs.io/>`_
- `Issue Tracker <https://github.com/kvesteri/sqlalchemy-utils/issues>`_
- `Code <https://github.com/kvesteri/sqlalchemy-utils/>`_
-
-.. |Build| image::
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml/badge.svg?branch=master
- :target:
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml
- :alt: Tests
-.. |Version Status| image:: https://img.shields.io/pypi/v/SQLAlchemy-Utils.svg
- :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
-.. |Downloads| image:: https://img.shields.io/pypi/dm/SQLAlchemy-Utils.svg
- :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/sqlalchemy_utils-0.42.1/SQLAlchemy_Utils.egg-info/PKG-INFO
new/sqlalchemy_utils-0.43.0/SQLAlchemy_Utils.egg-info/PKG-INFO
--- old/sqlalchemy_utils-0.42.1/SQLAlchemy_Utils.egg-info/PKG-INFO
2025-12-13 04:12:59.000000000 +0100
+++ new/sqlalchemy_utils-0.43.0/SQLAlchemy_Utils.egg-info/PKG-INFO
2026-10-09 17:46:12.877671700 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: SQLAlchemy-Utils
-Version: 0.42.1
+Version: 0.43.0
Summary: Various utility functions for SQLAlchemy.
Author: Ryan Leckey, Janne Vanhala, Vesa Uimonen
Author-email: Konsta Vesterinen <[email protected]>
@@ -10,19 +10,19 @@
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
-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: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Libraries :: Python Modules
-Requires-Python: >=3.9
+Requires-Python: >=3.10
Description-Content-Type: text/x-rst
License-File: LICENSE
-Requires-Dist: SQLAlchemy>=1.4
+Requires-Dist: SQLAlchemy>=2.0
Provides-Extra: test
-Requires-Dist: pytest==7.4.4; extra == "test"
+Requires-Dist: pytest<10,>=9.0; extra == "test"
Requires-Dist: Pygments>=1.2; extra == "test"
Requires-Dist: Jinja2>=2.3; extra == "test"
Requires-Dist: docutils>=0.10; extra == "test"
@@ -34,8 +34,6 @@
Requires-Dist: pytz>=2014.2; extra == "test"
Requires-Dist: python-dateutil>=2.6; extra == "test"
Requires-Dist: pymysql; extra == "test"
-Requires-Dist: flake8>=2.4.0; extra == "test"
-Requires-Dist: isort>=4.2.2; extra == "test"
Requires-Dist: pyodbc; extra == "test"
Provides-Extra: babel
Requires-Dist: Babel>=1.3; extra == "babel"
@@ -58,7 +56,7 @@
Provides-Extra: encrypted
Requires-Dist: cryptography>=0.6; extra == "encrypted"
Provides-Extra: test-all
-Requires-Dist: pytest==7.4.4; extra == "test-all"
+Requires-Dist: pytest<10,>=9.0; extra == "test-all"
Requires-Dist: Pygments>=1.2; extra == "test-all"
Requires-Dist: Jinja2>=2.3; extra == "test-all"
Requires-Dist: docutils>=0.10; extra == "test-all"
@@ -70,8 +68,6 @@
Requires-Dist: pytz>=2014.2; extra == "test-all"
Requires-Dist: python-dateutil>=2.6; extra == "test-all"
Requires-Dist: pymysql; extra == "test-all"
-Requires-Dist: flake8>=2.4.0; extra == "test-all"
-Requires-Dist: isort>=4.2.2; extra == "test-all"
Requires-Dist: pyodbc; extra == "test-all"
Requires-Dist: Babel>=1.3; extra == "test-all"
Requires-Dist: arrow>=0.3.4; extra == "test-all"
@@ -88,8 +84,6 @@
SQLAlchemy-Utils
================
-|Build| |Version Status| |Downloads|
-
Various utility functions, new data types and helpers for SQLAlchemy.
@@ -99,11 +93,3 @@
- `Documentation <https://sqlalchemy-utils.readthedocs.io/>`_
- `Issue Tracker <https://github.com/kvesteri/sqlalchemy-utils/issues>`_
- `Code <https://github.com/kvesteri/sqlalchemy-utils/>`_
-
-.. |Build| image::
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml/badge.svg?branch=master
- :target:
https://github.com/kvesteri/sqlalchemy-utils/actions/workflows/test.yaml
- :alt: Tests
-.. |Version Status| image:: https://img.shields.io/pypi/v/SQLAlchemy-Utils.svg
- :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
-.. |Downloads| image:: https://img.shields.io/pypi/dm/SQLAlchemy-Utils.svg
- :target: https://pypi.python.org/pypi/SQLAlchemy-Utils/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/sqlalchemy_utils-0.42.1/SQLAlchemy_Utils.egg-info/requires.txt
new/sqlalchemy_utils-0.43.0/SQLAlchemy_Utils.egg-info/requires.txt
--- old/sqlalchemy_utils-0.42.1/SQLAlchemy_Utils.egg-info/requires.txt
2025-12-13 04:12:59.000000000 +0100
+++ new/sqlalchemy_utils-0.43.0/SQLAlchemy_Utils.egg-info/requires.txt
2026-10-09 17:46:12.879012600 +0200
@@ -1,4 +1,4 @@
-SQLAlchemy>=1.4
+SQLAlchemy>=2.0
[arrow]
arrow>=0.3.4
@@ -25,7 +25,7 @@
phonenumbers>=5.9.2
[test]
-pytest==7.4.4
+pytest<10,>=9.0
Pygments>=1.2
Jinja2>=2.3
docutils>=0.10
@@ -37,12 +37,10 @@
pytz>=2014.2
python-dateutil>=2.6
pymysql
-flake8>=2.4.0
-isort>=4.2.2
pyodbc
[test_all]
-pytest==7.4.4
+pytest<10,>=9.0
Pygments>=1.2
Jinja2>=2.3
docutils>=0.10
@@ -54,8 +52,6 @@
pytz>=2014.2
python-dateutil>=2.6
pymysql
-flake8>=2.4.0
-isort>=4.2.2
pyodbc
Babel>=1.3
arrow>=0.3.4
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/docs/installation.rst
new/sqlalchemy_utils-0.43.0/docs/installation.rst
--- old/sqlalchemy_utils-0.42.1/docs/installation.rst 2025-08-30
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/docs/installation.rst 2026-08-06
17:05:53.343715700 +0200
@@ -6,12 +6,7 @@
Supported platforms
-------------------
-SQLAlchemy-Utils is currently tested against the following Python platforms:
-
-- CPython 3.9
-- CPython 3.10
-- CPython 3.11
-- CPython 3.12
+SQLAlchemy-Utils is tested against supported CPython versions.
Installing an official release
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/pyproject.toml
new/sqlalchemy_utils-0.43.0/pyproject.toml
--- old/sqlalchemy_utils-0.42.1/pyproject.toml 2025-12-13 04:12:35.000000000
+0100
+++ new/sqlalchemy_utils-0.43.0/pyproject.toml 2026-10-09 17:35:51.230171000
+0200
@@ -4,7 +4,7 @@
[project]
name = "SQLAlchemy-Utils"
-version = "0.42.1"
+version = "0.43.0"
description = "Various utility functions for SQLAlchemy."
readme = "README.rst"
license = "BSD-3-Clause"
@@ -14,22 +14,22 @@
{name = "Janne Vanhala"},
{name = "Vesa Uimonen"}
]
-requires-python = ">=3.9"
+requires-python = ">=3.10"
classifiers = [
"Environment :: Web Environment",
"Intended Audience :: Developers",
"Operating System :: OS Independent",
"Programming Language :: Python",
- "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",
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
- "SQLAlchemy>=1.4",
+ "SQLAlchemy>=2.0",
]
[project.urls]
@@ -37,7 +37,7 @@
[project.optional-dependencies]
test = [
- "pytest==7.4.4",
+ "pytest>=9.0,<10",
"Pygments>=1.2",
"Jinja2>=2.3",
"docutils>=0.10",
@@ -49,8 +49,6 @@
"pytz>=2014.2",
"python-dateutil>=2.6",
"pymysql",
- "flake8>=2.4.0",
- "isort>=4.2.2",
"pyodbc",
]
babel = ["Babel>=1.3"]
@@ -64,7 +62,7 @@
url = ["furl >= 0.4.1"]
encrypted = ["cryptography>=0.6"]
test_all = [
- "pytest==7.4.4",
+ "pytest>=9.0,<10",
"Pygments>=1.2",
"Jinja2>=2.3",
"docutils>=0.10",
@@ -76,8 +74,6 @@
"pytz>=2014.2",
"python-dateutil>=2.6",
"pymysql",
- "flake8>=2.4.0",
- "isort>=4.2.2",
"pyodbc",
"Babel>=1.3",
"arrow>=0.3.4",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/__init__.py
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/__init__.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/__init__.py 2025-08-30
20:44:39.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/__init__.py 2026-10-09
17:35:51.230389800 +0200
@@ -101,4 +101,4 @@
refresh_materialized_view,
)
-__version__ = '0.42.0'
+__version__ = '0.43.0'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/database.py
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/database.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/database.py
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/database.py
2026-08-06 17:05:53.345343600 +0200
@@ -1,7 +1,6 @@
import itertools
import os
from collections.abc import Mapping, Sequence
-from copy import copy
import sqlalchemy as sa
from sqlalchemy.engine.url import make_url
@@ -416,13 +415,8 @@
:param database: New database to set.
"""
- if hasattr(url, '_replace'):
- # Cannot use URL.set() as database may need to be set to None.
- ret = url._replace(database=database)
- else: # SQLAlchemy <1.4
- url = copy(url)
- url.database = database
- ret = url
+ # Cannot use URL.set() as database may need to be set to None.
+ ret = url._replace(database=database)
assert ret.database == database, ret
return ret
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/mock.py
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/mock.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/mock.py
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/mock.py
2026-08-06 17:05:53.345567200 +0200
@@ -47,10 +47,7 @@
def dump(*args, **kw):
return None
- try:
- engine = sa.create_mock_engine(bind_url, executor=dump)
- except AttributeError: # SQLAlchemy <1.4
- engine = sa.create_engine(bind_url, strategy='mock', executor=dump)
+ engine = sa.create_mock_engine(bind_url, executor=dump)
return engine
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/orm.py
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/orm.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/orm.py
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/orm.py
2026-08-06 17:05:53.345759400 +0200
@@ -10,10 +10,7 @@
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm.exc import UnmappedInstanceError
-try:
- from sqlalchemy.orm.context import _ColumnEntity, _MapperEntity
-except ImportError: # SQLAlchemy <1.4
- from sqlalchemy.orm.query import _ColumnEntity, _MapperEntity
+from sqlalchemy.orm.context import _ColumnEntity, _MapperEntity
from sqlalchemy.orm.session import object_session
from sqlalchemy.orm.util import AliasedInsp
@@ -281,12 +278,9 @@
if isinstance(mixed, sa.orm.attributes.InstrumentedAttribute):
mixed = mixed.class_
if isinstance(mixed, sa.Table):
- if hasattr(mapperlib, '_all_registries'):
- all_mappers = set()
- for mapper_registry in mapperlib._all_registries():
- all_mappers.update(mapper_registry.mappers)
- else: # SQLAlchemy <1.4
- all_mappers = mapperlib._mapper_registry
+ all_mappers = set()
+ for mapper_registry in mapperlib._all_registries():
+ all_mappers.update(mapper_registry.mappers)
mappers = [mapper for mapper in all_mappers if mixed in mapper.tables]
if len(mappers) > 1:
raise ValueError("Multiple mappers found for table '%s'." %
mixed.name)
@@ -449,11 +443,10 @@
SA Table object, SA Mapper, SA declarative class, SA declarative class
instance or an alias of any of these objects
"""
+ if isinstance(mixed, sa.SelectBase):
+ return mixed.subquery().c
if isinstance(mixed, sa.sql.selectable.Selectable):
- try:
- return mixed.selected_columns
- except AttributeError: # SQLAlchemy <1.4
- return mixed.c
+ return mixed.c
if isinstance(mixed, sa.orm.util.AliasedClass):
return sa.inspect(mixed).mapper.columns
if isinstance(mixed, sa.orm.Mapper):
@@ -520,10 +513,7 @@
def _get_query_compile_state(query):
- if hasattr(query, '_compile_state'):
- return query._compile_state()
- else: # SQLAlchemy <1.4
- return query
+ return query._compile_state()
def get_polymorphic_mappers(mixed):
@@ -878,7 +868,4 @@
def _get_class_registry(class_):
- try:
- return class_.registry._class_registry
- except AttributeError: # SQLAlchemy <1.4
- return class_._decl_class_registry
+ return class_.registry._class_registry
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/sort_query.py
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/sort_query.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/functions/sort_query.py
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/functions/sort_query.py
2026-08-06 17:05:53.345980400 +0200
@@ -40,10 +40,7 @@
"""
order_by_func = sa.asc
- try:
- order_by_clauses = query._order_by_clauses
- except AttributeError: # SQLAlchemy <1.4
- order_by_clauses = query._order_by
+ order_by_clauses = query._order_by_clauses
if not order_by_clauses:
column = None
else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/generic.py
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/generic.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/generic.py 2025-08-30
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/generic.py 2026-10-09
17:35:51.230671200 +0200
@@ -1,112 +1,51 @@
+import builtins
from collections.abc import Iterable
import sqlalchemy as sa
-from sqlalchemy.ext.hybrid import hybrid_property
-from sqlalchemy.orm import attributes, class_mapper, ColumnProperty
-from sqlalchemy.orm.interfaces import MapperProperty, PropComparator
-from sqlalchemy.orm.session import _state_session
-from sqlalchemy.util import set_creation_order
+from sqlalchemy.ext.hybrid import Comparator, hybrid_property
+from sqlalchemy.orm import object_session
from .exceptions import ImproperlyConfigured
from .functions import identity
-from .functions.orm import _get_class_registry
-class GenericAttributeImpl(attributes.ScalarAttributeImpl):
- def __init__(self, *args, **kwargs):
- """
- The constructor of attributes.AttributeImpl changed in SQLAlchemy
2.0.22,
- adding a 'default_function' required positional argument before
'dispatch'.
- This adjustment ensures compatibility across versions by inserting
None for
- 'default_function' in versions >= 2.0.22.
-
- Arguments received: (class, key, dispatch)
- Required by AttributeImpl: (class, key, default_function, dispatch)
- Setting None as default_function here.
- """
- # Adjust for SQLAlchemy version change
- sqlalchemy_version = tuple(map(int, sa.__version__.split('.')))
- if sqlalchemy_version >= (2, 0, 22):
- args = (*args[:2], None, *args[2:])
-
- super().__init__(*args, **kwargs)
-
- def get(self, state, dict_, passive=attributes.PASSIVE_OFF):
- if self.key in dict_:
- return dict_[self.key]
-
- # Retrieve the session bound to the state in order to perform
- # a lazy query for the attribute.
- session = _state_session(state)
- if session is None:
- # State is not bound to a session; we cannot proceed.
- return None
-
- # Find class for discriminator.
- # TODO: Perhaps optimize with some sort of lookup?
- discriminator = self.get_state_discriminator(state)
- target_class = _get_class_registry(state.class_).get(discriminator)
-
- if target_class is None:
- # Unknown discriminator; return nothing.
- return None
-
- id = self.get_state_id(state)
-
- target = session.get(target_class, id)
-
- # Return found (or not found) target.
- return target
-
- def get_state_discriminator(self, state):
- discriminator = self.parent_token.discriminator
- if isinstance(discriminator, hybrid_property):
- return getattr(state.obj(), discriminator.__name__)
- else:
- return state.attrs[discriminator.key].value
-
- def get_state_id(self, state):
- # Lookup row with the discriminator and id.
- return tuple(state.attrs[id.key].value for id in self.parent_token.id)
-
- def set(
- self,
- state,
- dict_,
- initiator,
- passive=attributes.PASSIVE_OFF,
- check_old=None,
- pop=False,
- ):
- # Set us on the state.
- dict_[self.key] = initiator
-
- if initiator is None:
- # Nullify relationship args
- for id in self.parent_token.id:
- dict_[id.key] = None
- dict_[self.parent_token.discriminator.key] = None
- else:
- # Get the primary key of the initiator and ensure we
- # can support this assignment.
- class_ = type(initiator)
- mapper = class_mapper(class_)
-
- pk = mapper.identity_key_from_instance(initiator)[1]
-
- # Set the identifier and the discriminator.
- discriminator = class_.__name__
+class GenericComparator(Comparator):
+ def __init__(self, relationship, cls):
+ self.relationship = relationship
+ self.cls = cls
+
+ def __clause_element__(self):
+ raise NotImplementedError(
+ 'A generic relationship can only be compared against a model '
+ 'instance or with is_type().'
+ )
- for index, id in enumerate(self.parent_token.id):
- dict_[id.key] = pk[index]
- dict_[self.parent_token.discriminator.key] = discriminator
+ def __eq__(self, other):
+ discriminator, ids = self.relationship._attributes(self.cls)
+ q = discriminator == type(other).__name__
+ for column, value in zip(ids, identity(other)):
+ q &= column == value
+ return q
+
+ def __ne__(self, other):
+ return ~(self == other)
+
+ def is_type(self, other):
+ discriminator, _ = self.relationship._attributes(self.cls)
+ class_names = [
+ mapper.class_.__name__ for mapper in
sa.inspect(other).self_and_descendants
+ ]
+ return discriminator.in_(class_names)
-class GenericRelationshipProperty(MapperProperty):
+class GenericRelationship:
"""A generic form of the relationship property.
Creates a 1 to many relationship between the parent model
- and any other models using a discriminator (the table name).
+ and any other models using a discriminator (the class name).
+
+ Implemented as a hybrid property on top of the discriminator and id
+ attributes, so it only relies on public SQLAlchemy APIs.
:param discriminator:
Field to discriminate which model we are referring to.
@@ -114,87 +53,94 @@
Field to point to the model we are referring to.
"""
- def __init__(self, discriminator, id, doc=None):
- super().__init__()
- self._discriminator_col = discriminator
- self._id_cols = id
- self._id = None
- self._discriminator = None
- self.doc = doc
-
- set_creation_order(self)
-
- def _column_to_property(self, column):
- if isinstance(column, hybrid_property):
- attr_key = column.__name__
- for key, attr in self.parent.all_orm_descriptors.items():
- if key == attr_key:
- return attr
- else:
- for attr in self.parent.attrs.values():
- if isinstance(attr, ColumnProperty):
- if attr.columns[0].name == column.name:
- return attr
-
- def init(self):
- def convert_strings(column):
- if isinstance(column, str):
- return self.parent.columns[column]
- return column
-
- self._discriminator_col = convert_strings(self._discriminator_col)
- self._id_cols = convert_strings(self._id_cols)
-
- if isinstance(self._id_cols, Iterable):
- self._id_cols = list(map(convert_strings, self._id_cols))
+ def __init__(self, discriminator, id):
+ self.discriminator = discriminator
+ if isinstance(id, Iterable) and not isinstance(id, str):
+ self.ids = list(id)
else:
- self._id_cols = [self._id_cols]
-
- self.discriminator = self._column_to_property(self._discriminator_col)
+ self.ids = [id]
+ # Key for caching the assigned target object on the instance dict.
+ self.cache_key = f'_generic_relationship_{builtins.id(self)}'
- if self.discriminator is None:
- raise ImproperlyConfigured('Could not find discriminator
descriptor.')
+ def _key(self, cls, column):
+ if isinstance(column, str):
+ return column
+ if isinstance(column, hybrid_property):
+ return column.__name__
+ try:
+ return sa.inspect(cls).mapper.get_property_by_column(column).key
+ except sa.orm.exc.UnmappedColumnError:
+ raise ImproperlyConfigured(
+ f'Could not find generic relationship attribute for
{column!r}.'
+ )
- self.id = list(map(self._column_to_property, self._id_cols))
+ def _keys(self, cls):
+ return (
+ self._key(cls, self.discriminator),
+ [self._key(cls, column) for column in self.ids],
+ )
- class Comparator(PropComparator):
- def __init__(self, prop, parentmapper):
- self.property = prop
- self._parententity = parentmapper
-
- def __eq__(self, other):
- discriminator = type(other).__name__
- q = self.property._discriminator_col == discriminator
- other_id = identity(other)
- for index, id in enumerate(self.property._id_cols):
- q &= id == other_id[index]
- return q
-
- def __ne__(self, other):
- return ~(self == other)
-
- def is_type(self, other):
- mapper = sa.inspect(other)
- # Iterate through the weak sequence in order to get the actual
- # mappers
- class_names = [other.__name__]
- class_names.extend(
- [submapper.class_.__name__ for submapper in
mapper._inheriting_mappers]
- )
+ def _attributes(self, cls):
+ discriminator, ids = self._keys(cls)
+ return getattr(cls, discriminator), [getattr(cls, key) for key in ids]
+
+ def _target_class(self, cls, discriminator):
+ for mapper in sa.inspect(cls).registry.mappers:
+ if mapper.class_.__name__ == discriminator:
+ return mapper.class_
+
+ def get(self, obj):
+ discriminator_key, id_keys = self._keys(type(obj))
+ discriminator = getattr(obj, discriminator_key)
+ id = tuple(getattr(obj, key) for key in id_keys)
+
+ # Return the object assigned to this relationship as long as the
+ # discriminator and id still point to it.
+ cached = obj.__dict__.get(self.cache_key)
+ if (
+ cached is not None
+ and type(cached).__name__ == discriminator
+ and identity(cached) == id
+ ):
+ return cached
+
+ # Use the session the object is bound to in order to perform
+ # a lazy query for the target.
+ session = object_session(obj)
+ if session is None:
+ return None
- return self.property._discriminator_col.in_(class_names)
+ target_class = self._target_class(type(obj), discriminator)
+ if target_class is None:
+ # Unknown discriminator; return nothing.
+ return None
- def instrument_class(self, mapper):
- attributes.register_attribute(
- mapper.class_,
- self.key,
- comparator=self.Comparator(self, mapper),
- parententity=mapper,
- doc=self.doc,
- impl_class=GenericAttributeImpl,
- parent_token=self,
- )
+ return session.get(target_class, id)
+ def set(self, obj, value):
+ discriminator_key, id_keys = self._keys(type(obj))
+ obj.__dict__[self.cache_key] = value
-def generic_relationship(*args, **kwargs):
- return GenericRelationshipProperty(*args, **kwargs)
+ if value is None:
+ # Nullify relationship args
+ for key in id_keys:
+ setattr(obj, key, None)
+ setattr(obj, discriminator_key, None)
+ else:
+ for key, pk in zip(id_keys, identity(value)):
+ setattr(obj, key, pk)
+ setattr(obj, discriminator_key, type(value).__name__)
+
+ def comparator(self, cls):
+ return GenericComparator(self, cls)
+
+
+def generic_relationship(discriminator, id, doc=None):
+ relationship = GenericRelationship(discriminator, id)
+ prop = hybrid_property(
+ relationship.get,
+ relationship.set,
+ custom_comparator=relationship.comparator,
+ )
+ prop.__doc__ = doc
+ return prop
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/choice.py
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/choice.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/choice.py
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/choice.py
2026-08-06 17:05:53.346468000 +0200
@@ -183,9 +183,9 @@
return value
def process_result_value(self, value, dialect):
- if value:
- return Choice(value, self.choices_dict[value])
- return value
+ if value is None:
+ return None
+ return Choice(value, self.choices_dict[value])
class EnumTypeImpl:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/password.py
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/password.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/password.py
2025-08-30 20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/password.py
2026-08-06 17:05:53.347087600 +0200
@@ -42,8 +42,10 @@
def __eq__(self, value):
if self.hash is None or value is None:
- # Ensure that we don't continue comparison if one of us is None.
- return self.hash is value
+ # If either the hash or the value is None,
+ # equivalence depends on *all* values being None,
+ # including the secret.
+ return self.hash is None and self.secret is None and value is None
if isinstance(value, Password):
# Comparing 2 hashes isn't very useful; but this equality
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/uuid.py
new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/uuid.py
--- old/sqlalchemy_utils-0.42.1/sqlalchemy_utils/types/uuid.py 2025-08-30
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/sqlalchemy_utils/types/uuid.py 2026-08-06
17:05:53.347343000 +0200
@@ -3,11 +3,8 @@
from sqlalchemy import types, util
from sqlalchemy.dialects import mssql, postgresql
-from ..compat import get_sqlalchemy_version
from .scalar_coercible import ScalarCoercible
-sqlalchemy_version = get_sqlalchemy_version()
-
class UUIDType(ScalarCoercible, types.TypeDecorator):
"""
@@ -71,16 +68,8 @@
return value
- # sqlalchemy >= 1.4.30 quotes UUID's automatically.
- # It is only necessary to quote UUID's in sqlalchemy < 1.4.30.
- if sqlalchemy_version < (1, 4, 30):
-
- def process_literal_param(self, value, dialect):
- return f"'{value}'" if value else value
- else:
-
- def process_literal_param(self, value, dialect):
- return value
+ def process_literal_param(self, value, dialect):
+ return value
def process_bind_param(self, value, dialect):
if value is None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/tests/conftest.py
new/sqlalchemy_utils-0.43.0/tests/conftest.py
--- old/sqlalchemy_utils-0.42.1/tests/conftest.py 2025-08-30
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/tests/conftest.py 2026-10-09
17:35:51.230960800 +0200
@@ -64,7 +64,7 @@
def postgresql_dsn(
postgresql_db_user, postgresql_db_password, postgresql_db_host, db_name
):
- return 'postgresql://{}:{}@{}/{}'.format(
+ return 'postgresql+psycopg2://{}:{}@{}/{}'.format(
postgresql_db_user, postgresql_db_password, postgresql_db_host, db_name
)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/sqlalchemy_utils-0.42.1/tests/functions/test_database.py
new/sqlalchemy_utils-0.43.0/tests/functions/test_database.py
--- old/sqlalchemy_utils-0.42.1/tests/functions/test_database.py
2024-03-14 18:37:59.000000000 +0100
+++ new/sqlalchemy_utils-0.43.0/tests/functions/test_database.py
2026-08-06 17:05:53.347864600 +0200
@@ -2,7 +2,6 @@
import sqlalchemy as sa
from sqlalchemy_utils import create_database, database_exists, drop_database
-from sqlalchemy_utils.compat import get_sqlalchemy_version
pymysql = None
try:
@@ -10,8 +9,6 @@
except ImportError:
pass
-sqlalchemy_version = get_sqlalchemy_version()
-
class DatabaseTest:
def test_create_and_drop(self, dsn):
@@ -101,7 +98,6 @@
)
[email protected]('sqlalchemy_version < (2, 0, 0)')
class TestDatabasePostgresPsycoPG3(DatabaseTest):
@pytest.fixture
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/sqlalchemy_utils-0.42.1/tests/generic_relationship/__init__.py
new/sqlalchemy_utils-0.43.0/tests/generic_relationship/__init__.py
--- old/sqlalchemy_utils-0.42.1/tests/generic_relationship/__init__.py
2024-03-14 18:37:59.000000000 +0100
+++ new/sqlalchemy_utils-0.43.0/tests/generic_relationship/__init__.py
2026-10-09 17:35:51.231217400 +0200
@@ -1,3 +1,6 @@
+from sqlalchemy.orm import aliased
+
+
class GenericRelationshipTestCase:
def test_set_as_none(self, Event):
event = Event()
@@ -100,3 +103,46 @@
statement = Event.object.is_type(User)
q = session.query(Event).filter(statement)
assert q.first() is not None
+
+ def test_reassign_persistent_relationship(self, session, User, Event):
+ user1 = User()
+ user2 = User()
+ session.add_all([user1, user2])
+ session.commit()
+ event = Event(object=user1)
+ session.add(event)
+ session.commit()
+
+ event.object = user2
+ session.commit()
+ session.expunge(event)
+
+ event = session.query(Event).one()
+ assert event.object == user2
+
+ def test_compare_aliased_query(self, session, User, Event):
+ user = User()
+ session.add(user)
+ session.commit()
+ event = Event(object=user)
+ session.add(event)
+ session.commit()
+
+ event_alias = aliased(Event)
+ query = session.query(event_alias)
+ assert query.filter(event_alias.object == user).one() == event
+
+ def test_change_identifier(self, session, User, Event):
+ user1 = User()
+ user2 = User()
+ session.add_all([user1, user2])
+ session.commit()
+ event = Event(object=user1)
+ session.add(event)
+ session.commit()
+
+ event.object_id = user2.id
+ if hasattr(event, 'object_code'):
+ event.object_code = user2.code
+
+ assert event.object == user2
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/tests/types/test_choice.py
new/sqlalchemy_utils-0.43.0/tests/types/test_choice.py
--- old/sqlalchemy_utils-0.42.1/tests/types/test_choice.py 2025-08-30
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/tests/types/test_choice.py 2026-08-06
17:05:53.348252300 +0200
@@ -209,3 +209,66 @@
session.commit()
assert order_nullable.status is None
+
+
+class TestChoiceTypeFalsyCode:
+ """A list-of-tuples ``ChoiceType`` must coerce *every* non-NULL value
+ back into a :class:`Choice`, including false-y values like ``0``
+ or the empty string.
+ """
+
+ @pytest.fixture
+ def Item(self, Base):
+ class Item(Base):
+ STATUSES = [(0, 'inactive'), (1, 'active')]
+
+ __tablename__ = 'item'
+ id = sa.Column(sa.Integer, primary_key=True)
+ status = sa.Column(ChoiceType(STATUSES, impl=sa.Integer()))
+
+ return Item
+
+ @pytest.fixture
+ def init_models(self, Item):
+ pass
+
+ def test_result_value_wraps_falsy_int_code(self):
+ type_ = ChoiceType([(0, 'inactive'), (1, 'active')], impl=sa.Integer())
+ result = type_.process_result_value(0, None)
+ assert isinstance(result, Choice)
+ assert result.code == 0
+ assert result.value == 'inactive'
+
+ def test_result_value_wraps_empty_string_code(self):
+ type_ = ChoiceType([('', 'blank'), ('a', 'Letter A')])
+ result = type_.process_result_value('', None)
+ assert isinstance(result, Choice)
+ assert result.code == ''
+ assert result.value == 'blank'
+
+ def test_matches_enum_sibling_for_falsy_code(self):
+ class Status(Enum):
+ inactive = 0
+ active = 1
+
+ tuple_type = ChoiceType([(0, 'inactive'), (1, 'active')],
impl=sa.Integer())
+ enum_type = ChoiceType(Status, impl=sa.Integer())
+
+ # Both siblings must wrap the falsy code, not leak the raw scalar.
+ assert type(tuple_type.process_result_value(0, None)) is Choice
+ assert enum_type.process_result_value(0, None) is Status.inactive
+ assert tuple_type.process_result_value(0, None) is not None
+
+ def test_falsy_code_roundtrips_through_database(self, session, Item):
+ item = Item()
+ item.status = 0
+
+ session.add(item)
+ session.commit()
+
+ item = session.query(Item).first()
+ assert isinstance(item.status, Choice)
+ assert type(item.status.code) is int # must be an int, not a bool
+ assert item.status.code == 0
+ assert item.status.value == 'inactive'
+ assert item.status == 0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/tests/types/test_json.py
new/sqlalchemy_utils-0.43.0/tests/types/test_json.py
--- old/sqlalchemy_utils-0.42.1/tests/types/test_json.py 2025-08-30
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/tests/types/test_json.py 2026-08-06
17:05:53.348627000 +0200
@@ -82,10 +82,7 @@
def test_unhashable_type(self, AAA, BBB, session):
"""Verify there are no TypeErrors with certain JSON queries.
- This test will fail under these conditions:
-
- 1. sqlalchemy versions 1.4.19 through 1.4.23 are installed.
- 2. `JSONType.hashable` is not set to False.
+ This test will fail if `JSONType.hashable` is not set to False.
For more info, see:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/sqlalchemy_utils-0.42.1/tests/types/test_password.py
new/sqlalchemy_utils-0.43.0/tests/types/test_password.py
--- old/sqlalchemy_utils-0.42.1/tests/types/test_password.py 2025-08-30
20:37:27.000000000 +0200
+++ new/sqlalchemy_utils-0.43.0/tests/types/test_password.py 2026-08-06
17:05:53.349164500 +0200
@@ -164,7 +164,7 @@
def test_update_none(self, session, User):
"""
Should be able to change a password from ``None`` to a valid
- password.
+ password, and the change must be persisted to the database.
"""
obj = User()
@@ -178,6 +178,36 @@
session.commit()
+ # Re-read from the database to confirm the None -> 'b' update was
+ # actually persisted and not dropped by change detection.
+ session.expire_all()
+ obj = session.get(User, obj.id)
+
+ assert obj.password is not None
+ assert obj.password == b'b'
+ assert obj.password != 'a'
+
+ def test_pending_password_not_equal_to_none(self):
+ """
+ A pending (coerced but not-yet-hashed) password holds a real secret
+ and must not compare equal to ``None``.
+
+ If it did, SQLAlchemy's scalar change detection would treat setting a
+ value on a previously-``NULL`` column as "unchanged" and skip the
+ ``UPDATE``, silently dropping the new password.
+ """
+
+ pending = Password('b', secret=True)
+
+ assert pending.hash is None
+ assert pending.secret == 'b'
+ assert not (pending == None) # noqa: E711
+ assert pending != None # noqa: E711
+
+ # A genuinely empty password still equals ``None``.
+ empty = Password(None)
+ assert empty == None # noqa: E711
+
def test_compare_none(self, User):
"""
Should be able to compare a password of ``None``.