diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 585ffc0455..855f1e2d9f 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,6 @@ jobs: if: steps.cache.outputs.cache-hit != 'true' run: python -m poetry install - name: Lint - if: ${{ matrix.python-version != '3.6.15' }} 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..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.6 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/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..03bbc80e49 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: @@ -81,11 +81,11 @@ 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` * `python3.7` -* `python3.6` The code would look like this: 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..a0cec7b9da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,10 +17,11 @@ 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", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Database", "Topic :: Database :: Database Engines/Servers", "Topic :: Internet", @@ -30,10 +31,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.11" pydantic = "^1.8.2" -sqlalchemy2-stubs = {version = "*", allow-prereleases = true} [tool.poetry.dev-dependencies] pytest = "^7.0.1" @@ -50,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 diff --git a/sqlmodel/__init__.py b/sqlmodel/__init__.py index 720aa8c929..7028ce0018 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,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 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/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..650dd92b27 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, Optional, Sequence, Tuple, 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,48 +32,8 @@ 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]): - 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() # type: ignore - - 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() +class Result(_Result[Tuple[_T]], Generic[_T]): + ... diff --git a/sqlmodel/main.py b/sqlmodel/main.py index d95c498507..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 @@ -478,6 +479,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 @@ -521,7 +523,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..9a07956d92 100644 --- a/sqlmodel/orm/session.py +++ b/sqlmodel/orm/session.py @@ -1,11 +1,22 @@ -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 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 +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 @@ -21,7 +32,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 +46,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 +63,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, @@ -74,8 +85,8 @@ 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, - bind_arguments: Optional[Mapping[str, Any]] = None, + 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, **kw: Any, @@ -118,17 +129,18 @@ 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, - entity: Type[_TSelectParam], + entity: Union[Type[_TSelectParam], "_Mapper[_TSelectParam]"], 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: Optional[Mapping[Any, Any]] = util.EMPTY_DICT, + execution_options: Mapping[Any, Any] = util.EMPTY_DICT, + bind_arguments: Optional[Dict[str, Any]] = None, ) -> Optional[_TSelectParam]: return super().get( entity, @@ -138,4 +150,5 @@ def get( with_for_update=with_for_update, identity_token=identity_token, execution_options=execution_options, + bind_arguments=bind_arguments, ) diff --git a/sqlmodel/sql/expression.py b/sqlmodel/sql/expression.py index 31c0bc1a1e..a0ac1bd9d9 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 @@ -24,36 +22,17 @@ _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, Generic[_TSelect]): - inherit_cache = True +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, 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[Tuple[_TSelect]], Generic[_TSelect]): + inherit_cache = True if TYPE_CHECKING: # pragma: no cover @@ -138,12 +117,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 +133,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 +141,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 +149,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 +157,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 +166,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 +175,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 +184,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 +193,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 +202,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 +211,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 +220,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 +229,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 +239,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 +249,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 +259,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 +269,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 +279,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 +289,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 +299,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 +309,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 +319,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 +329,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 +339,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 +349,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 +359,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 +369,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 +379,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 +389,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,14 +396,14 @@ 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 diff --git a/sqlmodel/sql/expression.py.jinja2 b/sqlmodel/sql/expression.py.jinja2 index 51f04a215d..4284543fe2 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[Tuple[_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[Tuple[_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 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 cf008563f4..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}, - {"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..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}, - {"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..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}, - {"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..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}, - {"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"