From 190975420840845bfbe54bef9e128c8abe8b6762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Magist=C3=A0?= Date: Mon, 15 Jun 2026 18:35:39 +0200 Subject: [PATCH 01/22] refactor: enhance type hinting for field constructors --- tortoise/fields/base.py | 72 ++++++++++++++++++++++++++++- tortoise/fields/data.py | 85 ++++++++++++++++++++++++----------- tortoise/fields/relational.py | 26 ++++++++--- 3 files changed, 151 insertions(+), 32 deletions(-) diff --git a/tortoise/fields/base.py b/tortoise/fields/base.py index 81db786c0..aa82dd9ba 100644 --- a/tortoise/fields/base.py +++ b/tortoise/fields/base.py @@ -7,7 +7,7 @@ from collections.abc import Callable from enum import Enum from functools import reduce -from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload +from typing import TYPE_CHECKING, Any, Generic, TypedDict, TypeVar, overload from pypika_tortoise.terms import Term @@ -90,6 +90,76 @@ class OnDelete(StrEnum): NO_ACTION = OnDelete.NO_ACTION +class _FieldKwargsCommon(TypedDict, total=False): + """:class:`Field` constructor arguments that are never declared as explicit parameters. + + Used with :data:`typing.Unpack` to give ``**kwargs`` explicit type hints. This is the + smallest set; fields that declare ``unique``/``db_index``/``primary_key`` explicitly + (e.g. ``TextField``) unpack this directly to avoid PEP 692 parameter-name collisions. + """ + + source_field: str | None + generated: bool + default: Any + db_default: Any + description: str | None + model: Model | None + validators: list[Validator | Callable] + pk: bool # deprecated alias for primary_key + index: bool # deprecated alias for db_index + + +class _FieldKwargsNoPk(_FieldKwargsCommon, total=False): + """Common arguments excluding ``primary_key`` and ``null``. + + For constructors that declare ``primary_key`` and ``null`` as explicit parameters + (e.g. ``IntField``). + """ + + unique: bool + db_index: bool | None + + +class FieldKwargs(_FieldKwargsNoPk, total=False): + """Common arguments excluding ``null``. + + For constructors that declare only ``null`` as an explicit parameter (the majority). + """ + + primary_key: bool | None + + +class JSONFieldKwargs(FieldKwargs, total=False): + """Constructor arguments for :class:`JSONField`. + + ``JSONField`` declares neither ``null`` nor ``primary_key`` explicitly, and also accepts + a custom ``field_type`` (e.g. a Pydantic model class). + """ + + null: bool + field_type: Any + +class RelationalFieldKwargs(FieldKwargs, total=False): + """Constructor arguments for :func:`ForeignKeyField` and :func:`OneToOneField`. + + Extends the common :class:`~tortoise.fields.base.FieldKwargs` with ``to_field``. + ``null`` is declared as an explicit parameter on those constructors, so it is omitted. + """ + + to_field: str | None + + +class ManyToManyFieldKwargs(_FieldKwargsCommon, total=False): + """Constructor arguments for :func:`ManyToManyField`. + + ``unique`` is declared as an explicit parameter, so it is omitted here; the deprecated + ``create_unique_index`` alias is still accepted. + """ + + create_unique_index: bool # deprecated alias for unique + + + class _FieldMeta(type): # TODO: Require functions to return field instances instead of this hack def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict) -> type: diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index 6109d0022..0ba5fa94d 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -4,6 +4,7 @@ import datetime import functools import json +import sys import warnings from collections.abc import Callable from decimal import Decimal @@ -17,7 +18,18 @@ from tortoise import timezone from tortoise.exceptions import ConfigurationError, FieldError -from tortoise.fields.base import Field +from tortoise.fields.base import ( + Field, + FieldKwargs, + JSONFieldKwargs, + _FieldKwargsCommon, + _FieldKwargsNoPk, +) + +if sys.version_info >= (3, 11): + from typing import Unpack +else: # pragma: no cover + from typing_extensions import Unpack from tortoise.timezone import get_default_timezone, get_timezone, get_use_tz, localtime from tortoise.validators import MaxLengthValidator @@ -107,7 +119,7 @@ def __init__( primary_key: bool | None = None, *, null: Literal[False] = False, - **kwargs: Any, + **kwargs: Unpack[_FieldKwargsNoPk], ) -> None: ... @overload @@ -116,7 +128,7 @@ def __init__( primary_key: bool | None = None, *, null: Literal[True], - **kwargs: Any, + **kwargs: Unpack[_FieldKwargsNoPk], ) -> None: ... def __init__(self, primary_key: bool | None = None, **kwargs: Any) -> None: @@ -222,12 +234,20 @@ class CharField(Field[T_STR]): @overload def __init__( - self: CharField[str], max_length: int, *, null: Literal[False] = False, **kwargs: Any + self: CharField[str], + max_length: int, + *, + null: Literal[False] = False, + **kwargs: Unpack[FieldKwargs], ) -> None: ... @overload def __init__( - self: CharField[str | None], max_length: int, *, null: Literal[True], **kwargs: Any + self: CharField[str | None], + max_length: int, + *, + null: Literal[True], + **kwargs: Unpack[FieldKwargs], ) -> None: ... def __init__(self, max_length: int, **kwargs: Any) -> None: @@ -269,7 +289,7 @@ def __init__( primary_key: bool | None = None, unique: bool = False, db_index: bool = False, - **kwargs: Any, + **kwargs: Unpack[_FieldKwargsCommon], ) -> None: if primary_key or kwargs.get("pk"): warnings.warn( @@ -315,12 +335,12 @@ class BooleanField(Field[T_BOOL]): @overload def __init__( - self: BooleanField[bool], *, null: Literal[False] = False, **kwargs: Any + self: BooleanField[bool], *, null: Literal[False] = False, **kwargs: Unpack[FieldKwargs] ) -> None: ... @overload def __init__( - self: BooleanField[bool | None], *, null: Literal[True], **kwargs: Any + self: BooleanField[bool | None], *, null: Literal[True], **kwargs: Unpack[FieldKwargs] ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -357,7 +377,7 @@ def __init__( decimal_places: int, *, null: Literal[False] = False, - **kwargs: Any, + **kwargs: Unpack[FieldKwargs], ) -> None: ... @overload @@ -367,7 +387,7 @@ def __init__( decimal_places: int, *, null: Literal[True], - **kwargs: Any, + **kwargs: Unpack[FieldKwargs], ) -> None: ... def __init__(self, max_digits: int, decimal_places: int, **kwargs: Any) -> None: @@ -440,7 +460,7 @@ def __init__( auto_now_add: bool = False, *, null: Literal[False] = False, - **kwargs: Any, + **kwargs: Unpack[FieldKwargs], ) -> None: ... @overload @@ -450,7 +470,7 @@ def __init__( auto_now_add: bool = False, *, null: Literal[True], - **kwargs: Any, + **kwargs: Unpack[FieldKwargs], ) -> None: ... def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any) -> None: @@ -530,12 +550,15 @@ class DateField(Field[T_DATE], datetime.date): @overload def __init__( - self: DateField[datetime.date], *, null: Literal[False] = False, **kwargs: Any + self: DateField[datetime.date], + *, + null: Literal[False] = False, + **kwargs: Unpack[FieldKwargs], ) -> None: ... @overload def __init__( - self: DateField[datetime.date | None], *, null: Literal[True], **kwargs: Any + self: DateField[datetime.date | None], *, null: Literal[True], **kwargs: Unpack[FieldKwargs] ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -574,7 +597,7 @@ def __init__( auto_now_add: bool = False, *, null: Literal[False] = False, - **kwargs: Any, + **kwargs: Unpack[FieldKwargs], ) -> None: ... @overload @@ -584,7 +607,7 @@ def __init__( auto_now_add: bool = False, *, null: Literal[True], - **kwargs: Any, + **kwargs: Unpack[FieldKwargs], ) -> None: ... def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any) -> None: @@ -654,12 +677,18 @@ class TimeDeltaField(Field[T_TIMEDELTA]): @overload def __init__( - self: TimeDeltaField[datetime.timedelta], *, null: Literal[False] = False, **kwargs: Any + self: TimeDeltaField[datetime.timedelta], + *, + null: Literal[False] = False, + **kwargs: Unpack[FieldKwargs], ) -> None: ... @overload def __init__( - self: TimeDeltaField[datetime.timedelta | None], *, null: Literal[True], **kwargs: Any + self: TimeDeltaField[datetime.timedelta | None], + *, + null: Literal[True], + **kwargs: Unpack[FieldKwargs], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -692,11 +721,13 @@ class FloatField(Field[T_FLOAT], float): @overload def __init__( - self: FloatField[float], *, null: Literal[False] = False, **kwargs: Any + self: FloatField[float], *, null: Literal[False] = False, **kwargs: Unpack[FieldKwargs] ) -> None: ... @overload - def __init__(self: FloatField[float | None], *, null: Literal[True], **kwargs: Any) -> None: ... + def __init__( + self: FloatField[float | None], *, null: Literal[True], **kwargs: Unpack[FieldKwargs] + ) -> None: ... def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) @@ -748,7 +779,7 @@ def __init__( self, encoder: JsonDumpsFunc = JSON_DUMPS, decoder: JsonLoadsFunc = JSON_LOADS, - **kwargs: Any, + **kwargs: Unpack[JSONFieldKwargs], ) -> None: super().__init__(**kwargs) self.encoder = encoder @@ -820,10 +851,14 @@ class _db_postgres: SQL_TYPE = "UUID" @overload - def __init__(self: UUIDField[UUID], *, null: Literal[False] = False, **kwargs: Any) -> None: ... + def __init__( + self: UUIDField[UUID], *, null: Literal[False] = False, **kwargs: Unpack[FieldKwargs] + ) -> None: ... @overload - def __init__(self: UUIDField[UUID | None], *, null: Literal[True], **kwargs: Any) -> None: ... + def __init__( + self: UUIDField[UUID | None], *, null: Literal[True], **kwargs: Unpack[FieldKwargs] + ) -> None: ... def __init__(self, **kwargs: Any) -> None: if (kwargs.get("primary_key") or kwargs.get("pk", False)) and "default" not in kwargs: @@ -852,12 +887,12 @@ class BinaryField(Field[T_BINARY], bytes): # type: ignore @overload def __init__( - self: BinaryField[bytes], *, null: Literal[False] = False, **kwargs: Any + self: BinaryField[bytes], *, null: Literal[False] = False, **kwargs: Unpack[FieldKwargs] ) -> None: ... @overload def __init__( - self: BinaryField[bytes | None], *, null: Literal[True], **kwargs: Any + self: BinaryField[bytes | None], *, null: Literal[True], **kwargs: Unpack[FieldKwargs] ) -> None: ... def __init__(self, **kwargs: Any) -> None: diff --git a/tortoise/fields/relational.py b/tortoise/fields/relational.py index 442707848..d69e6c24d 100644 --- a/tortoise/fields/relational.py +++ b/tortoise/fields/relational.py @@ -1,5 +1,6 @@ from __future__ import annotations +import sys import warnings from collections.abc import AsyncGenerator, Generator, Iterator from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, overload @@ -7,7 +8,19 @@ from pypika_tortoise.queries import Table from tortoise.exceptions import ConfigurationError, NoValuesFetched, OperationalError -from tortoise.fields.base import CASCADE, SET_NULL, Field, OnDelete +from tortoise.fields.base import ( + CASCADE, + SET_NULL, + Field, + ManyToManyFieldKwargs, + OnDelete, + RelationalFieldKwargs, +) + +if sys.version_info >= (3, 11): + from typing import Unpack +else: # pragma: no cover + from typing_extensions import Unpack if TYPE_CHECKING: # pragma: nocoverage from tortoise.backends.base.client import BaseDBAsyncClient @@ -17,6 +30,7 @@ MODEL = TypeVar("MODEL", bound="Model") + class _NoneAwaitable: __slots__ = () @@ -441,7 +455,7 @@ def OneToOneField( db_constraint: bool = True, *, null: Literal[True], - **kwargs: Any, + **kwargs: Unpack[RelationalFieldKwargs], ) -> OneToOneNullableRelation[MODEL]: ... @@ -452,7 +466,7 @@ def OneToOneField( on_delete: OnDelete = CASCADE, db_constraint: bool = True, null: Literal[False] = False, - **kwargs: Any, + **kwargs: Unpack[RelationalFieldKwargs], ) -> OneToOneRelation[MODEL]: ... @@ -516,7 +530,7 @@ def ForeignKeyField( db_constraint: bool = True, *, null: Literal[True], - **kwargs: Any, + **kwargs: Unpack[RelationalFieldKwargs], ) -> ForeignKeyNullableRelation[MODEL]: ... @@ -527,7 +541,7 @@ def ForeignKeyField( on_delete: OnDelete = CASCADE, db_constraint: bool = True, null: Literal[False] = False, - **kwargs: Any, + **kwargs: Unpack[RelationalFieldKwargs], ) -> ForeignKeyRelation[MODEL]: ... @@ -592,7 +606,7 @@ def ManyToManyField( on_delete: OnDelete = CASCADE, db_constraint: bool = True, unique: bool = True, - **kwargs: Any, + **kwargs: Unpack[ManyToManyFieldKwargs], ) -> ManyToManyRelation[MODEL]: """ ManyToMany relation field. From 390b8df4e2918a89dc6cc6300f046aa3830952ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Magist=C3=A0?= Date: Mon, 15 Jun 2026 18:36:05 +0200 Subject: [PATCH 02/22] updated changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 78e832ee1..1a4a22ab2 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -17,6 +17,7 @@ Added - ``QuerySet.contains()`` method to check if an object exists in a queryset. - Added comprehensive EXPLAIN support for MySQL and PostgreSQL. - Built-in ``DomainNameValidator``, ``URLValidator``, and ``EmailValidator`` classes for common validation patterns. (#2162) +- Typed ``**kwargs`` on field constructors via PEP 692 (``Unpack[TypedDict]``), so IDEs and type checkers can autocomplete and validate common field arguments (``default``, ``null``, ``unique``, ``db_index``, ``description``, etc.). (#2168) Fixed ^^^^^ From dbd4819c9726a49f8822954e01a41919b460718a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Magist=C3=A0?= Date: Tue, 16 Jun 2026 07:58:31 +0200 Subject: [PATCH 03/22] run `make style` --- tortoise/fields/base.py | 2 +- tortoise/fields/relational.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tortoise/fields/base.py b/tortoise/fields/base.py index aa82dd9ba..bd6562c0f 100644 --- a/tortoise/fields/base.py +++ b/tortoise/fields/base.py @@ -139,6 +139,7 @@ class JSONFieldKwargs(FieldKwargs, total=False): null: bool field_type: Any + class RelationalFieldKwargs(FieldKwargs, total=False): """Constructor arguments for :func:`ForeignKeyField` and :func:`OneToOneField`. @@ -159,7 +160,6 @@ class ManyToManyFieldKwargs(_FieldKwargsCommon, total=False): create_unique_index: bool # deprecated alias for unique - class _FieldMeta(type): # TODO: Require functions to return field instances instead of this hack def __new__(mcs, name: str, bases: tuple[type, ...], attrs: dict) -> type: diff --git a/tortoise/fields/relational.py b/tortoise/fields/relational.py index d69e6c24d..b5e7ab568 100644 --- a/tortoise/fields/relational.py +++ b/tortoise/fields/relational.py @@ -30,7 +30,6 @@ MODEL = TypeVar("MODEL", bound="Model") - class _NoneAwaitable: __slots__ = () From fab2ca66a88aea011db2752c75016803285a00cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Magist=C3=A0?= Date: Wed, 17 Jun 2026 08:47:53 +0200 Subject: [PATCH 04/22] Added two @overload signatures with explicit null: Literal[False] / null: Literal[True] --- tortoise/fields/data.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index 0ba5fa94d..4e3453429 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -284,12 +284,34 @@ class TextField(Field[str], str): # type: ignore indexable = False SQL_TYPE = "TEXT" + @overload + def __init__( + self, + *, + primary_key: bool | None = None, + unique: bool = False, + db_index: bool = False, + null: Literal[False] = False, + **kwargs: Unpack[_FieldKwargsCommon], + ) -> None: ... + + @overload def __init__( self, + *, primary_key: bool | None = None, unique: bool = False, db_index: bool = False, + null: Literal[True], **kwargs: Unpack[_FieldKwargsCommon], + ) -> None: ... + + def __init__( + self, + primary_key: bool | None = None, + unique: bool = False, + db_index: bool = False, + **kwargs: Any, ) -> None: if primary_key or kwargs.get("pk"): warnings.warn( From ae4953c608c71203646817165f5d3625d1c040fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Magist=C3=A0?= Date: Wed, 17 Jun 2026 08:57:58 +0200 Subject: [PATCH 05/22] fix: changed Textfield type to Field[T_STR] instead of Field[str] --- tortoise/fields/data.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index 4e3453429..ac70562e4 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -276,7 +276,7 @@ def SQL_TYPE(self) -> str: return f"NVARCHAR2({self.field.max_length})" -class TextField(Field[str], str): # type: ignore +class TextField(Field[T_STR], str): # type: ignore """ Large Text field. """ @@ -286,7 +286,7 @@ class TextField(Field[str], str): # type: ignore @overload def __init__( - self, + self: TextField[str], *, primary_key: bool | None = None, unique: bool = False, @@ -297,7 +297,7 @@ def __init__( @overload def __init__( - self, + self: TextField[str | None], *, primary_key: bool | None = None, unique: bool = False, From 538d1f9c41c83ef9a59c98ee948c4a0c5be84958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Magist=C3=A0?= Date: Thu, 18 Jun 2026 16:44:54 +0200 Subject: [PATCH 06/22] refactor: changed import order for consistency --- tortoise/fields/data.py | 60 ++++++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 15 deletions(-) diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index ac70562e4..f36332432 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -25,11 +25,6 @@ _FieldKwargsCommon, _FieldKwargsNoPk, ) - -if sys.version_info >= (3, 11): - from typing import Unpack -else: # pragma: no cover - from typing_extensions import Unpack from tortoise.timezone import get_default_timezone, get_timezone, get_use_tz, localtime from tortoise.validators import MaxLengthValidator @@ -42,7 +37,9 @@ try: from pydantic import BaseModel as _PydanticBaseModel - from pydantic._internal._model_construction import ModelMetaclass as _PydanticModelMetaclass + from pydantic._internal._model_construction import ( + ModelMetaclass as _PydanticModelMetaclass, + ) except ImportError: _PydanticBaseModel = None # type: ignore[assignment,misc] _PydanticModelMetaclass = None # type: ignore[assignment,misc] @@ -50,6 +47,12 @@ if TYPE_CHECKING: # pragma: nocoverage from tortoise.models import Model + +if sys.version_info >= (3, 11): + from typing import Unpack +else: # pragma: no cover + from typing_extensions import Unpack + __all__ = ( "BigIntField", "BinaryField", @@ -357,12 +360,18 @@ class BooleanField(Field[T_BOOL]): @overload def __init__( - self: BooleanField[bool], *, null: Literal[False] = False, **kwargs: Unpack[FieldKwargs] + self: BooleanField[bool], + *, + null: Literal[False] = False, + **kwargs: Unpack[FieldKwargs], ) -> None: ... @overload def __init__( - self: BooleanField[bool | None], *, null: Literal[True], **kwargs: Unpack[FieldKwargs] + self: BooleanField[bool | None], + *, + null: Literal[True], + **kwargs: Unpack[FieldKwargs], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -580,7 +589,10 @@ def __init__( @overload def __init__( - self: DateField[datetime.date | None], *, null: Literal[True], **kwargs: Unpack[FieldKwargs] + self: DateField[datetime.date | None], + *, + null: Literal[True], + **kwargs: Unpack[FieldKwargs], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -743,12 +755,18 @@ class FloatField(Field[T_FLOAT], float): @overload def __init__( - self: FloatField[float], *, null: Literal[False] = False, **kwargs: Unpack[FieldKwargs] + self: FloatField[float], + *, + null: Literal[False] = False, + **kwargs: Unpack[FieldKwargs], ) -> None: ... @overload def __init__( - self: FloatField[float | None], *, null: Literal[True], **kwargs: Unpack[FieldKwargs] + self: FloatField[float | None], + *, + null: Literal[True], + **kwargs: Unpack[FieldKwargs], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -874,12 +892,18 @@ class _db_postgres: @overload def __init__( - self: UUIDField[UUID], *, null: Literal[False] = False, **kwargs: Unpack[FieldKwargs] + self: UUIDField[UUID], + *, + null: Literal[False] = False, + **kwargs: Unpack[FieldKwargs], ) -> None: ... @overload def __init__( - self: UUIDField[UUID | None], *, null: Literal[True], **kwargs: Unpack[FieldKwargs] + self: UUIDField[UUID | None], + *, + null: Literal[True], + **kwargs: Unpack[FieldKwargs], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -909,12 +933,18 @@ class BinaryField(Field[T_BINARY], bytes): # type: ignore @overload def __init__( - self: BinaryField[bytes], *, null: Literal[False] = False, **kwargs: Unpack[FieldKwargs] + self: BinaryField[bytes], + *, + null: Literal[False] = False, + **kwargs: Unpack[FieldKwargs], ) -> None: ... @overload def __init__( - self: BinaryField[bytes | None], *, null: Literal[True], **kwargs: Unpack[FieldKwargs] + self: BinaryField[bytes | None], + *, + null: Literal[True], + **kwargs: Unpack[FieldKwargs], ) -> None: ... def __init__(self, **kwargs: Any) -> None: From 8cb6ba78a7d3ca84577859898c361dab7f1b4823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Magist=C3=A0?= Date: Mon, 22 Jun 2026 08:14:32 +0200 Subject: [PATCH 07/22] refactor: moved imports under TYPE_CHECKING --- tortoise/fields/data.py | 56 ++++++++++++++++++-------- tortoise/fields/relational.py | 76 ++++++++++++++++++++++++----------- 2 files changed, 92 insertions(+), 40 deletions(-) diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index f36332432..7834ba12b 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -4,7 +4,6 @@ import datetime import functools import json -import sys import warnings from collections.abc import Callable from decimal import Decimal @@ -45,13 +44,14 @@ _PydanticModelMetaclass = None # type: ignore[assignment,misc] if TYPE_CHECKING: # pragma: nocoverage - from tortoise.models import Model + import sys + from tortoise.models import Model -if sys.version_info >= (3, 11): - from typing import Unpack -else: # pragma: no cover - from typing_extensions import Unpack + if sys.version_info >= (3, 11): + from typing import Unpack + else: # pragma: no cover + from typing_extensions import Unpack __all__ = ( "BigIntField", @@ -333,7 +333,9 @@ def __init__( stacklevel=2, ) if index or db_index: - raise ConfigurationError("TextField can't be indexed, consider CharField") + raise ConfigurationError( + "TextField can't be indexed, consider CharField" + ) elif db_index: raise ConfigurationError("TextField can't be indexed, consider CharField") @@ -429,7 +431,9 @@ def __init__(self, max_digits: int, decimal_places: int, **kwargs: Any) -> None: super().__init__(**kwargs) self.max_digits = max_digits self.decimal_places = decimal_places - self.quant = Decimal("1" if decimal_places == 0 else f"1.{('0' * decimal_places)}") + self.quant = Decimal( + "1" if decimal_places == 0 else f"1.{('0' * decimal_places)}" + ) def to_python_value(self, value: Any) -> Decimal | None: if value is not None: @@ -454,7 +458,9 @@ def function_cast(self, term: Term) -> Term: DatetimeFieldQueryValueType = TypeVar( "DatetimeFieldQueryValueType", datetime.datetime, int, float, str ) -DateFieldQueryValueType = TypeVar("DateFieldQueryValueType", datetime.date, int, float, str) +DateFieldQueryValueType = TypeVar( + "DateFieldQueryValueType", datetime.date, int, float, str +) class DatetimeField(Field[T_DATETIME], datetime.datetime): @@ -504,7 +510,9 @@ def __init__( **kwargs: Unpack[FieldKwargs], ) -> None: ... - def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any) -> None: + def __init__( + self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any + ) -> None: if auto_now_add and auto_now: raise ConfigurationError("You can choose only 'auto_now' or 'auto_now_add'") super().__init__(**kwargs) @@ -644,7 +652,9 @@ def __init__( **kwargs: Unpack[FieldKwargs], ) -> None: ... - def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any) -> None: + def __init__( + self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any + ) -> None: if auto_now_add and auto_now: raise ConfigurationError("You can choose only 'auto_now' or 'auto_now_add'") super().__init__(**kwargs) @@ -743,7 +753,9 @@ def to_db_value( if value is None: return None - return (value.days * 86400000000) + (value.seconds * 1000000) + value.microseconds + return ( + (value.days * 86400000000) + (value.seconds * 1000000) + value.microseconds + ) class FloatField(Field[T_FLOAT], float): @@ -907,7 +919,9 @@ def __init__( ) -> None: ... def __init__(self, **kwargs: Any) -> None: - if (kwargs.get("primary_key") or kwargs.get("pk", False)) and "default" not in kwargs: + if ( + kwargs.get("primary_key") or kwargs.get("pk", False) + ) and "default" not in kwargs: kwargs["default"] = uuid4 super().__init__(**kwargs) @@ -982,7 +996,9 @@ def __init__( # Automatic description for the field if not specified by the user if description is None: - description = "\n".join([f"{e.name}: {int(e.value)}" for e in enum_type])[:2048] + description = "\n".join([f"{e.name}: {int(e.value)}" for e in enum_type])[ + :2048 + ] super().__init__(description=description, **kwargs) self.enum_type = enum_type @@ -991,7 +1007,9 @@ def to_python_value(self, value: int | None) -> IntEnum | None: value = self.enum_type(value) if value is not None else None return value - def to_db_value(self, value: IntEnum | None | int, instance: type[Model] | Model) -> int | None: + def to_db_value( + self, value: IntEnum | None | int, instance: type[Model] | Model + ) -> int | None: if isinstance(value, IntEnum): value = int(value.value) if isinstance(value, int): @@ -1038,7 +1056,9 @@ def __init__( ) -> None: # Automatic description for the field if not specified by the user if description is None: - description = "\n".join([f"{e.name}: {str(e.value)}" for e in enum_type])[:2048] + description = "\n".join([f"{e.name}: {str(e.value)}" for e in enum_type])[ + :2048 + ] # Automatic CharField max_length if max_length == 0: @@ -1053,7 +1073,9 @@ def __init__( def to_python_value(self, value: str | None) -> Enum | None: return self.enum_type(value) if value is not None else None - def to_db_value(self, value: Enum | None | str, instance: type[Model] | Model) -> str | None: + def to_db_value( + self, value: Enum | None | str, instance: type[Model] | Model + ) -> str | None: self.validate(value) if isinstance(value, Enum): return str(value.value) diff --git a/tortoise/fields/relational.py b/tortoise/fields/relational.py index b5e7ab568..617e3e869 100644 --- a/tortoise/fields/relational.py +++ b/tortoise/fields/relational.py @@ -1,6 +1,5 @@ from __future__ import annotations -import sys import warnings from collections.abc import AsyncGenerator, Generator, Iterator from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, overload @@ -17,16 +16,18 @@ RelationalFieldKwargs, ) -if sys.version_info >= (3, 11): - from typing import Unpack -else: # pragma: no cover - from typing_extensions import Unpack - if TYPE_CHECKING: # pragma: nocoverage + import sys + from tortoise.backends.base.client import BaseDBAsyncClient from tortoise.models import Model from tortoise.queryset import Q, QuerySet + if sys.version_info >= (3, 11): + from typing import Unpack + else: # pragma: no cover + from typing_extensions import Unpack + MODEL = TypeVar("MODEL", bound="Model") @@ -132,7 +133,9 @@ def offset(self, offset: int) -> QuerySet[MODEL]: """ return self._query.offset(offset) - async def create(self, using_db: BaseDBAsyncClient | None = None, **kwargs: Any) -> MODEL: + async def create( + self, using_db: BaseDBAsyncClient | None = None, **kwargs: Any + ) -> MODEL: """ Create a related record in the DB and returns the object, automatically setting the foreign key relationship to the parent instance. @@ -164,7 +167,9 @@ async def create(self, using_db: BaseDBAsyncClient | None = None, **kwargs: Any) # Call remote model's create method return await self.remote_model.create(using_db=using_db, **kwargs) - def _set_result_for_query(self, sequence: list[MODEL], attr: str | None = None) -> None: + def _set_result_for_query( + self, sequence: list[MODEL], attr: str | None = None + ) -> None: self._fetched = True self.related_objects = sequence if attr: @@ -182,12 +187,18 @@ class ManyToManyRelation(ReverseRelation[MODEL]): Many-to-many relation container for :func:`.ManyToManyField`. """ - def __init__(self, instance: Model, m2m_field: ManyToManyFieldInstance[MODEL]) -> None: - super().__init__(m2m_field.related_model, m2m_field.related_name, instance, "pk") + def __init__( + self, instance: Model, m2m_field: ManyToManyFieldInstance[MODEL] + ) -> None: + super().__init__( + m2m_field.related_model, m2m_field.related_name, instance, "pk" + ) self.field = m2m_field self.instance = instance - async def add(self, *instances: MODEL, using_db: BaseDBAsyncClient | None = None) -> None: + async def add( + self, *instances: MODEL, using_db: BaseDBAsyncClient | None = None + ) -> None: """ Adds one or more of ``instances`` to the relation. @@ -206,16 +217,25 @@ async def add(self, *instances: MODEL, using_db: BaseDBAsyncClient | None = None pks_f: list = [] for instance_to_add in instances: if not instance_to_add._saved_in_db: - raise OperationalError(f"You should first call .save() on {instance_to_add}") + raise OperationalError( + f"You should first call .save() on {instance_to_add}" + ) pk_f = related_pk_formatting_func(instance_to_add.pk, instance_to_add) pks_f.append(pk_f) through_table = Table(self.field.through, schema=self.field.through_schema) backward_key, forward_key = self.field.backward_key, self.field.forward_key - backward_field, forward_field = through_table[backward_key], through_table[forward_key] + backward_field, forward_field = ( + through_table[backward_key], + through_table[forward_key], + ) select_query = ( - db.query_class.from_(through_table).where(backward_field == pk_b).select(forward_key) + db.query_class.from_(through_table) + .where(backward_field == pk_b) + .select(forward_key) + ) + criterion = ( + forward_field == pks_f[0] if len(pks_f) == 1 else forward_field.isin(pks_f) ) - criterion = forward_field == pks_f[0] if len(pks_f) == 1 else forward_field.isin(pks_f) select_query = select_query.where(criterion) _, already_existing_relations_raw = await db.execute_query( @@ -227,7 +247,9 @@ async def add(self, *instances: MODEL, using_db: BaseDBAsyncClient | None = None } if pks_f_to_insert := set(pks_f) - already_existing_forward_pks: - query = db.query_class.into(through_table).columns(forward_field, backward_field) + query = db.query_class.into(through_table).columns( + forward_field, backward_field + ) for pk_f in pks_f_to_insert: query = query.insert(pk_f, pk_b) await db.execute_query(*query.get_parameterized_sql()) @@ -238,7 +260,9 @@ async def clear(self, using_db: BaseDBAsyncClient | None = None) -> None: """ await self._remove_or_clear(using_db=using_db) - async def remove(self, *instances: MODEL, using_db: BaseDBAsyncClient | None = None) -> None: + async def remove( + self, *instances: MODEL, using_db: BaseDBAsyncClient | None = None + ) -> None: """ Removes one or more of ``instances`` from the relation. @@ -263,9 +287,9 @@ async def _remove_or_clear( if instances: related_pk_formatting_func = type(instances[0])._meta.pk.to_db_value if len(instances) == 1: - condition &= through_table[self.field.forward_key] == related_pk_formatting_func( - instances[0].pk, instances[0] - ) + condition &= through_table[ + self.field.forward_key + ] == related_pk_formatting_func(instances[0].pk, instances[0]) else: condition &= through_table[self.field.forward_key].isin( [related_pk_formatting_func(i.pk, i) for i in instances] @@ -293,7 +317,9 @@ def __init__( if TYPE_CHECKING: @overload - def __get__(self, instance: None, owner: type[Model]) -> RelationalField[MODEL]: ... + def __get__( + self, instance: None, owner: type[Model] + ) -> RelationalField[MODEL]: ... @overload def __get__(self, instance: Model, owner: type[Model]) -> MODEL: ... @@ -322,7 +348,9 @@ def validate_model_name(cls, model_name: str | type[Model]) -> None: ) from None elif len(model_name.split(".")) != 2: field_type = cls.__name__.replace("Instance", "") - raise ConfigurationError(f'{field_type} accepts model name in format "app.Model"') + raise ConfigurationError( + f'{field_type} accepts model name in format "app.Model"' + ) class ForeignKeyFieldInstance(RelationalField[MODEL]): @@ -342,7 +370,9 @@ def __init__( "on_delete can only be CASCADE, RESTRICT, SET_NULL, SET_DEFAULT or NO_ACTION" ) if on_delete == SET_NULL and not bool(kwargs.get("null")): - raise ConfigurationError("If on_delete is SET_NULL, then field must have null=True set") + raise ConfigurationError( + "If on_delete is SET_NULL, then field must have null=True set" + ) self.on_delete = on_delete def describe(self, serializable: bool) -> dict: From 3a8b565e3cf217e1137fee21bc3426d5498c34b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrea=20Magist=C3=A0?= Date: Mon, 22 Jun 2026 08:16:05 +0200 Subject: [PATCH 08/22] run `make style` --- tortoise/fields/data.py | 44 +++++++------------------- tortoise/fields/relational.py | 58 ++++++++++------------------------- 2 files changed, 27 insertions(+), 75 deletions(-) diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index 7834ba12b..ce297e002 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -333,9 +333,7 @@ def __init__( stacklevel=2, ) if index or db_index: - raise ConfigurationError( - "TextField can't be indexed, consider CharField" - ) + raise ConfigurationError("TextField can't be indexed, consider CharField") elif db_index: raise ConfigurationError("TextField can't be indexed, consider CharField") @@ -431,9 +429,7 @@ def __init__(self, max_digits: int, decimal_places: int, **kwargs: Any) -> None: super().__init__(**kwargs) self.max_digits = max_digits self.decimal_places = decimal_places - self.quant = Decimal( - "1" if decimal_places == 0 else f"1.{('0' * decimal_places)}" - ) + self.quant = Decimal("1" if decimal_places == 0 else f"1.{('0' * decimal_places)}") def to_python_value(self, value: Any) -> Decimal | None: if value is not None: @@ -458,9 +454,7 @@ def function_cast(self, term: Term) -> Term: DatetimeFieldQueryValueType = TypeVar( "DatetimeFieldQueryValueType", datetime.datetime, int, float, str ) -DateFieldQueryValueType = TypeVar( - "DateFieldQueryValueType", datetime.date, int, float, str -) +DateFieldQueryValueType = TypeVar("DateFieldQueryValueType", datetime.date, int, float, str) class DatetimeField(Field[T_DATETIME], datetime.datetime): @@ -510,9 +504,7 @@ def __init__( **kwargs: Unpack[FieldKwargs], ) -> None: ... - def __init__( - self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any - ) -> None: + def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any) -> None: if auto_now_add and auto_now: raise ConfigurationError("You can choose only 'auto_now' or 'auto_now_add'") super().__init__(**kwargs) @@ -652,9 +644,7 @@ def __init__( **kwargs: Unpack[FieldKwargs], ) -> None: ... - def __init__( - self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any - ) -> None: + def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any) -> None: if auto_now_add and auto_now: raise ConfigurationError("You can choose only 'auto_now' or 'auto_now_add'") super().__init__(**kwargs) @@ -753,9 +743,7 @@ def to_db_value( if value is None: return None - return ( - (value.days * 86400000000) + (value.seconds * 1000000) + value.microseconds - ) + return (value.days * 86400000000) + (value.seconds * 1000000) + value.microseconds class FloatField(Field[T_FLOAT], float): @@ -919,9 +907,7 @@ def __init__( ) -> None: ... def __init__(self, **kwargs: Any) -> None: - if ( - kwargs.get("primary_key") or kwargs.get("pk", False) - ) and "default" not in kwargs: + if (kwargs.get("primary_key") or kwargs.get("pk", False)) and "default" not in kwargs: kwargs["default"] = uuid4 super().__init__(**kwargs) @@ -996,9 +982,7 @@ def __init__( # Automatic description for the field if not specified by the user if description is None: - description = "\n".join([f"{e.name}: {int(e.value)}" for e in enum_type])[ - :2048 - ] + description = "\n".join([f"{e.name}: {int(e.value)}" for e in enum_type])[:2048] super().__init__(description=description, **kwargs) self.enum_type = enum_type @@ -1007,9 +991,7 @@ def to_python_value(self, value: int | None) -> IntEnum | None: value = self.enum_type(value) if value is not None else None return value - def to_db_value( - self, value: IntEnum | None | int, instance: type[Model] | Model - ) -> int | None: + def to_db_value(self, value: IntEnum | None | int, instance: type[Model] | Model) -> int | None: if isinstance(value, IntEnum): value = int(value.value) if isinstance(value, int): @@ -1056,9 +1038,7 @@ def __init__( ) -> None: # Automatic description for the field if not specified by the user if description is None: - description = "\n".join([f"{e.name}: {str(e.value)}" for e in enum_type])[ - :2048 - ] + description = "\n".join([f"{e.name}: {str(e.value)}" for e in enum_type])[:2048] # Automatic CharField max_length if max_length == 0: @@ -1073,9 +1053,7 @@ def __init__( def to_python_value(self, value: str | None) -> Enum | None: return self.enum_type(value) if value is not None else None - def to_db_value( - self, value: Enum | None | str, instance: type[Model] | Model - ) -> str | None: + def to_db_value(self, value: Enum | None | str, instance: type[Model] | Model) -> str | None: self.validate(value) if isinstance(value, Enum): return str(value.value) diff --git a/tortoise/fields/relational.py b/tortoise/fields/relational.py index 617e3e869..8523efa18 100644 --- a/tortoise/fields/relational.py +++ b/tortoise/fields/relational.py @@ -133,9 +133,7 @@ def offset(self, offset: int) -> QuerySet[MODEL]: """ return self._query.offset(offset) - async def create( - self, using_db: BaseDBAsyncClient | None = None, **kwargs: Any - ) -> MODEL: + async def create(self, using_db: BaseDBAsyncClient | None = None, **kwargs: Any) -> MODEL: """ Create a related record in the DB and returns the object, automatically setting the foreign key relationship to the parent instance. @@ -167,9 +165,7 @@ async def create( # Call remote model's create method return await self.remote_model.create(using_db=using_db, **kwargs) - def _set_result_for_query( - self, sequence: list[MODEL], attr: str | None = None - ) -> None: + def _set_result_for_query(self, sequence: list[MODEL], attr: str | None = None) -> None: self._fetched = True self.related_objects = sequence if attr: @@ -187,18 +183,12 @@ class ManyToManyRelation(ReverseRelation[MODEL]): Many-to-many relation container for :func:`.ManyToManyField`. """ - def __init__( - self, instance: Model, m2m_field: ManyToManyFieldInstance[MODEL] - ) -> None: - super().__init__( - m2m_field.related_model, m2m_field.related_name, instance, "pk" - ) + def __init__(self, instance: Model, m2m_field: ManyToManyFieldInstance[MODEL]) -> None: + super().__init__(m2m_field.related_model, m2m_field.related_name, instance, "pk") self.field = m2m_field self.instance = instance - async def add( - self, *instances: MODEL, using_db: BaseDBAsyncClient | None = None - ) -> None: + async def add(self, *instances: MODEL, using_db: BaseDBAsyncClient | None = None) -> None: """ Adds one or more of ``instances`` to the relation. @@ -217,9 +207,7 @@ async def add( pks_f: list = [] for instance_to_add in instances: if not instance_to_add._saved_in_db: - raise OperationalError( - f"You should first call .save() on {instance_to_add}" - ) + raise OperationalError(f"You should first call .save() on {instance_to_add}") pk_f = related_pk_formatting_func(instance_to_add.pk, instance_to_add) pks_f.append(pk_f) through_table = Table(self.field.through, schema=self.field.through_schema) @@ -229,13 +217,9 @@ async def add( through_table[forward_key], ) select_query = ( - db.query_class.from_(through_table) - .where(backward_field == pk_b) - .select(forward_key) - ) - criterion = ( - forward_field == pks_f[0] if len(pks_f) == 1 else forward_field.isin(pks_f) + db.query_class.from_(through_table).where(backward_field == pk_b).select(forward_key) ) + criterion = forward_field == pks_f[0] if len(pks_f) == 1 else forward_field.isin(pks_f) select_query = select_query.where(criterion) _, already_existing_relations_raw = await db.execute_query( @@ -247,9 +231,7 @@ async def add( } if pks_f_to_insert := set(pks_f) - already_existing_forward_pks: - query = db.query_class.into(through_table).columns( - forward_field, backward_field - ) + query = db.query_class.into(through_table).columns(forward_field, backward_field) for pk_f in pks_f_to_insert: query = query.insert(pk_f, pk_b) await db.execute_query(*query.get_parameterized_sql()) @@ -260,9 +242,7 @@ async def clear(self, using_db: BaseDBAsyncClient | None = None) -> None: """ await self._remove_or_clear(using_db=using_db) - async def remove( - self, *instances: MODEL, using_db: BaseDBAsyncClient | None = None - ) -> None: + async def remove(self, *instances: MODEL, using_db: BaseDBAsyncClient | None = None) -> None: """ Removes one or more of ``instances`` from the relation. @@ -287,9 +267,9 @@ async def _remove_or_clear( if instances: related_pk_formatting_func = type(instances[0])._meta.pk.to_db_value if len(instances) == 1: - condition &= through_table[ - self.field.forward_key - ] == related_pk_formatting_func(instances[0].pk, instances[0]) + condition &= through_table[self.field.forward_key] == related_pk_formatting_func( + instances[0].pk, instances[0] + ) else: condition &= through_table[self.field.forward_key].isin( [related_pk_formatting_func(i.pk, i) for i in instances] @@ -317,9 +297,7 @@ def __init__( if TYPE_CHECKING: @overload - def __get__( - self, instance: None, owner: type[Model] - ) -> RelationalField[MODEL]: ... + def __get__(self, instance: None, owner: type[Model]) -> RelationalField[MODEL]: ... @overload def __get__(self, instance: Model, owner: type[Model]) -> MODEL: ... @@ -348,9 +326,7 @@ def validate_model_name(cls, model_name: str | type[Model]) -> None: ) from None elif len(model_name.split(".")) != 2: field_type = cls.__name__.replace("Instance", "") - raise ConfigurationError( - f'{field_type} accepts model name in format "app.Model"' - ) + raise ConfigurationError(f'{field_type} accepts model name in format "app.Model"') class ForeignKeyFieldInstance(RelationalField[MODEL]): @@ -370,9 +346,7 @@ def __init__( "on_delete can only be CASCADE, RESTRICT, SET_NULL, SET_DEFAULT or NO_ACTION" ) if on_delete == SET_NULL and not bool(kwargs.get("null")): - raise ConfigurationError( - "If on_delete is SET_NULL, then field must have null=True set" - ) + raise ConfigurationError("If on_delete is SET_NULL, then field must have null=True set") self.on_delete = on_delete def describe(self, serializable: bool) -> dict: From 64eac3cbd78d46be84879962fb28e536c29ba2fe Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sat, 1 Aug 2026 17:49:06 +0900 Subject: [PATCH 09/22] refactor: update type hints for field kwargs to use generic types / Restored `TextField` not nullable because it's not supported on actual database --- tortoise/fields/base.py | 20 +++++------ tortoise/fields/data.py | 75 ++++++++++++++--------------------------- 2 files changed, 36 insertions(+), 59 deletions(-) diff --git a/tortoise/fields/base.py b/tortoise/fields/base.py index bd6562c0f..4f996197b 100644 --- a/tortoise/fields/base.py +++ b/tortoise/fields/base.py @@ -90,7 +90,7 @@ class OnDelete(StrEnum): NO_ACTION = OnDelete.NO_ACTION -class _FieldKwargsCommon(TypedDict, total=False): +class _FieldKwargsCommon(Generic[VALUE], TypedDict, total=False): """:class:`Field` constructor arguments that are never declared as explicit parameters. Used with :data:`typing.Unpack` to give ``**kwargs`` explicit type hints. This is the @@ -100,8 +100,8 @@ class _FieldKwargsCommon(TypedDict, total=False): source_field: str | None generated: bool - default: Any - db_default: Any + default: VALUE + db_default: VALUE description: str | None model: Model | None validators: list[Validator | Callable] @@ -109,7 +109,7 @@ class _FieldKwargsCommon(TypedDict, total=False): index: bool # deprecated alias for db_index -class _FieldKwargsNoPk(_FieldKwargsCommon, total=False): +class _FieldKwargsNoPk(_FieldKwargsCommon[VALUE], total=False): """Common arguments excluding ``primary_key`` and ``null``. For constructors that declare ``primary_key`` and ``null`` as explicit parameters @@ -120,7 +120,7 @@ class _FieldKwargsNoPk(_FieldKwargsCommon, total=False): db_index: bool | None -class FieldKwargs(_FieldKwargsNoPk, total=False): +class FieldKwargs(_FieldKwargsNoPk[VALUE], total=False): """Common arguments excluding ``null``. For constructors that declare only ``null`` as an explicit parameter (the majority). @@ -129,7 +129,7 @@ class FieldKwargs(_FieldKwargsNoPk, total=False): primary_key: bool | None -class JSONFieldKwargs(FieldKwargs, total=False): +class JSONFieldKwargs(FieldKwargs[VALUE], total=False): """Constructor arguments for :class:`JSONField`. ``JSONField`` declares neither ``null`` nor ``primary_key`` explicitly, and also accepts @@ -140,7 +140,7 @@ class JSONFieldKwargs(FieldKwargs, total=False): field_type: Any -class RelationalFieldKwargs(FieldKwargs, total=False): +class RelationalFieldKwargs(FieldKwargs[VALUE], total=False): """Constructor arguments for :func:`ForeignKeyField` and :func:`OneToOneField`. Extends the common :class:`~tortoise.fields.base.FieldKwargs` with ``to_field``. @@ -150,7 +150,7 @@ class RelationalFieldKwargs(FieldKwargs, total=False): to_field: str | None -class ManyToManyFieldKwargs(_FieldKwargsCommon, total=False): +class ManyToManyFieldKwargs(_FieldKwargsCommon[VALUE], total=False): """Constructor arguments for :func:`ManyToManyField`. ``unique`` is declared as an explicit parameter, so it is omitted here; the deprecated @@ -293,8 +293,8 @@ def __init__( generated: bool = False, primary_key: bool | None = None, null: bool = False, - default: Any = None, - db_default: Any = DB_DEFAULT_NOT_SET, + default: VALUE = None, + db_default: VALUE = DB_DEFAULT_NOT_SET, unique: bool = False, db_index: bool | None = None, description: str | None = None, diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index ce297e002..1db19b067 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -122,7 +122,7 @@ def __init__( primary_key: bool | None = None, *, null: Literal[False] = False, - **kwargs: Unpack[_FieldKwargsNoPk], + **kwargs: Unpack[_FieldKwargsNoPk[T_INT]], ) -> None: ... @overload @@ -131,7 +131,7 @@ def __init__( primary_key: bool | None = None, *, null: Literal[True], - **kwargs: Unpack[_FieldKwargsNoPk], + **kwargs: Unpack[_FieldKwargsNoPk[T_INT]], ) -> None: ... def __init__(self, primary_key: bool | None = None, **kwargs: Any) -> None: @@ -241,7 +241,7 @@ def __init__( max_length: int, *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_STR]], ) -> None: ... @overload @@ -250,7 +250,7 @@ def __init__( max_length: int, *, null: Literal[True], - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_STR]], ) -> None: ... def __init__(self, max_length: int, **kwargs: Any) -> None: @@ -279,7 +279,7 @@ def SQL_TYPE(self) -> str: return f"NVARCHAR2({self.field.max_length})" -class TextField(Field[T_STR], str): # type: ignore +class TextField(Field[str], str): # type: ignore """ Large Text field. """ @@ -287,42 +287,19 @@ class TextField(Field[T_STR], str): # type: ignore indexable = False SQL_TYPE = "TEXT" - @overload - def __init__( - self: TextField[str], - *, - primary_key: bool | None = None, - unique: bool = False, - db_index: bool = False, - null: Literal[False] = False, - **kwargs: Unpack[_FieldKwargsCommon], - ) -> None: ... - - @overload - def __init__( - self: TextField[str | None], - *, - primary_key: bool | None = None, - unique: bool = False, - db_index: bool = False, - null: Literal[True], - **kwargs: Unpack[_FieldKwargsCommon], - ) -> None: ... - def __init__( self, - primary_key: bool | None = None, - unique: bool = False, - db_index: bool = False, - **kwargs: Any, + **kwargs: Unpack[FieldKwargs], ) -> None: - if primary_key or kwargs.get("pk"): + db_index = kwargs.pop("db_index") + + if kwargs.get("primary_key") or kwargs.get("pk"): warnings.warn( "TextField as a PrimaryKey is Deprecated, use CharField instead", DeprecationWarning, stacklevel=2, ) - if unique: + if kwargs.get("unique"): raise ConfigurationError( "TextField doesn't support unique indexes, consider CharField or another strategy" ) @@ -337,7 +314,7 @@ def __init__( elif db_index: raise ConfigurationError("TextField can't be indexed, consider CharField") - super().__init__(primary_key=primary_key, **kwargs) + super().__init__(**kwargs) class _db_mysql: SQL_TYPE = "LONGTEXT" @@ -408,7 +385,7 @@ def __init__( decimal_places: int, *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_DECIMAL]], ) -> None: ... @overload @@ -418,7 +395,7 @@ def __init__( decimal_places: int, *, null: Literal[True], - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_DECIMAL]], ) -> None: ... def __init__(self, max_digits: int, decimal_places: int, **kwargs: Any) -> None: @@ -491,7 +468,7 @@ def __init__( auto_now_add: bool = False, *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_DATETIME]], ) -> None: ... @overload @@ -501,7 +478,7 @@ def __init__( auto_now_add: bool = False, *, null: Literal[True], - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_DATETIME]], ) -> None: ... def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any) -> None: @@ -584,7 +561,7 @@ def __init__( self: DateField[datetime.date], *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_DATE]], ) -> None: ... @overload @@ -592,7 +569,7 @@ def __init__( self: DateField[datetime.date | None], *, null: Literal[True], - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_DATE]], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -714,7 +691,7 @@ def __init__( self: TimeDeltaField[datetime.timedelta], *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_TIMEDELTA]], ) -> None: ... @overload @@ -722,7 +699,7 @@ def __init__( self: TimeDeltaField[datetime.timedelta | None], *, null: Literal[True], - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_TIMEDELTA]], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -758,7 +735,7 @@ def __init__( self: FloatField[float], *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_FLOAT]], ) -> None: ... @overload @@ -766,7 +743,7 @@ def __init__( self: FloatField[float | None], *, null: Literal[True], - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_FLOAT]], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -819,7 +796,7 @@ def __init__( self, encoder: JsonDumpsFunc = JSON_DUMPS, decoder: JsonLoadsFunc = JSON_LOADS, - **kwargs: Unpack[JSONFieldKwargs], + **kwargs: Unpack[JSONFieldKwargs[T]], ) -> None: super().__init__(**kwargs) self.encoder = encoder @@ -895,7 +872,7 @@ def __init__( self: UUIDField[UUID], *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_UUID]], ) -> None: ... @overload @@ -903,7 +880,7 @@ def __init__( self: UUIDField[UUID | None], *, null: Literal[True], - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_UUID]], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -936,7 +913,7 @@ def __init__( self: BinaryField[bytes], *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_BINARY]], ) -> None: ... @overload @@ -944,7 +921,7 @@ def __init__( self: BinaryField[bytes | None], *, null: Literal[True], - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[T_BINARY]], ) -> None: ... def __init__(self, **kwargs: Any) -> None: From 6dcf541a99786931db2005695168321fe5a11171 Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sat, 1 Aug 2026 18:02:49 +0900 Subject: [PATCH 10/22] made style --- docs/conf.py | 4 +- docs/sphinx_autodoc_typehints.py | 222 +++++++++++++++++-------------- 2 files changed, 123 insertions(+), 103 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index dc3829e78..d58fdffaa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -203,9 +203,7 @@ def get_version_info(): # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', # 'searchbox.html']``. # -html_sidebars = { - "**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"] -} +html_sidebars = {"**": ["logo-text.html", "globaltoc.html", "localtoc.html", "searchbox.html"]} # -- Options for HTMLHelp output --------------------------------------------- diff --git a/docs/sphinx_autodoc_typehints.py b/docs/sphinx_autodoc_typehints.py index f8c0dde56..b317c38cc 100644 --- a/docs/sphinx_autodoc_typehints.py +++ b/docs/sphinx_autodoc_typehints.py @@ -15,25 +15,33 @@ Protocol = None logger = logging.getLogger(__name__) -pydata_annotations = {'Any', 'AnyStr', 'Callable', 'ClassVar', 'NoReturn', 'Optional', 'Tuple', - 'Union'} +pydata_annotations = { + "Any", + "AnyStr", + "Callable", + "ClassVar", + "NoReturn", + "Optional", + "Tuple", + "Union", +} def format_annotation(annotation, fully_qualified=False): - if inspect.isclass(annotation) and annotation.__module__ == 'builtins': - if annotation.__qualname__ == 'NoneType': - return '``None``' + if inspect.isclass(annotation) and annotation.__module__ == "builtins": + if annotation.__qualname__ == "NoneType": + return "``None``" else: - return f':py:class:`{annotation.__qualname__}`' + return f":py:class:`{annotation.__qualname__}`" annotation_cls = annotation if inspect.isclass(annotation) else type(annotation) - if annotation_cls.__module__ == 'typing': - class_name = str(annotation).split('[')[0].split('.')[-1] + if annotation_cls.__module__ == "typing": + class_name = str(annotation).split("[")[0].split(".")[-1] params = None - module = 'typing' - extra = '' + module = "typing" + extra = "" - origin = getattr(annotation, '__origin__', None) + origin = getattr(annotation, "__origin__", None) if inspect.isclass(origin): annotation_cls = annotation.__origin__ try: @@ -44,13 +52,13 @@ def format_annotation(annotation, fully_qualified=False): pass # annotation_cls was either the "type" object or typing.Type if annotation is Any: - return ':py:data:`{}typing.Any`'.format("" if fully_qualified else "~") + return ":py:data:`{}typing.Any`".format("" if fully_qualified else "~") elif annotation is AnyStr: - return ':py:data:`{}typing.AnyStr`'.format("" if fully_qualified else "~") + return ":py:data:`{}typing.AnyStr`".format("" if fully_qualified else "~") elif isinstance(annotation, TypeVar): bound = annotation.__bound__ if bound: - if 'ForwardRef(' in str(bound): + if "ForwardRef(" in str(bound): try: bound = bound._evaluate(sys.modules[annotation.__module__].__dict__, None) except: @@ -59,28 +67,34 @@ def format_annotation(annotation, fully_qualified=False): except: bound = bound.__forward_arg__ return format_annotation(bound, fully_qualified) - return f'\\{annotation!r}' - elif (annotation is Union or getattr(annotation, '__origin__', None) is Union or - hasattr(annotation, '__union_params__')): - if hasattr(annotation, '__union_params__'): + return f"\\{annotation!r}" + elif ( + annotation is Union + or getattr(annotation, "__origin__", None) is Union + or hasattr(annotation, "__union_params__") + ): + if hasattr(annotation, "__union_params__"): params = annotation.__union_params__ - elif hasattr(annotation, '__args__'): + elif hasattr(annotation, "__args__"): params = annotation.__args__ - if params and len(params) == 2 and (hasattr(params[1], '__qualname__') and - params[1].__qualname__ == 'NoneType'): - class_name = 'Optional' + if ( + params + and len(params) == 2 + and (hasattr(params[1], "__qualname__") and params[1].__qualname__ == "NoneType") + ): + class_name = "Optional" params = (params[0],) - elif annotation_cls.__qualname__ == 'Tuple' and hasattr(annotation, '__tuple_params__'): + elif annotation_cls.__qualname__ == "Tuple" and hasattr(annotation, "__tuple_params__"): params = annotation.__tuple_params__ if annotation.__tuple_use_ellipsis__: params += (Ellipsis,) - elif annotation_cls.__qualname__ == 'Callable': + elif annotation_cls.__qualname__ == "Callable": arg_annotations = result_annotation = None - if hasattr(annotation, '__result__'): + if hasattr(annotation, "__result__"): arg_annotations = annotation.__args__ result_annotation = annotation.__result__ - elif getattr(annotation, '__args__', None): + elif getattr(annotation, "__args__", None): arg_annotations = annotation.__args__[:-1] result_annotation = annotation.__args__[-1] @@ -88,66 +102,74 @@ def format_annotation(annotation, fully_qualified=False): params = [Ellipsis, result_annotation] elif arg_annotations is not None: params = [ - '\\[{}]'.format( - ', '.join( - format_annotation(param, fully_qualified) - for param in arg_annotations)), - result_annotation + "\\[{}]".format( + ", ".join( + format_annotation(param, fully_qualified) for param in arg_annotations + ) + ), + result_annotation, ] - elif str(annotation).startswith('typing.ClassVar[') and hasattr(annotation, '__type__'): + elif str(annotation).startswith("typing.ClassVar[") and hasattr(annotation, "__type__"): # < py3.7 params = (annotation.__type__,) - elif hasattr(annotation, 'type_var'): + elif hasattr(annotation, "type_var"): # Type alias class_name = annotation.name params = (annotation.type_var,) - elif getattr(annotation, '__args__', None) is not None: + elif getattr(annotation, "__args__", None) is not None: params = annotation.__args__ - elif hasattr(annotation, '__parameters__'): + elif hasattr(annotation, "__parameters__"): params = annotation.__parameters__ if params: - extra = '\\[{}]'.format(', '.join( - format_annotation(param, fully_qualified) for param in params)) + extra = "\\[{}]".format( + ", ".join(format_annotation(param, fully_qualified) for param in params) + ) - return '{prefix}`{qualify}{module}.{name}`{extra}'.format( - prefix=':py:data:' if class_name in pydata_annotations else ':py:class:', + return "{prefix}`{qualify}{module}.{name}`{extra}".format( + prefix=":py:data:" if class_name in pydata_annotations else ":py:class:", qualify="" if fully_qualified else "~", module=module, name=class_name, - extra=extra + extra=extra, ) elif annotation is Ellipsis: - return '...' - elif (inspect.isfunction(annotation) and annotation.__module__ == 'typing' and - hasattr(annotation, '__name__') and hasattr(annotation, '__supertype__')): - return ':py:func:`{qualify}typing.NewType`\\(:py:data:`~{name}`, {extra})'.format( + return "..." + elif ( + inspect.isfunction(annotation) + and annotation.__module__ == "typing" + and hasattr(annotation, "__name__") + and hasattr(annotation, "__supertype__") + ): + return ":py:func:`{qualify}typing.NewType`\\(:py:data:`~{name}`, {extra})".format( qualify="" if fully_qualified else "~", name=annotation.__name__, extra=format_annotation(annotation.__supertype__, fully_qualified), ) - elif inspect.isclass(annotation) or inspect.isclass(getattr(annotation, '__origin__', None)): + elif inspect.isclass(annotation) or inspect.isclass(getattr(annotation, "__origin__", None)): if not inspect.isclass(annotation): annotation_cls = annotation.__origin__ - extra = '' + extra = "" try: mro = annotation_cls.mro() except TypeError: pass else: if Generic in mro or (Protocol and Protocol in mro): - params = (getattr(annotation, '__parameters__', None) or - getattr(annotation, '__args__', None)) + params = getattr(annotation, "__parameters__", None) or getattr( + annotation, "__args__", None + ) if params: - extra = '\\[{}]'.format(', '.join( - format_annotation(param, fully_qualified) for param in params)) + extra = "\\[{}]".format( + ", ".join(format_annotation(param, fully_qualified) for param in params) + ) - return ':py:class:`{qualify}{module}.{name}`{extra}'.format( + return ":py:class:`{qualify}{module}.{name}`{extra}".format( qualify="" if fully_qualified else "~", module=annotation.__module__, name=annotation_cls.__qualname__, - extra=extra + extra=extra, ) return str(annotation) @@ -157,31 +179,31 @@ def process_signature(app, what: str, name: str, obj, options, signature, return if not callable(obj): return - if what in ('class', 'exception'): - obj = getattr(obj, '__init__', getattr(obj, '__new__', None)) + if what in ("class", "exception"): + obj = getattr(obj, "__init__", getattr(obj, "__new__", None)) - if not getattr(obj, '__annotations__', None): + if not getattr(obj, "__annotations__", None): return obj = inspect.unwrap(obj) signature = Signature(obj) parameters = [ - param.replace(annotation=inspect.Parameter.empty) - for param in signature.parameters.values() + param.replace(annotation=inspect.Parameter.empty) for param in signature.parameters.values() ] - if '' in obj.__qualname__: + if "" in obj.__qualname__: logger.warning( 'Cannot treat a function defined as a local function: "%s" (use @functools.wraps)', - name) + name, + ) return if parameters: - if what in ('class', 'exception'): + if what in ("class", "exception"): del parameters[0] - elif what == 'method': + elif what == "method": outer = inspect.getmodule(obj) - for clsname in obj.__qualname__.split('.')[:-1]: + for clsname in obj.__qualname__.split(".")[:-1]: outer = getattr(outer, clsname) method_name = obj.__name__ @@ -189,18 +211,16 @@ def process_signature(app, what: str, name: str, obj, options, signature, return # If the method starts with double underscore (dunder) # Python applies mangling so we need to prepend the class name. # This doesn't happen if it always ends with double underscore. - class_name = obj.__qualname__.split('.')[-2] + class_name = obj.__qualname__.split(".")[-2] method_name = f"_{class_name}{method_name}" method_object = outer.__dict__[method_name] if outer else obj if not isinstance(method_object, (classmethod, staticmethod)): del parameters[0] - signature = signature.replace( - parameters=parameters, - return_annotation=inspect.Signature.empty) + signature = signature.replace(parameters=parameters, return_annotation=inspect.Signature.empty) - return stringify_signature(signature).replace('\\', '\\\\'), None + return stringify_signature(signature).replace("\\", "\\\\"), None def get_all_type_hints(obj, name): @@ -216,8 +236,9 @@ def get_all_type_hints(obj, name): try: rv = get_type_hints(obj, localns=type_globals.__dict__) except Exception as exc: - logger.warning('Cannot resolve forward reference in type annotations of "%s": %s', - name, exc) + logger.warning( + 'Cannot resolve forward reference in type annotations of "%s": %s', name, exc + ) rv = obj.__annotations__ if rv: @@ -238,8 +259,9 @@ def get_all_type_hints(obj, name): try: rv = get_type_hints(obj, localns=type_globals.__dict__) except: - logger.warning('Cannot resolve forward reference in type annotations of "%s": %s', - name, exc) + logger.warning( + 'Cannot resolve forward reference in type annotations of "%s": %s', name, exc + ) rv = obj.__annotations__ return rv @@ -248,14 +270,15 @@ def get_all_type_hints(obj, name): def backfill_type_hints(obj, name): import ast - parse_kwargs = {'type_comments': True} + parse_kwargs = {"type_comments": True} def _one_child(module): children = module.body # use the body to ignore type comments if len(children) != 1: logger.warning( - 'Did not get exactly one node from AST for "%s", got %s', name, len(children)) + 'Did not get exactly one node from AST for "%s", got %s', name, len(children) + ) return return children[0] @@ -278,14 +301,14 @@ def _one_child(module): return {} try: - comment_args_str, comment_returns = type_comment.split(' -> ') + comment_args_str, comment_returns = type_comment.split(" -> ") except ValueError: logger.warning('Unparsable type hint comment for "%s": Expected to contain ` -> `', name) return {} rv = {} if comment_returns: - rv['return'] = comment_returns + rv["return"] = comment_returns args = load_args(obj_ast) comment_args = split_type_comment_args(comment_args_str) @@ -317,7 +340,7 @@ def _one_child(module): def load_args(obj_ast): func_args = obj_ast.args args = [] - pos_only = getattr(func_args, 'posonlyargs', None) + pos_only = getattr(func_args, "posonlyargs", None) if pos_only: args.extend(pos_only) @@ -351,7 +374,7 @@ def add(val): add(comment[start_arg_at:at]) start_arg_at = at + 1 - add(comment[start_arg_at: at + 1]) + add(comment[start_arg_at : at + 1]) return result @@ -360,22 +383,23 @@ def process_docstring(app, what, name, obj, options, lines): obj = obj.fget if callable(obj): - if what in ('class', 'exception'): - obj = getattr(obj, '__init__') + if what in ("class", "exception"): + obj = getattr(obj, "__init__") obj = inspect.unwrap(obj) type_hints = get_all_type_hints(obj, name) for argname, annotation in type_hints.items(): - if argname == 'return': + if argname == "return": continue # this is handled separately later - if argname.endswith('_'): - argname = f'{argname[:-1]}\\_' + if argname.endswith("_"): + argname = f"{argname[:-1]}\\_" formatted_annotation = format_annotation( - annotation, fully_qualified=app.config.typehints_fully_qualified) + annotation, fully_qualified=app.config.typehints_fully_qualified + ) - searchfor = f':param {argname}:' + searchfor = f":param {argname}:" insert_index = None for i, line in enumerate(lines): @@ -388,31 +412,29 @@ def process_docstring(app, what, name, obj, options, lines): insert_index = len(lines) if insert_index is not None: - lines.insert( - insert_index, - f':type {argname}: {formatted_annotation}' - ) + lines.insert(insert_index, f":type {argname}: {formatted_annotation}") - if 'return' in type_hints and what not in ('class', 'exception'): + if "return" in type_hints and what not in ("class", "exception"): formatted_annotation = format_annotation( - type_hints['return'], fully_qualified=app.config.typehints_fully_qualified) + type_hints["return"], fully_qualified=app.config.typehints_fully_qualified + ) insert_index = len(lines) for i, line in enumerate(lines): - if line.startswith(':rtype:'): + if line.startswith(":rtype:"): insert_index = None break - elif line.startswith(':return:') or line.startswith(':returns:'): + elif line.startswith(":return:") or line.startswith(":returns:"): insert_index = i if insert_index is not None: if insert_index == len(lines): # Ensure that :rtype: doesn't get joined with a paragraph of text, which # prevents it being interpreted. - lines.append('') + lines.append("") insert_index += 1 - lines.insert(insert_index, f':rtype: {formatted_annotation}') + lines.insert(insert_index, f":rtype: {formatted_annotation}") def builder_ready(app): @@ -421,10 +443,10 @@ def builder_ready(app): def setup(app): - app.add_config_value('set_type_checking_flag', False, 'html') - app.add_config_value('always_document_param_types', False, 'html') - app.add_config_value('typehints_fully_qualified', False, 'env') - app.connect('builder-inited', builder_ready) - app.connect('autodoc-process-signature', process_signature) - app.connect('autodoc-process-docstring', process_docstring) + app.add_config_value("set_type_checking_flag", False, "html") + app.add_config_value("always_document_param_types", False, "html") + app.add_config_value("typehints_fully_qualified", False, "env") + app.connect("builder-inited", builder_ready) + app.connect("autodoc-process-signature", process_signature) + app.connect("autodoc-process-docstring", process_docstring) return dict(parallel_read_safe=True) From 51d0c9053e9cc3343a96d63f542efca297651ff2 Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sat, 1 Aug 2026 18:06:04 +0900 Subject: [PATCH 11/22] - removed unused import - trying fix for TypedDicct iinheritance error --- tortoise/fields/base.py | 2 +- tortoise/fields/data.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tortoise/fields/base.py b/tortoise/fields/base.py index 4f996197b..f8458ec68 100644 --- a/tortoise/fields/base.py +++ b/tortoise/fields/base.py @@ -90,7 +90,7 @@ class OnDelete(StrEnum): NO_ACTION = OnDelete.NO_ACTION -class _FieldKwargsCommon(Generic[VALUE], TypedDict, total=False): +class _FieldKwargsCommon(TypedDict, Generic[VALUE], total=False): """:class:`Field` constructor arguments that are never declared as explicit parameters. Used with :data:`typing.Unpack` to give ``**kwargs`` explicit type hints. This is the diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index 1db19b067..ff6fa4bf2 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -21,7 +21,6 @@ Field, FieldKwargs, JSONFieldKwargs, - _FieldKwargsCommon, _FieldKwargsNoPk, ) from tortoise.timezone import get_default_timezone, get_timezone, get_use_tz, localtime From c740b0604733423796c18899c7af29050233ca4e Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sat, 1 Aug 2026 18:41:04 +0900 Subject: [PATCH 12/22] Removed positional pk warning test with `TextField` as updated TextField to only support leyword arguments --- tests/test_primary_key.py | 4 ---- tortoise/fields/data.py | 10 ++++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/test_primary_key.py b/tests/test_primary_key.py index d4033f5cb..5d1e97fb3 100644 --- a/tests/test_primary_key.py +++ b/tests/test_primary_key.py @@ -249,10 +249,6 @@ def test_warning(self): with pytest.warns(DeprecationWarning, match=self.message): f = fields.TextField(primary_key=True) assert f.pk is True - # Positional arg goes to primary_key, so only TextField as PK warning - with pytest.warns(DeprecationWarning, match=self.message): - f = fields.TextField(True) - assert f.pk is True @pytest.mark.asyncio async def test_pk_alias_warning(self): diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index ff6fa4bf2..ddfc667de 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -288,17 +288,19 @@ class TextField(Field[str], str): # type: ignore def __init__( self, - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[str]], ) -> None: - db_index = kwargs.pop("db_index") + primary_key = kwargs.get("primary_key", None) + db_index = kwargs.get("db_index", False) + unique = kwargs.get("unique", False) - if kwargs.get("primary_key") or kwargs.get("pk"): + if primary_key or kwargs.get("pk"): warnings.warn( "TextField as a PrimaryKey is Deprecated, use CharField instead", DeprecationWarning, stacklevel=2, ) - if kwargs.get("unique"): + if unique: raise ConfigurationError( "TextField doesn't support unique indexes, consider CharField or another strategy" ) From 67248467a4ada3bcb2fd7a3160fba253c896145a Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sat, 1 Aug 2026 18:53:31 +0900 Subject: [PATCH 13/22] Passed all tests for sqlite --- tests/test_primary_key.py | 3 +++ tortoise/fields/data.py | 12 ++++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_primary_key.py b/tests/test_primary_key.py index 5d1e97fb3..7b306d36c 100644 --- a/tests/test_primary_key.py +++ b/tests/test_primary_key.py @@ -249,6 +249,9 @@ def test_warning(self): with pytest.warns(DeprecationWarning, match=self.message): f = fields.TextField(primary_key=True) assert f.pk is True + with pytest.warns(DeprecationWarning, match=self.message): + f = fields.TextField(True) + assert f.pk is True @pytest.mark.asyncio async def test_pk_alias_warning(self): diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index ddfc667de..278f881ba 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -22,6 +22,7 @@ FieldKwargs, JSONFieldKwargs, _FieldKwargsNoPk, + _FieldKwargsCommon ) from tortoise.timezone import get_default_timezone, get_timezone, get_use_tz, localtime from tortoise.validators import MaxLengthValidator @@ -288,12 +289,11 @@ class TextField(Field[str], str): # type: ignore def __init__( self, - **kwargs: Unpack[FieldKwargs[str]], + primary_key: bool | None = None, + unique: bool = False, + db_index: bool = False, + **kwargs: Unpack[_FieldKwargsCommon[str]], ) -> None: - primary_key = kwargs.get("primary_key", None) - db_index = kwargs.get("db_index", False) - unique = kwargs.get("unique", False) - if primary_key or kwargs.get("pk"): warnings.warn( "TextField as a PrimaryKey is Deprecated, use CharField instead", @@ -315,7 +315,7 @@ def __init__( elif db_index: raise ConfigurationError("TextField can't be indexed, consider CharField") - super().__init__(**kwargs) + super().__init__(primary_key=primary_key, **kwargs) class _db_mysql: SQL_TYPE = "LONGTEXT" From ef7d8f48d46e8d86ad7c5c5899d6d80788e21a82 Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sat, 1 Aug 2026 18:55:01 +0900 Subject: [PATCH 14/22] Style fixed --- tortoise/fields/data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index 278f881ba..57b5f4484 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -21,8 +21,8 @@ Field, FieldKwargs, JSONFieldKwargs, + _FieldKwargsCommon, _FieldKwargsNoPk, - _FieldKwargsCommon ) from tortoise.timezone import get_default_timezone, get_timezone, get_use_tz, localtime from tortoise.validators import MaxLengthValidator From 79431d3836e649d066b6154f7bb1a90af1592863 Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sat, 1 Aug 2026 19:26:44 +0900 Subject: [PATCH 15/22] TextField null error fixed --- tortoise/fields/base.py | 10 ++++++---- tortoise/fields/data.py | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tortoise/fields/base.py b/tortoise/fields/base.py index f8458ec68..289a6688d 100644 --- a/tortoise/fields/base.py +++ b/tortoise/fields/base.py @@ -12,12 +12,14 @@ from pypika_tortoise.terms import Term from tortoise.exceptions import ConfigurationError, ValidationError +from tortoise.fields.db_defaults import SqlDefault from tortoise.validators import Validator if TYPE_CHECKING: # pragma: nocoverage from tortoise.models import Model if sys.version_info >= (3, 11): + from collections.abc import Awaitable from enum import StrEnum from typing import Self else: # pragma: no cover @@ -100,8 +102,8 @@ class _FieldKwargsCommon(TypedDict, Generic[VALUE], total=False): source_field: str | None generated: bool - default: VALUE - db_default: VALUE + default: VALUE | Callable[..., VALUE | Awaitable[VALUE]] | None + db_default: VALUE | SqlDefault | _DB_DEFAULT_NOT_SET description: str | None model: Model | None validators: list[Validator | Callable] @@ -293,8 +295,8 @@ def __init__( generated: bool = False, primary_key: bool | None = None, null: bool = False, - default: VALUE = None, - db_default: VALUE = DB_DEFAULT_NOT_SET, + default: VALUE | Callable[..., VALUE | Awaitable[VALUE]] | None = None, + db_default: VALUE | SqlDefault | _DB_DEFAULT_NOT_SET = DB_DEFAULT_NOT_SET, unique: bool = False, db_index: bool | None = None, description: str | None = None, diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index 57b5f4484..c3f023e6c 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -292,6 +292,7 @@ def __init__( primary_key: bool | None = None, unique: bool = False, db_index: bool = False, + null: bool = False, **kwargs: Unpack[_FieldKwargsCommon[str]], ) -> None: if primary_key or kwargs.get("pk"): @@ -315,7 +316,7 @@ def __init__( elif db_index: raise ConfigurationError("TextField can't be indexed, consider CharField") - super().__init__(primary_key=primary_key, **kwargs) + super().__init__(primary_key=primary_key, null=null, **kwargs) class _db_mysql: SQL_TYPE = "LONGTEXT" From 16c9b37f73c9f3a3ecc65be56c49c2a124062aaa Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sat, 1 Aug 2026 20:35:13 +0900 Subject: [PATCH 16/22] Made work under 3.11 --- tortoise/fields/base.py | 9 ++++---- tortoise/fields/data.py | 51 ++++++++++++++++++++--------------------- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/tortoise/fields/base.py b/tortoise/fields/base.py index 289a6688d..015a7441f 100644 --- a/tortoise/fields/base.py +++ b/tortoise/fields/base.py @@ -7,23 +7,22 @@ from collections.abc import Callable from enum import Enum from functools import reduce -from typing import TYPE_CHECKING, Any, Generic, TypedDict, TypeVar, overload +from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload from pypika_tortoise.terms import Term from tortoise.exceptions import ConfigurationError, ValidationError -from tortoise.fields.db_defaults import SqlDefault from tortoise.validators import Validator +from tortoise.fields.db_defaults import SqlDefault if TYPE_CHECKING: # pragma: nocoverage from tortoise.models import Model if sys.version_info >= (3, 11): - from collections.abc import Awaitable from enum import StrEnum - from typing import Self + from typing import Self, Awaitable, TypedDict else: # pragma: no cover - from typing_extensions import Self + from typing_extensions import Self, TypedDict class StrEnum(str, Enum): __str__ = str.__str__ diff --git a/tortoise/fields/data.py b/tortoise/fields/data.py index c3f023e6c..799e4729c 100644 --- a/tortoise/fields/data.py +++ b/tortoise/fields/data.py @@ -21,7 +21,7 @@ Field, FieldKwargs, JSONFieldKwargs, - _FieldKwargsCommon, + TextFieldKwargs, _FieldKwargsNoPk, ) from tortoise.timezone import get_default_timezone, get_timezone, get_use_tz, localtime @@ -122,7 +122,7 @@ def __init__( primary_key: bool | None = None, *, null: Literal[False] = False, - **kwargs: Unpack[_FieldKwargsNoPk[T_INT]], + **kwargs: Unpack[_FieldKwargsNoPk[int]], ) -> None: ... @overload @@ -131,7 +131,7 @@ def __init__( primary_key: bool | None = None, *, null: Literal[True], - **kwargs: Unpack[_FieldKwargsNoPk[T_INT]], + **kwargs: Unpack[_FieldKwargsNoPk[int]], ) -> None: ... def __init__(self, primary_key: bool | None = None, **kwargs: Any) -> None: @@ -241,7 +241,7 @@ def __init__( max_length: int, *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs[T_STR]], + **kwargs: Unpack[FieldKwargs[str]], ) -> None: ... @overload @@ -250,7 +250,7 @@ def __init__( max_length: int, *, null: Literal[True], - **kwargs: Unpack[FieldKwargs[T_STR]], + **kwargs: Unpack[FieldKwargs[str]], ) -> None: ... def __init__(self, max_length: int, **kwargs: Any) -> None: @@ -292,8 +292,7 @@ def __init__( primary_key: bool | None = None, unique: bool = False, db_index: bool = False, - null: bool = False, - **kwargs: Unpack[_FieldKwargsCommon[str]], + **kwargs: Unpack[TextFieldKwargs], ) -> None: if primary_key or kwargs.get("pk"): warnings.warn( @@ -316,7 +315,7 @@ def __init__( elif db_index: raise ConfigurationError("TextField can't be indexed, consider CharField") - super().__init__(primary_key=primary_key, null=null, **kwargs) + super().__init__(primary_key=primary_key, **kwargs) class _db_mysql: SQL_TYPE = "LONGTEXT" @@ -342,7 +341,7 @@ def __init__( self: BooleanField[bool], *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[bool]], ) -> None: ... @overload @@ -350,7 +349,7 @@ def __init__( self: BooleanField[bool | None], *, null: Literal[True], - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[bool]], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -387,7 +386,7 @@ def __init__( decimal_places: int, *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs[T_DECIMAL]], + **kwargs: Unpack[FieldKwargs[Decimal]], ) -> None: ... @overload @@ -397,7 +396,7 @@ def __init__( decimal_places: int, *, null: Literal[True], - **kwargs: Unpack[FieldKwargs[T_DECIMAL]], + **kwargs: Unpack[FieldKwargs[Decimal]], ) -> None: ... def __init__(self, max_digits: int, decimal_places: int, **kwargs: Any) -> None: @@ -470,7 +469,7 @@ def __init__( auto_now_add: bool = False, *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs[T_DATETIME]], + **kwargs: Unpack[FieldKwargs[datetime.datetime]], ) -> None: ... @overload @@ -480,7 +479,7 @@ def __init__( auto_now_add: bool = False, *, null: Literal[True], - **kwargs: Unpack[FieldKwargs[T_DATETIME]], + **kwargs: Unpack[FieldKwargs[datetime.datetime]], ) -> None: ... def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any) -> None: @@ -563,7 +562,7 @@ def __init__( self: DateField[datetime.date], *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs[T_DATE]], + **kwargs: Unpack[FieldKwargs[datetime.date]], ) -> None: ... @overload @@ -571,7 +570,7 @@ def __init__( self: DateField[datetime.date | None], *, null: Literal[True], - **kwargs: Unpack[FieldKwargs[T_DATE]], + **kwargs: Unpack[FieldKwargs[datetime.date]], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -610,7 +609,7 @@ def __init__( auto_now_add: bool = False, *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[datetime.time]], ) -> None: ... @overload @@ -620,7 +619,7 @@ def __init__( auto_now_add: bool = False, *, null: Literal[True], - **kwargs: Unpack[FieldKwargs], + **kwargs: Unpack[FieldKwargs[datetime.time]], ) -> None: ... def __init__(self, auto_now: bool = False, auto_now_add: bool = False, **kwargs: Any) -> None: @@ -693,7 +692,7 @@ def __init__( self: TimeDeltaField[datetime.timedelta], *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs[T_TIMEDELTA]], + **kwargs: Unpack[FieldKwargs[datetime.timedelta]], ) -> None: ... @overload @@ -701,7 +700,7 @@ def __init__( self: TimeDeltaField[datetime.timedelta | None], *, null: Literal[True], - **kwargs: Unpack[FieldKwargs[T_TIMEDELTA]], + **kwargs: Unpack[FieldKwargs[datetime.timedelta]], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -737,7 +736,7 @@ def __init__( self: FloatField[float], *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs[T_FLOAT]], + **kwargs: Unpack[FieldKwargs[float]], ) -> None: ... @overload @@ -745,7 +744,7 @@ def __init__( self: FloatField[float | None], *, null: Literal[True], - **kwargs: Unpack[FieldKwargs[T_FLOAT]], + **kwargs: Unpack[FieldKwargs[float]], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -874,7 +873,7 @@ def __init__( self: UUIDField[UUID], *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs[T_UUID]], + **kwargs: Unpack[FieldKwargs[UUID]], ) -> None: ... @overload @@ -882,7 +881,7 @@ def __init__( self: UUIDField[UUID | None], *, null: Literal[True], - **kwargs: Unpack[FieldKwargs[T_UUID]], + **kwargs: Unpack[FieldKwargs[UUID]], ) -> None: ... def __init__(self, **kwargs: Any) -> None: @@ -915,7 +914,7 @@ def __init__( self: BinaryField[bytes], *, null: Literal[False] = False, - **kwargs: Unpack[FieldKwargs[T_BINARY]], + **kwargs: Unpack[FieldKwargs[bytes]], ) -> None: ... @overload @@ -923,7 +922,7 @@ def __init__( self: BinaryField[bytes | None], *, null: Literal[True], - **kwargs: Unpack[FieldKwargs[T_BINARY]], + **kwargs: Unpack[FieldKwargs[bytes]], ) -> None: ... def __init__(self, **kwargs: Any) -> None: From acdad82c5ad1a41862a13ea072b8cca10e1788e0 Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sat, 1 Aug 2026 20:39:21 +0900 Subject: [PATCH 17/22] Fixed errors --- tortoise/fields/base.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tortoise/fields/base.py b/tortoise/fields/base.py index 015a7441f..f11977500 100644 --- a/tortoise/fields/base.py +++ b/tortoise/fields/base.py @@ -12,15 +12,16 @@ from pypika_tortoise.terms import Term from tortoise.exceptions import ConfigurationError, ValidationError -from tortoise.validators import Validator from tortoise.fields.db_defaults import SqlDefault +from tortoise.validators import Validator if TYPE_CHECKING: # pragma: nocoverage from tortoise.models import Model if sys.version_info >= (3, 11): + from collections.abc import Awaitable from enum import StrEnum - from typing import Self, Awaitable, TypedDict + from typing import Self, TypedDict else: # pragma: no cover from typing_extensions import Self, TypedDict @@ -130,6 +131,15 @@ class FieldKwargs(_FieldKwargsNoPk[VALUE], total=False): primary_key: bool | None +class TextFieldKwargs(_FieldKwargsCommon[VALUE], total=False): + """Constructor arguments for :class:`TextField`. + + ``TextField`` doesn't declare ``null`` explicitly, so it includes it here. + """ + + null: bool + + class JSONFieldKwargs(FieldKwargs[VALUE], total=False): """Constructor arguments for :class:`JSONField`. @@ -138,7 +148,7 @@ class JSONFieldKwargs(FieldKwargs[VALUE], total=False): """ null: bool - field_type: Any + field_type: VALUE class RelationalFieldKwargs(FieldKwargs[VALUE], total=False): From 3a40c34298e592ff439907159c50624fc8b2655d Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sat, 1 Aug 2026 20:46:38 +0900 Subject: [PATCH 18/22] Moved `Awaitable` importion to global version --- tortoise/fields/base.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tortoise/fields/base.py b/tortoise/fields/base.py index f11977500..76daf7ca5 100644 --- a/tortoise/fields/base.py +++ b/tortoise/fields/base.py @@ -4,7 +4,7 @@ import operator import sys import warnings -from collections.abc import Callable +from collections.abc import Awaitable, Callable from enum import Enum from functools import reduce from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload @@ -19,10 +19,11 @@ from tortoise.models import Model if sys.version_info >= (3, 11): - from collections.abc import Awaitable from enum import StrEnum from typing import Self, TypedDict else: # pragma: no cover + from collections.abc import Awaitable + from typing_extensions import Self, TypedDict class StrEnum(str, Enum): From 99bc5e1929809f7c59d950578687d029a2464dc2 Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sat, 1 Aug 2026 20:52:04 +0900 Subject: [PATCH 19/22] JSON field error fixed --- tortoise/fields/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tortoise/fields/base.py b/tortoise/fields/base.py index 76daf7ca5..1b6c9fc5f 100644 --- a/tortoise/fields/base.py +++ b/tortoise/fields/base.py @@ -149,7 +149,7 @@ class JSONFieldKwargs(FieldKwargs[VALUE], total=False): """ null: bool - field_type: VALUE + field_type: type[Any] class RelationalFieldKwargs(FieldKwargs[VALUE], total=False): From 533c3bee278ed6b9122f6c4217ce6bf9696d9eaa Mon Sep 17 00:00:00 2001 From: ropmyung Date: Sun, 2 Aug 2026 16:03:13 +0900 Subject: [PATCH 20/22] restored test comment --- tests/test_primary_key.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_primary_key.py b/tests/test_primary_key.py index 7b306d36c..d4033f5cb 100644 --- a/tests/test_primary_key.py +++ b/tests/test_primary_key.py @@ -249,6 +249,7 @@ def test_warning(self): with pytest.warns(DeprecationWarning, match=self.message): f = fields.TextField(primary_key=True) assert f.pk is True + # Positional arg goes to primary_key, so only TextField as PK warning with pytest.warns(DeprecationWarning, match=self.message): f = fields.TextField(True) assert f.pk is True From 969a3282d6b7e6f9ffdd35385ce906824435d9ac Mon Sep 17 00:00:00 2001 From: ropmyung Date: Wed, 5 Aug 2026 00:59:53 +0900 Subject: [PATCH 21/22] Added comment for TypedDict on 3.10 --- tortoise/fields/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tortoise/fields/base.py b/tortoise/fields/base.py index 1b6c9fc5f..e3fc6d1fe 100644 --- a/tortoise/fields/base.py +++ b/tortoise/fields/base.py @@ -23,8 +23,9 @@ from typing import Self, TypedDict else: # pragma: no cover from collections.abc import Awaitable - from typing_extensions import Self, TypedDict + # Under python 3.11, typing.TypedDict does not support multiple inheritance woth non-TypedDict + # So for Generic, used typing-extensions for TypedDict class StrEnum(str, Enum): __str__ = str.__str__ From 8fd4a4ecffc808c944b1f270d49d4b530576bc10 Mon Sep 17 00:00:00 2001 From: ropmyung Date: Wed, 5 Aug 2026 01:01:20 +0900 Subject: [PATCH 22/22] Made style --- tortoise/fields/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tortoise/fields/base.py b/tortoise/fields/base.py index e3fc6d1fe..1b7135f6e 100644 --- a/tortoise/fields/base.py +++ b/tortoise/fields/base.py @@ -23,6 +23,7 @@ from typing import Self, TypedDict else: # pragma: no cover from collections.abc import Awaitable + from typing_extensions import Self, TypedDict # Under python 3.11, typing.TypedDict does not support multiple inheritance woth non-TypedDict # So for Generic, used typing-extensions for TypedDict