From 60318b442a610ef733d01b43f86b0e3a2e3ebef8 Mon Sep 17 00:00:00 2001 From: Mohamed Farahat Date: Tue, 7 Mar 2023 02:08:36 +0200 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] `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/21] 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/21] 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/21] 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/21] _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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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, )