Skip to content

Commit 6291fde

Browse files
authored
Merge branch 'main' into rjmill/fix-generic
2 parents d8b7c88 + 940f665 commit 6291fde

15 files changed

Lines changed: 436 additions & 128 deletions

.github/labeler.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ internal:
1818
- .pre-commit-config.yaml
1919
- pdm_build.py
2020
- requirements*.txt
21+
- uv.lock
2122
- all-globs-to-all-files:
2223
- '!docs/**'
2324
- '!sqlmodel/**'

.github/workflows/issue-manager.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ jobs:
4141
"message": "As this PR has been waiting for the original user for a while but seems to be inactive, it's now going to be closed. But if there's anyone interested, feel free to create a new PR.",
4242
"reminder": {
4343
"before": "P3D",
44-
"message": "Heads-up: this will be closed in 3 days unless theres new activity."
44+
"message": "Heads-up: this will be closed in 3 days unless there's new activity."
4545
}
4646
},
4747
"invalid": {
4848
"delay": 0,
4949
"message": "This was marked as invalid and will be closed now. If this is an error, please provide additional details."
50+
},
51+
"maybe-ai": {
52+
"delay": 0,
53+
"message": "This was marked as potentially AI generated and will be closed now. If this is an error, please provide additional details, make sure to read the docs about contributing and AI."
5054
}
5155
}

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ repos:
2828
language: unsupported
2929
types: [python]
3030

31+
- id: local-mypy
32+
name: mypy check
33+
entry: uv run mypy sqlmodel tests/test_select_typing.py
34+
require_serial: true
35+
language: unsupported
36+
pass_filenames: false
37+
3138
- id: generate-select
3239
language: unsupported
3340
name: generate-select

docs/contributing.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,39 @@ This helps to make sure that:
158158
* The documentation is up-to-date.
159159
* The documentation examples can be run as is.
160160
* Most of the features are covered by the documentation, ensured by test coverage.
161+
162+
## Automated Code and AI
163+
164+
You are encouraged to use all the tools you want to do your work and contribute as efficiently as possible, this includes AI (LLM) tools, etc. Nevertheless, contributions should have meaningful human intervention, judgement, context, etc.
165+
166+
If the **human effort** put in a PR, e.g. writing LLM prompts, is **less** than the **effort we would need to put** to **review it**, please **don't** submit the PR.
167+
168+
Think of it this way: we can already write LLM prompts or run automated tools ourselves, and that would be faster than reviewing external PRs.
169+
170+
### Closing Automated and AI PRs
171+
172+
If we see PRs that seem AI generated or automated in similar ways, we'll flag them and close them.
173+
174+
The same applies to comments and descriptions, please don't copy paste the content generated by an LLM.
175+
176+
### Human Effort Denial of Service
177+
178+
Using automated tools and AI to submit PRs or comments that we have to carefully review and handle would be the equivalent of a <a href="https://en.wikipedia.org/wiki/Denial-of-service_attack" class="external-link" target="_blank">Denial-of-service attack</a> on our human effort.
179+
180+
It would be very little effort from the person submitting the PR (an LLM prompt) that generates a large amount of effort on our side (carefully reviewing code).
181+
182+
Please don't do that.
183+
184+
We'll need to block accounts that spam us with repeated automated PRs or comments.
185+
186+
### Use Tools Wisely
187+
188+
As Uncle Ben said:
189+
190+
<blockquote>
191+
With great <strike>power</strike> <strong>tools</strong> comes great responsibility.
192+
</blockquote>
193+
194+
Avoid inadvertently doing harm.
195+
196+
You have amazing tools at hand, use them wisely to help effectively.

