sqlmodel-mypy-plugin 0.16.0__tar.gz → 0.18.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/AGENTS.md +5 -2
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/PKG-INFO +26 -3
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/README.md +25 -2
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/ROADMAP.md +86 -48
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/pyproject.toml +1 -1
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/src/sqlmodel_mypy/__init__.py +1 -1
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/src/sqlmodel_mypy/plugin.py +530 -49
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/src/sqlmodel_mypy/transform.py +72 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/configs/mypy-plugin-strict-typed-execute.ini +20 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/aliased_joins.py +32 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/field_validation_alias.py +10 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/getattr_column_property.py +16 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/select_varargs.py +36 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/session_execute_typed.py +32 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/table_via_model_config.py +35 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/tuple_expression.py +14 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin-strict-typed-execute_ini/session_execute_typed.py +35 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin-strict_ini/aliased_joins.py +33 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin-strict_ini/select_varargs.py +37 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin-strict_ini/table_via_model_config.py +37 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin-strict_ini/tuple_expression.py +14 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin_ini/aliased_joins.py +33 -0
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/outputs/mypy-plugin_ini/select_varargs.py +37 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/test_mypy.py +7 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/test_plugin_unit.py +915 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/test_transform_unit.py +42 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/modules/select_varargs.py +0 -18
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/.gitignore +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/.pre-commit-config.yaml +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/LICENSE +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/src/sqlmodel_mypy/persisted.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/src/sqlmodel_mypy/py.typed +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/conftest.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/mypy-plugin.ini +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/configs/pyproject-plugin.toml +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/annotated_field_metadata.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/docs_parity_many_to_many_create_data.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/docs_parity_read_connected_data.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/docs_parity_relationship_attributes.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/docs_parity_update_patterns.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/expression_typing_strict.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/field_sa_type.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/label.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/nullable_defaults.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/selectinload.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/session_exec.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/session_get.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/table_dunders.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/modules/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-pydantic-first_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-pydantic-last_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/annotated_field_metadata.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_many_to_many_create_data.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_read_connected_data.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_relationship_attributes.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_update_patterns.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/smoke_test.py +0 -0
- {sqlmodel_mypy_plugin-0.16.0 → sqlmodel_mypy_plugin-0.18.0}/tests/test_persisted_helpers.py +0 -0
|
@@ -47,8 +47,11 @@ make update-mypy
|
|
|
47
47
|
## What the plugin does (scope)
|
|
48
48
|
|
|
49
49
|
- Collects SQLModel fields and determines required/optional kwargs based on `sqlmodel.Field(...)`.
|
|
50
|
-
-
|
|
50
|
+
- For `table=True` models, includes `sqlmodel.Relationship(...)` members as **optional** constructor kwargs.
|
|
51
51
|
- Synthesizes `__init__` and `model_construct` signatures for SQLModel subclasses during mypy semantic analysis.
|
|
52
|
+
- Adjusts `table=True` SQLModel class attribute types to behave like SQLAlchemy expressions (e.g. `User.id` and relationship expression/comparator patterns).
|
|
53
|
+
- Exposes SQLAlchemy table metadata on `table=True` models (e.g. `Model.__table__`).
|
|
54
|
+
- Adds targeted typing fixes for common query patterns/helpers (`select(...)` 5+ entities fallback, `sqlmodel.tuple_(...)` as an expression, outer-join `None` propagation, broadened `Session.exec(...)`, optional typed `Session.execute(...)`, and best-effort `column_property(...)` return typing).
|
|
52
55
|
|
|
53
56
|
## Repo layout (where things live)
|
|
54
57
|
|
|
@@ -83,4 +86,4 @@ make update-mypy
|
|
|
83
86
|
- To release:
|
|
84
87
|
- Bump the version.
|
|
85
88
|
- Tag `vX.Y.Z` and push the tag.
|
|
86
|
-
- GitHub Actions publishes to PyPI via **trusted publishing**
|
|
89
|
+
- GitHub Actions publishes to PyPI via **trusted publishing**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-mypy-plugin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.18.0
|
|
4
4
|
Summary: Mypy plugin for SQLModel (better constructor/Field typing).
|
|
5
5
|
Project-URL: Homepage, https://github.com/sertione/sqlmodel-mypy-plugin
|
|
6
6
|
Project-URL: Repository, https://github.com/sertione/sqlmodel-mypy-plugin
|
|
@@ -68,11 +68,18 @@ Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
|
|
|
68
68
|
- Outer-join `None` propagation in `select(A, B).join(B, isouter=True)` result tuples.
|
|
69
69
|
- Broaden `Session.exec()` / `AsyncSession.exec()` typing to accept SQLAlchemy `Executable` statements (e.g.
|
|
70
70
|
`text(...)`), not just `select(...)`.
|
|
71
|
+
- Optional typed `Session.execute()` / `AsyncSession.execute()` for `select(...)` (opt-in; see `typed_execute`).
|
|
71
72
|
- Accept SQLAlchemy TypeEngine instances in `Field(sa_type=...)` (e.g. `DateTime(timezone=True)`, `String(50)`)
|
|
72
73
|
without `# type: ignore` in strict mode.
|
|
73
74
|
- Accept `model_config = ConfigDict(...)` overrides on SQLModel subclasses in strict mode.
|
|
74
75
|
- Compatible with `pydantic.mypy` **when `sqlmodel_mypy.plugin` is listed first**.
|
|
75
76
|
|
|
77
|
+
## Install
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
uv add sqlmodel-mypy-plugin
|
|
81
|
+
```
|
|
82
|
+
|
|
76
83
|
## Install (dev)
|
|
77
84
|
|
|
78
85
|
Prereqs: **Python 3.10+** and [`uv`](https://docs.astral.sh/uv/).
|
|
@@ -119,6 +126,9 @@ Supported options:
|
|
|
119
126
|
`__init__` / `model_construct` / constructor signatures, so mypy reports unexpected keyword arguments.
|
|
120
127
|
- **`warn_untyped_fields`** (default: `true`): When `true`, emit error code `sqlmodel-field` for untyped
|
|
121
128
|
declarations like `x = Field(...)` / `x = Relationship(...)` (use `x: T = ...` instead).
|
|
129
|
+
- **`typed_execute`** (default: `false`): When `true`, type SQLModel’s deprecated `Session.execute()` /
|
|
130
|
+
`AsyncSession.execute()` for common `select(...)` statements by recovering `Result[...]` generics. This is meant to
|
|
131
|
+
reduce “type clutter” during migrations; prefer `Session.exec()` for new code.
|
|
122
132
|
- **`debug_dataclass_transform`** (default: `false`): **Advanced/debug-only**. When `true`, keep SQLModel’s
|
|
123
133
|
`__dataclass_transform__` handling enabled in mypy (useful for debugging plugin interactions; not recommended
|
|
124
134
|
for normal use).
|
|
@@ -130,6 +140,7 @@ Supported options:
|
|
|
130
140
|
init_typed = false
|
|
131
141
|
init_forbid_extra = false
|
|
132
142
|
warn_untyped_fields = true
|
|
143
|
+
typed_execute = false
|
|
133
144
|
debug_dataclass_transform = false
|
|
134
145
|
```
|
|
135
146
|
|
|
@@ -140,12 +151,15 @@ debug_dataclass_transform = false
|
|
|
140
151
|
init_typed = false
|
|
141
152
|
init_forbid_extra = false
|
|
142
153
|
warn_untyped_fields = true
|
|
154
|
+
typed_execute = false
|
|
143
155
|
debug_dataclass_transform = false
|
|
144
156
|
```
|
|
145
157
|
|
|
146
158
|
## Error codes
|
|
147
159
|
|
|
148
160
|
- `sqlmodel-field`: field-related plugin errors (e.g. untyped `x = Field(...)`).
|
|
161
|
+
- `sqlmodel-plugin-order`: plugin ordering errors (e.g. `pydantic.mypy` enabled before `sqlmodel_mypy.plugin`).
|
|
162
|
+
Fix: list `sqlmodel_mypy.plugin` first (see [Enable in mypy](#enable-in-mypy)).
|
|
149
163
|
|
|
150
164
|
## SQL expression typing
|
|
151
165
|
|
|
@@ -170,13 +184,22 @@ tbl = User.__table__
|
|
|
170
184
|
|
|
171
185
|
### `getattr(Model, "field")` support
|
|
172
186
|
|
|
173
|
-
If you build query filters dynamically, `getattr(Model, "field")` with a **string literal**
|
|
174
|
-
direct `Model.field` access on `table=True` models:
|
|
187
|
+
If you build query filters dynamically, `getattr(Model, "field")` with a **string literal** (or a `Final` / string
|
|
188
|
+
`Literal[...]` name) is typed the same as direct `Model.field` access on `table=True` models:
|
|
175
189
|
|
|
176
190
|
```py
|
|
177
191
|
stmt = select(User).where(getattr(User, "name").like("%x%"))
|
|
178
192
|
```
|
|
179
193
|
|
|
194
|
+
Example using a `Final` constant:
|
|
195
|
+
|
|
196
|
+
```py
|
|
197
|
+
from typing import Final
|
|
198
|
+
|
|
199
|
+
FIELD: Final = "name"
|
|
200
|
+
stmt = select(User).where(getattr(User, FIELD).like("%x%"))
|
|
201
|
+
```
|
|
202
|
+
|
|
180
203
|
Non-literal names (runtime strings) are intentionally left as `Any` (you’ll still need a cast or a different
|
|
181
204
|
pattern).
|
|
182
205
|
|
|
@@ -22,11 +22,18 @@ Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
|
|
|
22
22
|
- Outer-join `None` propagation in `select(A, B).join(B, isouter=True)` result tuples.
|
|
23
23
|
- Broaden `Session.exec()` / `AsyncSession.exec()` typing to accept SQLAlchemy `Executable` statements (e.g.
|
|
24
24
|
`text(...)`), not just `select(...)`.
|
|
25
|
+
- Optional typed `Session.execute()` / `AsyncSession.execute()` for `select(...)` (opt-in; see `typed_execute`).
|
|
25
26
|
- Accept SQLAlchemy TypeEngine instances in `Field(sa_type=...)` (e.g. `DateTime(timezone=True)`, `String(50)`)
|
|
26
27
|
without `# type: ignore` in strict mode.
|
|
27
28
|
- Accept `model_config = ConfigDict(...)` overrides on SQLModel subclasses in strict mode.
|
|
28
29
|
- Compatible with `pydantic.mypy` **when `sqlmodel_mypy.plugin` is listed first**.
|
|
29
30
|
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
uv add sqlmodel-mypy-plugin
|
|
35
|
+
```
|
|
36
|
+
|
|
30
37
|
## Install (dev)
|
|
31
38
|
|
|
32
39
|
Prereqs: **Python 3.10+** and [`uv`](https://docs.astral.sh/uv/).
|
|
@@ -73,6 +80,9 @@ Supported options:
|
|
|
73
80
|
`__init__` / `model_construct` / constructor signatures, so mypy reports unexpected keyword arguments.
|
|
74
81
|
- **`warn_untyped_fields`** (default: `true`): When `true`, emit error code `sqlmodel-field` for untyped
|
|
75
82
|
declarations like `x = Field(...)` / `x = Relationship(...)` (use `x: T = ...` instead).
|
|
83
|
+
- **`typed_execute`** (default: `false`): When `true`, type SQLModel’s deprecated `Session.execute()` /
|
|
84
|
+
`AsyncSession.execute()` for common `select(...)` statements by recovering `Result[...]` generics. This is meant to
|
|
85
|
+
reduce “type clutter” during migrations; prefer `Session.exec()` for new code.
|
|
76
86
|
- **`debug_dataclass_transform`** (default: `false`): **Advanced/debug-only**. When `true`, keep SQLModel’s
|
|
77
87
|
`__dataclass_transform__` handling enabled in mypy (useful for debugging plugin interactions; not recommended
|
|
78
88
|
for normal use).
|
|
@@ -84,6 +94,7 @@ Supported options:
|
|
|
84
94
|
init_typed = false
|
|
85
95
|
init_forbid_extra = false
|
|
86
96
|
warn_untyped_fields = true
|
|
97
|
+
typed_execute = false
|
|
87
98
|
debug_dataclass_transform = false
|
|
88
99
|
```
|
|
89
100
|
|
|
@@ -94,12 +105,15 @@ debug_dataclass_transform = false
|
|
|
94
105
|
init_typed = false
|
|
95
106
|
init_forbid_extra = false
|
|
96
107
|
warn_untyped_fields = true
|
|
108
|
+
typed_execute = false
|
|
97
109
|
debug_dataclass_transform = false
|
|
98
110
|
```
|
|
99
111
|
|
|
100
112
|
## Error codes
|
|
101
113
|
|
|
102
114
|
- `sqlmodel-field`: field-related plugin errors (e.g. untyped `x = Field(...)`).
|
|
115
|
+
- `sqlmodel-plugin-order`: plugin ordering errors (e.g. `pydantic.mypy` enabled before `sqlmodel_mypy.plugin`).
|
|
116
|
+
Fix: list `sqlmodel_mypy.plugin` first (see [Enable in mypy](#enable-in-mypy)).
|
|
103
117
|
|
|
104
118
|
## SQL expression typing
|
|
105
119
|
|
|
@@ -124,13 +138,22 @@ tbl = User.__table__
|
|
|
124
138
|
|
|
125
139
|
### `getattr(Model, "field")` support
|
|
126
140
|
|
|
127
|
-
If you build query filters dynamically, `getattr(Model, "field")` with a **string literal**
|
|
128
|
-
direct `Model.field` access on `table=True` models:
|
|
141
|
+
If you build query filters dynamically, `getattr(Model, "field")` with a **string literal** (or a `Final` / string
|
|
142
|
+
`Literal[...]` name) is typed the same as direct `Model.field` access on `table=True` models:
|
|
129
143
|
|
|
130
144
|
```py
|
|
131
145
|
stmt = select(User).where(getattr(User, "name").like("%x%"))
|
|
132
146
|
```
|
|
133
147
|
|
|
148
|
+
Example using a `Final` constant:
|
|
149
|
+
|
|
150
|
+
```py
|
|
151
|
+
from typing import Final
|
|
152
|
+
|
|
153
|
+
FIELD: Final = "name"
|
|
154
|
+
stmt = select(User).where(getattr(User, FIELD).like("%x%"))
|
|
155
|
+
```
|
|
156
|
+
|
|
134
157
|
Non-literal names (runtime strings) are intentionally left as `Any` (you’ll still need a cast or a different
|
|
135
158
|
pattern).
|
|
136
159
|
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
- Decide approach: plugin hooks vs stub overlays vs relying on SQLAlchemy typing (documented trade-offs).
|
|
24
24
|
- Compatibility matrix CI across mypy/SQLModel versions (and pin policy).
|
|
25
25
|
- **Plugin ordering guardrails (mypy limitation)**
|
|
26
|
-
- **Reality check**: mypy generally uses the **first plugin that claims a hook**, so true
|
|
26
|
+
- **Reality check**: mypy generally uses the **first plugin that claims a hook**, so true "order independence"
|
|
27
27
|
between `sqlmodel_mypy.plugin` and `pydantic.mypy` is not realistically achievable today.
|
|
28
28
|
- Enforce and clearly explain that `sqlmodel_mypy.plugin` must be listed **before** `pydantic.mypy`:
|
|
29
29
|
- Source: `src/sqlmodel_mypy/plugin.py` (`SQLModelMypyPlugin.__init__`, `_sqlmodel_metaclass_callback()`).
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
|
|
32
32
|
## v0.4 (Docs-aligned relationship constructors) - DONE
|
|
33
33
|
|
|
34
|
-
**Motivation**: SQLModel
|
|
34
|
+
**Motivation**: SQLModel's official docs pass relationship attributes in constructors (e.g. `Hero(team=team)`,
|
|
35
35
|
`Team(heroes=[...])`). v0.4 accepts relationship kwargs in generated `__init__` / `model_construct` for
|
|
36
|
-
`table=True` models, so `init_forbid_extra=true` no longer causes false-positive
|
|
36
|
+
`table=True` models, so `init_forbid_extra=true` no longer causes false-positive "unexpected keyword argument"
|
|
37
37
|
errors for documented patterns.
|
|
38
38
|
|
|
39
39
|
- **Accept relationship kwargs in generated `__init__` / `model_construct` (table models)**
|
|
@@ -63,17 +63,17 @@ errors for documented patterns.
|
|
|
63
63
|
pattern (and keep types conservative).
|
|
64
64
|
- Update/add mypy integration tests to match the chosen strategy.
|
|
65
65
|
|
|
66
|
-
## v0.5 (Model-kind awareness + reduce
|
|
66
|
+
## v0.5 (Model-kind awareness + reduce "surprising" typing) - DONE
|
|
67
67
|
|
|
68
68
|
- **Table-model awareness for SQLAlchemy expression typing**
|
|
69
69
|
- Only treat class attributes as SQLAlchemy expressions (e.g. wrap to `InstrumentedAttribute[T]`) for `table=True`
|
|
70
70
|
SQLModel models.
|
|
71
|
-
- Avoid changing attribute types for
|
|
71
|
+
- Avoid changing attribute types for "data models" (non-table SQLModel subclasses) used for FastAPI schemas, e.g.
|
|
72
72
|
`TeamBase`, `HeroCreate`, `HeroPublic`.
|
|
73
73
|
- Docs: `https://sqlmodel.tiangolo.com/tutorial/fastapi/relationships/`
|
|
74
74
|
|
|
75
75
|
- **Pydantic/Field alias & config parity checkpoints**
|
|
76
|
-
- Verify our generated signatures stay compatible with SQLModel
|
|
76
|
+
- Verify our generated signatures stay compatible with SQLModel's `Field(...)` alias behavior (alias propagation
|
|
77
77
|
and precedence in `validation_alias` / `serialization_alias`).
|
|
78
78
|
- Source: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py` (`Field()` implementation).
|
|
79
79
|
- Document what is supported vs intentionally out of scope when users rely on aliases in constructor kwargs.
|
|
@@ -94,20 +94,20 @@ errors for documented patterns.
|
|
|
94
94
|
- **More query-shape coverage**
|
|
95
95
|
- Multi-entity selects, joins/outer-joins, and relationship-based filters across common SQLModel tutorial patterns.
|
|
96
96
|
- **Document trade-offs (and pick a default strategy)**
|
|
97
|
-
- Plugin hooks vs stub overlays vs relying purely on SQLAlchemy typing (with
|
|
97
|
+
- Plugin hooks vs stub overlays vs relying purely on SQLAlchemy typing (with "when to use what" guidance).
|
|
98
98
|
|
|
99
99
|
## v0.8 (Field `sa_type=` supports SQLAlchemy TypeEngine instances) - DONE
|
|
100
100
|
|
|
101
|
-
**Motivation**: SQLModel
|
|
101
|
+
**Motivation**: SQLModel's `Field(sa_type=...)` is a supported API, but real-world usage commonly passes
|
|
102
102
|
**instantiated SQLAlchemy types** (e.g. `DateTime(timezone=True)`, `String(50)`), which triggers mypy
|
|
103
103
|
`call-overload` errors under `--strict`. Users currently work around this with `sa_column=Column(...)` or
|
|
104
|
-
`# type: ignore`, which is exactly the
|
|
104
|
+
`# type: ignore`, which is exactly the "types clutter" we want to remove.
|
|
105
105
|
|
|
106
106
|
- **Fix mypy `call-overload` for `Field(sa_type=<TypeEngine instance>)`**
|
|
107
107
|
- Upstream issue reports:
|
|
108
108
|
- `https://github.com/fastapi/sqlmodel/discussions/955`
|
|
109
109
|
- `https://github.com/fastapi/sqlmodel/discussions/1228`
|
|
110
|
-
- Upstream pending fix (adjust `sa_type` annotation to match SQLAlchemy
|
|
110
|
+
- Upstream pending fix (adjust `sa_type` annotation to match SQLAlchemy's `Column` type argument rules):
|
|
111
111
|
- `https://github.com/fastapi/sqlmodel/pull/1345`
|
|
112
112
|
- Source references:
|
|
113
113
|
- SQLModel `Field()` signature: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py`
|
|
@@ -122,7 +122,7 @@ errors for documented patterns.
|
|
|
122
122
|
touching runtime behavior.
|
|
123
123
|
|
|
124
124
|
- **Tests**
|
|
125
|
-
- Add a mypy integration module that reproduces the failure in strict mode and asserts it
|
|
125
|
+
- Add a mypy integration module that reproduces the failure in strict mode and asserts it's fixed without
|
|
126
126
|
`# type: ignore`, e.g.:
|
|
127
127
|
- `Field(sa_type=DateTime(timezone=True))`
|
|
128
128
|
- `Field(sa_type=String(50))`
|
|
@@ -133,7 +133,7 @@ errors for documented patterns.
|
|
|
133
133
|
|
|
134
134
|
**Motivation**: SQLModel is built on Pydantic v2, and users often want to set things like `extra="forbid"` on
|
|
135
135
|
schema-like models (e.g. `HeroCreate`, `HeroUpdate`) via `model_config = ConfigDict(...)`. Today this can fail mypy
|
|
136
|
-
strict with an
|
|
136
|
+
strict with an "incompatible types in assignment" error (`ConfigDict` vs `SQLModelConfig`), forcing users into
|
|
137
137
|
`# type: ignore` or `cast(...)`.
|
|
138
138
|
|
|
139
139
|
- **Fix mypy incompatibility for `model_config` overrides**
|
|
@@ -148,7 +148,7 @@ strict with an “incompatible types in assignment” error (`ConfigDict` vs `SQ
|
|
|
148
148
|
so assignment from `ConfigDict(...)` is always accepted.
|
|
149
149
|
- **Plugin adjustment**: during semantic analysis, rewrite the inferred type of `model_config` on SQLModel subclasses
|
|
150
150
|
to a compatible supertype (while keeping runtime untouched).
|
|
151
|
-
- Ensure we don
|
|
151
|
+
- Ensure we don't regress table-model behavior: SQLModel uses `model_config["table"]` and `model_config["registry"]`
|
|
152
152
|
at runtime (see `SQLModelMetaclass.__new__`).
|
|
153
153
|
|
|
154
154
|
- **Tests**
|
|
@@ -160,8 +160,8 @@ strict with an “incompatible types in assignment” error (`ConfigDict` vs `SQ
|
|
|
160
160
|
## v0.10 (`select()` varargs: avoid overload ceiling) - DONE
|
|
161
161
|
|
|
162
162
|
**Motivation**: SQLModel provides its own typed `select()` overloads, but they are generated only up to **4**
|
|
163
|
-
entities. In real code it
|
|
164
|
-
can trigger
|
|
163
|
+
entities. In real code it's common to select more than 4 columns/entities (especially with joins/aggregates), which
|
|
164
|
+
can trigger "No overload variant of `select` matches argument types …" even though runtime works.
|
|
165
165
|
|
|
166
166
|
- **Fix overload ceiling for `sqlmodel.select(...)`**
|
|
167
167
|
- Source reference (generated overloads; note the 4-entity cap):
|
|
@@ -171,26 +171,26 @@ can trigger “No overload variant of `select` matches argument types …” eve
|
|
|
171
171
|
- `https://github.com/fastapi/sqlmodel/issues/271`
|
|
172
172
|
|
|
173
173
|
- **Implementation options (pick default; document trade-offs)**
|
|
174
|
-
- **Stub overlay (likely simplest)**: add a final
|
|
174
|
+
- **Stub overlay (likely simplest)**: add a final "varargs fallback" overload, e.g.:
|
|
175
175
|
- `@overload`
|
|
176
176
|
- `def select(__ent0: Any, __ent1: Any, __ent2: Any, __ent3: Any, __ent4: Any, *entities: Any) -> Select[tuple[Any, ...]]: ...`
|
|
177
177
|
so mypy always has a matching overload beyond 4 args.
|
|
178
178
|
- **Plugin signature hook**: provide a mypy hook for `sqlmodel.sql.expression.select` that adds the same fallback
|
|
179
179
|
overload, keeping the existing 1–4 entity overload precision.
|
|
180
|
-
- Optionally, generate more precise overloads (e.g. up to 8) if we
|
|
180
|
+
- Optionally, generate more precise overloads (e.g. up to 8) if we're willing to carry the maintenance cost.
|
|
181
181
|
|
|
182
182
|
- **Tests**
|
|
183
183
|
- Add a mypy integration module that selects 5+ entities and asserts no `call-overload` errors under strict mode.
|
|
184
184
|
- Decide and lock down the fallback return type (likely `Select[tuple[Any, ...]]`) to keep behavior predictable.
|
|
185
185
|
|
|
186
|
-
## v0.11 (SQLAlchemy
|
|
186
|
+
## v0.11 (SQLAlchemy "extras" coverage + SQLModel `Select` join parity) - DONE
|
|
187
187
|
|
|
188
188
|
- **Lock down common SQLAlchemy helpers used with SQLModel**
|
|
189
189
|
- `.label(...)` on `table=True` model fields typed as SQLAlchemy expressions.
|
|
190
190
|
- ORM loader options like `selectinload(Model.relationship)` on `table=True` relationships.
|
|
191
191
|
- `Select.execution_options(...)` chaining without losing the `Select[...]` generic (including outer-join `None` propagation).
|
|
192
|
-
- **Fix outer-join `None` propagation for SQLModel
|
|
193
|
-
- Apply the join/outerjoin return-type adjustment to both SQLAlchemy `Select` and SQLModel
|
|
192
|
+
- **Fix outer-join `None` propagation for SQLModel's own `Select` wrapper**
|
|
193
|
+
- Apply the join/outerjoin return-type adjustment to both SQLAlchemy `Select` and SQLModel's `sqlmodel.sql._expression_select_cls.Select`.
|
|
194
194
|
- **Tests**
|
|
195
195
|
- Add/extend mypy integration coverage and commit golden outputs for the new cases.
|
|
196
196
|
|
|
@@ -201,7 +201,7 @@ models do provide this at runtime for `table=True`, but type checkers can miss i
|
|
|
201
201
|
|
|
202
202
|
- **Expose and type `__table__` on `table=True` SQLModel models**
|
|
203
203
|
- Provide a plugin-generated `__table__` attribute typed as `sqlalchemy.sql.schema.Table` (with safe fallbacks
|
|
204
|
-
when stubs don
|
|
204
|
+
when stubs don't expose `Table`).
|
|
205
205
|
- **Tests**
|
|
206
206
|
- Add mypy integration coverage for `Model.__table__` and `Session.get` / `AsyncSession.get` with SQLModel
|
|
207
207
|
table models.
|
|
@@ -230,8 +230,8 @@ signatures (required/optional + alias kwargs) when `Field(...)` metadata lives i
|
|
|
230
230
|
(`_collect_member_from_stmt`) so class-call signatures stay consistent with generated `__init__`.
|
|
231
231
|
- Source references:
|
|
232
232
|
- SQLModel `Field()` overloads + alias precedence: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py`
|
|
233
|
-
- `Annotated` handling in SQLModel
|
|
234
|
-
- Release note:
|
|
233
|
+
- `Annotated` handling in SQLModel's type parsing: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py`
|
|
234
|
+
- Release note: "Fix support for types with `Optional[Annotated[x, f()]]`…" (PR #1093):
|
|
235
235
|
`https://github.com/fastapi/sqlmodel/pull/1093`
|
|
236
236
|
|
|
237
237
|
## v0.15 (Outer-join `None` propagation for relationship-attribute joins) - DONE
|
|
@@ -253,20 +253,20 @@ signatures (required/optional + alias kwargs) when `Field(...)` metadata lives i
|
|
|
253
253
|
|
|
254
254
|
## v0.16 (Strict typing ergonomics add-ons, opt-in) - DONE
|
|
255
255
|
|
|
256
|
-
**Motivation**: strict typing + SQLModel often means the database populates values
|
|
257
|
-
refresh), especially primary keys (`id: int | None`). Mypy can
|
|
256
|
+
**Motivation**: strict typing + SQLModel often means the database populates values "later" (after flush/commit/
|
|
257
|
+
refresh), especially primary keys (`id: int | None`). Mypy can't infer those runtime effects, and users end up with
|
|
258
258
|
repeated `assert obj.id is not None` (type clutter).
|
|
259
259
|
|
|
260
260
|
- **Provide a tiny opt-in narrowing helper**
|
|
261
261
|
- Docs context (why `id` starts as `None` and is populated after commit/refresh):
|
|
262
262
|
- `https://sqlmodel.tiangolo.com/tutorial/automatic-id-none-refresh/`
|
|
263
|
-
- Ship a small helper in this package (runtime-free or near-zero runtime) that helps users narrow
|
|
263
|
+
- Ship a small helper in this package (runtime-free or near-zero runtime) that helps users narrow "persisted" models
|
|
264
264
|
without `cast(...)`/`# type: ignore`, e.g.:
|
|
265
265
|
- `assert_persisted(obj)` / `require_id(obj)` / `ensure_persisted(obj)` returning a `TypeGuard[...]` or using
|
|
266
266
|
overloads to narrow `id` to `int`.
|
|
267
267
|
- Keep it optional and well-documented; default plugin behavior should remain conservative.
|
|
268
268
|
|
|
269
|
-
## v0.17 (`tuple_()` should return a SQL expression, not a Python tuple)
|
|
269
|
+
## v0.17 (`tuple_()` should return a SQL expression, not a Python tuple) - DONE
|
|
270
270
|
|
|
271
271
|
**Motivation**: SQLModel re-exports `tuple_` from `sqlmodel.sql.expression`, but its typing currently says it returns a
|
|
272
272
|
built-in `tuple[Any, ...]`. In SQLAlchemy, `tuple_()` returns a SQL expression object (`sqlalchemy.sql.elements.Tuple`)
|
|
@@ -286,26 +286,64 @@ for otherwise normal SQLAlchemy patterns.
|
|
|
286
286
|
- Add a mypy integration module demonstrating composite IN typing without ignores, e.g.:
|
|
287
287
|
- `tuple_(User.id, User.team_id).in_([(1, 2)])`
|
|
288
288
|
|
|
289
|
-
## v0.18 (
|
|
289
|
+
## v0.18 (Comprehensive typing polish) - DONE
|
|
290
290
|
|
|
291
|
-
|
|
292
|
-
table model even when it isn’t declared as `class Model(SQLModel, table=True)`. Today our plugin/transformer only
|
|
293
|
-
looks at the class keyword, so “table-only” behaviors may not activate (expression typing, relationship kwargs in
|
|
294
|
-
constructor signatures, `__table__`, etc.).
|
|
291
|
+
This release consolidates several enhancements to round out SQLModel/mypy strict-mode ergonomics.
|
|
295
292
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
293
|
+
### Recognize `table=True` via `model_config["table"]`
|
|
294
|
+
|
|
295
|
+
SQLModel's runtime `is_table_model_class()` checks `model_config.get("table")`, so a class can be a table model even
|
|
296
|
+
without `class Model(SQLModel, table=True)`.
|
|
297
|
+
|
|
298
|
+
- Extend semantic analysis in `src/sqlmodel_mypy/transform.py` to detect `model_config = ConfigDict(table=True)`.
|
|
299
|
+
- Persist the result in `TypeInfo.metadata["sqlmodel-mypy-metadata"]`, then update `_is_table_model(...)` in both
|
|
300
|
+
`plugin.py` and `transform.py` to consult metadata first.
|
|
301
|
+
- Source references:
|
|
302
|
+
- SQLModel metaclass: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py` (`SQLModelMetaclass.__new__`)
|
|
303
|
+
- SQLModel runtime table detection: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py`
|
|
304
|
+
|
|
305
|
+
### `select(5+)`: recover tuple element types
|
|
306
|
+
|
|
307
|
+
v0.10 fixed the overload ceiling by accepting `select(5+)`, but the fallback return type was `Select[tuple[Any, ...]]`.
|
|
308
|
+
|
|
309
|
+
- Add a best-effort return-type hook that recovers element types from inferred argument expression types.
|
|
310
|
+
- Code: `_sqlmodel_select_signature_callback()`, `_sqlmodel_select_return_type_callback()`, `_sqlmodel_select_entity_result_type()`
|
|
311
|
+
- Tests: `tests/mypy/modules/select_varargs.py`
|
|
312
|
+
|
|
313
|
+
### Outer-join `None` propagation for aliased/variable join targets
|
|
314
|
+
|
|
315
|
+
Outer-join `None` propagation previously only worked when the join target was a direct model ref or relationship
|
|
316
|
+
attribute, not when users join via an alias variable (e.g. `team_alias = aliased(Team)`).
|
|
317
|
+
|
|
318
|
+
- Extend join target type inference to handle typed variables (including `aliased(Model)` results).
|
|
319
|
+
- Code: `_typeinfo_from_join_target_expr()`, `_typeinfo_from_typed_join_target_expr()`, `_typeinfo_from_typed_join_target_type()`
|
|
320
|
+
- Tests: `tests/mypy/modules/aliased_joins.py`
|
|
321
|
+
|
|
322
|
+
### `getattr(Model, NAME)`: support `Final`/`Literal[...]` names
|
|
323
|
+
|
|
324
|
+
Dynamic query builders often use constants for attribute names; without this, `getattr(...)` falls back to `Any`.
|
|
325
|
+
|
|
326
|
+
- Extend `getattr(Model, NAME)` typing beyond string literals by accepting name args typed as string `Literal[...]`
|
|
327
|
+
(and best-effort `Final` constants where mypy retains the literal).
|
|
328
|
+
- Code: `_sqlmodel_getattr_return_type_callback()`
|
|
329
|
+
- Tests: `tests/mypy/modules/getattr_column_property.py`
|
|
330
|
+
|
|
331
|
+
### `Field(validation_alias=...)`: Pydantic v2 alias parity
|
|
332
|
+
|
|
333
|
+
Pydantic v2 supports `validation_alias=AliasPath(...)` / `AliasChoices(...)`, but SQLModel's `Field(...)` typing is
|
|
334
|
+
narrower; strict mypy users otherwise need `cast(...)`/`# type: ignore`.
|
|
335
|
+
|
|
336
|
+
- Widen `Field(validation_alias=...)` to accept `pydantic.aliases.AliasPath | AliasChoices` (idempotent if upstream
|
|
337
|
+
is already widened).
|
|
338
|
+
- Code: `_sqlmodel_field_signature_callback()`
|
|
339
|
+
- Tests: `tests/mypy/modules/field_validation_alias.py`
|
|
340
|
+
|
|
341
|
+
### Optional typed `Session.execute()` / `AsyncSession.execute()`
|
|
342
|
+
|
|
343
|
+
SQLModel overrides `execute()` and types it as `Result[Any]`, shadowing SQLAlchemy's typed overloads. Even though
|
|
344
|
+
`execute()` is deprecated in SQLModel docs, users still rely on it during migrations.
|
|
345
|
+
|
|
346
|
+
- Add an opt-in mypy plugin behavior that types `execute(select(...))` as `Result[...]` (and preserves async wrapping).
|
|
347
|
+
- Config: `typed_execute = true` under `[sqlmodel-mypy]`
|
|
348
|
+
- Code: `SQLModelPluginConfig.typed_execute`, `_sqlmodel_session_execute_return_type_callback()`, `_row_tuple_type_from_typed_statement()`
|
|
349
|
+
- Tests: `tests/mypy/configs/mypy-plugin-strict-typed-execute.ini`, `tests/mypy/modules/session_execute_typed.py`
|