sqlmodel-mypy-plugin 0.12.2__tar.gz → 0.16.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.12.2 → sqlmodel_mypy_plugin-0.16.0}/PKG-INFO +36 -5
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/README.md +35 -4
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/ROADMAP.md +110 -1
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/pyproject.toml +1 -1
- sqlmodel_mypy_plugin-0.16.0/src/sqlmodel_mypy/__init__.py +5 -0
- sqlmodel_mypy_plugin-0.16.0/src/sqlmodel_mypy/persisted.py +35 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/src/sqlmodel_mypy/plugin.py +342 -4
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/src/sqlmodel_mypy/transform.py +267 -2
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/modules/annotated_field_metadata.py +34 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/modules/docs_parity_many_to_many_create_data.py +51 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/modules/docs_parity_read_connected_data.py +48 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/modules/docs_parity_relationship_attributes.py +48 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/modules/docs_parity_update_patterns.py +40 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/modules/getattr_column_property.py +45 -0
- {sqlmodel_mypy_plugin-0.12.2/tests/mypy/outputs/mypy-plugin_ini → sqlmodel_mypy_plugin-0.16.0/tests/mypy/modules}/joins.py +19 -2
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/modules/persisted_helpers.py +27 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/outputs/mypy-plugin-strict_ini/annotated_field_metadata.py +38 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_many_to_many_create_data.py +52 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_read_connected_data.py +49 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_relationship_attributes.py +49 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_update_patterns.py +41 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +55 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/outputs/mypy-plugin-strict_ini/persisted_helpers.py +28 -0
- sqlmodel_mypy_plugin-0.16.0/tests/mypy/outputs/mypy-plugin_ini/joins.py +55 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/test_mypy.py +8 -0
- sqlmodel_mypy_plugin-0.16.0/tests/test_persisted_helpers.py +24 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/test_plugin_unit.py +598 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/test_transform_unit.py +105 -0
- sqlmodel_mypy_plugin-0.12.2/src/sqlmodel_mypy/__init__.py +0 -3
- sqlmodel_mypy_plugin-0.12.2/tests/mypy/modules/joins.py +0 -34
- sqlmodel_mypy_plugin-0.12.2/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -35
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/.gitignore +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/.pre-commit-config.yaml +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/AGENTS.md +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/LICENSE +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/src/sqlmodel_mypy/py.typed +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/conftest.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/configs/mypy-plugin.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/configs/pyproject-plugin.toml +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/expression_typing_strict.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/field_sa_type.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/label.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/nullable_defaults.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/selectinload.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/session_exec.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/session_get.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/table_dunders.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/modules/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-pydantic-first_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-pydantic-last_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.12.2 → sqlmodel_mypy_plugin-0.16.0}/tests/smoke_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-mypy-plugin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.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
|
|
@@ -168,6 +168,18 @@ You can also use SQLAlchemy table metadata attributes without `attr-defined` noi
|
|
|
168
168
|
tbl = User.__table__
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
+
### `getattr(Model, "field")` support
|
|
172
|
+
|
|
173
|
+
If you build query filters dynamically, `getattr(Model, "field")` with a **string literal** is typed the same as
|
|
174
|
+
direct `Model.field` access on `table=True` models:
|
|
175
|
+
|
|
176
|
+
```py
|
|
177
|
+
stmt = select(User).where(getattr(User, "name").like("%x%"))
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Non-literal names (runtime strings) are intentionally left as `Any` (you’ll still need a cast or a different
|
|
181
|
+
pattern).
|
|
182
|
+
|
|
171
183
|
### Relationship comparator typing
|
|
172
184
|
|
|
173
185
|
Relationship attributes declared via `Relationship(...)` are typed as SQLAlchemy expressions at class level,
|
|
@@ -181,9 +193,11 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
|
|
|
181
193
|
|
|
182
194
|
### Out of scope
|
|
183
195
|
|
|
184
|
-
-
|
|
185
|
-
|
|
186
|
-
`
|
|
196
|
+
- **Most SQLAlchemy-only declaration helpers**: this plugin doesn’t try to “fix” the full *declaration-site* typing
|
|
197
|
+
for SQLAlchemy ORM APIs that aren’t part of SQLModel’s surface area.
|
|
198
|
+
- Exception: best-effort return typing for `column_property(...)` (infers `Mapped[T]` from `ScalarSelect[T]` /
|
|
199
|
+
`ColumnElement[T]`) to support patterns like `Model._foo = column_property(...)` combined with
|
|
200
|
+
`getattr(Model, "_foo")` in query builders.
|
|
187
201
|
- **Pydantic `@computed_field`**: not touched by this plugin (it should type-check via normal property typing / Pydantic
|
|
188
202
|
typing).
|
|
189
203
|
|
|
@@ -206,6 +220,23 @@ Limitations:
|
|
|
206
220
|
- Mypy can’t express “either field name or alias is required”, so aliased required fields may not be reported as
|
|
207
221
|
missing at type-check time.
|
|
208
222
|
|
|
223
|
+
## Persisted model helpers (opt-in)
|
|
224
|
+
|
|
225
|
+
Strict typing + SQLModel often means table-model IDs start as `None` and are populated after flush/commit/refresh
|
|
226
|
+
(see [SQLModel: Automatic IDs, None, and Refresh](https://sqlmodel.tiangolo.com/tutorial/automatic-id-none-refresh/)).
|
|
227
|
+
Mypy can’t infer those runtime effects, so you can end up with repeated `assert obj.id is not None`.
|
|
228
|
+
|
|
229
|
+
This package ships small helpers you can import (they do **not** affect plugin behavior):
|
|
230
|
+
|
|
231
|
+
```py
|
|
232
|
+
from sqlmodel_mypy import has_id, require_id
|
|
233
|
+
|
|
234
|
+
hero_id: int = require_id(hero) # raises ValueError if hero.id is None
|
|
235
|
+
|
|
236
|
+
if has_id(hero):
|
|
237
|
+
ok_id: int = hero.id
|
|
238
|
+
```
|
|
239
|
+
|
|
209
240
|
## Development
|
|
210
241
|
|
|
211
242
|
- List available commands: `make help`
|
|
@@ -213,7 +244,7 @@ Limitations:
|
|
|
213
244
|
- Typecheck plugin code: `make typecheck`
|
|
214
245
|
- Run tests: `make test`
|
|
215
246
|
- Run all quality gates: `make check`
|
|
216
|
-
- Update mypy
|
|
247
|
+
- Update mypy "golden" outputs: `make update-mypy`
|
|
217
248
|
|
|
218
249
|
See [`AGENTS.md`](AGENTS.md) for repo conventions and workflow notes.
|
|
219
250
|
|
|
@@ -122,6 +122,18 @@ You can also use SQLAlchemy table metadata attributes without `attr-defined` noi
|
|
|
122
122
|
tbl = User.__table__
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
### `getattr(Model, "field")` support
|
|
126
|
+
|
|
127
|
+
If you build query filters dynamically, `getattr(Model, "field")` with a **string literal** is typed the same as
|
|
128
|
+
direct `Model.field` access on `table=True` models:
|
|
129
|
+
|
|
130
|
+
```py
|
|
131
|
+
stmt = select(User).where(getattr(User, "name").like("%x%"))
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Non-literal names (runtime strings) are intentionally left as `Any` (you’ll still need a cast or a different
|
|
135
|
+
pattern).
|
|
136
|
+
|
|
125
137
|
### Relationship comparator typing
|
|
126
138
|
|
|
127
139
|
Relationship attributes declared via `Relationship(...)` are typed as SQLAlchemy expressions at class level,
|
|
@@ -135,9 +147,11 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
|
|
|
135
147
|
|
|
136
148
|
### Out of scope
|
|
137
149
|
|
|
138
|
-
-
|
|
139
|
-
|
|
140
|
-
`
|
|
150
|
+
- **Most SQLAlchemy-only declaration helpers**: this plugin doesn’t try to “fix” the full *declaration-site* typing
|
|
151
|
+
for SQLAlchemy ORM APIs that aren’t part of SQLModel’s surface area.
|
|
152
|
+
- Exception: best-effort return typing for `column_property(...)` (infers `Mapped[T]` from `ScalarSelect[T]` /
|
|
153
|
+
`ColumnElement[T]`) to support patterns like `Model._foo = column_property(...)` combined with
|
|
154
|
+
`getattr(Model, "_foo")` in query builders.
|
|
141
155
|
- **Pydantic `@computed_field`**: not touched by this plugin (it should type-check via normal property typing / Pydantic
|
|
142
156
|
typing).
|
|
143
157
|
|
|
@@ -160,6 +174,23 @@ Limitations:
|
|
|
160
174
|
- Mypy can’t express “either field name or alias is required”, so aliased required fields may not be reported as
|
|
161
175
|
missing at type-check time.
|
|
162
176
|
|
|
177
|
+
## Persisted model helpers (opt-in)
|
|
178
|
+
|
|
179
|
+
Strict typing + SQLModel often means table-model IDs start as `None` and are populated after flush/commit/refresh
|
|
180
|
+
(see [SQLModel: Automatic IDs, None, and Refresh](https://sqlmodel.tiangolo.com/tutorial/automatic-id-none-refresh/)).
|
|
181
|
+
Mypy can’t infer those runtime effects, so you can end up with repeated `assert obj.id is not None`.
|
|
182
|
+
|
|
183
|
+
This package ships small helpers you can import (they do **not** affect plugin behavior):
|
|
184
|
+
|
|
185
|
+
```py
|
|
186
|
+
from sqlmodel_mypy import has_id, require_id
|
|
187
|
+
|
|
188
|
+
hero_id: int = require_id(hero) # raises ValueError if hero.id is None
|
|
189
|
+
|
|
190
|
+
if has_id(hero):
|
|
191
|
+
ok_id: int = hero.id
|
|
192
|
+
```
|
|
193
|
+
|
|
163
194
|
## Development
|
|
164
195
|
|
|
165
196
|
- List available commands: `make help`
|
|
@@ -167,7 +198,7 @@ Limitations:
|
|
|
167
198
|
- Typecheck plugin code: `make typecheck`
|
|
168
199
|
- Run tests: `make test`
|
|
169
200
|
- Run all quality gates: `make check`
|
|
170
|
-
- Update mypy
|
|
201
|
+
- Update mypy "golden" outputs: `make update-mypy`
|
|
171
202
|
|
|
172
203
|
See [`AGENTS.md`](AGENTS.md) for repo conventions and workflow notes.
|
|
173
204
|
|
|
@@ -22,7 +22,12 @@
|
|
|
22
22
|
- Ensure common SQLModel re-exports (`select`, `and_`, `or_`, `col`, etc.) preserve SQLAlchemy typing.
|
|
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 “order independence”
|
|
27
|
+
between `sqlmodel_mypy.plugin` and `pydantic.mypy` is not realistically achievable today.
|
|
28
|
+
- Enforce and clearly explain that `sqlmodel_mypy.plugin` must be listed **before** `pydantic.mypy`:
|
|
29
|
+
- Source: `src/sqlmodel_mypy/plugin.py` (`SQLModelMypyPlugin.__init__`, `_sqlmodel_metaclass_callback()`).
|
|
30
|
+
- User-facing docs: `README.md` (plugin ordering section).
|
|
26
31
|
|
|
27
32
|
## v0.4 (Docs-aligned relationship constructors) - DONE
|
|
28
33
|
|
|
@@ -200,3 +205,107 @@ models do provide this at runtime for `table=True`, but type checkers can miss i
|
|
|
200
205
|
- **Tests**
|
|
201
206
|
- Add mypy integration coverage for `Model.__table__` and `Session.get` / `AsyncSession.get` with SQLModel
|
|
202
207
|
table models.
|
|
208
|
+
|
|
209
|
+
## v0.12.4 (Dynamic query-builder helpers) - DONE
|
|
210
|
+
|
|
211
|
+
- Type `getattr(Model, "field")` like `Model.field` for `table=True` SQLModel models (string-literal names only).
|
|
212
|
+
- Best-effort typing for `column_property(...)` return type (infer `Mapped[T]`) to support
|
|
213
|
+
`Model._foo = column_property(...)` patterns.
|
|
214
|
+
|
|
215
|
+
## v0.14 (`typing.Annotated[...]` field metadata support) - DONE
|
|
216
|
+
|
|
217
|
+
**Motivation**: SQLModel (Pydantic v2) supports `Annotated`-driven typing patterns, and users will naturally try to
|
|
218
|
+
use them to reduce `= Field(...)` assignment noise. Under `--strict`, we should still generate correct constructor
|
|
219
|
+
signatures (required/optional + alias kwargs) when `Field(...)` metadata lives inside `Annotated[...]`.
|
|
220
|
+
|
|
221
|
+
- **Support `Annotated[T, Field(...)]` for constructor/signature generation**
|
|
222
|
+
- Examples we should handle (at least best-effort):
|
|
223
|
+
- `id: Annotated[int | None, Field(default=None, primary_key=True)]`
|
|
224
|
+
- `name: Annotated[str, Field(alias="full_name")]`
|
|
225
|
+
- The historical SQLModel edge-case: `Optional[Annotated[T, ...]]` (see upstream release notes).
|
|
226
|
+
- Implementation sketch:
|
|
227
|
+
- Extend semantic-phase collection in `src/sqlmodel_mypy/transform.py` to extract Field metadata from the
|
|
228
|
+
annotation when there is no `= Field(...)` call in the class body.
|
|
229
|
+
- Mirror the same logic in checker-time signature collection in `src/sqlmodel_mypy/plugin.py`
|
|
230
|
+
(`_collect_member_from_stmt`) so class-call signatures stay consistent with generated `__init__`.
|
|
231
|
+
- Source references:
|
|
232
|
+
- SQLModel `Field()` overloads + alias precedence: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py`
|
|
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
|
+
`https://github.com/fastapi/sqlmodel/pull/1093`
|
|
236
|
+
|
|
237
|
+
## v0.15 (Outer-join `None` propagation for relationship-attribute joins) - DONE
|
|
238
|
+
|
|
239
|
+
**Motivation**: we currently propagate `None` on outer-joins only when the join target is a direct model class
|
|
240
|
+
(e.g. `.join(Team, isouter=True)`). A common ORM pattern is to join via relationship attributes
|
|
241
|
+
(e.g. `.join(Hero.team, isouter=True)`), and the result tuple should still reflect that the joined entity may be
|
|
242
|
+
`None`.
|
|
243
|
+
|
|
244
|
+
- **Expand join-target detection**
|
|
245
|
+
- Extend `src/sqlmodel_mypy/plugin.py` (`_sqlalchemy_select_join_like_return_type`) to also detect join targets
|
|
246
|
+
expressed as relationship attributes (e.g. `Hero.team`, `Team.heroes`) and map them to the target model type
|
|
247
|
+
for `None` propagation.
|
|
248
|
+
- References:
|
|
249
|
+
- SQLModel outer-join behavior (baseline example): `https://sqlmodel.tiangolo.com/tutorial/connect/read-connected-data/`
|
|
250
|
+
- SQLAlchemy relationship-attribute joins (`select(User).join(User.addresses)`): `https://docs.sqlalchemy.org/en/20/orm/queryguide/select.html#simple-relationship-joins`
|
|
251
|
+
- Add mypy integration coverage under `tests/mypy/modules/` for relationship-attribute joins, including the SQLModel
|
|
252
|
+
docs pattern of `Team: None` on outer-joins.
|
|
253
|
+
|
|
254
|
+
## v0.16 (Strict typing ergonomics add-ons, opt-in) - DONE
|
|
255
|
+
|
|
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
|
+
repeated `assert obj.id is not None` (type clutter).
|
|
259
|
+
|
|
260
|
+
- **Provide a tiny opt-in narrowing helper**
|
|
261
|
+
- Docs context (why `id` starts as `None` and is populated after commit/refresh):
|
|
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 “persisted” models
|
|
264
|
+
without `cast(...)`/`# type: ignore`, e.g.:
|
|
265
|
+
- `assert_persisted(obj)` / `require_id(obj)` / `ensure_persisted(obj)` returning a `TypeGuard[...]` or using
|
|
266
|
+
overloads to narrow `id` to `int`.
|
|
267
|
+
- Keep it optional and well-documented; default plugin behavior should remain conservative.
|
|
268
|
+
|
|
269
|
+
## v0.17 (`tuple_()` should return a SQL expression, not a Python tuple)
|
|
270
|
+
|
|
271
|
+
**Motivation**: SQLModel re-exports `tuple_` from `sqlmodel.sql.expression`, but its typing currently says it returns a
|
|
272
|
+
built-in `tuple[Any, ...]`. In SQLAlchemy, `tuple_()` returns a SQL expression object (`sqlalchemy.sql.elements.Tuple`)
|
|
273
|
+
which supports expression methods like `.in_(...)`. Under `--strict` this mismatch forces `cast(...)` / `# type: ignore`
|
|
274
|
+
for otherwise normal SQLAlchemy patterns.
|
|
275
|
+
|
|
276
|
+
- **Fix `sqlmodel.tuple_` / `sqlmodel.sql.expression.tuple_` return type**
|
|
277
|
+
- Add a plugin return-type hook for:
|
|
278
|
+
- `sqlmodel.sql.expression.tuple_`
|
|
279
|
+
- `sqlmodel.tuple_` (re-export)
|
|
280
|
+
- Prefer returning `sqlalchemy.sql.elements.Tuple` (or fall back to `ColumnElement[tuple[Any, ...]]` if stubs vary).
|
|
281
|
+
- Source references:
|
|
282
|
+
- SQLModel wrapper: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/sql/expression.py`
|
|
283
|
+
- SQLAlchemy `tuple_` constructor: `https://github.com/sqlalchemy/sqlalchemy/blob/main/lib/sqlalchemy/sql/_elements_constructors.py`
|
|
284
|
+
- SQLAlchemy `Tuple` expression class: `https://github.com/sqlalchemy/sqlalchemy/blob/main/lib/sqlalchemy/sql/elements.py`
|
|
285
|
+
- **Tests**
|
|
286
|
+
- Add a mypy integration module demonstrating composite IN typing without ignores, e.g.:
|
|
287
|
+
- `tuple_(User.id, User.team_id).in_([(1, 2)])`
|
|
288
|
+
|
|
289
|
+
## v0.18 (Recognize `table=True` via `model_config["table"]`, not just class kwargs)
|
|
290
|
+
|
|
291
|
+
**Motivation**: SQLModel’s runtime `is_table_model_class()` checks `model_config.get("table")`, so a class can be a
|
|
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.).
|
|
295
|
+
|
|
296
|
+
- **Detect table models via `model_config` (metadata-driven)**
|
|
297
|
+
- Extend semantic analysis in `src/sqlmodel_mypy/transform.py` to detect a statically-known class-body assignment like:
|
|
298
|
+
- `model_config = ConfigDict(table=True)` or equivalent SQLModel config helpers
|
|
299
|
+
- Persist the result in `TypeInfo.metadata["sqlmodel-mypy-metadata"]`, then update `_is_table_model(...)` in:
|
|
300
|
+
- `src/sqlmodel_mypy/plugin.py`
|
|
301
|
+
- `src/sqlmodel_mypy/transform.py`
|
|
302
|
+
to consult metadata first (then fall back to `table=True` in the class definition).
|
|
303
|
+
- Source references:
|
|
304
|
+
- SQLModel metaclass reads config: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py` (`SQLModelMetaclass.__new__`)
|
|
305
|
+
- SQLModel runtime table detection: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py` (`is_table_model_class`)
|
|
306
|
+
- **Tests**
|
|
307
|
+
- Add mypy integration coverage for a table model enabled via `model_config` and assert the same behavior as
|
|
308
|
+
`table=True`:
|
|
309
|
+
- class attribute expression typing (e.g. `User.id.in_([1])`)
|
|
310
|
+
- constructor accepts relationship kwargs when configured (`init_forbid_extra=true`)
|
|
311
|
+
- `User.__table__` is typed and present
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Protocol, TypeVar
|
|
4
|
+
|
|
5
|
+
from typing_extensions import TypeIs
|
|
6
|
+
|
|
7
|
+
TId_co = TypeVar("TId_co", covariant=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HasOptionalId(Protocol[TId_co]):
|
|
11
|
+
"""An object with an optional primary key (populated later by the DB)."""
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def id(self) -> TId_co | None: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class HasId(HasOptionalId[TId_co], Protocol[TId_co]):
|
|
18
|
+
"""An object with a non-optional primary key."""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def id(self) -> TId_co: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def has_id(obj: HasOptionalId[TId_co]) -> TypeIs[HasId[TId_co]]:
|
|
25
|
+
"""Return True if `obj.id` is not None, and narrow `obj.id` accordingly."""
|
|
26
|
+
|
|
27
|
+
return obj.id is not None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def require_id(obj: HasOptionalId[TId_co]) -> TId_co:
|
|
31
|
+
"""Return `obj.id` if present; raise if it's None."""
|
|
32
|
+
|
|
33
|
+
if obj.id is None:
|
|
34
|
+
raise ValueError("Expected object to have a non-None `id` (persisted object).")
|
|
35
|
+
return obj.id
|