diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 5f72d0990..277a1117d 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -12,7 +12,6 @@ jobs: - '3.12' - '3.13' - '3.14' - tox_env: ['sqlalchemy14', 'sqlalchemy2'] runs-on: ${{ matrix.os }} services: postgres: @@ -68,7 +67,7 @@ jobs: - name: Run tests env: SQLALCHEMY_UTILS_TEST_POSTGRESQL_PASSWORD: postgres - run: tox -e ${{ matrix.tox_env }} + run: tox -e py${{ matrix.python-version }} docs: runs-on: 'ubuntu-latest' diff --git a/CHANGES.rst b/CHANGES.rst index 8e6bfb506..4966569c2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,6 +6,7 @@ Here you can see the full list of changes between each SQLAlchemy-Utils release. Unreleased ^^^^^^^^^^ +- 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) diff --git a/pyproject.toml b/pyproject.toml index b0fecdc54..76239815f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "SQLAlchemy>=1.4", + "SQLAlchemy>=2.0", ] [project.urls] diff --git a/sqlalchemy_utils/functions/database.py b/sqlalchemy_utils/functions/database.py index bda84c2fb..b0c0bc301 100644 --- a/sqlalchemy_utils/functions/database.py +++ b/sqlalchemy_utils/functions/database.py @@ -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 @@ def _set_url_database(url: sa.engine.url.URL, database): :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 --git a/sqlalchemy_utils/functions/mock.py b/sqlalchemy_utils/functions/mock.py index dd7a075f9..cc7df6da9 100644 --- a/sqlalchemy_utils/functions/mock.py +++ b/sqlalchemy_utils/functions/mock.py @@ -47,10 +47,7 @@ def render_literal_value(self, value, type_): 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 --git a/sqlalchemy_utils/functions/orm.py b/sqlalchemy_utils/functions/orm.py index 60c10b4db..fbe1cbab6 100644 --- a/sqlalchemy_utils/functions/orm.py +++ b/sqlalchemy_utils/functions/orm.py @@ -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 @@ def get_mapper(mixed): 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 @@ def get_columns(mixed): 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 quote(mixed, ident): 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 naturally_equivalent(obj, obj2): 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 --git a/sqlalchemy_utils/functions/sort_query.py b/sqlalchemy_utils/functions/sort_query.py index 3c1b008e4..9375b0738 100644 --- a/sqlalchemy_utils/functions/sort_query.py +++ b/sqlalchemy_utils/functions/sort_query.py @@ -40,10 +40,7 @@ def make_order_by_deterministic(query): """ 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 --git a/sqlalchemy_utils/types/uuid.py b/sqlalchemy_utils/types/uuid.py index 60a86379a..c4480242b 100644 --- a/sqlalchemy_utils/types/uuid.py +++ b/sqlalchemy_utils/types/uuid.py @@ -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 @@ def _coerce(value): 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 --git a/tests/functions/test_database.py b/tests/functions/test_database.py index cdd631ad6..5f81ca507 100644 --- a/tests/functions/test_database.py +++ b/tests/functions/test_database.py @@ -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 @@ def dsn(self, postgresql_db_user, postgresql_db_password): ) -@pytest.mark.skipif('sqlalchemy_version < (2, 0, 0)') class TestDatabasePostgresPsycoPG3(DatabaseTest): @pytest.fixture diff --git a/tests/types/test_json.py b/tests/types/test_json.py index d6ae64157..2ab58102c 100644 --- a/tests/types/test_json.py +++ b/tests/types/test_json.py @@ -82,10 +82,7 @@ def test_compilation(self, Document, session): 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 --git a/tox.ini b/tox.ini index ff06d92a7..8aaf02138 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - py{3.10, 3.11, 3.12, 3.13, 3.14}-sqlalchemy{14, 2} + py{3.10, 3.11, 3.12, 3.13, 3.14} ruff docs labels = @@ -12,22 +12,6 @@ commands = deps = .[test_all] pytest-cov - sqlalchemy14: SQLAlchemy>=1.4,<1.5 - sqlalchemy2: SQLAlchemy>=2 - ; It's sometimes necessary to test against specific sqlalchemy versions - ; to verify bug or feature behavior before or after a specific version. - ; - ; You can choose the sqlalchemy version using this command syntax: - ; $ tox -e py39-sqlalchemy1_4_xx - ; - ; sqlalchemy 1.4.19 through 1.4.23 have JSON-related TypeErrors. See #543. - sqlalchemy1_4_19: SQLAlchemy==1.4.19 - ; sqlalchemy <1.4.28 threw a DeprecationWarning when copying a URL. See #573. - sqlalchemy1_4_27: SQLAlchemy==1.4.27 - ; sqlalchemy 1.4.30 introduced UUID literal quoting. See #580. - sqlalchemy1_4_29: SQLAlchemy==1.4.29 -setenv = - SQLALCHEMY_WARN_20 = true passenv = SQLALCHEMY_UTILS_TEST_* recreate = True