From 90cb4e0112ef7f55a2166f17ebc2e7c12c3c4fa0 Mon Sep 17 00:00:00 2001 From: Jakob Gerhard Martinussen Date: Fri, 17 Oct 2025 15:35:05 +0200 Subject: [PATCH 1/2] feat: Add typing for GeneratedField --- django-stubs/db/models/__init__.pyi | 1 + django-stubs/db/models/fields/generated.pyi | 83 +++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 django-stubs/db/models/fields/generated.pyi diff --git a/django-stubs/db/models/__init__.pyi b/django-stubs/db/models/__init__.pyi index f8eda8773..acfa37723 100644 --- a/django-stubs/db/models/__init__.pyi +++ b/django-stubs/db/models/__init__.pyi @@ -83,6 +83,7 @@ from .fields.files import FieldFile as FieldFile from .fields.files import FileDescriptor as FileDescriptor from .fields.files import FileField as FileField from .fields.files import ImageField as ImageField +from .fields.generated import GeneratedField as GeneratedField from .fields.json import JSONField as JSONField from .fields.proxy import OrderWrt as OrderWrt from .fields.related import ForeignKey as ForeignKey diff --git a/django-stubs/db/models/fields/generated.pyi b/django-stubs/db/models/fields/generated.pyi new file mode 100644 index 000000000..5a5b42c0f --- /dev/null +++ b/django-stubs/db/models/fields/generated.pyi @@ -0,0 +1,83 @@ +from collections.abc import Iterable +from typing import Any, Literal, Never, TypeVar, overload + +from django.db.models import Combinable, Expression, ForeignObjectRel +from django.db.models.expressions import Col + +from . import Field, _ErrorMessagesToOverride, _ValidatorCallable +from .mixins import CheckFieldDefaultMixin + +_GT = TypeVar("_GT", bound=Any | None) + +class GeneratedField(CheckFieldDefaultMixin, Field[Never, _GT]): + generated: Literal[True] + db_returning: Literal[True] + expression: Combinable | Expression + + @overload + def __new__( + cls, + *, + expression: Combinable | Expression, + output_field: Field[Any, _GT], + db_persist: bool, + # + verbose_name: str | None = ..., + name: str | None = ..., + primary_key: bool = ..., + max_length: int | None = ..., + unique: bool = ..., + blank: Literal[True] = ..., + null: Literal[False] = False, + db_index: bool = ..., + rel: ForeignObjectRel | None = ..., + editable: Literal[False] = ..., + serialize: bool = ..., + unique_for_date: str | None = ..., + unique_for_month: str | None = ..., + unique_for_year: str | None = ..., + choices: Iterable[tuple[Any, str] | tuple[str, Iterable[tuple[Any, str]]]] = ..., + help_text: str = ..., + db_column: str | None = ..., + db_tablespace: str | None = ..., + auto_created: bool = ..., + validators: Iterable[_ValidatorCallable] = ..., + error_messages: _ErrorMessagesToOverride | None = ..., + db_comment: str | None = ..., + ) -> GeneratedField[_GT]: ... + @overload + def __new__( + cls, + *, + expression: Combinable | Expression, + output_field: Field[Any, _GT], + db_persist: bool, + # + verbose_name: str | None = ..., + name: str | None = ..., + primary_key: bool = ..., + max_length: int | None = ..., + unique: bool = ..., + blank: Literal[True] = ..., + null: Literal[True], + db_index: bool = ..., + rel: ForeignObjectRel | None = ..., + editable: Literal[False] = ..., + serialize: bool = ..., + unique_for_date: str | None = ..., + unique_for_month: str | None = ..., + unique_for_year: str | None = ..., + choices: Iterable[tuple[Any, str] | tuple[str, Iterable[tuple[Any, str]]]] = ..., + help_text: str = ..., + db_column: str | None = ..., + db_tablespace: str | None = ..., + auto_created: bool = ..., + validators: Iterable[_ValidatorCallable] = ..., + error_messages: _ErrorMessagesToOverride | None = ..., + db_comment: str | None = ..., + ) -> GeneratedField[_GT | None]: ... + @property + def cached_col(self) -> Col: ... + def generated_sql(self, connection: Any) -> tuple[str, list[Any]]: ... + +__all__ = ["GeneratedField"] From f92536c4227c3141b4297437d10241b0a5aef436 Mon Sep 17 00:00:00 2001 From: Jakob Gerhard Martinussen Date: Fri, 17 Oct 2025 15:53:47 +0200 Subject: [PATCH 2/2] chore(tests): Add tests for GeneratedField --- tests/trout/models.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/trout/models.py b/tests/trout/models.py index 6f4cb2dd0..e55ffcf43 100644 --- a/tests/trout/models.py +++ b/tests/trout/models.py @@ -42,6 +42,8 @@ ) from psycopg2.extras import execute_values +from django.db.models.fields.generated import GeneratedField + class CustomChoices(models.Choices): A = "B", "A" @@ -269,6 +271,18 @@ class Comment(models.Model): object_id_field="generic_id", ) + generated_field = GeneratedField( + expression=models.F("integer") + 1, + output_field=models.IntegerField(), + db_persist=True, + ) + nullable_generated_field = GeneratedField( + expression=models.F("integer_nullable") + 1, + output_field=models.IntegerField(null=True), + db_persist=True, + null=True, + ) + def process_non_nullable( x: Union[ @@ -669,6 +683,12 @@ def main() -> None: if not isinstance(comment.other_metadata, dict): print() # type: ignore [unreachable] + process_non_nullable(comment.generated_field) + if isinstance(comment.nullable_generated_field, type(None)): + print(comment.nullable_generated_field) + if not isinstance(comment.generated_field, int): + print() # type: ignore [unreachable] + async def main_async() -> None: comment = await Comment.objects.aget(pk=123)