From c987a7ab56c0114f0d2196215558761bdcce7de0 Mon Sep 17 00:00:00 2001 From: Lucas Vinicius Amaral de Oliveira Date: Mon, 13 Oct 2025 17:15:23 -0300 Subject: [PATCH] Add support for sqlalchemy 2.0 Add a new job for sqlalchemy 2.0 and fixed all issues. RFDAP-8594 --- setup.py | 2 +- src/serialchemy/__init__.py | 9 +++-- src/serialchemy/_tests/sample_model.py | 23 +++++++++---- .../sample_model_imperative/orm_mapping.py | 2 +- .../_tests/test_datetimeserializer.py | 8 +++-- src/serialchemy/_tests/test_field.py | 3 +- src/serialchemy/_tests/test_func.py | 4 +-- src/serialchemy/_tests/test_nested_fields.py | 6 ++-- .../_tests/test_polymorphic_serializer.py | 10 ++++-- src/serialchemy/_tests/test_readme.py | 17 +++++++--- src/serialchemy/_tests/test_serialization.py | 34 ++++++++++--------- src/serialchemy/datetime_serializer.py | 8 +++-- src/serialchemy/enum_field.py | 7 +++- src/serialchemy/enum_serializer.py | 9 ++--- src/serialchemy/nested_fields.py | 2 +- src/serialchemy/polymorphic_serializer.py | 1 + src/serialchemy/serializer.py | 3 +- src/serialchemy/swagger_spec.py | 8 +++-- tox.ini | 3 +- 19 files changed, 104 insertions(+), 55 deletions(-) diff --git a/setup.py b/setup.py index a5e10ae..e5da422 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ with open("CHANGELOG.rst", encoding="UTF-8") as changelog_file: history = changelog_file.read() -requirements = ["sqlalchemy>=1.4,<2.0"] +requirements = ["sqlalchemy>=1.4"] extras_require = { "docs": ["sphinx >= 1.4", "sphinx_rtd_theme", "sphinx-autodoc-typehints", "typing_extensions"], "testing": [ diff --git a/src/serialchemy/__init__.py b/src/serialchemy/__init__.py index eb76994..0c84bbb 100644 --- a/src/serialchemy/__init__.py +++ b/src/serialchemy/__init__.py @@ -1,7 +1,10 @@ from .enum_field import EnumKeyField from .field import Field from .model_serializer import ModelSerializer -from .nested_fields import ( - NestedAttributesField, NestedModelField, NestedModelListField, PrimaryKeyField) +from .nested_fields import NestedAttributesField +from .nested_fields import NestedModelField +from .nested_fields import NestedModelListField +from .nested_fields import PrimaryKeyField from .polymorphic_serializer import PolymorphicModelSerializer -from .serializer import ColumnSerializer, Serializer +from .serializer import ColumnSerializer +from .serializer import Serializer diff --git a/src/serialchemy/_tests/sample_model.py b/src/serialchemy/_tests/sample_model.py index 61c9cf9..af2768c 100644 --- a/src/serialchemy/_tests/sample_model.py +++ b/src/serialchemy/_tests/sample_model.py @@ -1,17 +1,27 @@ from datetime import datetime from enum import Enum -from sqlalchemy import Column, Date, DateTime, Float, ForeignKey, Integer, String, Table + +from sqlalchemy import Column +from sqlalchemy import Date +from sqlalchemy import DateTime +from sqlalchemy import Float +from sqlalchemy import ForeignKey +from sqlalchemy import Integer +from sqlalchemy import String +from sqlalchemy import Table from sqlalchemy.ext.associationproxy import association_proxy -from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import object_session, relationship +from sqlalchemy.orm import declarative_base +from sqlalchemy.orm import object_session +from sqlalchemy.orm import relationship from sqlalchemy.sql import sqltypes from sqlalchemy_utils import ChoiceType from serialchemy.enum_field import EnumKeyField from serialchemy.field import Field from serialchemy.model_serializer import ModelSerializer -from serialchemy.nested_fields import NestedModelField, NestedModelListField +from serialchemy.nested_fields import NestedModelField +from serialchemy.nested_fields import NestedModelListField Base = declarative_base() @@ -75,11 +85,13 @@ class ContractType(Enum): CONTRACTOR = 'Contractor' OTHER = 'Other' + class MaritalStatus(Enum): SINGLE = 'Single' MARRIED = 'Married' DIVORCED = 'Divorced' + class Employee(Base): __tablename__ = 'Employee' @@ -101,7 +113,6 @@ class Employee(Base): contract_type = Column(ChoiceType(ContractType)) marital_status = Column(sqltypes.Enum(MaritalStatus)) - password = Column(String) created_at = Column(DateTime, default=datetime(2000, 1, 2)) @@ -128,7 +139,7 @@ class Engineer(Employee): __tablename__ = 'Engineer' - id = Column('eng_id',Integer, ForeignKey('Employee.id'), primary_key=True) + id = Column('eng_id', Integer, ForeignKey('Employee.id'), primary_key=True) engineer_name = Column(String(30)) __mapper_args__ = {'polymorphic_identity': 'Engineer', 'polymorphic_on': Employee.role} diff --git a/src/serialchemy/_tests/sample_model_imperative/orm_mapping.py b/src/serialchemy/_tests/sample_model_imperative/orm_mapping.py index 557ad52..00baa7f 100644 --- a/src/serialchemy/_tests/sample_model_imperative/orm_mapping.py +++ b/src/serialchemy/_tests/sample_model_imperative/orm_mapping.py @@ -18,11 +18,11 @@ from serialchemy._tests.sample_model_imperative.model import Contact from serialchemy._tests.sample_model_imperative.model import ContactType from serialchemy._tests.sample_model_imperative.model import ContractType -from serialchemy._tests.sample_model_imperative.model import MaritalStatus from serialchemy._tests.sample_model_imperative.model import Department from serialchemy._tests.sample_model_imperative.model import Employee from serialchemy._tests.sample_model_imperative.model import Engineer from serialchemy._tests.sample_model_imperative.model import Manager +from serialchemy._tests.sample_model_imperative.model import MaritalStatus from serialchemy._tests.sample_model_imperative.model import SpecialistEngineer mapper_registry = registry() diff --git a/src/serialchemy/_tests/test_datetimeserializer.py b/src/serialchemy/_tests/test_datetimeserializer.py index 0a106d7..c6d7652 100644 --- a/src/serialchemy/_tests/test_datetimeserializer.py +++ b/src/serialchemy/_tests/test_datetimeserializer.py @@ -1,8 +1,12 @@ -from datetime import datetime, timedelta, timezone, date +from datetime import date +from datetime import datetime +from datetime import timedelta +from datetime import timezone import pytest -from serialchemy.datetime_serializer import DateTimeSerializer, DateSerializer +from serialchemy.datetime_serializer import DateSerializer +from serialchemy.datetime_serializer import DateTimeSerializer @pytest.mark.parametrize( diff --git a/src/serialchemy/_tests/test_field.py b/src/serialchemy/_tests/test_field.py index 6565eaf..ad146b4 100644 --- a/src/serialchemy/_tests/test_field.py +++ b/src/serialchemy/_tests/test_field.py @@ -1,4 +1,5 @@ -from serialchemy import Field, Serializer +from serialchemy import Field +from serialchemy import Serializer class CustomSerializer(Serializer): diff --git a/src/serialchemy/_tests/test_func.py b/src/serialchemy/_tests/test_func.py index 9c6f77d..5977b23 100644 --- a/src/serialchemy/_tests/test_func.py +++ b/src/serialchemy/_tests/test_func.py @@ -14,14 +14,14 @@ def seed_data(model, db_session): password='somepass', role='Employee', company=company, - marital_status=model.MaritalStatus.DIVORCED + marital_status=model.MaritalStatus.DIVORCED, ) db_session.add(employee) db_session.commit() def test_dump(model, db_session, data_regression): - employee = db_session.query(model.Employee).get(2) + employee = db_session.get(model.Employee, 2) serial = func.dump(employee, nest_foreign_keys=True) data_regression.check(serial, basename='test_dump') diff --git a/src/serialchemy/_tests/test_nested_fields.py b/src/serialchemy/_tests/test_nested_fields.py index 33434d7..6fd5d98 100644 --- a/src/serialchemy/_tests/test_nested_fields.py +++ b/src/serialchemy/_tests/test_nested_fields.py @@ -99,7 +99,7 @@ def test_custom_serializer(model, serializer_strategy, db_session, data_regressi if serializer_strategy == "NestedModelFields" else EmployeeSerializerNestedAttrsFields ) - emp = db_session.query(model.Employee).get(1) + emp = db_session.get(model.Employee, 1) serializer = serializer_class(model.Employee) serialized = serializer.dump(emp) data_regression.check( @@ -127,7 +127,7 @@ def test_deserialize_with_custom_serializer(model, db_session, data_regression): def test_deserialize_existing_model(model, db_session): - original = db_session.query(model.Employee).get(1) + original = db_session.get(model.Employee, 1) assert original.firstname == "Jim" assert original.address.zip is None @@ -156,7 +156,7 @@ def test_deserialize_existing_model(model, db_session): def test_empty_nested(model, db_session): serializer = getEmployeeSerializerNestedModelFields(model)(model.Employee) - serialized = serializer.dump(db_session.query(model.Employee).get(3)) + serialized = serializer.dump(db_session.get(model.Employee, 3)) assert serialized["company"] is None model = serializer.load(serialized, session=db_session) assert model.company is None diff --git a/src/serialchemy/_tests/test_polymorphic_serializer.py b/src/serialchemy/_tests/test_polymorphic_serializer.py index 2bed560..250910b 100644 --- a/src/serialchemy/_tests/test_polymorphic_serializer.py +++ b/src/serialchemy/_tests/test_polymorphic_serializer.py @@ -1,6 +1,10 @@ from enum import Enum -from sqlalchemy import Column, ForeignKey, Integer, String -from sqlalchemy.ext.declarative import declarative_base + +from sqlalchemy import Column +from sqlalchemy import ForeignKey +from sqlalchemy import Integer +from sqlalchemy import String +from sqlalchemy.orm import declarative_base from serialchemy import PolymorphicModelSerializer @@ -43,4 +47,4 @@ class TestA(BaseTest): assert serialized_obj["type"] == TestEnum.TYPE_A.value loaded_obj = serializer.load(serialized_obj, session=db_session) - assert isinstance(loaded_obj, TestA) \ No newline at end of file + assert isinstance(loaded_obj, TestA) diff --git a/src/serialchemy/_tests/test_readme.py b/src/serialchemy/_tests/test_readme.py index 22631fb..c4a2361 100644 --- a/src/serialchemy/_tests/test_readme.py +++ b/src/serialchemy/_tests/test_readme.py @@ -1,9 +1,14 @@ from datetime import datetime -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, select -from sqlalchemy.ext.declarative import declarative_base - -from sqlalchemy.orm import column_property, relationship +from sqlalchemy import Column +from sqlalchemy import DateTime +from sqlalchemy import ForeignKey +from sqlalchemy import Integer +from sqlalchemy import select +from sqlalchemy import String +from sqlalchemy.orm import column_property +from sqlalchemy.orm import declarative_base +from sqlalchemy.orm import relationship Base = declarative_base() @@ -23,7 +28,9 @@ class Employee(Base): admission = Column(DateTime, default=datetime(2000, 1, 1)) company_id = Column(ForeignKey('Company.id')) company = relationship(Company) - company_name = column_property(select([Company.name]).where(Company.id == company_id)) + company_name = column_property( + select(Company.name).where(Company.id == company_id).scalar_subquery() + ) password = Column(String) diff --git a/src/serialchemy/_tests/test_serialization.py b/src/serialchemy/_tests/test_serialization.py index 50beea4..c2f7854 100644 --- a/src/serialchemy/_tests/test_serialization.py +++ b/src/serialchemy/_tests/test_serialization.py @@ -1,3 +1,5 @@ +from sqlalchemy.orm import Session + from serialchemy.enum_field import EnumKeyField from serialchemy.field import Field from serialchemy.func import dump @@ -85,7 +87,7 @@ class EmployeeSerializer(ModelSerializer): def test_model_dump(model, db_session, data_regression): seed_data(db_session, model) - emp = db_session.query(model.Employee).get(1) + emp = db_session.get(model.Employee, 1) serializer = ModelSerializer(model.Employee) serialized = serializer.dump(emp) data_regression.check(serialized, basename='test_model_dump') @@ -94,7 +96,7 @@ def test_model_dump(model, db_session, data_regression): def test_enum_key_field_dump(model, db_session, data_regression): seed_data(db_session, model) - emp = db_session.query(model.Employee).get(1) + emp = db_session.get(model.Employee, 1) serializer = getEmployeeSerializer(model)(model.Employee) serialized = serializer.dump(emp) data_regression.check(serialized, basename='test_enum_key_field_dump') @@ -140,7 +142,7 @@ class EmployeeSerializerPrimaryKeyFields(ModelSerializer): company = PrimaryKeyField(model.Company) serializer = EmployeeSerializerPrimaryKeyFields(model.Employee) - employee = db_session.query(model.Employee).get(2) + employee = db_session.get(model.Employee, 2) serialized = serializer.dump(employee) data_regression.check(serialized, basename='test_one2one_pk_field') @@ -152,14 +154,14 @@ class CompanySerializer(ModelSerializer): employees = PrimaryKeyField(model.Employee) serializer = CompanySerializer(model.Company) - company = db_session.query(model.Company).get(5) + company = db_session.get(model.Company, 5) serialized = serializer.dump(company) data_regression.check(serialized) serialized['employees'] = [2, 3] company = serializer.load(serialized, existing_model=company, session=db_session) - assert company.employees[0] == db_session.query(model.Employee).get(2) - assert company.employees[1] == db_session.query(model.Employee).get(3) + assert company.employees[0] == db_session.get(model.Employee, 2) + assert company.employees[1] == db_session.get(model.Employee, 3) def test_property_serialization(model, db_session): @@ -169,7 +171,7 @@ class EmployeeSerializerHybridProperty(ModelSerializer): full_name = Field(dump_only=True) serializer = EmployeeSerializerHybridProperty(model.Employee) - serialized = serializer.dump(db_session.query(model.Employee).get(2)) + serialized = serializer.dump(db_session.get(model.Employee, 2)) assert serialized['full_name'] is not None @@ -177,7 +179,7 @@ def test_protected_field_default_creation(model, db_session): seed_data(db_session, model) serializer = ModelSerializer(model.Employee) - employee = db_session.query(model.Employee).get(1) + employee = db_session.get(model.Employee, 1) assert employee._salary == 400 serialized = serializer.dump(employee) assert serialized.get('role') == 'Manager' @@ -193,7 +195,7 @@ def test_inherited_model_serialization(model, db_session): serializer = PolymorphicModelSerializer(model.Employee) - manager = db_session.query(model.Employee).get(1) + manager = db_session.get(model.Employee, 1) assert isinstance(manager, model.Manager) serialized = serializer.dump(manager) @@ -201,7 +203,7 @@ def test_inherited_model_serialization(model, db_session): entity = serializer.load(serialized, session=db_session) assert hasattr(entity, 'manager_name') - engineer = db_session.query(model.Employee).get(2) + engineer = db_session.get(model.Employee, 2) assert isinstance(engineer, model.Engineer) serialized = serializer.dump(engineer) @@ -209,7 +211,7 @@ def test_inherited_model_serialization(model, db_session): entity = serializer.load(serialized, session=db_session) assert hasattr(entity, 'engineer_name') - engineer = db_session.query(model.Employee).get(4) + engineer = db_session.get(model.Employee, 4) assert isinstance(engineer, model.SpecialistEngineer) serialized = serializer.dump(engineer) @@ -218,18 +220,18 @@ def test_inherited_model_serialization(model, db_session): assert hasattr(entity, 'specialization') -def test_nested_inherited_model_serialization(model, db_session): +def test_nested_inherited_model_serialization(model, db_session: Session) -> None: seed_data(db_session, model) serializer = PolymorphicModelSerializer(model.Engineer) - engineer = db_session.query(model.Employee).get(2) + engineer = db_session.get(model.Employee, 2) assert isinstance(engineer, model.Engineer) serialized = serializer.dump(engineer) assert serialized.get('role') == 'Engineer' assert 'specialization' not in serialized.keys() - specialist_engineer = db_session.query(model.Employee).get(4) + specialist_engineer = db_session.get(model.Employee, 4) assert isinstance(specialist_engineer, model.SpecialistEngineer) serialized = serializer.dump(specialist_engineer) assert serialized.get('role') == 'Specialist Engineer' @@ -278,10 +280,10 @@ class EmployeeSerializerCreationOnlyField(ModelSerializer): assert employee.firstname == 'Other' -def test_dump_choice_type(model, db_session, data_regression): +def test_dump_choice_type(model, db_session: Session, data_regression): seed_data(db_session, model) - tychus = db_session.query(model.Employee).get(3) + tychus = db_session.get(model.Employee, 3) serializer = ModelSerializer(model.Employee) dump = serializer.dump(tychus) data_regression.check(dump, basename='test_dump_choice_type') diff --git a/src/serialchemy/datetime_serializer.py b/src/serialchemy/datetime_serializer.py index 4749030..a853855 100644 --- a/src/serialchemy/datetime_serializer.py +++ b/src/serialchemy/datetime_serializer.py @@ -1,8 +1,12 @@ import re import warnings -from datetime import datetime, timedelta, timezone, date +from datetime import date +from datetime import datetime +from datetime import timedelta +from datetime import timezone -from .serializer import Serializer, ColumnSerializer +from .serializer import ColumnSerializer +from .serializer import Serializer DATETIME_REGEX = ( r"(?P\d{2,4})-(?P\d{2})-(?P\d{2})" diff --git a/src/serialchemy/enum_field.py b/src/serialchemy/enum_field.py index e2968c8..8d08bbc 100644 --- a/src/serialchemy/enum_field.py +++ b/src/serialchemy/enum_field.py @@ -4,4 +4,9 @@ class EnumKeyField(Field): def __init__(self, enum_class, dump_only=False, load_only=False, creation_only=False): - super().__init__(dump_only=dump_only, load_only=load_only, creation_only=creation_only, serializer=EnumKeySerializer(enum_class)) \ No newline at end of file + super().__init__( + dump_only=dump_only, + load_only=load_only, + creation_only=creation_only, + serializer=EnumKeySerializer(enum_class), + ) diff --git a/src/serialchemy/enum_serializer.py b/src/serialchemy/enum_serializer.py index 42c668d..dcf538e 100644 --- a/src/serialchemy/enum_serializer.py +++ b/src/serialchemy/enum_serializer.py @@ -1,8 +1,8 @@ -from typing import Type - from enum import Enum +from typing import Type -from .serializer import ColumnSerializer, Serializer +from .serializer import ColumnSerializer +from .serializer import Serializer class EnumSerializer(ColumnSerializer): @@ -15,6 +15,7 @@ def load(self, serialized, session=None): enum = getattr(self.column.type, 'enum_class') return enum(serialized) + class EnumKeySerializer(Serializer): def __init__(self, enum_class: Type[Enum]) -> None: super().__init__() @@ -26,4 +27,4 @@ def dump(self, value): return value.name def load(self, serialized, session=None): - return self.enum_class[serialized] \ No newline at end of file + return self.enum_class[serialized] diff --git a/src/serialchemy/nested_fields.py b/src/serialchemy/nested_fields.py index 27ea72f..8e63011 100644 --- a/src/serialchemy/nested_fields.py +++ b/src/serialchemy/nested_fields.py @@ -75,7 +75,7 @@ def load(self, serialized, session): if session is None: raise RuntimeError("Session object is required to deserialize a nested object") with session.no_autoflush: - existing_model = session.query(class_mapper).get(pk) + existing_model = session.get(class_mapper, pk) return self.serializer.load(serialized, existing_model, session=session) else: # No primary key, just create a new model entity diff --git a/src/serialchemy/polymorphic_serializer.py b/src/serialchemy/polymorphic_serializer.py index abd5fe6..d0b0740 100644 --- a/src/serialchemy/polymorphic_serializer.py +++ b/src/serialchemy/polymorphic_serializer.py @@ -1,4 +1,5 @@ import enum + from sqlalchemy.orm import class_mapper from serialchemy import ModelSerializer diff --git a/src/serialchemy/serializer.py b/src/serialchemy/serializer.py index e3bd004..2f749d6 100644 --- a/src/serialchemy/serializer.py +++ b/src/serialchemy/serializer.py @@ -1,4 +1,5 @@ -from abc import ABC, abstractmethod +from abc import ABC +from abc import abstractmethod class Serializer(ABC): diff --git a/src/serialchemy/swagger_spec.py b/src/serialchemy/swagger_spec.py index 8b7f450..f738752 100644 --- a/src/serialchemy/swagger_spec.py +++ b/src/serialchemy/swagger_spec.py @@ -1,8 +1,12 @@ from sqlalchemy import DateTime -from sqlalchemy_utils import PasswordType, JSONType +from sqlalchemy_utils import JSONType +from sqlalchemy_utils import PasswordType +from .field import Field +from .field import NestedAttributesField +from .field import NestedModelField +from .field import PrimaryKeyField from .model_serializer import ModelSerializer -from .field import Field, NestedModelField, NestedAttributesField, PrimaryKeyField SWAGGER_BASIC_TYPES = { str: dict(type='string'), diff --git a/tox.ini b/tox.ini index ed64f60..ac95c36 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{38,39,310,311,312}-sqla{14}, linting, docs +envlist = py{38,39,310,311,312}-sqla{14,20}, linting, docs isolated_build = true [gh-actions] @@ -16,6 +16,7 @@ commands = pytest --cov={envsitepackagesdir}/serialchemy --cov-report=xml --pyargs serialchemy deps = sqla14: sqlalchemy>=1.4,<2 + sqla20: sqlalchemy>=2 [testenv:linting] skip_install = True