From 60318b442a610ef733d01b43f86b0e3a2e3ebef8 Mon Sep 17 00:00:00 2001 From: Mohamed Farahat Date: Tue, 7 Mar 2023 02:08:36 +0200 Subject: [PATCH 01/39] migration to sqlalchemy 2.0 --- docs_src/tutorial/many_to_many/tutorial003.py | 30 +++++++-------- .../back_populates/tutorial003.py | 30 +++++++-------- pyproject.toml | 6 +-- sqlmodel/__init__.py | 2 - sqlmodel/main.py | 1 + sqlmodel/sql/expression.py | 38 +++---------------- .../test_multiple_models/test_tutorial001.py | 4 +- .../test_multiple_models/test_tutorial002.py | 4 +- .../test_indexes/test_tutorial001.py | 4 +- .../test_indexes/test_tutorial006.py | 4 +- 10 files changed, 46 insertions(+), 77 deletions(-) diff --git a/docs_src/tutorial/many_to_many/tutorial003.py b/docs_src/tutorial/many_to_many/tutorial003.py index 1e03c4af89..cec6e56560 100644 --- a/docs_src/tutorial/many_to_many/tutorial003.py +++ b/docs_src/tutorial/many_to_many/tutorial003.py @@ -3,25 +3,12 @@ from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select -class HeroTeamLink(SQLModel, table=True): - team_id: Optional[int] = Field( - default=None, foreign_key="team.id", primary_key=True - ) - hero_id: Optional[int] = Field( - default=None, foreign_key="hero.id", primary_key=True - ) - is_training: bool = False - - team: "Team" = Relationship(back_populates="hero_links") - hero: "Hero" = Relationship(back_populates="team_links") - - class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True) headquarters: str - hero_links: List[HeroTeamLink] = Relationship(back_populates="team") + hero_links: List["HeroTeamLink"] = Relationship(back_populates="team") class Hero(SQLModel, table=True): @@ -30,7 +17,20 @@ class Hero(SQLModel, table=True): secret_name: str age: Optional[int] = Field(default=None, index=True) - team_links: List[HeroTeamLink] = Relationship(back_populates="hero") + team_links: List["HeroTeamLink"] = Relationship(back_populates="hero") + + +class HeroTeamLink(SQLModel, table=True): + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", primary_key=True + ) + hero_id: Optional[int] = Field( + default=None, foreign_key="hero.id", primary_key=True + ) + is_training: bool = False + + team: "Team" = Relationship(back_populates="hero_links") + hero: "Hero" = Relationship(back_populates="team_links") sqlite_file_name = "database.db" diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py index 98e197002e..8d91a0bc25 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py @@ -3,6 +3,21 @@ from sqlmodel import Field, Relationship, SQLModel, create_engine +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional["Team"] = Relationship(back_populates="heroes") + + weapon_id: Optional[int] = Field(default=None, foreign_key="weapon.id") + weapon: Optional["Weapon"] = Relationship(back_populates="hero") + + powers: List["Power"] = Relationship(back_populates="hero") + + class Weapon(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True) @@ -26,21 +41,6 @@ class Team(SQLModel, table=True): heroes: List["Hero"] = Relationship(back_populates="team") -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional[Team] = Relationship(back_populates="heroes") - - weapon_id: Optional[int] = Field(default=None, foreign_key="weapon.id") - weapon: Optional[Weapon] = Relationship(back_populates="hero") - - powers: List[Power] = Relationship(back_populates="hero") - - sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" diff --git a/pyproject.toml b/pyproject.toml index e3b1d3c279..d3e64088ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,6 @@ classifiers = [ "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -30,10 +29,9 @@ classifiers = [ ] [tool.poetry.dependencies] -python = "^3.6.1" -SQLAlchemy = ">=1.4.17,<=1.4.41" +python = "^3.7" +SQLAlchemy = ">=2.0.0,<=2.0.5.post1" pydantic = "^1.8.2" -sqlalchemy2-stubs = {version = "*", allow-prereleases = true} [tool.poetry.dev-dependencies] pytest = "^7.0.1" diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 720aa8c929..6cdf4e7fc2 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -21,7 +21,6 @@ from sqlalchemy.schema import PrimaryKeyConstraint as PrimaryKeyConstraint from sqlalchemy.schema import Sequence as Sequence from sqlalchemy.schema import Table as Table -from sqlalchemy.schema import ThreadLocalMetaData as ThreadLocalMetaData from sqlalchemy.schema import UniqueConstraint as UniqueConstraint from sqlalchemy.sql import alias as alias from sqlalchemy.sql import all_ as all_ @@ -71,7 +70,6 @@ from sqlalchemy.sql import outerjoin as outerjoin from sqlalchemy.sql import outparam as outparam from sqlalchemy.sql import over as over -from sqlalchemy.sql import subquery as subquery from sqlalchemy.sql import table as table from sqlalchemy.sql import tablesample as tablesample from sqlalchemy.sql import text as text diff --git a/sqlmodel/main.py b/sqlmodel/main.py index d95c498507..b07e4a7e17 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -478,6 +478,7 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty]] # type: ignore __name__: ClassVar[str] metadata: ClassVar[MetaData] + __allow_unmapped__ = True # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six class Config: orm_mode = True diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 31c0bc1a1e..48853cff01 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -138,12 +138,12 @@ class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMet @overload -def select(entity_0: _TScalar_0, **kw: Any) -> SelectOfScalar[_TScalar_0]: # type: ignore +def select(entity_0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore ... @overload -def select(entity_0: Type[_TModel_0], **kw: Any) -> SelectOfScalar[_TModel_0]: # type: ignore +def select(entity_0: Type[_TModel_0]) -> SelectOfScalar[_TModel_0]: # type: ignore ... @@ -154,7 +154,6 @@ def select(entity_0: Type[_TModel_0], **kw: Any) -> SelectOfScalar[_TModel_0]: def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TScalar_1]]: ... @@ -163,7 +162,6 @@ def select( # type: ignore def select( # type: ignore entity_0: _TScalar_0, entity_1: Type[_TModel_1], - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TModel_1]]: ... @@ -172,7 +170,6 @@ def select( # type: ignore def select( # type: ignore entity_0: Type[_TModel_0], entity_1: _TScalar_1, - **kw: Any, ) -> Select[Tuple[_TModel_0, _TScalar_1]]: ... @@ -181,7 +178,6 @@ def select( # type: ignore def select( # type: ignore entity_0: Type[_TModel_0], entity_1: Type[_TModel_1], - **kw: Any, ) -> Select[Tuple[_TModel_0, _TModel_1]]: ... @@ -191,7 +187,6 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, entity_2: _TScalar_2, - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2]]: ... @@ -201,7 +196,6 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, entity_2: Type[_TModel_2], - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TScalar_1, _TModel_2]]: ... @@ -211,7 +205,6 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: Type[_TModel_1], entity_2: _TScalar_2, - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TModel_1, _TScalar_2]]: ... @@ -221,7 +214,6 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: Type[_TModel_1], entity_2: Type[_TModel_2], - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TModel_1, _TModel_2]]: ... @@ -231,7 +223,6 @@ def select( # type: ignore entity_0: Type[_TModel_0], entity_1: _TScalar_1, entity_2: _TScalar_2, - **kw: Any, ) -> Select[Tuple[_TModel_0, _TScalar_1, _TScalar_2]]: ... @@ -241,7 +232,6 @@ def select( # type: ignore entity_0: Type[_TModel_0], entity_1: _TScalar_1, entity_2: Type[_TModel_2], - **kw: Any, ) -> Select[Tuple[_TModel_0, _TScalar_1, _TModel_2]]: ... @@ -251,7 +241,6 @@ def select( # type: ignore entity_0: Type[_TModel_0], entity_1: Type[_TModel_1], entity_2: _TScalar_2, - **kw: Any, ) -> Select[Tuple[_TModel_0, _TModel_1, _TScalar_2]]: ... @@ -261,7 +250,6 @@ def select( # type: ignore entity_0: Type[_TModel_0], entity_1: Type[_TModel_1], entity_2: Type[_TModel_2], - **kw: Any, ) -> Select[Tuple[_TModel_0, _TModel_1, _TModel_2]]: ... @@ -272,7 +260,6 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: _TScalar_3, - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... @@ -283,7 +270,6 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: Type[_TModel_3], - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TModel_3]]: ... @@ -294,7 +280,6 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: Type[_TModel_2], entity_3: _TScalar_3, - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TScalar_1, _TModel_2, _TScalar_3]]: ... @@ -305,7 +290,6 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: Type[_TModel_2], entity_3: Type[_TModel_3], - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TScalar_1, _TModel_2, _TModel_3]]: ... @@ -316,7 +300,6 @@ def select( # type: ignore entity_1: Type[_TModel_1], entity_2: _TScalar_2, entity_3: _TScalar_3, - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TModel_1, _TScalar_2, _TScalar_3]]: ... @@ -327,7 +310,6 @@ def select( # type: ignore entity_1: Type[_TModel_1], entity_2: _TScalar_2, entity_3: Type[_TModel_3], - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TModel_1, _TScalar_2, _TModel_3]]: ... @@ -338,7 +320,6 @@ def select( # type: ignore entity_1: Type[_TModel_1], entity_2: Type[_TModel_2], entity_3: _TScalar_3, - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TModel_1, _TModel_2, _TScalar_3]]: ... @@ -349,7 +330,6 @@ def select( # type: ignore entity_1: Type[_TModel_1], entity_2: Type[_TModel_2], entity_3: Type[_TModel_3], - **kw: Any, ) -> Select[Tuple[_TScalar_0, _TModel_1, _TModel_2, _TModel_3]]: ... @@ -360,7 +340,6 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: _TScalar_3, - **kw: Any, ) -> Select[Tuple[_TModel_0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... @@ -371,7 +350,6 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: Type[_TModel_3], - **kw: Any, ) -> Select[Tuple[_TModel_0, _TScalar_1, _TScalar_2, _TModel_3]]: ... @@ -382,7 +360,6 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: Type[_TModel_2], entity_3: _TScalar_3, - **kw: Any, ) -> Select[Tuple[_TModel_0, _TScalar_1, _TModel_2, _TScalar_3]]: ... @@ -393,7 +370,6 @@ def select( # type: ignore entity_1: _TScalar_1, entity_2: Type[_TModel_2], entity_3: Type[_TModel_3], - **kw: Any, ) -> Select[Tuple[_TModel_0, _TScalar_1, _TModel_2, _TModel_3]]: ... @@ -404,7 +380,6 @@ def select( # type: ignore entity_1: Type[_TModel_1], entity_2: _TScalar_2, entity_3: _TScalar_3, - **kw: Any, ) -> Select[Tuple[_TModel_0, _TModel_1, _TScalar_2, _TScalar_3]]: ... @@ -415,7 +390,6 @@ def select( # type: ignore entity_1: Type[_TModel_1], entity_2: _TScalar_2, entity_3: Type[_TModel_3], - **kw: Any, ) -> Select[Tuple[_TModel_0, _TModel_1, _TScalar_2, _TModel_3]]: ... @@ -426,7 +400,6 @@ def select( # type: ignore entity_1: Type[_TModel_1], entity_2: Type[_TModel_2], entity_3: _TScalar_3, - **kw: Any, ) -> Select[Tuple[_TModel_0, _TModel_1, _TModel_2, _TScalar_3]]: ... @@ -437,7 +410,6 @@ def select( # type: ignore entity_1: Type[_TModel_1], entity_2: Type[_TModel_2], entity_3: Type[_TModel_3], - **kw: Any, ) -> Select[Tuple[_TModel_0, _TModel_1, _TModel_2, _TModel_3]]: ... @@ -445,10 +417,10 @@ def select( # type: ignore # Generated overloads end -def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]: # type: ignore +def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore if len(entities) == 1: - return SelectOfScalar._create(*entities, **kw) # type: ignore - return Select._create(*entities, **kw) # type: ignore + return SelectOfScalar(*entities) # type: ignore + return Select(*entities) # type: ignore # TODO: add several @overload from Python types to SQLAlchemy equivalents diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index cf008563f4..2fbc83286a 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -173,8 +173,8 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, + {"name": "ix_hero_name", "column_names": ["name"], "unique": 0, 'dialect_options': {}}, + {"name": "ix_hero_age", "column_names": ["age"], "unique": 0, 'dialect_options': {}}, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 57393a7ddc..a70e546abb 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -173,8 +173,8 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, + {"name": "ix_hero_age", "column_names": ["age"], "unique": 0, 'dialect_options': {}}, + {"name": "ix_hero_name", "column_names": ["name"], "unique": 0, 'dialect_options': {}}, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" diff --git a/tests/test_tutorial/test_indexes/test_tutorial001.py b/tests/test_tutorial/test_indexes/test_tutorial001.py index 596207737d..2394c48feb 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial001.py +++ b/tests/test_tutorial/test_indexes/test_tutorial001.py @@ -25,8 +25,8 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, + {"name": "ix_hero_name", "column_names": ["name"], "unique": 0, 'dialect_options': {}}, + {"name": "ix_hero_age", "column_names": ["age"], "unique": 0, 'dialect_options': {}}, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" diff --git a/tests/test_tutorial/test_indexes/test_tutorial006.py b/tests/test_tutorial/test_indexes/test_tutorial006.py index e26f8b2ed8..8283d44082 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial006.py +++ b/tests/test_tutorial/test_indexes/test_tutorial006.py @@ -26,8 +26,8 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0}, - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0}, + {"name": "ix_hero_name", "column_names": ["name"], "unique": 0, 'dialect_options': {}}, + {"name": "ix_hero_age", "column_names": ["age"], "unique": 0, 'dialect_options': {}}, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" From 48ddc61add9c54385909ec7c23f8fefd92eb41c6 Mon Sep 17 00:00:00 2001 From: Mohamed Farahat Date: Fri, 24 Mar 2023 14:56:26 +0200 Subject: [PATCH 02/39] fix some linting errors --- sqlmodel/engine/create.py | 2 +- sqlmodel/engine/result.py | 20 +++++----- sqlmodel/main.py | 4 +- sqlmodel/orm/session.py | 2 +- sqlmodel/sql/expression.py | 37 +++++-------------- sqlmodel/sql/sqltypes.py | 6 +-- .../test_multiple_models/test_tutorial001.py | 14 ++++++- .../test_multiple_models/test_tutorial002.py | 14 ++++++- .../test_indexes/test_tutorial001.py | 14 ++++++- .../test_indexes/test_tutorial006.py | 14 ++++++- 10 files changed, 74 insertions(+), 53 deletions(-) diff --git a/sqlmodel/engine/create.py b/sqlmodel/engine/create.py index b2d567b1b1..97481259e2 100644 --- a/sqlmodel/engine/create.py +++ b/sqlmodel/engine/create.py @@ -136,4 +136,4 @@ def create_engine( if not isinstance(query_cache_size, _DefaultPlaceholder): current_kwargs["query_cache_size"] = query_cache_size current_kwargs.update(kwargs) - return _create_engine(url, **current_kwargs) # type: ignore + return _create_engine(url, **current_kwargs) diff --git a/sqlmodel/engine/result.py b/sqlmodel/engine/result.py index 7a25422227..17020d9995 100644 --- a/sqlmodel/engine/result.py +++ b/sqlmodel/engine/result.py @@ -1,4 +1,4 @@ -from typing import Generic, Iterator, List, Optional, TypeVar +from typing import Generic, Iterator, List, Optional, Sequence, TypeVar from sqlalchemy.engine.result import Result as _Result from sqlalchemy.engine.result import ScalarResult as _ScalarResult @@ -6,24 +6,24 @@ _T = TypeVar("_T") -class ScalarResult(_ScalarResult, Generic[_T]): - def all(self) -> List[_T]: +class ScalarResult(_ScalarResult[_T], Generic[_T]): + def all(self) -> Sequence[_T]: return super().all() - def partitions(self, size: Optional[int] = None) -> Iterator[List[_T]]: + def partitions(self, size: Optional[int] = None) -> Iterator[Sequence[_T]]: return super().partitions(size) - def fetchall(self) -> List[_T]: + def fetchall(self) -> Sequence[_T]: return super().fetchall() - def fetchmany(self, size: Optional[int] = None) -> List[_T]: + def fetchmany(self, size: Optional[int] = None) -> Sequence[_T]: return super().fetchmany(size) def __iter__(self) -> Iterator[_T]: return super().__iter__() def __next__(self) -> _T: - return super().__next__() # type: ignore + return super().__next__() def first(self) -> Optional[_T]: return super().first() @@ -32,10 +32,10 @@ def one_or_none(self) -> Optional[_T]: return super().one_or_none() def one(self) -> _T: - return super().one() # type: ignore + return super().one() -class Result(_Result, Generic[_T]): +class Result(_Result[_T], Generic[_T]): def scalars(self, index: int = 0) -> ScalarResult[_T]: return super().scalars(index) # type: ignore @@ -76,4 +76,4 @@ def one(self) -> _T: # type: ignore return super().one() # type: ignore def scalar(self) -> Optional[_T]: - return super().scalar() + return super().scalar() # type: ignore diff --git a/sqlmodel/main.py b/sqlmodel/main.py index b07e4a7e17..658e5384d8 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -478,7 +478,7 @@ class SQLModel(BaseModel, metaclass=SQLModelMetaclass, registry=default_registry __sqlmodel_relationships__: ClassVar[Dict[str, RelationshipProperty]] # type: ignore __name__: ClassVar[str] metadata: ClassVar[MetaData] - __allow_unmapped__ = True # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six + __allow_unmapped__ = True # https://docs.sqlalchemy.org/en/20/changelog/migration_20.html#migration-20-step-six class Config: orm_mode = True @@ -522,7 +522,7 @@ def __setattr__(self, name: str, value: Any) -> None: return else: # Set in SQLAlchemy, before Pydantic to trigger events and updates - if getattr(self.__config__, "table", False) and is_instrumented(self, name): + if getattr(self.__config__, "table", False) and is_instrumented(self, name): # type: ignore set_attribute(self, name, value) # Set in Pydantic model to trigger possible validation changes, only for # non relationship values diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 1692fdcbcb..64f6ad7967 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -118,7 +118,7 @@ def query(self, *entities: Any, **kwargs: Any) -> "_Query[Any]": Or otherwise you might want to use `session.execute()` instead of `session.query()`. """ - return super().query(*entities, **kwargs) + return super().query(*entities, **kwargs) # type: ignore def get( self, diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 48853cff01..fff2696306 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -26,34 +26,15 @@ # Workaround Generics incompatibility in Python 3.6 # Ref: https://github.com/python/typing/issues/449#issuecomment-316061322 -if sys.version_info.minor >= 7: +class Select(_Select[_TSelect], Generic[_TSelect]): + inherit_cache = True - class Select(_Select, Generic[_TSelect]): - inherit_cache = True - - # This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different - # purpose. This is the same as a normal SQLAlchemy Select class where there's only one - # entity, so the result will be converted to a scalar by default. This way writing - # for loops on the results will feel natural. - class SelectOfScalar(_Select, Generic[_TSelect]): - inherit_cache = True - -else: - from typing import GenericMeta # type: ignore - - class GenericSelectMeta(GenericMeta, _Select.__class__): # type: ignore - pass - - class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - inherit_cache = True - - class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - inherit_cache = True - - # Cast them for editors to work correctly, from several tricks tried, this works - # for both VS Code and PyCharm - Select = cast("Select", _Py36Select) # type: ignore - SelectOfScalar = cast("SelectOfScalar", _Py36SelectOfScalar) # type: ignore +# This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different +# purpose. This is the same as a normal SQLAlchemy Select class where there's only one +# entity, so the result will be converted to a scalar by default. This way writing +# for loops on the results will feel natural. +class SelectOfScalar(_Select[_TSelect], Generic[_TSelect]): + inherit_cache = True if TYPE_CHECKING: # pragma: no cover @@ -427,4 +408,4 @@ def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore def col(column_expression: Any) -> ColumnClause: # type: ignore if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)): raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}") - return column_expression + return column_expression # type: ignore diff --git a/sqlmodel/sql/sqltypes.py b/sqlmodel/sql/sqltypes.py index 09b8239476..da6551b790 100644 --- a/sqlmodel/sql/sqltypes.py +++ b/sqlmodel/sql/sqltypes.py @@ -16,7 +16,7 @@ class AutoString(types.TypeDecorator): # type: ignore def load_dialect_impl(self, dialect: Dialect) -> "types.TypeEngine[Any]": impl = cast(types.String, self.impl) if impl.length is None and dialect.name == "mysql": - return dialect.type_descriptor(types.String(self.mysql_default_length)) # type: ignore + return dialect.type_descriptor(types.String(self.mysql_default_length)) return super().load_dialect_impl(dialect) @@ -35,9 +35,9 @@ class GUID(types.TypeDecorator): # type: ignore def load_dialect_impl(self, dialect: Dialect) -> TypeEngine: # type: ignore if dialect.name == "postgresql": - return dialect.type_descriptor(UUID()) # type: ignore + return dialect.type_descriptor(UUID()) else: - return dialect.type_descriptor(CHAR(32)) # type: ignore + return dialect.type_descriptor(CHAR(32)) def process_bind_param(self, value: Any, dialect: Dialect) -> Optional[str]: if value is None: diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index 2fbc83286a..d05c4a2a5f 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -173,8 +173,18 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0, 'dialect_options': {}}, - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0, 'dialect_options': {}}, + { + "name": "ix_hero_name", + "column_names": ["name"], + "unique": 0, + "dialect_options": {}, + }, + { + "name": "ix_hero_age", + "column_names": ["age"], + "unique": 0, + "dialect_options": {}, + }, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index a70e546abb..a8b5b7b1c3 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -173,8 +173,18 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0, 'dialect_options': {}}, - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0, 'dialect_options': {}}, + { + "name": "ix_hero_age", + "column_names": ["age"], + "unique": 0, + "dialect_options": {}, + }, + { + "name": "ix_hero_name", + "column_names": ["name"], + "unique": 0, + "dialect_options": {}, + }, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" diff --git a/tests/test_tutorial/test_indexes/test_tutorial001.py b/tests/test_tutorial/test_indexes/test_tutorial001.py index 2394c48feb..bc89522a67 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial001.py +++ b/tests/test_tutorial/test_indexes/test_tutorial001.py @@ -25,8 +25,18 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0, 'dialect_options': {}}, - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0, 'dialect_options': {}}, + { + "name": "ix_hero_name", + "column_names": ["name"], + "unique": 0, + "dialect_options": {}, + }, + { + "name": "ix_hero_age", + "column_names": ["age"], + "unique": 0, + "dialect_options": {}, + }, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" diff --git a/tests/test_tutorial/test_indexes/test_tutorial006.py b/tests/test_tutorial/test_indexes/test_tutorial006.py index 8283d44082..8d574dd0df 100644 --- a/tests/test_tutorial/test_indexes/test_tutorial006.py +++ b/tests/test_tutorial/test_indexes/test_tutorial006.py @@ -26,8 +26,18 @@ def test_tutorial(clear_sqlmodel): insp: Inspector = inspect(mod.engine) indexes = insp.get_indexes(str(mod.Hero.__tablename__)) expected_indexes = [ - {"name": "ix_hero_name", "column_names": ["name"], "unique": 0, 'dialect_options': {}}, - {"name": "ix_hero_age", "column_names": ["age"], "unique": 0, 'dialect_options': {}}, + { + "name": "ix_hero_name", + "column_names": ["name"], + "unique": 0, + "dialect_options": {}, + }, + { + "name": "ix_hero_age", + "column_names": ["age"], + "unique": 0, + "dialect_options": {}, + }, ] for index in expected_indexes: assert index in indexes, "This expected index should be in the indexes in DB" From b48423fb33e5eb7d440fab3b6730f9ea1ca1a9ae Mon Sep 17 00:00:00 2001 From: Mohamed Farahat Date: Fri, 24 Mar 2023 14:57:16 +0200 Subject: [PATCH 03/39] remove unused imports --- sqlmodel/sql/expression.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index fff2696306..66e785323a 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -1,6 +1,5 @@ # WARNING: do not modify this code, it is generated by expression.py.jinja2 -import sys from datetime import datetime from typing import ( TYPE_CHECKING, @@ -12,7 +11,6 @@ Type, TypeVar, Union, - cast, overload, ) from uuid import UUID From 9c219d93a4f9bd102e38a807a8a3baa1fa561ebc Mon Sep 17 00:00:00 2001 From: farahats9 Date: Fri, 31 Mar 2023 12:58:30 +0200 Subject: [PATCH 04/39] Update sqlmodel/sql/expression.py Co-authored-by: Stefan Borer --- sqlmodel/sql/expression.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 66e785323a..dd31897b1d 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -22,8 +22,6 @@ _TSelect = TypeVar("_TSelect") -# Workaround Generics incompatibility in Python 3.6 -# Ref: https://github.com/python/typing/issues/449#issuecomment-316061322 class Select(_Select[_TSelect], Generic[_TSelect]): inherit_cache = True From 37ed97994424f35bef861b3986d08334223c656b Mon Sep 17 00:00:00 2001 From: Mohamed Farahat Date: Fri, 31 Mar 2023 12:59:56 +0200 Subject: [PATCH 05/39] reflecting python 3.6 deprecation in docs and tests --- .github/workflows/test.yml | 4 ++-- README.md | 2 +- docs/contributing.md | 2 +- docs/features.md | 2 +- docs/index.md | 2 +- docs/tutorial/index.md | 3 +-- pyproject.toml | 6 ++++-- scripts/lint.sh | 2 -- scripts/test.sh | 1 + 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 585ffc0455..3937770b12 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python-version: ["3.6.15", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] fail-fast: false steps: @@ -54,7 +54,7 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install - name: Lint - if: ${{ matrix.python-version != '3.6.15' }} + if: ${{ matrix.python-version != '3.7' }} run: python -m poetry run bash scripts/lint.sh - run: mkdir coverage - name: Test diff --git a/README.md b/README.md index 5721f1cdb0..df1e3906b9 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as ## Requirements -A recent and currently supported version of Python (right now, Python supports versions 3.6 and above). +A recent and currently supported version of Python (right now, Python supports versions 3.7 and above). As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel. diff --git a/docs/contributing.md b/docs/contributing.md index f2964fba9b..90babf15bd 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -8,7 +8,7 @@ If you already cloned the repository and you know that you need to deep dive in ### Python -SQLModel supports Python 3.6 and above, but for development you should have at least **Python 3.7**. +SQLModel supports Python 3.7 and above, but for development you should have at least **Python 3.7**. ### Poetry diff --git a/docs/features.md b/docs/features.md index 09de0c17f9..2d5e11d84f 100644 --- a/docs/features.md +++ b/docs/features.md @@ -12,7 +12,7 @@ Nevertheless, SQLModel is completely **independent** of FastAPI and can be used ## Just Modern Python -It's all based on standard modern **Python** type annotations. No new syntax to learn. Just standard modern Python. +It's all based on standard modern **Python** type annotations. No new syntax to learn. Just standard modern Python. If you need a 2 minute refresher of how to use Python types (even if you don't use SQLModel or FastAPI), check the FastAPI tutorial section: Python types intro. diff --git a/docs/index.md b/docs/index.md index 5721f1cdb0..df1e3906b9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -50,7 +50,7 @@ It combines SQLAlchemy and Pydantic and tries to simplify the code you write as ## Requirements -A recent and currently supported version of Python (right now, Python supports versions 3.6 and above). +A recent and currently supported version of Python (right now, Python supports versions 3.7 and above). As **SQLModel** is based on **Pydantic** and **SQLAlchemy**, it requires them. They will be automatically installed when you install SQLModel. diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index 33cf6226c4..beb0d4129f 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -64,7 +64,7 @@ $ cd sqlmodel-tutorial Make sure you have an officially supported version of Python. -Currently it is **Python 3.6** and above (Python 3.5 was already deprecated). +Currently it is **Python 3.7** and above (Python 3.6 was already deprecated). You can check which version you have with: @@ -85,7 +85,6 @@ You might want to try with the specific versions, for example with: * `python3.9` * `python3.8` * `python3.7` -* `python3.6` The code would look like this: diff --git a/pyproject.toml b/pyproject.toml index d3e64088ab..859ed488f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,8 @@ classifiers = [ "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", "Topic :: Database", "Topic :: Database :: Database Engines/Servers", "Topic :: Internet", @@ -48,8 +50,8 @@ fastapi = "^0.68.1" requests = "^2.26.0" autoflake = "^1.4" isort = "^5.9.3" -async_generator = {version = "*", python = "~3.6"} -async-exit-stack = {version = "*", python = "~3.6"} +async_generator = {version = "*", python = "~3.7"} +async-exit-stack = {version = "*", python = "~3.7"} [build-system] requires = ["poetry-core"] diff --git a/scripts/lint.sh b/scripts/lint.sh index 02568cda6b..4191d90f1f 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -7,5 +7,3 @@ mypy sqlmodel flake8 sqlmodel tests docs_src black sqlmodel tests docs_src --check isort sqlmodel tests docs_src scripts --check-only -# TODO: move this to test.sh after deprecating Python 3.6 -CHECK_JINJA=1 python scripts/generate_select.py diff --git a/scripts/test.sh b/scripts/test.sh index 9b758bdbdf..1460a9c7ec 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -3,6 +3,7 @@ set -e set -x +CHECK_JINJA=1 python scripts/generate_select.py coverage run -m pytest tests coverage combine coverage report --show-missing From 050cf02a35b670eedeb814606d27869c3a723328 Mon Sep 17 00:00:00 2001 From: Mohamed Farahat Date: Fri, 31 Mar 2023 13:18:38 +0200 Subject: [PATCH 06/39] resolving @sbor23 comments --- .github/workflows/test.yml | 1 - docs/contributing.md | 2 +- docs/tutorial/index.md | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3937770b12..855f1e2d9f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,7 +54,6 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install - name: Lint - if: ${{ matrix.python-version != '3.7' }} run: python -m poetry run bash scripts/lint.sh - run: mkdir coverage - name: Test diff --git a/docs/contributing.md b/docs/contributing.md index 90babf15bd..3682c23ae1 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -8,7 +8,7 @@ If you already cloned the repository and you know that you need to deep dive in ### Python -SQLModel supports Python 3.7 and above, but for development you should have at least **Python 3.7**. +SQLModel supports Python 3.7 and above. ### Poetry diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index beb0d4129f..03bbc80e49 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -81,6 +81,7 @@ There's a chance that you have multiple Python versions installed. You might want to try with the specific versions, for example with: +* `python3.11` * `python3.10` * `python3.9` * `python3.8` From fbff99c22bb17598b249d632661d7d1d7bf3cc91 Mon Sep 17 00:00:00 2001 From: Mohamed Farahat Date: Sun, 30 Apr 2023 17:59:23 +0300 Subject: [PATCH 07/39] add the new Subquery class --- sqlmodel/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 6cdf4e7fc2..7028ce0018 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -70,6 +70,7 @@ from sqlalchemy.sql import outerjoin as outerjoin from sqlalchemy.sql import outparam as outparam from sqlalchemy.sql import over as over +from sqlalchemy.sql import Subquery as Subquery from sqlalchemy.sql import table as table from sqlalchemy.sql import tablesample as tablesample from sqlalchemy.sql import text as text From 6d8f527297816056293be770dd5cbd079f828191 Mon Sep 17 00:00:00 2001 From: Mohamed Farahat Date: Sun, 30 Apr 2023 18:32:27 +0300 Subject: [PATCH 08/39] update to latest sqlalchemy version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 859ed488f5..a0cec7b9da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" -SQLAlchemy = ">=2.0.0,<=2.0.5.post1" +SQLAlchemy = ">=2.0.0,<=2.0.11" pydantic = "^1.8.2" [tool.poetry.dev-dependencies] From 4ce8d0720e9d76069257e46c35b1219b8ab72142 Mon Sep 17 00:00:00 2001 From: Mohamed Farahat Date: Sun, 30 Apr 2023 18:42:46 +0300 Subject: [PATCH 09/39] fix jinja2 template --- sqlmodel/sql/expression.py | 2 ++ sqlmodel/sql/expression.py.jinja2 | 55 ++++++++++--------------------- 2 files changed, 19 insertions(+), 38 deletions(-) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index dd31897b1d..faa2762e3e 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -22,9 +22,11 @@ _TSelect = TypeVar("_TSelect") + class Select(_Select[_TSelect], Generic[_TSelect]): inherit_cache = True + # This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different # purpose. This is the same as a normal SQLAlchemy Select class where there's only one # entity, so the result will be converted to a scalar by default. This way writing diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index 51f04a215d..49b7678fb0 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -1,4 +1,3 @@ -import sys from datetime import datetime from typing import ( TYPE_CHECKING, @@ -10,7 +9,6 @@ from typing import ( Type, TypeVar, Union, - cast, overload, ) from uuid import UUID @@ -22,36 +20,15 @@ from sqlalchemy.sql.expression import Select as _Select _TSelect = TypeVar("_TSelect") -# Workaround Generics incompatibility in Python 3.6 -# Ref: https://github.com/python/typing/issues/449#issuecomment-316061322 -if sys.version_info.minor >= 7: +class Select(_Select[_TSelect], Generic[_TSelect]): + inherit_cache = True - class Select(_Select, Generic[_TSelect]): - inherit_cache = True - - # This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different - # purpose. This is the same as a normal SQLAlchemy Select class where there's only one - # entity, so the result will be converted to a scalar by default. This way writing - # for loops on the results will feel natural. - class SelectOfScalar(_Select, Generic[_TSelect]): - inherit_cache = True - -else: - from typing import GenericMeta # type: ignore - - class GenericSelectMeta(GenericMeta, _Select.__class__): # type: ignore - pass - - class _Py36Select(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - inherit_cache = True - - class _Py36SelectOfScalar(_Select, Generic[_TSelect], metaclass=GenericSelectMeta): - inherit_cache = True - - # Cast them for editors to work correctly, from several tricks tried, this works - # for both VS Code and PyCharm - Select = cast("Select", _Py36Select) # type: ignore - SelectOfScalar = cast("SelectOfScalar", _Py36SelectOfScalar) # type: ignore +# This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different +# purpose. This is the same as a normal SQLAlchemy Select class where there's only one +# entity, so the result will be converted to a scalar by default. This way writing +# for loops on the results will feel natural. +class SelectOfScalar(_Select[_TSelect], Generic[_TSelect]): + inherit_cache = True if TYPE_CHECKING: # pragma: no cover @@ -59,6 +36,7 @@ if TYPE_CHECKING: # pragma: no cover # Generated TypeVars start + {% for i in range(number_of_types) %} _TScalar_{{ i }} = TypeVar( "_TScalar_{{ i }}", @@ -82,12 +60,12 @@ _TModel_{{ i }} = TypeVar("_TModel_{{ i }}", bound="SQLModel") # Generated TypeVars end @overload -def select(entity_0: _TScalar_0, **kw: Any) -> SelectOfScalar[_TScalar_0]: # type: ignore +def select(entity_0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore ... @overload -def select(entity_0: Type[_TModel_0], **kw: Any) -> SelectOfScalar[_TModel_0]: # type: ignore +def select(entity_0: Type[_TModel_0]) -> SelectOfScalar[_TModel_0]: # type: ignore ... @@ -97,7 +75,7 @@ def select(entity_0: Type[_TModel_0], **kw: Any) -> SelectOfScalar[_TModel_0]: @overload def select( # type: ignore - {% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %}**kw: Any, + {% for arg in signature[0] %}{{ arg.name }}: {{ arg.annotation }}, {% endfor %} ) -> Select[Tuple[{%for ret in signature[1] %}{{ ret }} {% if not loop.last %}, {% endif %}{% endfor %}]]: ... @@ -105,14 +83,15 @@ def select( # type: ignore # Generated overloads end -def select(*entities: Any, **kw: Any) -> Union[Select, SelectOfScalar]: # type: ignore + +def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore if len(entities) == 1: - return SelectOfScalar._create(*entities, **kw) # type: ignore - return Select._create(*entities, **kw) # type: ignore + return SelectOfScalar(*entities) # type: ignore + return Select(*entities) # type: ignore # TODO: add several @overload from Python types to SQLAlchemy equivalents def col(column_expression: Any) -> ColumnClause: # type: ignore if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)): raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}") - return column_expression + return column_expression # type: ignore From ecfb3218f166e4ee445c416fb629dc62be6639d0 Mon Sep 17 00:00:00 2001 From: Peter Landry Date: Wed, 26 Jul 2023 13:37:18 +0200 Subject: [PATCH 10/39] `Result` expects a type `Tuple[_T]` --- sqlmodel/engine/result.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmodel/engine/result.py b/sqlmodel/engine/result.py index 17020d9995..a0ddf283b9 100644 --- a/sqlmodel/engine/result.py +++ b/sqlmodel/engine/result.py @@ -1,4 +1,4 @@ -from typing import Generic, Iterator, List, Optional, Sequence, TypeVar +from typing import Generic, Iterator, List, Optional, Sequence, Tuple, TypeVar from sqlalchemy.engine.result import Result as _Result from sqlalchemy.engine.result import ScalarResult as _ScalarResult @@ -35,7 +35,7 @@ def one(self) -> _T: return super().one() -class Result(_Result[_T], Generic[_T]): +class Result(_Result[Tuple[_T]], Generic[_T]): def scalars(self, index: int = 0) -> ScalarResult[_T]: return super().scalars(index) # type: ignore From 3738a7f6f968e8ab09c3a4b6fcb95c6bb0415cf8 Mon Sep 17 00:00:00 2001 From: Peter Landry Date: Wed, 26 Jul 2023 13:37:39 +0200 Subject: [PATCH 11/39] Remove unused type ignore --- sqlmodel/engine/result.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmodel/engine/result.py b/sqlmodel/engine/result.py index a0ddf283b9..ecdb6cd547 100644 --- a/sqlmodel/engine/result.py +++ b/sqlmodel/engine/result.py @@ -67,7 +67,7 @@ def one_or_none(self) -> Optional[_T]: # type: ignore return super().one_or_none() # type: ignore def scalar_one(self) -> _T: - return super().scalar_one() # type: ignore + return super().scalar_one() def scalar_one_or_none(self) -> Optional[_T]: return super().scalar_one_or_none() @@ -76,4 +76,4 @@ def one(self) -> _T: # type: ignore return super().one() # type: ignore def scalar(self) -> Optional[_T]: - return super().scalar() # type: ignore + return super().scalar() From 814988b907ac7fc06d52376c4b35e8ab686c4d55 Mon Sep 17 00:00:00 2001 From: Peter Landry Date: Wed, 26 Jul 2023 14:05:06 +0200 Subject: [PATCH 12/39] Result seems well enough typed in SqlAlchemy now we can simply shim over --- sqlmodel/engine/result.py | 44 ++------------------------------------- 1 file changed, 2 insertions(+), 42 deletions(-) diff --git a/sqlmodel/engine/result.py b/sqlmodel/engine/result.py index ecdb6cd547..650dd92b27 100644 --- a/sqlmodel/engine/result.py +++ b/sqlmodel/engine/result.py @@ -1,4 +1,4 @@ -from typing import Generic, Iterator, List, Optional, Sequence, Tuple, TypeVar +from typing import Generic, Iterator, Optional, Sequence, Tuple, TypeVar from sqlalchemy.engine.result import Result as _Result from sqlalchemy.engine.result import ScalarResult as _ScalarResult @@ -36,44 +36,4 @@ def one(self) -> _T: class Result(_Result[Tuple[_T]], Generic[_T]): - def scalars(self, index: int = 0) -> ScalarResult[_T]: - return super().scalars(index) # type: ignore - - def __iter__(self) -> Iterator[_T]: # type: ignore - return super().__iter__() # type: ignore - - def __next__(self) -> _T: # type: ignore - return super().__next__() # type: ignore - - def partitions(self, size: Optional[int] = None) -> Iterator[List[_T]]: # type: ignore - return super().partitions(size) # type: ignore - - def fetchall(self) -> List[_T]: # type: ignore - return super().fetchall() # type: ignore - - def fetchone(self) -> Optional[_T]: # type: ignore - return super().fetchone() # type: ignore - - def fetchmany(self, size: Optional[int] = None) -> List[_T]: # type: ignore - return super().fetchmany() # type: ignore - - def all(self) -> List[_T]: # type: ignore - return super().all() # type: ignore - - def first(self) -> Optional[_T]: # type: ignore - return super().first() # type: ignore - - def one_or_none(self) -> Optional[_T]: # type: ignore - return super().one_or_none() # type: ignore - - def scalar_one(self) -> _T: - return super().scalar_one() - - def scalar_one_or_none(self) -> Optional[_T]: - return super().scalar_one_or_none() - - def one(self) -> _T: # type: ignore - return super().one() # type: ignore - - def scalar(self) -> Optional[_T]: - return super().scalar() + ... From 118ca33d365a7ece781a0eddf17d89e465a80f16 Mon Sep 17 00:00:00 2001 From: Peter Landry Date: Wed, 26 Jul 2023 14:07:52 +0200 Subject: [PATCH 13/39] Implicit export of ForwardRef was remove in pydantic --- sqlmodel/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 658e5384d8..d05fdcc8b7 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -11,6 +11,7 @@ Callable, ClassVar, Dict, + ForwardRef, List, Mapping, Optional, @@ -29,7 +30,7 @@ from pydantic.fields import FieldInfo as PydanticFieldInfo from pydantic.fields import ModelField, Undefined, UndefinedType from pydantic.main import ModelMetaclass, validate_model -from pydantic.typing import ForwardRef, NoArgAnyCallable, resolve_annotations +from pydantic.typing import NoArgAnyCallable, resolve_annotations from pydantic.utils import ROOT_KEY, Representation from sqlalchemy import Boolean, Column, Date, DateTime from sqlalchemy import Enum as sa_Enum From 91707292c1cabcde3ebc0d1e07be6e430b49e214 Mon Sep 17 00:00:00 2001 From: Peter Landry Date: Wed, 26 Jul 2023 14:11:13 +0200 Subject: [PATCH 14/39] _Select expects a `Tuple[Any, ...]` --- sqlmodel/sql/expression.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index faa2762e3e..a0ac1bd9d9 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -23,7 +23,7 @@ _TSelect = TypeVar("_TSelect") -class Select(_Select[_TSelect], Generic[_TSelect]): +class Select(_Select[Tuple[_TSelect]], Generic[_TSelect]): inherit_cache = True @@ -31,7 +31,7 @@ class Select(_Select[_TSelect], Generic[_TSelect]): # purpose. This is the same as a normal SQLAlchemy Select class where there's only one # entity, so the result will be converted to a scalar by default. This way writing # for loops on the results will feel natural. -class SelectOfScalar(_Select[_TSelect], Generic[_TSelect]): +class SelectOfScalar(_Select[Tuple[_TSelect]], Generic[_TSelect]): inherit_cache = True From 7f5ba1c118cda7eb603e9b84695b88ff480ebba1 Mon Sep 17 00:00:00 2001 From: Peter Landry Date: Wed, 26 Jul 2023 14:21:03 +0200 Subject: [PATCH 15/39] Use Dict type instead of Mapping for SqlAlchemy compat --- sqlmodel/orm/session.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 64f6ad7967..3b8cec5eed 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -1,4 +1,14 @@ -from typing import Any, Mapping, Optional, Sequence, Type, TypeVar, Union, overload +from typing import ( + Any, + Dict, + Mapping, + Optional, + Sequence, + Type, + TypeVar, + Union, + overload, +) from sqlalchemy import util from sqlalchemy.orm import Query as _Query @@ -21,7 +31,7 @@ def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, **kw: Any, @@ -35,7 +45,7 @@ def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, **kw: Any, @@ -52,7 +62,7 @@ def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, **kw: Any, @@ -75,7 +85,7 @@ def execute( statement: _Executable, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Optional[Mapping[str, Any]] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, **kw: Any, From e942e5e22c38e3d210ab16da0e778b713a3b871a Mon Sep 17 00:00:00 2001 From: Peter Landry Date: Wed, 26 Jul 2023 14:24:01 +0200 Subject: [PATCH 16/39] Execution options are not Optional in SA --- sqlmodel/orm/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 3b8cec5eed..0305dfe463 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -84,7 +84,7 @@ def execute( self, statement: _Executable, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, - execution_options: Optional[Mapping[str, Any]] = util.EMPTY_DICT, + execution_options: Mapping[str, Any] = util.EMPTY_DICT, bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, From ef9f00a6e11945fd014c9b1c13872755fe09a791 Mon Sep 17 00:00:00 2001 From: Peter Landry Date: Wed, 26 Jul 2023 14:26:41 +0200 Subject: [PATCH 17/39] Another instance of non-optional execution_options --- sqlmodel/orm/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 0305dfe463..33f57abcf8 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -138,7 +138,7 @@ def get( populate_existing: bool = False, with_for_update: Optional[Union[Literal[True], Mapping[str, Any]]] = None, identity_token: Optional[Any] = None, - execution_options: Optional[Mapping[Any, Any]] = util.EMPTY_DICT, + execution_options: Mapping[Any, Any] = util.EMPTY_DICT, ) -> Optional[_TSelectParam]: return super().get( entity, From 643cea59c56ca79ad3e9d42ef5c0f6be7081f0ec Mon Sep 17 00:00:00 2001 From: Peter Landry Date: Wed, 26 Jul 2023 14:49:28 +0200 Subject: [PATCH 18/39] Fix Tuple in jinja template as well --- sqlmodel/sql/expression.py.jinja2 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index 49b7678fb0..4284543fe2 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -20,14 +20,14 @@ from sqlalchemy.sql.expression import Select as _Select _TSelect = TypeVar("_TSelect") -class Select(_Select[_TSelect], Generic[_TSelect]): +class Select(_Select[Tuple[_TSelect]], Generic[_TSelect]): inherit_cache = True # This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different # purpose. This is the same as a normal SQLAlchemy Select class where there's only one # entity, so the result will be converted to a scalar by default. This way writing # for loops on the results will feel natural. -class SelectOfScalar(_Select[_TSelect], Generic[_TSelect]): +class SelectOfScalar(_Select[Tuple[_TSelect]], Generic[_TSelect]): inherit_cache = True From b89adbbab39fbe06bd9be9e82fcd39c96527334c Mon Sep 17 00:00:00 2001 From: Peter Landry Date: Wed, 26 Jul 2023 17:39:35 +0200 Subject: [PATCH 19/39] Use ForUpdateArg from sqlalchemy --- sqlmodel/orm/session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 33f57abcf8..11fdcc4be1 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -14,6 +14,7 @@ from sqlalchemy.orm import Query as _Query from sqlalchemy.orm import Session as _Session from sqlalchemy.sql.base import Executable as _Executable +from sqlalchemy.sql.selectable import ForUpdateArg as _ForUpdateArg from sqlmodel.sql.expression import Select, SelectOfScalar from typing_extensions import Literal @@ -136,7 +137,7 @@ def get( ident: Any, options: Optional[Sequence[Any]] = None, populate_existing: bool = False, - with_for_update: Optional[Union[Literal[True], Mapping[str, Any]]] = None, + with_for_update: Optional[_ForUpdateArg] = None, identity_token: Optional[Any] = None, execution_options: Mapping[Any, Any] = util.EMPTY_DICT, ) -> Optional[_TSelectParam]: From eff0803f0d6ac6e7456b0d3db0388774b7a79cee Mon Sep 17 00:00:00 2001 From: Peter Landry Date: Wed, 26 Jul 2023 18:05:49 +0200 Subject: [PATCH 20/39] Fix signature for `Session.get` --- sqlmodel/orm/session.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 11fdcc4be1..65214a6146 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -11,6 +11,7 @@ ) from sqlalchemy import util +from sqlalchemy.orm import Mapper as _Mapper from sqlalchemy.orm import Query as _Query from sqlalchemy.orm import Session as _Session from sqlalchemy.sql.base import Executable as _Executable @@ -133,13 +134,14 @@ def query(self, *entities: Any, **kwargs: Any) -> "_Query[Any]": def get( self, - entity: Type[_TSelectParam], + entity: Union[Type[_TSelectParam], "_Mapper[_TSelectParam]"], ident: Any, options: Optional[Sequence[Any]] = None, populate_existing: bool = False, with_for_update: Optional[_ForUpdateArg] = None, identity_token: Optional[Any] = None, execution_options: Mapping[Any, Any] = util.EMPTY_DICT, + bind_arguments: Optional[Dict[str, Any]] = None, ) -> Optional[_TSelectParam]: return super().get( entity, @@ -149,4 +151,5 @@ def get( with_for_update=with_for_update, identity_token=identity_token, execution_options=execution_options, + bind_arguments=bind_arguments ) From 1752f0b40aa858133c0293da1c936ca8118bece8 Mon Sep 17 00:00:00 2001 From: Mohamed Farahat Date: Thu, 27 Jul 2023 23:03:52 +0300 Subject: [PATCH 21/39] formatting and remove unused type --- sqlmodel/orm/session.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 65214a6146..9a07956d92 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -17,7 +17,6 @@ from sqlalchemy.sql.base import Executable as _Executable from sqlalchemy.sql.selectable import ForUpdateArg as _ForUpdateArg from sqlmodel.sql.expression import Select, SelectOfScalar -from typing_extensions import Literal from ..engine.result import Result, ScalarResult from ..sql.base import Executable @@ -151,5 +150,5 @@ def get( with_for_update=with_for_update, identity_token=identity_token, execution_options=execution_options, - bind_arguments=bind_arguments + bind_arguments=bind_arguments, ) From 4d264a3afcb1002ee3575514f2281916263b3e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 7 Nov 2023 15:09:00 +0300 Subject: [PATCH 22/39] =?UTF-8?q?=F0=9F=93=9D=20Do=20not=20claim=20Python?= =?UTF-8?q?=203.11=20yet,=20this=20will=20be=20done=20in=20a=20subsequent?= =?UTF-8?q?=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/tutorial/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/tutorial/index.md b/docs/tutorial/index.md index e8da8ece79..74107776c2 100644 --- a/docs/tutorial/index.md +++ b/docs/tutorial/index.md @@ -79,7 +79,6 @@ There's a chance that you have multiple Python versions installed. You might want to try with the specific versions, for example with: -* `python3.11` * `python3.10` * `python3.9` * `python3.8` From 5f52693b7e036d63b38c4beef84a7f4883f488c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Tue, 7 Nov 2023 16:00:26 +0300 Subject: [PATCH 23/39] =?UTF-8?q?=F0=9F=94=A5=20Remove=20no=20longer=20nec?= =?UTF-8?q?essary=20engine=20module,=20it=20was=20only=20to=20provide=20ty?= =?UTF-8?q?pes,=20now=20beautifully=20defined=20in=20SQLAlchemy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/engine/__init__.py | 0 sqlmodel/engine/create.py | 139 ------------------------------------ sqlmodel/engine/result.py | 39 ---------- 3 files changed, 178 deletions(-) delete mode 100644 sqlmodel/engine/__init__.py delete mode 100644 sqlmodel/engine/create.py delete mode 100644 sqlmodel/engine/result.py diff --git a/sqlmodel/engine/__init__.py b/sqlmodel/engine/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/sqlmodel/engine/create.py b/sqlmodel/engine/create.py deleted file mode 100644 index 97481259e2..0000000000 --- a/sqlmodel/engine/create.py +++ /dev/null @@ -1,139 +0,0 @@ -import json -import sqlite3 -from typing import Any, Callable, Dict, List, Optional, Type, Union - -from sqlalchemy import create_engine as _create_engine -from sqlalchemy.engine.url import URL -from sqlalchemy.future import Engine as _FutureEngine -from sqlalchemy.pool import Pool -from typing_extensions import Literal, TypedDict - -from ..default import Default, _DefaultPlaceholder - -# Types defined in sqlalchemy2-stubs, but can't be imported, so re-define here - -_Debug = Literal["debug"] - -_IsolationLevel = Literal[ - "SERIALIZABLE", - "REPEATABLE READ", - "READ COMMITTED", - "READ UNCOMMITTED", - "AUTOCOMMIT", -] -_ParamStyle = Literal["qmark", "numeric", "named", "format", "pyformat"] -_ResetOnReturn = Literal["rollback", "commit"] - - -class _SQLiteConnectArgs(TypedDict, total=False): - timeout: float - detect_types: Any - isolation_level: Optional[Literal["DEFERRED", "IMMEDIATE", "EXCLUSIVE"]] - check_same_thread: bool - factory: Type[sqlite3.Connection] - cached_statements: int - uri: bool - - -_ConnectArgs = Union[_SQLiteConnectArgs, Dict[str, Any]] - - -# Re-define create_engine to have by default future=True, and assume that's what is used -# Also show the default values used for each parameter, but don't set them unless -# explicitly passed as arguments by the user to prevent errors. E.g. SQLite doesn't -# support pool connection arguments. -def create_engine( - url: Union[str, URL], - *, - connect_args: _ConnectArgs = Default({}), # type: ignore - echo: Union[bool, _Debug] = Default(False), - echo_pool: Union[bool, _Debug] = Default(False), - enable_from_linting: bool = Default(True), - encoding: str = Default("utf-8"), - execution_options: Dict[Any, Any] = Default({}), - future: bool = True, - hide_parameters: bool = Default(False), - implicit_returning: bool = Default(True), - isolation_level: Optional[_IsolationLevel] = Default(None), - json_deserializer: Callable[..., Any] = Default(json.loads), - json_serializer: Callable[..., Any] = Default(json.dumps), - label_length: Optional[int] = Default(None), - logging_name: Optional[str] = Default(None), - max_identifier_length: Optional[int] = Default(None), - max_overflow: int = Default(10), - module: Optional[Any] = Default(None), - paramstyle: Optional[_ParamStyle] = Default(None), - pool: Optional[Pool] = Default(None), - poolclass: Optional[Type[Pool]] = Default(None), - pool_logging_name: Optional[str] = Default(None), - pool_pre_ping: bool = Default(False), - pool_size: int = Default(5), - pool_recycle: int = Default(-1), - pool_reset_on_return: Optional[_ResetOnReturn] = Default("rollback"), - pool_timeout: float = Default(30), - pool_use_lifo: bool = Default(False), - plugins: Optional[List[str]] = Default(None), - query_cache_size: Optional[int] = Default(None), - **kwargs: Any, -) -> _FutureEngine: - current_kwargs: Dict[str, Any] = { - "future": future, - } - if not isinstance(echo, _DefaultPlaceholder): - current_kwargs["echo"] = echo - if not isinstance(echo_pool, _DefaultPlaceholder): - current_kwargs["echo_pool"] = echo_pool - if not isinstance(enable_from_linting, _DefaultPlaceholder): - current_kwargs["enable_from_linting"] = enable_from_linting - if not isinstance(connect_args, _DefaultPlaceholder): - current_kwargs["connect_args"] = connect_args - if not isinstance(encoding, _DefaultPlaceholder): - current_kwargs["encoding"] = encoding - if not isinstance(execution_options, _DefaultPlaceholder): - current_kwargs["execution_options"] = execution_options - if not isinstance(hide_parameters, _DefaultPlaceholder): - current_kwargs["hide_parameters"] = hide_parameters - if not isinstance(implicit_returning, _DefaultPlaceholder): - current_kwargs["implicit_returning"] = implicit_returning - if not isinstance(isolation_level, _DefaultPlaceholder): - current_kwargs["isolation_level"] = isolation_level - if not isinstance(json_deserializer, _DefaultPlaceholder): - current_kwargs["json_deserializer"] = json_deserializer - if not isinstance(json_serializer, _DefaultPlaceholder): - current_kwargs["json_serializer"] = json_serializer - if not isinstance(label_length, _DefaultPlaceholder): - current_kwargs["label_length"] = label_length - if not isinstance(logging_name, _DefaultPlaceholder): - current_kwargs["logging_name"] = logging_name - if not isinstance(max_identifier_length, _DefaultPlaceholder): - current_kwargs["max_identifier_length"] = max_identifier_length - if not isinstance(max_overflow, _DefaultPlaceholder): - current_kwargs["max_overflow"] = max_overflow - if not isinstance(module, _DefaultPlaceholder): - current_kwargs["module"] = module - if not isinstance(paramstyle, _DefaultPlaceholder): - current_kwargs["paramstyle"] = paramstyle - if not isinstance(pool, _DefaultPlaceholder): - current_kwargs["pool"] = pool - if not isinstance(poolclass, _DefaultPlaceholder): - current_kwargs["poolclass"] = poolclass - if not isinstance(pool_logging_name, _DefaultPlaceholder): - current_kwargs["pool_logging_name"] = pool_logging_name - if not isinstance(pool_pre_ping, _DefaultPlaceholder): - current_kwargs["pool_pre_ping"] = pool_pre_ping - if not isinstance(pool_size, _DefaultPlaceholder): - current_kwargs["pool_size"] = pool_size - if not isinstance(pool_recycle, _DefaultPlaceholder): - current_kwargs["pool_recycle"] = pool_recycle - if not isinstance(pool_reset_on_return, _DefaultPlaceholder): - current_kwargs["pool_reset_on_return"] = pool_reset_on_return - if not isinstance(pool_timeout, _DefaultPlaceholder): - current_kwargs["pool_timeout"] = pool_timeout - if not isinstance(pool_use_lifo, _DefaultPlaceholder): - current_kwargs["pool_use_lifo"] = pool_use_lifo - if not isinstance(plugins, _DefaultPlaceholder): - current_kwargs["plugins"] = plugins - if not isinstance(query_cache_size, _DefaultPlaceholder): - current_kwargs["query_cache_size"] = query_cache_size - current_kwargs.update(kwargs) - return _create_engine(url, **current_kwargs) diff --git a/sqlmodel/engine/result.py b/sqlmodel/engine/result.py deleted file mode 100644 index 650dd92b27..0000000000 --- a/sqlmodel/engine/result.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Generic, Iterator, Optional, Sequence, Tuple, TypeVar - -from sqlalchemy.engine.result import Result as _Result -from sqlalchemy.engine.result import ScalarResult as _ScalarResult - -_T = TypeVar("_T") - - -class ScalarResult(_ScalarResult[_T], Generic[_T]): - def all(self) -> Sequence[_T]: - return super().all() - - def partitions(self, size: Optional[int] = None) -> Iterator[Sequence[_T]]: - return super().partitions(size) - - def fetchall(self) -> Sequence[_T]: - return super().fetchall() - - def fetchmany(self, size: Optional[int] = None) -> Sequence[_T]: - return super().fetchmany(size) - - def __iter__(self) -> Iterator[_T]: - return super().__iter__() - - def __next__(self) -> _T: - return super().__next__() - - def first(self) -> Optional[_T]: - return super().first() - - def one_or_none(self) -> Optional[_T]: - return super().one_or_none() - - def one(self) -> _T: - return super().one() - - -class Result(_Result[Tuple[_T]], Generic[_T]): - ... From 0468d6f3becbd7fa9b9643ec5902e05f05bbd671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 18:30:39 +0100 Subject: [PATCH 24/39] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20Revert=20altering=20?= =?UTF-8?q?tutorial,=20avoiding=20lack=20of=20support=20for=20forward=20re?= =?UTF-8?q?ferences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs_src/tutorial/many_to_many/tutorial003.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs_src/tutorial/many_to_many/tutorial003.py b/docs_src/tutorial/many_to_many/tutorial003.py index cec6e56560..1e03c4af89 100644 --- a/docs_src/tutorial/many_to_many/tutorial003.py +++ b/docs_src/tutorial/many_to_many/tutorial003.py @@ -3,12 +3,25 @@ from sqlmodel import Field, Relationship, Session, SQLModel, create_engine, select +class HeroTeamLink(SQLModel, table=True): + team_id: Optional[int] = Field( + default=None, foreign_key="team.id", primary_key=True + ) + hero_id: Optional[int] = Field( + default=None, foreign_key="hero.id", primary_key=True + ) + is_training: bool = False + + team: "Team" = Relationship(back_populates="hero_links") + hero: "Hero" = Relationship(back_populates="team_links") + + class Team(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True) headquarters: str - hero_links: List["HeroTeamLink"] = Relationship(back_populates="team") + hero_links: List[HeroTeamLink] = Relationship(back_populates="team") class Hero(SQLModel, table=True): @@ -17,20 +30,7 @@ class Hero(SQLModel, table=True): secret_name: str age: Optional[int] = Field(default=None, index=True) - team_links: List["HeroTeamLink"] = Relationship(back_populates="hero") - - -class HeroTeamLink(SQLModel, table=True): - team_id: Optional[int] = Field( - default=None, foreign_key="team.id", primary_key=True - ) - hero_id: Optional[int] = Field( - default=None, foreign_key="hero.id", primary_key=True - ) - is_training: bool = False - - team: "Team" = Relationship(back_populates="hero_links") - hero: "Hero" = Relationship(back_populates="team_links") + team_links: List[HeroTeamLink] = Relationship(back_populates="hero") sqlite_file_name = "database.db" From fcb92a013e8c5436c3cd761d118892dc0f170294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 18:32:51 +0100 Subject: [PATCH 25/39] =?UTF-8?q?=F0=9F=90=9B=20Fix=20support=20for=20forw?= =?UTF-8?q?ard=20references=20in=20relationships?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/main.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/sqlmodel/main.py b/sqlmodel/main.py index 06e58618aa..c30af5779f 100644 --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@ -45,12 +45,19 @@ inspect, ) from sqlalchemy import Enum as sa_Enum -from sqlalchemy.orm import RelationshipProperty, declared_attr, registry, relationship +from sqlalchemy.orm import ( + Mapped, + RelationshipProperty, + declared_attr, + registry, + relationship, +) from sqlalchemy.orm.attributes import set_attribute from sqlalchemy.orm.decl_api import DeclarativeMeta from sqlalchemy.orm.instrumentation import is_instrumented from sqlalchemy.sql.schema import MetaData from sqlalchemy.sql.sqltypes import LargeBinary, Time +from typing_extensions import get_origin from .sql.sqltypes import GUID, AutoString @@ -483,7 +490,16 @@ def __init__( # over anything else, use that and continue with the next attribute setattr(cls, rel_name, rel_info.sa_relationship) # Fix #315 continue - ann = cls.__annotations__[rel_name] + raw_ann = cls.__annotations__[rel_name] + origin = get_origin(raw_ann) + if origin is Mapped: + ann = raw_ann.__args__[0] + else: + ann = raw_ann + # Plain forward references, for models not yet defined, are not + # handled well by SQLAlchemy without Mapped, so, wrap the + # annotations in Mapped here + cls.__annotations__[rel_name] = Mapped[ann] # type: ignore[valid-type] temp_field = ModelField.infer( name=rel_name, value=rel_info, @@ -511,9 +527,7 @@ def __init__( rel_args.extend(rel_info.sa_relationship_args) if rel_info.sa_relationship_kwargs: rel_kwargs.update(rel_info.sa_relationship_kwargs) - rel_value: RelationshipProperty = relationship( # type: ignore - relationship_to, *rel_args, **rel_kwargs - ) + rel_value = relationship(relationship_to, *rel_args, **rel_kwargs) setattr(cls, rel_name, rel_value) # Fix #315 # SQLAlchemy no longer uses dict_ # Ref: https://github.com/sqlalchemy/sqlalchemy/commit/428ea01f00a9cc7f85e435018565eb6da7af1b77 From f02584b49e09c6e8c2591cbbcbc3e48a8c7ad2e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 18:34:48 +0100 Subject: [PATCH 26/39] =?UTF-8?q?=E2=9C=85=20Update=20tests=20for=20newest?= =?UTF-8?q?=20FastAPI=20with=20OpenAPI=203.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_fastapi/test_delete/test_tutorial001.py | 6 ++++-- .../test_fastapi/test_limit_and_offset/test_tutorial001.py | 6 ++++-- .../test_fastapi/test_multiple_models/test_tutorial001.py | 4 ++-- .../test_fastapi/test_multiple_models/test_tutorial002.py | 4 ++-- .../test_fastapi/test_read_one/test_tutorial001.py | 4 ++-- .../test_fastapi/test_relationships/test_tutorial001.py | 6 ++++-- .../test_fastapi/test_response_model/test_tutorial001.py | 4 ++-- .../test_session_with_dependency/test_tutorial001.py | 6 ++++-- .../test_fastapi/test_simple_hero_api/test_tutorial001.py | 4 ++-- .../test_fastapi/test_teams/test_tutorial001.py | 6 ++++-- .../test_fastapi/test_update/test_tutorial001.py | 6 ++++-- 11 files changed, 34 insertions(+), 22 deletions(-) diff --git a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py index b08affb920..6a55d6cb98 100644 --- a/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_delete/test_tutorial001.py @@ -59,7 +59,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -315,7 +315,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py index 0aee3ca004..2709231504 100644 --- a/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_limit_and_offset/test_tutorial001.py @@ -64,7 +64,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -239,7 +239,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py index 8d99cf9f5b..dc5a3cb8ff 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial001.py @@ -5,7 +5,7 @@ from sqlmodel.pool import StaticPool openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -103,7 +103,7 @@ "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py index 94a41b3076..e3c20404c0 100644 --- a/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py +++ b/tests/test_tutorial/test_fastapi/test_multiple_models/test_tutorial002.py @@ -5,7 +5,7 @@ from sqlmodel.pool import StaticPool openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -103,7 +103,7 @@ "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py index 0609ae41ff..0a599574d5 100644 --- a/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_read_one/test_tutorial001.py @@ -3,7 +3,7 @@ from sqlmodel.pool import StaticPool openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -135,7 +135,7 @@ "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py index 8869862e95..fb08b9a5fd 100644 --- a/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_relationships/test_tutorial001.py @@ -107,7 +107,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -622,7 +622,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py index ebb3046ef3..968fefa8ca 100644 --- a/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_response_model/test_tutorial001.py @@ -3,7 +3,7 @@ from sqlmodel.pool import StaticPool openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -91,7 +91,7 @@ "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py index cb0a6f9282..6f97cbf92b 100644 --- a/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_session_with_dependency/test_tutorial001.py @@ -59,7 +59,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -315,7 +315,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py index eb834ec2a4..435155d6e9 100644 --- a/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_simple_hero_api/test_tutorial001.py @@ -3,7 +3,7 @@ from sqlmodel.pool import StaticPool openapi_schema = { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -79,7 +79,7 @@ "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": {"anyOf": [{"type": "string"}, {"type": "integer"}]}, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py index e66c975142..42f87cef76 100644 --- a/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_teams/test_tutorial001.py @@ -94,7 +94,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -579,7 +579,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, diff --git a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py index 49906256c9..a4573ef11b 100644 --- a/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py +++ b/tests/test_tutorial/test_fastapi/test_update/test_tutorial001.py @@ -66,7 +66,7 @@ def test_tutorial(clear_sqlmodel): response = client.get("/openapi.json") assert response.status_code == 200, response.text assert response.json() == { - "openapi": "3.0.2", + "openapi": "3.1.0", "info": {"title": "FastAPI", "version": "0.1.0"}, "paths": { "/heroes/": { @@ -294,7 +294,9 @@ def test_tutorial(clear_sqlmodel): "loc": { "title": "Location", "type": "array", - "items": {"type": "string"}, + "items": { + "anyOf": [{"type": "string"}, {"type": "integer"}] + }, }, "msg": {"title": "Message", "type": "string"}, "type": {"title": "Error Type", "type": "string"}, From f4127d235525beee5570205dedafb7df2c4c8023 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 18:37:18 +0100 Subject: [PATCH 27/39] =?UTF-8?q?=E2=8F=AA=EF=B8=8F=20Revert=20altering=20?= =?UTF-8?q?tutorial=20to=20accomodate=20for=20the=20previous=20lack=20of?= =?UTF-8?q?=20support=20of=20forward=20references=20in=20relationships?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../back_populates/tutorial003.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py index 8d91a0bc25..98e197002e 100644 --- a/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py +++ b/docs_src/tutorial/relationship_attributes/back_populates/tutorial003.py @@ -3,21 +3,6 @@ from sqlmodel import Field, Relationship, SQLModel, create_engine -class Hero(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) - name: str = Field(index=True) - secret_name: str - age: Optional[int] = Field(default=None, index=True) - - team_id: Optional[int] = Field(default=None, foreign_key="team.id") - team: Optional["Team"] = Relationship(back_populates="heroes") - - weapon_id: Optional[int] = Field(default=None, foreign_key="weapon.id") - weapon: Optional["Weapon"] = Relationship(back_populates="hero") - - powers: List["Power"] = Relationship(back_populates="hero") - - class Weapon(SQLModel, table=True): id: Optional[int] = Field(default=None, primary_key=True) name: str = Field(index=True) @@ -41,6 +26,21 @@ class Team(SQLModel, table=True): heroes: List["Hero"] = Relationship(back_populates="team") +class Hero(SQLModel, table=True): + id: Optional[int] = Field(default=None, primary_key=True) + name: str = Field(index=True) + secret_name: str + age: Optional[int] = Field(default=None, index=True) + + team_id: Optional[int] = Field(default=None, foreign_key="team.id") + team: Optional[Team] = Relationship(back_populates="heroes") + + weapon_id: Optional[int] = Field(default=None, foreign_key="weapon.id") + weapon: Optional[Weapon] = Relationship(back_populates="hero") + + powers: List[Power] = Relationship(back_populates="hero") + + sqlite_file_name = "database.db" sqlite_url = f"sqlite:///{sqlite_file_name}" From 50cb798d0a71a76727ed8ddc0b097da079e9a5d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 22:24:24 +0100 Subject: [PATCH 28/39] =?UTF-8?q?=E2=9C=A8=20Update=20and=20refactor=20exp?= =?UTF-8?q?ression=20(and=20template)=20with=20new=20types=20from=20SQLAlc?= =?UTF-8?q?hemy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/sql/expression.py | 396 +++++++++++++++++++++++------- sqlmodel/sql/expression.py.jinja2 | 246 +++++++++++++++++-- 2 files changed, 534 insertions(+), 108 deletions(-) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 0c2df630c5..1a7068c53e 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -2,27 +2,237 @@ from datetime import datetime from typing import ( - TYPE_CHECKING, Any, + Iterable, Mapping, + Optional, Sequence, Tuple, Type, + TypeAlias, TypeVar, Union, overload, ) from uuid import UUID -from sqlalchemy import Column -from sqlalchemy.orm import InstrumentedAttribute -from sqlalchemy.sql.elements import ColumnClause +import sqlalchemy +from sqlalchemy import ( + Column, + ColumnElement, + Extract, + FunctionElement, + FunctionFilter, + Label, + Over, + TypeCoerce, + WithinGroup, +) +from sqlalchemy.orm import InstrumentedAttribute, Mapped +from sqlalchemy.sql._typing import ( + _ColumnExpressionArgument, + _ColumnExpressionOrLiteralArgument, + _ColumnExpressionOrStrLabelArgument, +) +from sqlalchemy.sql.elements import ( + BinaryExpression, + Case, + Cast, + CollectionAggregate, + ColumnClause, + SQLCoreOperations, + TryCast, + UnaryExpression, +) from sqlalchemy.sql.expression import Select as _Select +from sqlalchemy.sql.roles import TypedColumnsClauseRole +from sqlalchemy.sql.type_api import TypeEngine +from typing_extensions import Literal, Self + +_T = TypeVar("_T") + +_TypeEngineArgument: TypeAlias = Union[Type[TypeEngine[_T]], TypeEngine[_T]] + +# Redefine operatos that would only take a column expresion to also take the (virtual) +# types of Pydantic models, e.g. str instead of only Mapped[str]. + + +def all_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: + return sqlalchemy.all_(expr) # type: ignore[arg-type] + + +def and_( + initial_clause: Union[Literal[True], _ColumnExpressionArgument[bool], bool], + *clauses: Union[_ColumnExpressionArgument[bool], bool], +) -> ColumnElement[bool]: + return sqlalchemy.and_(initial_clause, *clauses) # type: ignore[arg-type] + + +def any_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: + return sqlalchemy.any_(expr) # type: ignore[arg-type] + + +def asc( + column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], +) -> UnaryExpression[_T]: + return sqlalchemy.asc(column) # type: ignore[arg-type] + + +def collate( + expression: Union[_ColumnExpressionArgument[str], str], collation: str +) -> BinaryExpression[str]: + return sqlalchemy.collate(expression, collation) # type: ignore[arg-type] + + +def between( + expr: Union[_ColumnExpressionOrLiteralArgument[_T], _T], + lower_bound: Any, + upper_bound: Any, + symmetric: bool = False, +) -> BinaryExpression[bool]: + return sqlalchemy.between(expr, lower_bound, upper_bound, symmetric=symmetric) # type: ignore[arg-type] + + +def not_(clause: Union[_ColumnExpressionArgument[_T], _T]) -> ColumnElement[_T]: + return sqlalchemy.not_(clause) # type: ignore[arg-type] + + +def case( + *whens: Union[ + Tuple[Union[_ColumnExpressionArgument[bool], bool], Any], Mapping[Any, Any] + ], + value: Optional[Any] = None, + else_: Optional[Any] = None, +) -> Case[Any]: + return sqlalchemy.case(*whens, value=value, else_=else_) # type: ignore[arg-type] + + +def cast( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> Cast[_T]: + return sqlalchemy.cast(expression, type_) # type: ignore[arg-type] + + +def try_cast( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> TryCast[_T]: + return sqlalchemy.try_cast(expression, type_) # type: ignore[arg-type] + + +def desc( + column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], +) -> UnaryExpression[_T]: + return sqlalchemy.desc(column) # type: ignore[arg-type] + + +def distinct(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.distinct(expr) # type: ignore[arg-type] + + +def bitwise_not(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.bitwise_not(expr) # type: ignore[arg-type] + + +def extract(field: str, expr: Union[_ColumnExpressionArgument[Any], Any]) -> Extract: + return sqlalchemy.extract(field, expr) # type: ignore[arg-type] + + +def funcfilter( + func: FunctionElement[_T], *criterion: Union[_ColumnExpressionArgument[bool], bool] +) -> FunctionFilter[_T]: + return sqlalchemy.funcfilter(func, *criterion) # type: ignore[arg-type] + + +def label( + name: str, + element: Union[_ColumnExpressionArgument[_T], _T], + type_: Optional["_TypeEngineArgument[_T]"] = None, +) -> Label[_T]: + return sqlalchemy.label(name, element, type_=type_) # type: ignore[arg-type] + + +def nulls_first( + column: Union[_ColumnExpressionArgument[_T], _T] +) -> UnaryExpression[_T]: + return sqlalchemy.nulls_first(column) # type: ignore[arg-type] + + +def nulls_last(column: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.nulls_last(column) # type: ignore[arg-type] + + +def or_( # type: ignore[empty-body] + initial_clause: Union[Literal[False], _ColumnExpressionArgument[bool], bool], + *clauses: Union[_ColumnExpressionArgument[bool], bool], +) -> ColumnElement[bool]: + return sqlalchemy.or_(initial_clause, *clauses) # type: ignore[arg-type] + + +def over( + element: FunctionElement[_T], + partition_by: Optional[ + Union[ + Iterable[Union[_ColumnExpressionArgument[Any], Any]], + _ColumnExpressionArgument[Any], + Any, + ] + ] = None, + order_by: Optional[ + Union[ + Iterable[Union[_ColumnExpressionArgument[Any], Any]], + _ColumnExpressionArgument[Any], + Any, + ] + ] = None, + range_: Optional[Tuple[Optional[int], Optional[int]]] = None, + rows: Optional[Tuple[Optional[int], Optional[int]]] = None, +) -> Over[_T]: + return sqlalchemy.over( + element, partition_by=partition_by, order_by=order_by, range_=range_, rows=rows + ) # type: ignore[arg-type] + + +def tuple_( + *clauses: Union[_ColumnExpressionArgument[Any], Any], + types: Optional[Sequence["_TypeEngineArgument[Any]"]] = None, +) -> Tuple[Any, ...]: + return sqlalchemy.tuple_(*clauses, types=types) # type: ignore[return-value] + + +def type_coerce( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> TypeCoerce[_T]: + return sqlalchemy.type_coerce(expression, type_) # type: ignore[arg-type] + + +def within_group( + element: FunctionElement[_T], *order_by: Union[_ColumnExpressionArgument[Any], Any] +) -> WithinGroup[_T]: + return sqlalchemy.within_group(element, *order_by) # type: ignore[arg-type] + + +# Separate this class in SelectBase, Select, and SelectOfScalar so that they can share +# where and having without having type overlap incompatibility in session.exec(). +class SelectBase(_Select[Tuple[_T]]): + inherit_cache = True + + def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + """Return a new `Select` construct with the given expression added to + its `WHERE` clause, joined to the existing clause via `AND`, if any. + """ + return super().where(*whereclause) # type: ignore[arg-type] -_TSelect = TypeVar("_TSelect") + def having(self, *having: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + """Return a new `Select` construct with the given expression added to + its `HAVING` clause, joined to the existing clause via `AND`, if any. + """ + return super().having(*having) # type: ignore[arg-type] -class Select(_Select[Tuple[_TSelect]]): +class Select(SelectBase[_T]): inherit_cache = True @@ -30,12 +240,15 @@ class Select(_Select[Tuple[_TSelect]]): # purpose. This is the same as a normal SQLAlchemy Select class where there's only one # entity, so the result will be converted to a scalar by default. This way writing # for loops on the results will feel natural. -class SelectOfScalar(_Select[Tuple[_TSelect]]): +class SelectOfScalar(SelectBase[_T]): inherit_cache = True -if TYPE_CHECKING: # pragma: no cover - from ..main import SQLModel +_TCCA = Union[ + TypedColumnsClauseRole[_T], + SQLCoreOperations[_T], + Type[_T], +] # Generated TypeVars start @@ -55,7 +268,7 @@ class SelectOfScalar(_Select[Tuple[_TSelect]]): None, ) -_TModel_0 = TypeVar("_TModel_0", bound="SQLModel") +_T0 = TypeVar("_T0") _TScalar_1 = TypeVar( @@ -73,7 +286,7 @@ class SelectOfScalar(_Select[Tuple[_TSelect]]): None, ) -_TModel_1 = TypeVar("_TModel_1", bound="SQLModel") +_T1 = TypeVar("_T1") _TScalar_2 = TypeVar( @@ -91,7 +304,7 @@ class SelectOfScalar(_Select[Tuple[_TSelect]]): None, ) -_TModel_2 = TypeVar("_TModel_2", bound="SQLModel") +_T2 = TypeVar("_T2") _TScalar_3 = TypeVar( @@ -109,19 +322,19 @@ class SelectOfScalar(_Select[Tuple[_TSelect]]): None, ) -_TModel_3 = TypeVar("_TModel_3", bound="SQLModel") +_T3 = TypeVar("_T3") # Generated TypeVars end @overload -def select(entity_0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore +def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore ... @overload -def select(entity_0: Type[_TModel_0]) -> SelectOfScalar[_TModel_0]: # type: ignore +def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ... @@ -139,24 +352,24 @@ def select( # type: ignore @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], -) -> Select[Tuple[_TScalar_0, _TModel_1]]: + __ent1: _TCCA[_T1], +) -> Select[Tuple[_TScalar_0, _T1]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, -) -> Select[Tuple[_TModel_0, _TScalar_1]]: +) -> Select[Tuple[_T0, _TScalar_1]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], -) -> Select[Tuple[_TModel_0, _TModel_1]]: + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], +) -> Select[Tuple[_T0, _T1]]: ... @@ -173,62 +386,62 @@ def select( # type: ignore def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, - entity_2: Type[_TModel_2], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TModel_2]]: + __ent2: _TCCA[_T2], +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, -) -> Select[Tuple[_TScalar_0, _TModel_1, _TScalar_2]]: +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], -) -> Select[Tuple[_TScalar_0, _TModel_1, _TModel_2]]: + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], +) -> Select[Tuple[_TScalar_0, _T1, _T2]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, entity_2: _TScalar_2, -) -> Select[Tuple[_TModel_0, _TScalar_1, _TScalar_2]]: +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, - entity_2: Type[_TModel_2], -) -> Select[Tuple[_TModel_0, _TScalar_1, _TModel_2]]: + __ent2: _TCCA[_T2], +) -> Select[Tuple[_T0, _TScalar_1, _T2]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, -) -> Select[Tuple[_TModel_0, _TModel_1, _TScalar_2]]: +) -> Select[Tuple[_T0, _T1, _TScalar_2]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], -) -> Select[Tuple[_TModel_0, _TModel_1, _TModel_2]]: + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], +) -> Select[Tuple[_T0, _T1, _T2]]: ... @@ -247,8 +460,8 @@ def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, entity_2: _TScalar_2, - entity_3: Type[_TModel_3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _TModel_3]]: + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _TScalar_1, _TScalar_2, _T3]]: ... @@ -256,9 +469,9 @@ def select( # type: ignore def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, - entity_2: Type[_TModel_2], + __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TModel_2, _TScalar_3]]: +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _TScalar_3]]: ... @@ -266,129 +479,129 @@ def select( # type: ignore def select( # type: ignore entity_0: _TScalar_0, entity_1: _TScalar_1, - entity_2: Type[_TModel_2], - entity_3: Type[_TModel_3], -) -> Select[Tuple[_TScalar_0, _TScalar_1, _TModel_2, _TModel_3]]: + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _TScalar_1, _T2, _T3]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TModel_1, _TScalar_2, _TScalar_3]]: +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _TScalar_3]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, - entity_3: Type[_TModel_3], -) -> Select[Tuple[_TScalar_0, _TModel_1, _TScalar_2, _TModel_3]]: + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _T1, _TScalar_2, _T3]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TScalar_0, _TModel_1, _TModel_2, _TScalar_3]]: +) -> Select[Tuple[_TScalar_0, _T1, _T2, _TScalar_3]]: ... @overload def select( # type: ignore entity_0: _TScalar_0, - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], - entity_3: Type[_TModel_3], -) -> Select[Tuple[_TScalar_0, _TModel_1, _TModel_2, _TModel_3]]: + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_TScalar_0, _T1, _T2, _T3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_TModel_0, _TScalar_1, _TScalar_2, _TScalar_3]]: +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _TScalar_3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, entity_2: _TScalar_2, - entity_3: Type[_TModel_3], -) -> Select[Tuple[_TModel_0, _TScalar_1, _TScalar_2, _TModel_3]]: + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _TScalar_1, _TScalar_2, _T3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, - entity_2: Type[_TModel_2], + __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TModel_0, _TScalar_1, _TModel_2, _TScalar_3]]: +) -> Select[Tuple[_T0, _TScalar_1, _T2, _TScalar_3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], + __ent0: _TCCA[_T0], entity_1: _TScalar_1, - entity_2: Type[_TModel_2], - entity_3: Type[_TModel_3], -) -> Select[Tuple[_TModel_0, _TScalar_1, _TModel_2, _TModel_3]]: + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _TScalar_1, _T2, _T3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, entity_3: _TScalar_3, -) -> Select[Tuple[_TModel_0, _TModel_1, _TScalar_2, _TScalar_3]]: +) -> Select[Tuple[_T0, _T1, _TScalar_2, _TScalar_3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], entity_2: _TScalar_2, - entity_3: Type[_TModel_3], -) -> Select[Tuple[_TModel_0, _TModel_1, _TScalar_2, _TModel_3]]: + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _T1, _TScalar_2, _T3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], entity_3: _TScalar_3, -) -> Select[Tuple[_TModel_0, _TModel_1, _TModel_2, _TScalar_3]]: +) -> Select[Tuple[_T0, _T1, _T2, _TScalar_3]]: ... @overload def select( # type: ignore - entity_0: Type[_TModel_0], - entity_1: Type[_TModel_1], - entity_2: Type[_TModel_2], - entity_3: Type[_TModel_3], -) -> Select[Tuple[_TModel_0, _TModel_1, _TModel_2, _TModel_3]]: + __ent0: _TCCA[_T0], + __ent1: _TCCA[_T1], + __ent2: _TCCA[_T2], + __ent3: _TCCA[_T3], +) -> Select[Tuple[_T0, _T1, _T2, _T3]]: ... @@ -397,12 +610,11 @@ def select( # type: ignore def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore if len(entities) == 1: - return SelectOfScalar(*entities) # type: ignore - return Select(*entities) # type: ignore + return SelectOfScalar(*entities) + return Select(*entities) -# TODO: add several @overload from Python types to SQLAlchemy equivalents -def col(column_expression: Any) -> ColumnClause: # type: ignore +def col(column_expression: _T) -> Mapped[_T]: if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)): raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}") return column_expression # type: ignore diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index 1b9261ec96..d8c6fe6cc3 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -1,37 +1,252 @@ from datetime import datetime from typing import ( - TYPE_CHECKING, Any, + Iterable, Mapping, + Optional, Sequence, Tuple, Type, + TypeAlias, TypeVar, Union, overload, ) from uuid import UUID -from sqlalchemy import Column -from sqlalchemy.orm import InstrumentedAttribute -from sqlalchemy.sql.elements import ColumnClause +import sqlalchemy +from sqlalchemy import ( + Column, + ColumnElement, + Extract, + FunctionElement, + FunctionFilter, + Label, + Over, + TypeCoerce, + WithinGroup, +) +from sqlalchemy.orm import InstrumentedAttribute, Mapped +from sqlalchemy.sql._typing import ( + _ColumnExpressionArgument, + _ColumnExpressionOrLiteralArgument, + _ColumnExpressionOrStrLabelArgument, +) +from sqlalchemy.sql.elements import ( + BinaryExpression, + Case, + Cast, + CollectionAggregate, + ColumnClause, + SQLCoreOperations, + TryCast, + UnaryExpression, +) from sqlalchemy.sql.expression import Select as _Select +from sqlalchemy.sql.roles import TypedColumnsClauseRole +from sqlalchemy.sql.type_api import TypeEngine +from typing_extensions import Literal, Self + +_T = TypeVar("_T") + +_TypeEngineArgument: TypeAlias = Union[Type[TypeEngine[_T]], TypeEngine[_T]] + +# Redefine operatos that would only take a column expresion to also take the (virtual) +# types of Pydantic models, e.g. str instead of only Mapped[str]. + + +def all_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: + return sqlalchemy.all_(expr) # type: ignore[arg-type] + + +def and_( + initial_clause: Union[Literal[True], _ColumnExpressionArgument[bool], bool], + *clauses: Union[_ColumnExpressionArgument[bool], bool], +) -> ColumnElement[bool]: + return sqlalchemy.and_(initial_clause, *clauses) # type: ignore[arg-type] + + +def any_(expr: Union[_ColumnExpressionArgument[_T], _T]) -> CollectionAggregate[bool]: + return sqlalchemy.any_(expr) # type: ignore[arg-type] + + +def asc( + column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], +) -> UnaryExpression[_T]: + return sqlalchemy.asc(column) # type: ignore[arg-type] + + +def collate( + expression: Union[_ColumnExpressionArgument[str], str], collation: str +) -> BinaryExpression[str]: + return sqlalchemy.collate(expression, collation) # type: ignore[arg-type] + + +def between( + expr: Union[_ColumnExpressionOrLiteralArgument[_T], _T], + lower_bound: Any, + upper_bound: Any, + symmetric: bool = False, +) -> BinaryExpression[bool]: + return sqlalchemy.between(expr, lower_bound, upper_bound, symmetric=symmetric) # type: ignore[arg-type] + + +def not_(clause: Union[_ColumnExpressionArgument[_T], _T]) -> ColumnElement[_T]: + return sqlalchemy.not_(clause) # type: ignore[arg-type] + + +def case( + *whens: Union[ + Tuple[Union[_ColumnExpressionArgument[bool], bool], Any], Mapping[Any, Any] + ], + value: Optional[Any] = None, + else_: Optional[Any] = None, +) -> Case[Any]: + return sqlalchemy.case(*whens, value=value, else_=else_) # type: ignore[arg-type] + + +def cast( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> Cast[_T]: + return sqlalchemy.cast(expression, type_) # type: ignore[arg-type] + + +def try_cast( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> TryCast[_T]: + return sqlalchemy.try_cast(expression, type_) # type: ignore[arg-type] + + +def desc( + column: Union[_ColumnExpressionOrStrLabelArgument[_T], _T], +) -> UnaryExpression[_T]: + return sqlalchemy.desc(column) # type: ignore[arg-type] + -_TSelect = TypeVar("_TSelect") +def distinct(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.distinct(expr) # type: ignore[arg-type] -class Select(_Select[Tuple[_TSelect]]): + +def bitwise_not(expr: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.bitwise_not(expr) # type: ignore[arg-type] + + +def extract(field: str, expr: Union[_ColumnExpressionArgument[Any], Any]) -> Extract: + return sqlalchemy.extract(field, expr) # type: ignore[arg-type] + + +def funcfilter( + func: FunctionElement[_T], *criterion: Union[_ColumnExpressionArgument[bool], bool] +) -> FunctionFilter[_T]: + return sqlalchemy.funcfilter(func, *criterion) # type: ignore[arg-type] + + +def label( + name: str, + element: Union[_ColumnExpressionArgument[_T], _T], + type_: Optional["_TypeEngineArgument[_T]"] = None, +) -> Label[_T]: + return sqlalchemy.label(name, element, type_=type_) # type: ignore[arg-type] + + +def nulls_first( + column: Union[_ColumnExpressionArgument[_T], _T] +) -> UnaryExpression[_T]: + return sqlalchemy.nulls_first(column) # type: ignore[arg-type] + + +def nulls_last(column: Union[_ColumnExpressionArgument[_T], _T]) -> UnaryExpression[_T]: + return sqlalchemy.nulls_last(column) # type: ignore[arg-type] + + +def or_( # type: ignore[empty-body] + initial_clause: Union[Literal[False], _ColumnExpressionArgument[bool], bool], + *clauses: Union[_ColumnExpressionArgument[bool], bool], +) -> ColumnElement[bool]: + return sqlalchemy.or_(initial_clause, *clauses) # type: ignore[arg-type] + + +def over( + element: FunctionElement[_T], + partition_by: Optional[ + Union[ + Iterable[Union[_ColumnExpressionArgument[Any], Any]], + _ColumnExpressionArgument[Any], + Any, + ] + ] = None, + order_by: Optional[ + Union[ + Iterable[Union[_ColumnExpressionArgument[Any], Any]], + _ColumnExpressionArgument[Any], + Any, + ] + ] = None, + range_: Optional[Tuple[Optional[int], Optional[int]]] = None, + rows: Optional[Tuple[Optional[int], Optional[int]]] = None, +) -> Over[_T]: + return sqlalchemy.over( + element, partition_by=partition_by, order_by=order_by, range_=range_, rows=rows + ) # type: ignore[arg-type] + + +def tuple_( + *clauses: Union[_ColumnExpressionArgument[Any], Any], + types: Optional[Sequence["_TypeEngineArgument[Any]"]] = None, +) -> Tuple[Any, ...]: + return sqlalchemy.tuple_(*clauses, types=types) # type: ignore[return-value] + + +def type_coerce( + expression: Union[_ColumnExpressionOrLiteralArgument[Any], Any], + type_: "_TypeEngineArgument[_T]", +) -> TypeCoerce[_T]: + return sqlalchemy.type_coerce(expression, type_) # type: ignore[arg-type] + + +def within_group( + element: FunctionElement[_T], *order_by: Union[_ColumnExpressionArgument[Any], Any] +) -> WithinGroup[_T]: + return sqlalchemy.within_group(element, *order_by) # type: ignore[arg-type] + + +# Separate this class in SelectBase, Select, and SelectOfScalar so that they can share +# where and having without having type overlap incompatibility in session.exec(). +class SelectBase(_Select[Tuple[_T]]): inherit_cache = True + def where(self, *whereclause: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + """Return a new `Select` construct with the given expression added to + its `WHERE` clause, joined to the existing clause via `AND`, if any. + """ + return super().where(*whereclause) # type: ignore[arg-type] + + def having(self, *having: Union[_ColumnExpressionArgument[bool], bool]) -> Self: + """Return a new `Select` construct with the given expression added to + its `HAVING` clause, joined to the existing clause via `AND`, if any. + """ + return super().having(*having) # type: ignore[arg-type] + + +class Select(SelectBase[_T]): + inherit_cache = True + + # This is not comparable to sqlalchemy.sql.selectable.ScalarSelect, that has a different # purpose. This is the same as a normal SQLAlchemy Select class where there's only one # entity, so the result will be converted to a scalar by default. This way writing # for loops on the results will feel natural. -class SelectOfScalar(_Select[Tuple[_TSelect]]): +class SelectOfScalar(SelectBase[_T]): inherit_cache = True -if TYPE_CHECKING: # pragma: no cover - from ..main import SQLModel +_TCCA = Union[ + TypedColumnsClauseRole[_T], + SQLCoreOperations[_T], + Type[_T], +] # Generated TypeVars start @@ -52,19 +267,19 @@ _TScalar_{{ i }} = TypeVar( None, ) -_TModel_{{ i }} = TypeVar("_TModel_{{ i }}", bound="SQLModel") +_T{{ i }} = TypeVar("_T{{ i }}") {% endfor %} # Generated TypeVars end @overload -def select(entity_0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore +def select(__ent0: _TScalar_0) -> SelectOfScalar[_TScalar_0]: # type: ignore ... @overload -def select(entity_0: Type[_TModel_0]) -> SelectOfScalar[_TModel_0]: # type: ignore +def select(__ent0: _TCCA[_T0]) -> SelectOfScalar[_T0]: ... @@ -85,12 +300,11 @@ def select( # type: ignore def select(*entities: Any) -> Union[Select, SelectOfScalar]: # type: ignore if len(entities) == 1: - return SelectOfScalar(*entities) # type: ignore - return Select(*entities) # type: ignore + return SelectOfScalar(*entities) + return Select(*entities) -# TODO: add several @overload from Python types to SQLAlchemy equivalents -def col(column_expression: Any) -> ColumnClause: # type: ignore +def col(column_expression: _T) -> Mapped[_T]: if not isinstance(column_expression, (ColumnClause, Column, InstrumentedAttribute)): raise RuntimeError(f"Not a SQLAlchemy column: {column_expression}") return column_expression # type: ignore From 2083ba6d38f2a5d36f71d3e700a7413430666987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 22:25:05 +0100 Subject: [PATCH 29/39] =?UTF-8?q?=F0=9F=8E=A8=20Update=20type=20annotation?= =?UTF-8?q?=20in=20sqltypes.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/sql/sqltypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sqlmodel/sql/sqltypes.py b/sqlmodel/sql/sqltypes.py index aa30950702..5a4bb04ef1 100644 --- a/sqlmodel/sql/sqltypes.py +++ b/sqlmodel/sql/sqltypes.py @@ -32,7 +32,7 @@ class GUID(types.TypeDecorator): # type: ignore impl = CHAR cache_ok = True - def load_dialect_impl(self, dialect: Dialect) -> TypeEngine: # type: ignore + def load_dialect_impl(self, dialect: Dialect) -> TypeEngine[Any]: if dialect.name == "postgresql": return dialect.type_descriptor(UUID()) else: From 8672bfac9317983404835ad3890d02905375eb59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 22:25:26 +0100 Subject: [PATCH 30/39] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20generatio?= =?UTF-8?q?n=20of=20expression.py=20with=20template?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/generate_select.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/generate_select.py b/scripts/generate_select.py index f8aa30023f..88e0e0a997 100644 --- a/scripts/generate_select.py +++ b/scripts/generate_select.py @@ -34,9 +34,9 @@ class Arg(BaseModel): arg = Arg(name=f"entity_{i}", annotation=t_var) ret_type = t_var else: - t_type = f"_TModel_{i}" - t_var = f"Type[{t_type}]" - arg = Arg(name=f"entity_{i}", annotation=t_var) + t_type = f"_T{i}" + t_var = f"_TCCA[{t_type}]" + arg = Arg(name=f"__ent{i}", annotation=t_var) ret_type = t_type args.append(arg) return_types.append(ret_type) From b433b7d0e00f967d4fd07fd2fa7c339084e4565c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 22:26:45 +0100 Subject: [PATCH 31/39] =?UTF-8?q?=E2=9C=A8=20Update=20Session=20types=20an?= =?UTF-8?q?d=20logic,=20add=20deprecations=20for=20discouraged=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/orm/session.py | 94 ++++++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/sqlmodel/orm/session.py b/sqlmodel/orm/session.py index 9d99206b4d..6050d5fbc1 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -4,24 +4,24 @@ Mapping, Optional, Sequence, - Type, TypeVar, Union, overload, ) from sqlalchemy import util -from sqlalchemy.orm import Mapper as _Mapper +from sqlalchemy.engine.interfaces import _CoreAnyExecuteParams +from sqlalchemy.engine.result import Result, ScalarResult, TupleResult from sqlalchemy.orm import Query as _Query from sqlalchemy.orm import Session as _Session +from sqlalchemy.orm._typing import OrmExecuteOptionsParameter +from sqlalchemy.sql._typing import _ColumnsClauseArgument from sqlalchemy.sql.base import Executable as _Executable -from sqlalchemy.sql.selectable import ForUpdateArg as _ForUpdateArg +from sqlmodel.sql.base import Executable +from sqlmodel.sql.expression import Select, SelectOfScalar +from typing_extensions import deprecated -from ..engine.result import Result, ScalarResult -from ..sql.base import Executable -from ..sql.expression import Select, SelectOfScalar - -_TSelectParam = TypeVar("_TSelectParam") +_TSelectParam = TypeVar("_TSelectParam", bound=Any) class Session(_Session): @@ -35,8 +35,7 @@ def exec( bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, - ) -> Result[_TSelectParam]: + ) -> TupleResult[_TSelectParam]: ... @overload @@ -49,7 +48,6 @@ def exec( bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, ) -> ScalarResult[_TSelectParam]: ... @@ -66,8 +64,7 @@ def exec( bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, - ) -> Union[Result[_TSelectParam], ScalarResult[_TSelectParam]]: + ) -> Union[TupleResult[_TSelectParam], ScalarResult[_TSelectParam]]: results = super().execute( statement, params=params, @@ -75,21 +72,40 @@ def exec( bind_arguments=bind_arguments, _parent_execute_state=_parent_execute_state, _add_event=_add_event, - **kw, ) if isinstance(statement, SelectOfScalar): - return results.scalars() # type: ignore + return results.scalars() return results # type: ignore - def execute( + @deprecated( + """ + 🚨 You probably want to use `session.exec()` instead of `session.execute()`. + + This is the original SQLAlchemy `session.execute()` method that returns objects + of type `Row`, and that you have to call `scalars()` to get the model objects. + + For example: + + ```Python + heroes = session.execute(select(Hero)).scalars().all() + ``` + + instead you could use `exec()`: + + ```Python + heroes = session.exec(select(Hero)).all() + ``` + """ + ) + def execute( # type: ignore self, statement: _Executable, - params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, - execution_options: Mapping[str, Any] = util.EMPTY_DICT, + params: Optional[_CoreAnyExecuteParams] = None, + *, + execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, ) -> Result[Any]: """ 🚨 You probably want to use `session.exec()` instead of `session.execute()`. @@ -109,17 +125,16 @@ def execute( heroes = session.exec(select(Hero)).all() ``` """ - return super().execute( # type: ignore + return super().execute( statement, params=params, execution_options=execution_options, bind_arguments=bind_arguments, _parent_execute_state=_parent_execute_state, _add_event=_add_event, - **kw, ) - def query(self, *entities: Any, **kwargs: Any) -> "_Query[Any]": + @deprecated( """ 🚨 You probably want to use `session.exec()` instead of `session.query()`. @@ -129,26 +144,17 @@ def query(self, *entities: Any, **kwargs: Any) -> "_Query[Any]": Or otherwise you might want to use `session.execute()` instead of `session.query()`. """ - return super().query(*entities, **kwargs) # type: ignore + ) + def query( # type: ignore + self, *entities: _ColumnsClauseArgument[Any], **kwargs: Any + ) -> _Query[Any]: + """ + 🚨 You probably want to use `session.exec()` instead of `session.query()`. - def get( - self, - entity: Union[Type[_TSelectParam], "_Mapper[_TSelectParam]"], - ident: Any, - options: Optional[Sequence[Any]] = None, - populate_existing: bool = False, - with_for_update: Optional[_ForUpdateArg] = None, - identity_token: Optional[Any] = None, - execution_options: Mapping[Any, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Dict[str, Any]] = None, - ) -> Optional[_TSelectParam]: - return super().get( - entity, - ident, - options=options, - populate_existing=populate_existing, - with_for_update=with_for_update, - identity_token=identity_token, - execution_options=execution_options, - bind_arguments=bind_arguments, - ) + `session.exec()` is SQLModel's own short version with increased type + annotations. + + Or otherwise you might want to use `session.execute()` instead of + `session.query()`. + """ + return super().query(*entities, **kwargs) From 4ada2038984176a62ff25306410e221c0054a209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 22:27:27 +0100 Subject: [PATCH 32/39] =?UTF-8?q?=E2=9C=A8=20Update=20import=20and=20expor?= =?UTF-8?q?t=20locations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/__init__.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 7e20e1ba41..6268fffa43 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,9 +1,11 @@ __version__ = "0.0.11" # Re-export from SQLAlchemy +from sqlalchemy import create_engine as create_engine from sqlalchemy.engine import create_mock_engine as create_mock_engine from sqlalchemy.engine import engine_from_config as engine_from_config from sqlalchemy.inspection import inspect as inspect +from sqlalchemy.orm import Mapped as Mapped from sqlalchemy.schema import BLANK_SCHEMA as BLANK_SCHEMA from sqlalchemy.schema import DDL as DDL from sqlalchemy.schema import CheckConstraint as CheckConstraint @@ -30,28 +32,15 @@ from sqlalchemy.sql import ( LABEL_STYLE_TABLENAME_PLUS_COL as LABEL_STYLE_TABLENAME_PLUS_COL, ) -from sqlalchemy.sql import Subquery as Subquery from sqlalchemy.sql import alias as alias -from sqlalchemy.sql import all_ as all_ -from sqlalchemy.sql import and_ as and_ -from sqlalchemy.sql import any_ as any_ -from sqlalchemy.sql import asc as asc -from sqlalchemy.sql import between as between from sqlalchemy.sql import bindparam as bindparam -from sqlalchemy.sql import case as case -from sqlalchemy.sql import cast as cast -from sqlalchemy.sql import collate as collate from sqlalchemy.sql import column as column from sqlalchemy.sql import delete as delete -from sqlalchemy.sql import desc as desc -from sqlalchemy.sql import distinct as distinct from sqlalchemy.sql import except_ as except_ from sqlalchemy.sql import except_all as except_all from sqlalchemy.sql import exists as exists -from sqlalchemy.sql import extract as extract from sqlalchemy.sql import false as false from sqlalchemy.sql import func as func -from sqlalchemy.sql import funcfilter as funcfilter from sqlalchemy.sql import insert as insert from sqlalchemy.sql import intersect as intersect from sqlalchemy.sql import intersect_all as intersect_all @@ -61,27 +50,19 @@ from sqlalchemy.sql import literal as literal from sqlalchemy.sql import literal_column as literal_column from sqlalchemy.sql import modifier as modifier -from sqlalchemy.sql import not_ as not_ from sqlalchemy.sql import null as null -from sqlalchemy.sql import nulls_first as nulls_first -from sqlalchemy.sql import nulls_last as nulls_last from sqlalchemy.sql import nullsfirst as nullsfirst from sqlalchemy.sql import nullslast as nullslast -from sqlalchemy.sql import or_ as or_ from sqlalchemy.sql import outerjoin as outerjoin from sqlalchemy.sql import outparam as outparam -from sqlalchemy.sql import over as over from sqlalchemy.sql import table as table from sqlalchemy.sql import tablesample as tablesample from sqlalchemy.sql import text as text from sqlalchemy.sql import true as true -from sqlalchemy.sql import tuple_ as tuple_ -from sqlalchemy.sql import type_coerce as type_coerce from sqlalchemy.sql import union as union from sqlalchemy.sql import union_all as union_all from sqlalchemy.sql import update as update from sqlalchemy.sql import values as values -from sqlalchemy.sql import within_group as within_group from sqlalchemy.types import ARRAY as ARRAY from sqlalchemy.types import BIGINT as BIGINT from sqlalchemy.types import BINARY as BINARY @@ -126,11 +107,30 @@ from sqlalchemy.types import UnicodeText as UnicodeText # From SQLModel, modifications of SQLAlchemy or equivalents of Pydantic -from .engine.create import create_engine as create_engine from .main import Field as Field from .main import Relationship as Relationship from .main import SQLModel as SQLModel from .orm.session import Session as Session +from .sql.expression import all_ as all_ +from .sql.expression import and_ as and_ +from .sql.expression import any_ as any_ +from .sql.expression import asc as asc +from .sql.expression import between as between +from .sql.expression import case as case +from .sql.expression import cast as cast from .sql.expression import col as col +from .sql.expression import collate as collate +from .sql.expression import desc as desc +from .sql.expression import distinct as distinct +from .sql.expression import extract as extract +from .sql.expression import funcfilter as funcfilter +from .sql.expression import not_ as not_ +from .sql.expression import nulls_first as nulls_first +from .sql.expression import nulls_last as nulls_last +from .sql.expression import or_ as or_ +from .sql.expression import over as over from .sql.expression import select as select +from .sql.expression import tuple_ as tuple_ +from .sql.expression import type_coerce as type_coerce +from .sql.expression import within_group as within_group from .sql.sqltypes import AutoString as AutoString From aa27b2b5dc298e8000fbfb7c41cb51ea01513ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 22:28:43 +0100 Subject: [PATCH 33/39] =?UTF-8?q?=F0=9F=94=A7=20Update=20mypy=20config=20f?= =?UTF-8?q?or=20docs=5Fsrc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 9a02fd7304..8d38a17a06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,6 +79,12 @@ strict = true module = "sqlmodel.sql.expression" warn_unused_ignores = false +[[tool.mypy.overrides]] +module = "docs_src.*" +disallow_incomplete_defs = false +disallow_untyped_defs = false +disallow_untyped_calls = false + [tool.ruff] select = [ "E", # pycodestyle errors From baa5e3ab8c67ae562a7e5688583ecb6b50d8172d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 22:29:14 +0100 Subject: [PATCH 34/39] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Update=20SQLAlchemy?= =?UTF-8?q?=20dependency=20pin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8d38a17a06..e3e1204d51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.7" -SQLAlchemy = ">=2.0.0,<=2.1.0" +SQLAlchemy = ">=2.0.0,<2.1.0" pydantic = "^1.9.0" [tool.poetry.group.dev.dependencies] From 80a5c52059f231cf493b3d20a2b10dcfa94af534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Fri, 17 Nov 2023 23:04:27 +0100 Subject: [PATCH 35/39] =?UTF-8?q?=E2=9C=A8=20Update=20AsyncSession=20with?= =?UTF-8?q?=20support=20for=20SQLAlchemy=202.0=20and=20new=20Session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/ext/asyncio/session.py | 144 ++++++++++++++++++++++---------- 1 file changed, 101 insertions(+), 43 deletions(-) diff --git a/sqlmodel/ext/asyncio/session.py b/sqlmodel/ext/asyncio/session.py index f500c44dc2..012d8ef5e4 100644 --- a/sqlmodel/ext/asyncio/session.py +++ b/sqlmodel/ext/asyncio/session.py @@ -1,45 +1,38 @@ -from typing import Any, Mapping, Optional, Sequence, TypeVar, Union, overload +from typing import ( + Any, + Dict, + Mapping, + Optional, + Sequence, + Type, + TypeVar, + Union, + cast, + overload, +) from sqlalchemy import util +from sqlalchemy.engine.interfaces import _CoreAnyExecuteParams +from sqlalchemy.engine.result import Result, ScalarResult, TupleResult from sqlalchemy.ext.asyncio import AsyncSession as _AsyncSession -from sqlalchemy.ext.asyncio import engine -from sqlalchemy.ext.asyncio.engine import AsyncConnection, AsyncEngine +from sqlalchemy.ext.asyncio.result import _ensure_sync_result +from sqlalchemy.ext.asyncio.session import _EXECUTE_OPTIONS +from sqlalchemy.orm._typing import OrmExecuteOptionsParameter +from sqlalchemy.sql.base import Executable as _Executable from sqlalchemy.util.concurrency import greenlet_spawn +from typing_extensions import deprecated -from ...engine.result import Result, ScalarResult from ...orm.session import Session from ...sql.base import Executable from ...sql.expression import Select, SelectOfScalar -_TSelectParam = TypeVar("_TSelectParam") +_TSelectParam = TypeVar("_TSelectParam", bound=Any) class AsyncSession(_AsyncSession): + sync_session_class: Type[Session] = Session sync_session: Session - def __init__( - self, - bind: Optional[Union[AsyncConnection, AsyncEngine]] = None, - binds: Optional[Mapping[object, Union[AsyncConnection, AsyncEngine]]] = None, - **kw: Any, - ): - # All the same code of the original AsyncSession - kw["future"] = True - if bind: - self.bind = bind - bind = engine._get_sync_engine_or_connection(bind) # type: ignore - - if binds: - self.binds = binds - binds = { - key: engine._get_sync_engine_or_connection(b) # type: ignore - for key, b in binds.items() - } - - self.sync_session = self._proxied = self._assign_proxied( # type: ignore - Session(bind=bind, binds=binds, **kw) # type: ignore - ) - @overload async def exec( self, @@ -47,11 +40,10 @@ async def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, - ) -> Result[_TSelectParam]: + ) -> TupleResult[_TSelectParam]: ... @overload @@ -61,10 +53,9 @@ async def exec( *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, execution_options: Mapping[str, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, + bind_arguments: Optional[Dict[str, Any]] = None, _parent_execute_state: Optional[Any] = None, _add_event: Optional[Any] = None, - **kw: Any, ) -> ScalarResult[_TSelectParam]: ... @@ -75,20 +66,87 @@ async def exec( SelectOfScalar[_TSelectParam], Executable[_TSelectParam], ], + *, params: Optional[Union[Mapping[str, Any], Sequence[Mapping[str, Any]]]] = None, - execution_options: Mapping[Any, Any] = util.EMPTY_DICT, - bind_arguments: Optional[Mapping[str, Any]] = None, - **kw: Any, - ) -> Union[Result[_TSelectParam], ScalarResult[_TSelectParam]]: - # TODO: the documentation says execution_options accepts a dict, but only - # util.immutabledict has the union() method. Is this a bug in SQLAlchemy? - execution_options = execution_options.union({"prebuffer_rows": True}) # type: ignore - - return await greenlet_spawn( + execution_options: Mapping[str, Any] = util.EMPTY_DICT, + bind_arguments: Optional[Dict[str, Any]] = None, + _parent_execute_state: Optional[Any] = None, + _add_event: Optional[Any] = None, + ) -> Union[TupleResult[_TSelectParam], ScalarResult[_TSelectParam]]: + if execution_options: + execution_options = util.immutabledict(execution_options).union( + _EXECUTE_OPTIONS + ) + else: + execution_options = _EXECUTE_OPTIONS + + result = await greenlet_spawn( self.sync_session.exec, statement, params=params, execution_options=execution_options, bind_arguments=bind_arguments, - **kw, + _parent_execute_state=_parent_execute_state, + _add_event=_add_event, + ) + result_value = await _ensure_sync_result( + cast(Result[_TSelectParam], result), self.exec + ) + return result_value # type: ignore + + @deprecated( + """ + 🚨 You probably want to use `session.exec()` instead of `session.execute()`. + + This is the original SQLAlchemy `session.execute()` method that returns objects + of type `Row`, and that you have to call `scalars()` to get the model objects. + + For example: + + ```Python + heroes = await session.execute(select(Hero)).scalars().all() + ``` + + instead you could use `exec()`: + + ```Python + heroes = await session.exec(select(Hero)).all() + ``` + """ + ) + async def execute( # type: ignore + self, + statement: _Executable, + params: Optional[_CoreAnyExecuteParams] = None, + *, + execution_options: OrmExecuteOptionsParameter = util.EMPTY_DICT, + bind_arguments: Optional[Dict[str, Any]] = None, + _parent_execute_state: Optional[Any] = None, + _add_event: Optional[Any] = None, + ) -> Result[Any]: + """ + 🚨 You probably want to use `session.exec()` instead of `session.execute()`. + + This is the original SQLAlchemy `session.execute()` method that returns objects + of type `Row`, and that you have to call `scalars()` to get the model objects. + + For example: + + ```Python + heroes = await session.execute(select(Hero)).scalars().all() + ``` + + instead you could use `exec()`: + + ```Python + heroes = await session.exec(select(Hero)).all() + ``` + """ + return await super().execute( + statement, + params=params, + execution_options=execution_options, + bind_arguments=bind_arguments, + _parent_execute_state=_parent_execute_state, + _add_event=_add_event, ) From 61f6a1fe45f6f2e9b415847864ef56a13d9c2d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Nov 2023 12:04:14 +0100 Subject: [PATCH 36/39] =?UTF-8?q?=E2=9C=A8=20Add=20some=20new=20re-exporte?= =?UTF-8?q?d=20objects=20from=20SQLAlchemy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 6268fffa43..e943257165 100644 --- a/sqlmodel/__init__.py +++ b/sqlmodel/__init__.py @@ -1,11 +1,12 @@ __version__ = "0.0.11" # Re-export from SQLAlchemy -from sqlalchemy import create_engine as create_engine +from sqlalchemy.engine import create_engine as create_engine from sqlalchemy.engine import create_mock_engine as create_mock_engine from sqlalchemy.engine import engine_from_config as engine_from_config from sqlalchemy.inspection import inspect as inspect -from sqlalchemy.orm import Mapped as Mapped +from sqlalchemy.pool import QueuePool as QueuePool +from sqlalchemy.pool import StaticPool as StaticPool from sqlalchemy.schema import BLANK_SCHEMA as BLANK_SCHEMA from sqlalchemy.schema import DDL as DDL from sqlalchemy.schema import CheckConstraint as CheckConstraint @@ -73,6 +74,8 @@ from sqlalchemy.types import DATE as DATE from sqlalchemy.types import DATETIME as DATETIME from sqlalchemy.types import DECIMAL as DECIMAL +from sqlalchemy.types import DOUBLE as DOUBLE +from sqlalchemy.types import DOUBLE_PRECISION as DOUBLE_PRECISION from sqlalchemy.types import FLOAT as FLOAT from sqlalchemy.types import INT as INT from sqlalchemy.types import INTEGER as INTEGER @@ -85,12 +88,14 @@ from sqlalchemy.types import TEXT as TEXT from sqlalchemy.types import TIME as TIME from sqlalchemy.types import TIMESTAMP as TIMESTAMP +from sqlalchemy.types import UUID as UUID from sqlalchemy.types import VARBINARY as VARBINARY from sqlalchemy.types import VARCHAR as VARCHAR from sqlalchemy.types import BigInteger as BigInteger from sqlalchemy.types import Boolean as Boolean from sqlalchemy.types import Date as Date from sqlalchemy.types import DateTime as DateTime +from sqlalchemy.types import Double as Double from sqlalchemy.types import Enum as Enum from sqlalchemy.types import Float as Float from sqlalchemy.types import Integer as Integer @@ -102,9 +107,11 @@ from sqlalchemy.types import String as String from sqlalchemy.types import Text as Text from sqlalchemy.types import Time as Time +from sqlalchemy.types import TupleType as TupleType from sqlalchemy.types import TypeDecorator as TypeDecorator from sqlalchemy.types import Unicode as Unicode from sqlalchemy.types import UnicodeText as UnicodeText +from sqlalchemy.types import Uuid as Uuid # From SQLModel, modifications of SQLAlchemy or equivalents of Pydantic from .main import Field as Field @@ -133,4 +140,5 @@ from .sql.expression import tuple_ as tuple_ from .sql.expression import type_coerce as type_coerce from .sql.expression import within_group as within_group +from .sql.sqltypes import GUID as GUID from .sql.sqltypes import AutoString as AutoString From 7e9971da5e947f16b5ef25420099927c2801bead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Nov 2023 12:11:36 +0100 Subject: [PATCH 37/39] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20type=20anno?= =?UTF-8?q?tations=20of=20expression.py=20to=20support=20old=20Pythons?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sqlmodel/sql/expression.py | 3 +-- sqlmodel/sql/expression.py.jinja2 | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 1a7068c53e..a8a572501c 100644 --- a/sqlmodel/sql/expression.py +++ b/sqlmodel/sql/expression.py @@ -9,7 +9,6 @@ Sequence, Tuple, Type, - TypeAlias, TypeVar, Union, overload, @@ -51,7 +50,7 @@ _T = TypeVar("_T") -_TypeEngineArgument: TypeAlias = Union[Type[TypeEngine[_T]], TypeEngine[_T]] +_TypeEngineArgument = Union[Type[TypeEngine[_T]], TypeEngine[_T]] # Redefine operatos that would only take a column expresion to also take the (virtual) # types of Pydantic models, e.g. str instead of only Mapped[str]. diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index d8c6fe6cc3..f1a25419c0 100644 --- a/sqlmodel/sql/expression.py.jinja2 +++ b/sqlmodel/sql/expression.py.jinja2 @@ -7,7 +7,6 @@ from typing import ( Sequence, Tuple, Type, - TypeAlias, TypeVar, Union, overload, @@ -49,7 +48,7 @@ from typing_extensions import Literal, Self _T = TypeVar("_T") -_TypeEngineArgument: TypeAlias = Union[Type[TypeEngine[_T]], TypeEngine[_T]] +_TypeEngineArgument = Union[Type[TypeEngine[_T]], TypeEngine[_T]] # Redefine operatos that would only take a column expresion to also take the (virtual) # types of Pydantic models, e.g. str instead of only Mapped[str]. From 62327ce9fd3118ac2fbdee53e0117dea190029ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Nov 2023 12:12:04 +0100 Subject: [PATCH 38/39] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Upgrade=20FastAPI=20?= =?UTF-8?q?and=20HTTPX=20dependency=20for=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3e1204d51..515bbaf66c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,9 +44,10 @@ pillow = "^9.3.0" cairosvg = "^2.5.2" mdx-include = "^1.4.1" coverage = {extras = ["toml"], version = ">=6.2,<8.0"} -fastapi = "^0.68.1" -requests = "^2.26.0" +fastapi = "^0.103.2" ruff = "^0.1.2" +# For FastAPI tests +httpx = "0.24.1" [build-system] requires = ["poetry-core"] From be5277e0bfb4fe28889385eba60ece39cdffc490 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Ram=C3=ADrez?= Date: Sat, 18 Nov 2023 12:25:17 +0100 Subject: [PATCH 39/39] =?UTF-8?q?=F0=9F=94=87=20Do=20not=20run=20mypy=20on?= =?UTF-8?q?=20Python=203.7=20as=20it=20behaves=20differently?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 201abc7c22..c3b07f484e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -56,6 +56,8 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install - name: Lint + # Do not run on Python 3.7 as mypy behaves differently + if: matrix.python-version != '3.7' run: python -m poetry run bash scripts/lint.sh - run: mkdir coverage - name: Test