docs/js/custom.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,11 @@ function setupTermynal() {
8181
}
8282
}
8383
saveBuffer();
84+
const inputCommands = useLines.filter(line => line.type === "input").map(line => line.value).join("\n");
85+
node.textContent = inputCommands;
8486
const div = document.createElement("div");
85-
node.replaceWith(div);
87+
node.style.display = "none";
88+
node.after(div);
8689
const termynal = new Termynal(div, {
8790
lineData: useLines,
8891
noInit: true,

docs/release-notes.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,36 @@
22

33
## Latest Changes
44

5+
### Internal
6+
7+
* 👷 Run mypy by pre-commit. PR [#1738](https://github.com/fastapi/sqlmodel/pull/1738) by [@YuriiMotov](https://github.com/YuriiMotov).
8+
* ⬆ Bump prek from 0.3.0 to 0.3.1. PR [#1739](https://github.com/fastapi/sqlmodel/pull/1739) by [@dependabot[bot]](https://github.com/apps/dependabot).
9+
10+
## 0.0.32
11+
12+
### Fixes
13+
14+
* 🐛 Fix support for `Annotated` fields with Pydantic 2.12+. PR [#1607](https://github.com/fastapi/sqlmodel/pull/1607) by [@vimota](https://github.com/vimota).
15+
16+
### Refactors
17+
18+
* ♻️ Import `Literal` from the `typing` module directly. PR [#1699](https://github.com/fastapi/sqlmodel/pull/1699) by [@svlandeg](https://github.com/svlandeg).
19+
520
### Docs
621

22+
* 📝 Add contribution instructions about LLM generated code and comments and automated tools for PRs. PR [#1712](https://github.com/fastapi/sqlmodel/pull/1712) by [@alejsdev](https://github.com/alejsdev).
23+
* 🐛 Fix copy button in `custom.js`. PR [#1711](https://github.com/fastapi/sqlmodel/pull/1711) by [@alejsdev](https://github.com/alejsdev).
724
* 📝 Remove duplicated word in `read-relationships.md`. PR [#1705](https://github.com/fastapi/sqlmodel/pull/1705) by [@stefmolin](https://github.com/stefmolin).
825

926
### Internal
1027

28+
* ⬆ Bump ruff from 0.14.13 to 0.14.14. PR [#1721](https://github.com/fastapi/sqlmodel/pull/1721) by [@dependabot[bot]](https://github.com/apps/dependabot).
29+
* ⬆ Bump prek from 0.2.30 to 0.3.0. PR [#1720](https://github.com/fastapi/sqlmodel/pull/1720) by [@dependabot[bot]](https://github.com/apps/dependabot).
30+
* 🔧 Ensure that an edit to `uv.lock` gets the `internal` label. PR [#1719](https://github.com/fastapi/sqlmodel/pull/1719) by [@svlandeg](https://github.com/svlandeg).
31+
* ⬆ Bump sqlalchemy from 2.0.45 to 2.0.46. PR [#1717](https://github.com/fastapi/sqlmodel/pull/1717) by [@dependabot[bot]](https://github.com/apps/dependabot).
32+
* ⬆ Bump typer from 0.21.0 to 0.21.1. PR [#1715](https://github.com/fastapi/sqlmodel/pull/1715) by [@dependabot[bot]](https://github.com/apps/dependabot).
33+
* ⬆ Bump ruff from 0.14.10 to 0.14.13. PR [#1714](https://github.com/fastapi/sqlmodel/pull/1714) by [@dependabot[bot]](https://github.com/apps/dependabot).
34+
* ⬆ Bump prek from 0.2.25 to 0.2.30. PR [#1716](https://github.com/fastapi/sqlmodel/pull/1716) by [@dependabot[bot]](https://github.com/apps/dependabot).
1135
* ⬆️ Update FastAPI version pin to `>=0.103.2` in tests. PR [#1709](https://github.com/fastapi/sqlmodel/pull/1709) by [@YuriiMotov](https://github.com/YuriiMotov).
1236
* 📌 Pin development Python version to 3.10, for `deploy_docs_status.py`. PR [#1707](https://github.com/fastapi/sqlmodel/pull/1707) by [@tiangolo](https://github.com/tiangolo).
1337
* ⬆️ Migrate to uv. PR [#1688](https://github.com/fastapi/sqlmodel/pull/1688) by [@DoctorJohn](https://github.com/DoctorJohn).

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ docs = [
6666
"mkdocstrings[python]==0.30.1",
6767
"pillow==11.3.0",
6868
"pyyaml>=5.3.1,<7.0.0",
69-
"typer==0.21.0",
69+
"typer==0.21.1",
7070
]
7171
github-actions = [
7272
"httpx>=0.27.0,<0.29.0",
@@ -85,7 +85,7 @@ tests = [
8585
"mypy==1.19.1",
8686
"pre-commit>=2.17.0,<5.0.0",
8787
"pytest>=7.0.1,<9.0.0",
88-
"ruff==0.14.10",
88+
"ruff==0.14.14",
8989
"typing-extensions==4.15.0",
9090
]
9191

sqlmodel/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "0.0.31"
1+
__version__ = "0.0.32"
22

33
# Re-export from SQLAlchemy
44
from sqlalchemy.engine import create_engine as create_engine

sqlmodel/main.py

Lines changed: 72 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import uuid
66
import weakref
77
from collections.abc import Mapping, Sequence, Set
8+
from dataclasses import dataclass
89
from datetime import date, datetime, time, timedelta
910
from decimal import Decimal
1011
from enum import Enum
@@ -14,6 +15,7 @@
1415
Any,
1516
Callable,
1617
ClassVar,
18+
Literal,
1719
Optional,
1820
TypeVar,
1921
Union,
@@ -48,7 +50,7 @@
4850
from sqlalchemy.orm.instrumentation import is_instrumented
4951
from sqlalchemy.sql.schema import MetaData
5052
from sqlalchemy.sql.sqltypes import LargeBinary, Time, Uuid
51-
from typing_extensions import Literal, TypeAlias, deprecated, get_origin
53+
from typing_extensions import TypeAlias, deprecated, get_origin
5254

5355
from ._compat import ( # type: ignore[attr-defined]
5456
PYDANTIC_MINOR_VERSION,
@@ -199,6 +201,38 @@ def __init__(
199201
self.sa_relationship_kwargs = sa_relationship_kwargs
200202

201203

204+
@dataclass
205+
class FieldInfoMetadata:
206+
primary_key: Union[bool, UndefinedType] = Undefined
207+
nullable: Union[bool, UndefinedType] = Undefined
208+
foreign_key: Any = Undefined
209+
ondelete: Union[OnDeleteType, UndefinedType] = Undefined
210+
unique: Union[bool, UndefinedType] = Undefined
211+
index: Union[bool, UndefinedType] = Undefined
212+
sa_type: Union[type[Any], UndefinedType] = Undefined
213+
sa_column: Union[Column[Any], UndefinedType] = Undefined
214+
sa_column_args: Union[Sequence[Any], UndefinedType] = Undefined
215+
sa_column_kwargs: Union[Mapping[str, Any], UndefinedType] = Undefined
216+
217+
218+
def _get_sqlmodel_field_metadata(field_info: Any) -> Optional[FieldInfoMetadata]:
219+
metadata_items = getattr(field_info, "metadata", None)
220+
if metadata_items:
221+
for meta in metadata_items:
222+
if isinstance(meta, FieldInfoMetadata):
223+
return meta
224+
return None
225+
226+
227+
def _get_sqlmodel_field_value(
228+
field_info: Any, attribute: str, default: Any = Undefined
229+
) -> Any:
230+
metadata = _get_sqlmodel_field_metadata(field_info)
231+
if metadata is not None and hasattr(metadata, attribute):
232+
return getattr(metadata, attribute)
233+
return getattr(field_info, attribute, default)
234+
235+
202236
# include sa_type, sa_column_args, sa_column_kwargs
203237
@overload
204238
def Field(
@@ -422,6 +456,20 @@ def Field(
422456
default_factory=default_factory,
423457
**field_info_kwargs,
424458
)
459+
field_metadata = FieldInfoMetadata(
460+
primary_key=primary_key,
461+
nullable=nullable,
462+
foreign_key=foreign_key,
463+
ondelete=ondelete,
464+
unique=unique,
465+
index=index,
466+
sa_type=sa_type,
467+
sa_column=sa_column,
468+
sa_column_args=sa_column_args,
469+
sa_column_kwargs=sa_column_kwargs,
470+
)
471+
if hasattr(field_info, "metadata"):
472+
field_info.metadata.append(field_metadata)
425473
return field_info
426474

427475

@@ -537,7 +585,9 @@ def __new__(
537585
for key, value in kwargs.items()
538586
if key.startswith("__pydantic_")
539587
)
540-
new_cls = super().__new__(cls, name, bases, dict_used, **config_kwargs)
588+
new_cls = cast(
589+
"SQLModel", super().__new__(cls, name, bases, dict_used, **config_kwargs)
590+
)
541591
new_cls.__annotations__ = {
542592
**relationship_annotations,
543593
**pydantic_annotations,
@@ -565,10 +615,10 @@ def get_config(name: str) -> Any:
565615
# This could be done by reading new_cls.model_config['table'] in FastAPI, but
566616
# that's very specific about SQLModel, so let's have another config that
567617
# other future tools based on Pydantic can use.
568-
new_cls.model_config["read_from_attributes"] = True
618+
new_cls.model_config["read_from_attributes"] = True # type: ignore[typeddict-unknown-key]
569619
# For compatibility with older versions
570620
# TODO: remove this in the future
571-
new_cls.model_config["read_with_orm_mode"] = True
621+
new_cls.model_config["read_with_orm_mode"] = True # type: ignore[typeddict-unknown-key]
572622

573623
config_registry = get_config("registry")
574624
if config_registry is not Undefined:
@@ -642,7 +692,7 @@ def __init__(
642692

643693
def get_sqlalchemy_type(field: Any) -> Any:
644694
field_info = field
645-
sa_type = getattr(field_info, "sa_type", Undefined) # noqa: B009
695+
sa_type = _get_sqlmodel_field_value(field_info, "sa_type", Undefined) # noqa: B009
646696
if sa_type is not Undefined:
647697
return sa_type
648698

@@ -696,39 +746,39 @@ def get_sqlalchemy_type(field: Any) -> Any:
696746

697747
def get_column_from_field(field: Any) -> Column: # type: ignore
698748
field_info = field
699-
sa_column = getattr(field_info, "sa_column", Undefined)
749+
sa_column = _get_sqlmodel_field_value(field_info, "sa_column", Undefined)
700750
if isinstance(sa_column, Column):
701751
return sa_column
702752
sa_type = get_sqlalchemy_type(field)
703-
primary_key = getattr(field_info, "primary_key", Undefined)
753+
primary_key = _get_sqlmodel_field_value(field_info, "primary_key", Undefined)
704754
if primary_key is Undefined:
705755
primary_key = False
706-
index = getattr(field_info, "index", Undefined)
756+
index = _get_sqlmodel_field_value(field_info, "index", Undefined)
707757
if index is Undefined:
708758
index = False
709759
nullable = not primary_key and is_field_noneable(field)
710760
# Override derived nullability if the nullable property is set explicitly
711761
# on the field
712-
field_nullable = getattr(field_info, "nullable", Undefined) # noqa: B009
762+
field_nullable = _get_sqlmodel_field_value(field_info, "nullable", Undefined)
713763
if field_nullable is not Undefined:
714764
assert not isinstance(field_nullable, UndefinedType)
715765
nullable = field_nullable
716766
args = []
717-
foreign_key = getattr(field_info, "foreign_key", Undefined)
767+
foreign_key = _get_sqlmodel_field_value(field_info, "foreign_key", Undefined)
718768
if foreign_key is Undefined:
719769
foreign_key = None
720-
unique = getattr(field_info, "unique", Undefined)
770+
unique = _get_sqlmodel_field_value(field_info, "unique", Undefined)
721771
if unique is Undefined:
722772
unique = False
723773
if foreign_key:
724-
if field_info.ondelete == "SET NULL" and not nullable:
774+
ondelete_value = _get_sqlmodel_field_value(field_info, "ondelete", Undefined)
775+
if ondelete_value is Undefined:
776+
ondelete_value = None
777+
if ondelete_value == "SET NULL" and not nullable:
725778
raise RuntimeError('ondelete="SET NULL" requires nullable=True')
726779
assert isinstance(foreign_key, str)
727-
ondelete = getattr(field_info, "ondelete", Undefined)
728-
if ondelete is Undefined:
729-
ondelete = None
730-
assert isinstance(ondelete, (str, type(None))) # for typing
731-
args.append(ForeignKey(foreign_key, ondelete=ondelete))
780+
assert isinstance(ondelete_value, (str, type(None))) # for typing
781+
args.append(ForeignKey(foreign_key, ondelete=ondelete_value))
732782
kwargs = {
733783
"primary_key": primary_key,
734784
"nullable": nullable,
@@ -742,13 +792,15 @@ def get_column_from_field(field: Any) -> Column: # type: ignore
742792
sa_default = field_info.default
743793
if sa_default is not Undefined:
744794
kwargs["default"] = sa_default
745-
sa_column_args = getattr(field_info, "sa_column_args", Undefined)
795+
sa_column_args = _get_sqlmodel_field_value(field_info, "sa_column_args", Undefined)
746796
if sa_column_args is not Undefined:
747797
args.extend(list(cast(Sequence[Any], sa_column_args)))
748-
sa_column_kwargs = getattr(field_info, "sa_column_kwargs", Undefined)
798+
sa_column_kwargs = _get_sqlmodel_field_value(
799+
field_info, "sa_column_kwargs", Undefined
800+
)
749801
if sa_column_kwargs is not Undefined:
750802
kwargs.update(cast(dict[Any, Any], sa_column_kwargs))
751-
return Column(sa_type, *args, **kwargs) # type: ignore
803+
return Column(sa_type, *args, **kwargs)
752804

753805

754806
class_registry = weakref.WeakValueDictionary() # type: ignore

sqlmodel/sql/expression.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from collections.abc import Iterable, Mapping, Sequence
22
from typing import (
33
Any,
4+
Literal,
45
Optional,
56
TypeVar,
67
Union,
@@ -34,7 +35,6 @@
3435
UnaryExpression,
3536
)
3637
from sqlalchemy.sql.type_api import TypeEngine
37-
from typing_extensions import Literal
3838

3939
from ._expression_select_cls import Select as Select
4040
from ._expression_select_cls import SelectOfScalar as SelectOfScalar

0 commit comments

Comments
 (0)