From: Yurii Motov Date: Fri, 20 Feb 2026 22:35:29 +0000 (+0100) Subject: Merge remote-tracking branch 'upstream/main' into add-missing-parameters-to-field X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=3ce28de729333428b029c0196ea370c4dff6d059;p=thirdparty%2Ffastapi%2Fsqlmodel.git Merge remote-tracking branch 'upstream/main' into add-missing-parameters-to-field --- 3ce28de729333428b029c0196ea370c4dff6d059 diff --cc sqlmodel/main.py index 2628dd796,300031de8..f72d6045e --- a/sqlmodel/main.py +++ b/sqlmodel/main.py @@@ -3,9 -3,9 +3,10 @@@ from __future__ import annotation import builtins import ipaddress import uuid +import warnings import weakref - from collections.abc import Mapping, Sequence, Set + from collections.abc import Callable, Mapping, Sequence, Set + from dataclasses import dataclass from datetime import date, datetime, time, timedelta from decimal import Decimal from enum import Enum @@@ -207,45 -238,40 +240,45 @@@ def _get_sqlmodel_field_value def Field( default: Any = Undefined, *, - default_factory: Optional[NoArgAnyCallable] = None, - alias: Optional[str] = None, - validation_alias: Optional[str] = None, - serialization_alias: Optional[str] = None, - title: Optional[str] = None, - field_title_generator: Optional[Callable[[str, PydanticFieldInfo], str]] = None, - description: Optional[str] = None, - examples: Optional[list[Any]] = None, - deprecated: Union[Deprecated, str, bool, None] = None, - exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, - exclude_if: Optional[Callable[[Any], bool]] = None, - include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, - const: Optional[bool] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - multiple_of: Optional[float] = None, - max_digits: Optional[int] = None, - decimal_places: Optional[int] = None, - min_items: Optional[int] = None, - max_items: Optional[int] = None, - unique_items: Optional[bool] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, + default_factory: NoArgAnyCallable | None = None, + alias: str | None = None, + validation_alias: str | None = None, + serialization_alias: str | None = None, + title: str | None = None, ++ field_title_generator: Callable[[str, PydanticFieldInfo], str] | None = None, + description: str | None = None, ++ examples: list[Any] | None = None, ++ deprecated: Deprecated | str | bool | None = None, + exclude: Set[int | str] | Mapping[int | str, Any] | Any = None, ++ exclude_if: Callable[[Any], bool] | None = None, + include: Set[int | str] | Mapping[int | str, Any] | Any = None, + const: bool | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + multiple_of: float | None = None, + max_digits: int | None = None, + decimal_places: int | None = None, + min_items: int | None = None, + max_items: int | None = None, + unique_items: bool | None = None, + min_length: int | None = None, + max_length: int | None = None, allow_mutation: bool = True, - regex: Optional[str] = None, - strict: Optional[bool] = None, - discriminator: Optional[str] = None, + regex: str | None = None, ++ strict: bool | None = None, + discriminator: str | None = None, repr: bool = True, - primary_key: Union[bool, UndefinedType] = Undefined, + primary_key: bool | UndefinedType = Undefined, foreign_key: Any = Undefined, - unique: Union[bool, UndefinedType] = Undefined, - nullable: Union[bool, UndefinedType] = Undefined, - index: Union[bool, UndefinedType] = Undefined, - sa_type: Union[type[Any], UndefinedType] = Undefined, - sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, - sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, - schema_extra: Optional[dict[str, Any]] = None, + unique: bool | UndefinedType = Undefined, + nullable: bool | UndefinedType = Undefined, + index: bool | UndefinedType = Undefined, + sa_type: type[Any] | UndefinedType = Undefined, + sa_column_args: Sequence[Any] | UndefinedType = Undefined, + sa_column_kwargs: Mapping[str, Any] | UndefinedType = Undefined, + schema_extra: dict[str, Any] | None = None, ) -> Any: ... @@@ -255,46 -281,41 +288,46 @@@ def Field( default: Any = Undefined, *, - default_factory: Optional[NoArgAnyCallable] = None, - alias: Optional[str] = None, - validation_alias: Optional[str] = None, - serialization_alias: Optional[str] = None, - title: Optional[str] = None, - field_title_generator: Optional[Callable[[str, PydanticFieldInfo], str]] = None, - description: Optional[str] = None, - examples: Optional[list[Any]] = None, - deprecated: Union[Deprecated, str, bool, None] = None, - exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, - exclude_if: Optional[Callable[[Any], bool]] = None, - include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, - const: Optional[bool] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - multiple_of: Optional[float] = None, - max_digits: Optional[int] = None, - decimal_places: Optional[int] = None, - min_items: Optional[int] = None, - max_items: Optional[int] = None, - unique_items: Optional[bool] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, + default_factory: NoArgAnyCallable | None = None, + alias: str | None = None, + validation_alias: str | None = None, + serialization_alias: str | None = None, + title: str | None = None, ++ field_title_generator: Callable[[str, PydanticFieldInfo], str] | None = None, + description: str | None = None, ++ examples: list[Any] | None = None, ++ deprecated: Deprecated | str | bool | None = None, + exclude: Set[int | str] | Mapping[int | str, Any] | Any = None, ++ exclude_if: Callable[[Any], bool] | None = None, + include: Set[int | str] | Mapping[int | str, Any] | Any = None, + const: bool | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + multiple_of: float | None = None, + max_digits: int | None = None, + decimal_places: int | None = None, + min_items: int | None = None, + max_items: int | None = None, + unique_items: bool | None = None, + min_length: int | None = None, + max_length: int | None = None, allow_mutation: bool = True, - regex: Optional[str] = None, - strict: Optional[bool] = None, - discriminator: Optional[str] = None, + regex: str | None = None, ++ strict: bool | None = None, + discriminator: str | None = None, repr: bool = True, - primary_key: Union[bool, UndefinedType] = Undefined, + primary_key: bool | UndefinedType = Undefined, foreign_key: str, - ondelete: Union[OnDeleteType, UndefinedType] = Undefined, - unique: Union[bool, UndefinedType] = Undefined, - nullable: Union[bool, UndefinedType] = Undefined, - index: Union[bool, UndefinedType] = Undefined, - sa_type: Union[type[Any], UndefinedType] = Undefined, - sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, - sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, - schema_extra: Optional[dict[str, Any]] = None, + ondelete: OnDeleteType | UndefinedType = Undefined, + unique: bool | UndefinedType = Undefined, + nullable: bool | UndefinedType = Undefined, + index: bool | UndefinedType = Undefined, + sa_type: type[Any] | UndefinedType = Undefined, + sa_column_args: Sequence[Any] | UndefinedType = Undefined, + sa_column_kwargs: Mapping[str, Any] | UndefinedType = Undefined, + schema_extra: dict[str, Any] | None = None, ) -> Any: ... @@@ -312,99 -333,77 +345,99 @@@ def Field( default: Any = Undefined, *, - default_factory: Optional[NoArgAnyCallable] = None, - alias: Optional[str] = None, - validation_alias: Optional[str] = None, - serialization_alias: Optional[str] = None, - title: Optional[str] = None, - field_title_generator: Optional[Callable[[str, PydanticFieldInfo], str]] = None, - description: Optional[str] = None, - examples: Optional[list[Any]] = None, - deprecated: Union[Deprecated, str, bool, None] = None, - exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, - exclude_if: Optional[Callable[[Any], bool]] = None, - include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, - const: Optional[bool] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - multiple_of: Optional[float] = None, - max_digits: Optional[int] = None, - decimal_places: Optional[int] = None, - min_items: Optional[int] = None, - max_items: Optional[int] = None, - unique_items: Optional[bool] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, + default_factory: NoArgAnyCallable | None = None, + alias: str | None = None, + validation_alias: str | None = None, + serialization_alias: str | None = None, + title: str | None = None, ++ field_title_generator: Callable[[str, PydanticFieldInfo], str] | None = None, + description: str | None = None, ++ examples: list[Any] | None = None, ++ deprecated: Deprecated | str | bool | None = None, + exclude: Set[int | str] | Mapping[int | str, Any] | Any = None, ++ exclude_if: Callable[[Any], bool] | None = None, + include: Set[int | str] | Mapping[int | str, Any] | Any = None, + const: bool | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + multiple_of: float | None = None, + max_digits: int | None = None, + decimal_places: int | None = None, + min_items: int | None = None, + max_items: int | None = None, + unique_items: bool | None = None, + min_length: int | None = None, + max_length: int | None = None, allow_mutation: bool = True, - regex: Optional[str] = None, - strict: Optional[bool] = None, - discriminator: Optional[str] = None, + regex: str | None = None, ++ strict: bool | None = None, + discriminator: str | None = None, repr: bool = True, - sa_column: Union[Column[Any], UndefinedType] = Undefined, - schema_extra: Optional[dict[str, Any]] = None, + sa_column: Column[Any] | UndefinedType = Undefined, + schema_extra: dict[str, Any] | None = None, ) -> Any: ... def Field( default: Any = Undefined, *, - default_factory: Optional[NoArgAnyCallable] = None, - alias: Optional[str] = None, - validation_alias: Optional[str] = None, - serialization_alias: Optional[str] = None, - title: Optional[str] = None, - field_title_generator: Optional[Callable[[str, PydanticFieldInfo], str]] = None, - description: Optional[str] = None, - examples: Optional[list[Any]] = None, - deprecated: Union[Deprecated, str, bool, None] = None, - exclude: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, - exclude_if: Optional[Callable[[Any], bool]] = None, - include: Union[Set[Union[int, str]], Mapping[Union[int, str], Any], Any] = None, - const: Optional[bool] = None, - gt: Optional[float] = None, - ge: Optional[float] = None, - lt: Optional[float] = None, - le: Optional[float] = None, - multiple_of: Optional[float] = None, - max_digits: Optional[int] = None, - decimal_places: Optional[int] = None, - min_items: Optional[int] = None, - max_items: Optional[int] = None, - unique_items: Optional[bool] = None, - min_length: Optional[int] = None, - max_length: Optional[int] = None, + default_factory: NoArgAnyCallable | None = None, + alias: str | None = None, + validation_alias: str | None = None, + serialization_alias: str | None = None, + title: str | None = None, ++ field_title_generator: Callable[[str, PydanticFieldInfo], str] | None = None, + description: str | None = None, ++ examples: list[Any] | None = None, ++ deprecated: Deprecated | str | bool | None = None, + exclude: Set[int | str] | Mapping[int | str, Any] | Any = None, ++ exclude_if: Callable[[Any], bool] | None = None, + include: Set[int | str] | Mapping[int | str, Any] | Any = None, + const: bool | None = None, + gt: float | None = None, + ge: float | None = None, + lt: float | None = None, + le: float | None = None, + multiple_of: float | None = None, + max_digits: int | None = None, + decimal_places: int | None = None, + min_items: int | None = None, + max_items: int | None = None, + unique_items: bool | None = None, + min_length: int | None = None, + max_length: int | None = None, allow_mutation: bool = True, - regex: Optional[str] = None, - strict: Optional[bool] = None, - discriminator: Optional[str] = None, + regex: str | None = None, ++ strict: bool | None = None, + discriminator: str | None = None, repr: bool = True, - primary_key: Union[bool, UndefinedType] = Undefined, + primary_key: bool | UndefinedType = Undefined, foreign_key: Any = Undefined, - ondelete: Union[OnDeleteType, UndefinedType] = Undefined, - unique: Union[bool, UndefinedType] = Undefined, - nullable: Union[bool, UndefinedType] = Undefined, - index: Union[bool, UndefinedType] = Undefined, - sa_type: Union[type[Any], UndefinedType] = Undefined, - sa_column: Union[Column, UndefinedType] = Undefined, # type: ignore - sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined, - sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined, - schema_extra: Optional[dict[str, Any]] = None, + ondelete: OnDeleteType | UndefinedType = Undefined, + unique: bool | UndefinedType = Undefined, + nullable: bool | UndefinedType = Undefined, + index: bool | UndefinedType = Undefined, + sa_type: type[Any] | UndefinedType = Undefined, + sa_column: Column | UndefinedType = Undefined, # type: ignore + sa_column_args: Sequence[Any] | UndefinedType = Undefined, + sa_column_kwargs: Mapping[str, Any] | UndefinedType = Undefined, + schema_extra: dict[str, Any] | None = None, ) -> Any: current_schema_extra = schema_extra or {} + + for param_name in ( + "strict", + "examples", + "deprecated", + "exclude_if", + "field_title_generator", + ): + if param_name in current_schema_extra: + msg = f"Pass `{param_name}` parameter directly to Field instead of passing it via `schema_extra`" + warnings.warn(msg, DeprecationWarning, stacklevel=2) + # Extract possible alias settings from schema_extra so we can control precedence schema_validation_alias = current_schema_extra.pop("validation_alias", None) schema_serialization_alias = current_schema_extra.pop("serialization_alias", None) diff --cc tests/test_pydantic/test_field.py index ab938b143,11f4150d9..ff83ff248 --- a/tests/test_pydantic/test_field.py +++ b/tests/test_pydantic/test_field.py @@@ -1,5 -1,5 +1,5 @@@ from decimal import Decimal - from typing import Any, Literal, Optional, Union -from typing import Literal ++from typing import Any, Literal import pytest from pydantic import ValidationError @@@ -54,200 -54,3 +54,200 @@@ def test_repr() instance = Model(id=123, foo="bar") assert "foo=" not in repr(instance) + + +def test_strict_true(): + class Model(SQLModel): - id: Optional[int] = Field(default=None, primary_key=True) ++ id: int | None = Field(default=None, primary_key=True) + val: int + val_strict: int = Field(strict=True) + + class ModelDB(Model, table=True): + pass + + Model(val=123, val_strict=456) + Model(val="123", val_strict=456) + + with pytest.raises(ValidationError): + Model(val=123, val_strict="456") + + engine = create_engine("sqlite://", echo=True) + + SQLModel.metadata.create_all(engine) + + model = ModelDB(val=123, val_strict=456) + with Session(engine) as session: + session.add(model) + session.commit() + session.refresh(model) + + assert model.val == 123 + assert model.val_strict == 456 + + +def test_strict_table_model(): + class Model(SQLModel, table=True): - id: Optional[int] = Field(default=None, primary_key=True) ++ id: int | None = Field(default=None, primary_key=True) + val_strict: int = Field(strict=True) + + engine = create_engine("sqlite://", echo=True) + + SQLModel.metadata.create_all(engine) + + model = Model(val_strict=456) + with Session(engine) as session: + session.add(model) + session.commit() + session.refresh(model) + + assert model.val_strict == 456 + + +@pytest.mark.parametrize("strict", [None, False]) - def test_strict_false(strict: Optional[bool]): ++def test_strict_false(strict: int | None): + class Model(SQLModel): + val: int = Field(strict=strict) + + Model(val=123) + Model(val="123") + + +def test_strict_via_schema_extra(): # Current workaround. Remove after some time + with pytest.warns( + DeprecationWarning, + match="Pass `strict` parameter directly to Field instead of passing it via `schema_extra`", + ): + + class Model(SQLModel): + val: int + val_strict: int = Field(schema_extra={"strict": True}) + + Model(val=123, val_strict=456) + Model(val="123", val_strict=456) + + with pytest.raises(ValidationError): + Model(val=123, val_strict="456") + + +def test_examples(): + class Model(SQLModel): + name: str = Field(examples=["Alice", "Bob"]) + + model_schema = Model.model_json_schema() + assert model_schema["properties"]["name"]["examples"] == ["Alice", "Bob"] + + +def test_examples_via_schema_extra(): # Current workaround. Remove after some time + with pytest.warns( + DeprecationWarning, + match="Pass `examples` parameter directly to Field instead of passing it via `schema_extra`", + ): + + class Model(SQLModel): + name: str = Field(schema_extra={"examples": ["Alice", "Bob"]}) + + model_schema = Model.model_json_schema() + assert model_schema["properties"]["name"]["examples"] == ["Alice", "Bob"] + + +def test_deprecated(): + class Model(SQLModel): + old_field: str = Field(deprecated=True) + another_old_field: str = Field(deprecated="This field is deprecated") + + model_schema = Model.model_json_schema() + assert model_schema["properties"]["old_field"]["deprecated"] is True + assert model_schema["properties"]["another_old_field"]["deprecated"] is True + + +def test_deprecated_via_schema_extra(): # Current workaround. Remove after some time + with pytest.warns( + DeprecationWarning, + match="Pass `deprecated` parameter directly to Field instead of passing it via `schema_extra`", + ): + + class Model(SQLModel): + old_field: str = Field(schema_extra={"deprecated": True}) + another_old_field: str = Field( + schema_extra={"deprecated": "This field is deprecated"} + ) + + model_schema = Model.model_json_schema() + assert model_schema["properties"]["old_field"]["deprecated"] is True + assert model_schema["properties"]["another_old_field"]["deprecated"] is True + + +def test_exclude_if(): + def is_empty_string(value: Any) -> bool: + return value == "" + + class Model(SQLModel): + name: str = Field(exclude_if=is_empty_string) + age: int + + model1 = Model(name="Alice", age=30) + model2 = Model(name="", age=25) + + dict1 = model1.model_dump() + dict2 = model2.model_dump() + + assert "name" in dict1 + assert dict1["name"] == "Alice" + + assert "name" not in dict2 + + +def test_exclude_if_via_schema_extra(): + def is_empty_string(value: Any) -> bool: + return value == "" + + with pytest.warns( + DeprecationWarning, + match="Pass `exclude_if` parameter directly to Field instead of passing it via `schema_extra`", + ): + + class Model(SQLModel): + name: str = Field(schema_extra={"exclude_if": is_empty_string}) + age: int + + model1 = Model(name="Alice", age=30) + model2 = Model(name="", age=25) + + dict1 = model1.model_dump() + dict2 = model2.model_dump() + + assert "name" in dict1 + assert dict1["name"] == "Alice" + + assert "name" not in dict2 + + +def test_field_title_generator(): + def upper(value: str, _: Any) -> str: + return value.upper() + + class Model(SQLModel): + name: str = Field(field_title_generator=upper) + age: int + + model_schema = Model.model_json_schema() + assert model_schema["properties"]["name"]["title"] == "NAME" + assert model_schema["properties"]["age"]["title"] == "Age" + + +def test_field_title_generator_via_schema_extra(): + def upper(value: str, _: Any) -> str: + return value.upper() + + with pytest.warns( + DeprecationWarning, + match="Pass `field_title_generator` parameter directly to Field instead of passing it via `schema_extra`", + ): + + class Model(SQLModel): + name: str = Field(schema_extra={"field_title_generator": upper}) + age: int + + model_schema = Model.model_json_schema() + assert model_schema["properties"]["name"]["title"] == "NAME" + assert model_schema["properties"]["age"]["title"] == "Age"