sqlmodel-mypy-plugin 0.12.0__tar.gz → 0.12.3__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.0 → sqlmodel_mypy_plugin-0.12.3}/PKG-INFO +40 -8
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/README.md +39 -7
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/ROADMAP.md +88 -1
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/pyproject.toml +1 -1
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/src/sqlmodel_mypy/__init__.py +1 -1
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/src/sqlmodel_mypy/plugin.py +404 -31
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/src/sqlmodel_mypy/transform.py +90 -7
- sqlmodel_mypy_plugin-0.12.3/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +20 -0
- sqlmodel_mypy_plugin-0.12.3/tests/mypy/modules/expression_typing_strict.py +20 -0
- sqlmodel_mypy_plugin-0.12.3/tests/mypy/modules/getattr_column_property.py +45 -0
- sqlmodel_mypy_plugin-0.12.3/tests/mypy/modules/nullable_defaults.py +26 -0
- {sqlmodel_mypy_plugin-0.12.0/tests/mypy/outputs/mypy-plugin-pydantic-last_ini → sqlmodel_mypy_plugin-0.12.3/tests/mypy/outputs/mypy-plugin-pydantic-first_ini}/relationships.py +0 -1
- {sqlmodel_mypy_plugin-0.12.0/tests/mypy/outputs/mypy-plugin-pydantic-first_ini → sqlmodel_mypy_plugin-0.12.3/tests/mypy/outputs/mypy-plugin-pydantic-last_ini}/relationships.py +1 -1
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +2 -2
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +7 -7
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +1 -1
- sqlmodel_mypy_plugin-0.12.3/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +27 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +2 -2
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +4 -4
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +3 -3
- sqlmodel_mypy_plugin-0.12.3/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +27 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +1 -1
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +4 -4
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/test_mypy.py +5 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/test_plugin_unit.py +418 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/test_transform_unit.py +94 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/.gitignore +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/.pre-commit-config.yaml +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/AGENTS.md +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/LICENSE +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/src/sqlmodel_mypy/py.typed +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/conftest.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/mypy-plugin.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/pyproject-plugin.toml +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/field_sa_type.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/label.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/selectinload.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/session_exec.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/session_get.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/table_dunders.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/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.12.
|
|
3
|
+
Version: 0.12.3
|
|
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
|
|
@@ -71,7 +71,7 @@ Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
|
|
|
71
71
|
- Accept SQLAlchemy TypeEngine instances in `Field(sa_type=...)` (e.g. `DateTime(timezone=True)`, `String(50)`)
|
|
72
72
|
without `# type: ignore` in strict mode.
|
|
73
73
|
- Accept `model_config = ConfigDict(...)` overrides on SQLModel subclasses in strict mode.
|
|
74
|
-
- Compatible with `pydantic.mypy`
|
|
74
|
+
- Compatible with `pydantic.mypy` **when `sqlmodel_mypy.plugin` is listed first**.
|
|
75
75
|
|
|
76
76
|
## Install (dev)
|
|
77
77
|
|
|
@@ -96,17 +96,33 @@ make check
|
|
|
96
96
|
plugins = sqlmodel_mypy.plugin
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
If you also use `pydantic.mypy`,
|
|
99
|
+
If you also use `pydantic.mypy`, **`sqlmodel_mypy.plugin` must be listed first**:
|
|
100
100
|
|
|
101
101
|
```ini
|
|
102
102
|
[mypy]
|
|
103
103
|
plugins = sqlmodel_mypy.plugin, pydantic.mypy
|
|
104
|
-
# or:
|
|
105
|
-
# plugins = pydantic.mypy, sqlmodel_mypy.plugin
|
|
106
104
|
```
|
|
107
105
|
|
|
106
|
+
This is not a preference: **mypy picks the first plugin that claims a hook**. If `pydantic.mypy` runs first, it
|
|
107
|
+
can treat SQLModel classes as plain Pydantic models and you’ll get incorrect typing (e.g. missing `__table__`,
|
|
108
|
+
broken SQLAlchemy expression types, and incorrect/empty constructor signatures).
|
|
109
|
+
|
|
108
110
|
## Plugin configuration
|
|
109
111
|
|
|
112
|
+
All options are **boolean**.
|
|
113
|
+
|
|
114
|
+
Supported options:
|
|
115
|
+
|
|
116
|
+
- **`init_typed`** (default: `false`): Generate `__init__` / constructor signatures using the declared field types.
|
|
117
|
+
When `false`, the plugin uses `Any` for parameter types (but still enforces required/optional fields).
|
|
118
|
+
- **`init_forbid_extra`** (default: `false`): When `true`, do **not** add `**kwargs: Any` to generated
|
|
119
|
+
`__init__` / `model_construct` / constructor signatures, so mypy reports unexpected keyword arguments.
|
|
120
|
+
- **`warn_untyped_fields`** (default: `true`): When `true`, emit error code `sqlmodel-field` for untyped
|
|
121
|
+
declarations like `x = Field(...)` / `x = Relationship(...)` (use `x: T = ...` instead).
|
|
122
|
+
- **`debug_dataclass_transform`** (default: `false`): **Advanced/debug-only**. When `true`, keep SQLModel’s
|
|
123
|
+
`__dataclass_transform__` handling enabled in mypy (useful for debugging plugin interactions; not recommended
|
|
124
|
+
for normal use).
|
|
125
|
+
|
|
110
126
|
`mypy.ini`:
|
|
111
127
|
|
|
112
128
|
```ini
|
|
@@ -114,6 +130,7 @@ plugins = sqlmodel_mypy.plugin, pydantic.mypy
|
|
|
114
130
|
init_typed = false
|
|
115
131
|
init_forbid_extra = false
|
|
116
132
|
warn_untyped_fields = true
|
|
133
|
+
debug_dataclass_transform = false
|
|
117
134
|
```
|
|
118
135
|
|
|
119
136
|
`pyproject.toml`:
|
|
@@ -123,6 +140,7 @@ warn_untyped_fields = true
|
|
|
123
140
|
init_typed = false
|
|
124
141
|
init_forbid_extra = false
|
|
125
142
|
warn_untyped_fields = true
|
|
143
|
+
debug_dataclass_transform = false
|
|
126
144
|
```
|
|
127
145
|
|
|
128
146
|
## Error codes
|
|
@@ -150,6 +168,18 @@ You can also use SQLAlchemy table metadata attributes without `attr-defined` noi
|
|
|
150
168
|
tbl = User.__table__
|
|
151
169
|
```
|
|
152
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
|
+
|
|
153
183
|
### Relationship comparator typing
|
|
154
184
|
|
|
155
185
|
Relationship attributes declared via `Relationship(...)` are typed as SQLAlchemy expressions at class level,
|
|
@@ -163,9 +193,11 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
|
|
|
163
193
|
|
|
164
194
|
### Out of scope
|
|
165
195
|
|
|
166
|
-
-
|
|
167
|
-
|
|
168
|
-
`
|
|
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.
|
|
169
201
|
- **Pydantic `@computed_field`**: not touched by this plugin (it should type-check via normal property typing / Pydantic
|
|
170
202
|
typing).
|
|
171
203
|
|
|
@@ -25,7 +25,7 @@ Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
|
|
|
25
25
|
- Accept SQLAlchemy TypeEngine instances in `Field(sa_type=...)` (e.g. `DateTime(timezone=True)`, `String(50)`)
|
|
26
26
|
without `# type: ignore` in strict mode.
|
|
27
27
|
- Accept `model_config = ConfigDict(...)` overrides on SQLModel subclasses in strict mode.
|
|
28
|
-
- Compatible with `pydantic.mypy`
|
|
28
|
+
- Compatible with `pydantic.mypy` **when `sqlmodel_mypy.plugin` is listed first**.
|
|
29
29
|
|
|
30
30
|
## Install (dev)
|
|
31
31
|
|
|
@@ -50,17 +50,33 @@ make check
|
|
|
50
50
|
plugins = sqlmodel_mypy.plugin
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
If you also use `pydantic.mypy`,
|
|
53
|
+
If you also use `pydantic.mypy`, **`sqlmodel_mypy.plugin` must be listed first**:
|
|
54
54
|
|
|
55
55
|
```ini
|
|
56
56
|
[mypy]
|
|
57
57
|
plugins = sqlmodel_mypy.plugin, pydantic.mypy
|
|
58
|
-
# or:
|
|
59
|
-
# plugins = pydantic.mypy, sqlmodel_mypy.plugin
|
|
60
58
|
```
|
|
61
59
|
|
|
60
|
+
This is not a preference: **mypy picks the first plugin that claims a hook**. If `pydantic.mypy` runs first, it
|
|
61
|
+
can treat SQLModel classes as plain Pydantic models and you’ll get incorrect typing (e.g. missing `__table__`,
|
|
62
|
+
broken SQLAlchemy expression types, and incorrect/empty constructor signatures).
|
|
63
|
+
|
|
62
64
|
## Plugin configuration
|
|
63
65
|
|
|
66
|
+
All options are **boolean**.
|
|
67
|
+
|
|
68
|
+
Supported options:
|
|
69
|
+
|
|
70
|
+
- **`init_typed`** (default: `false`): Generate `__init__` / constructor signatures using the declared field types.
|
|
71
|
+
When `false`, the plugin uses `Any` for parameter types (but still enforces required/optional fields).
|
|
72
|
+
- **`init_forbid_extra`** (default: `false`): When `true`, do **not** add `**kwargs: Any` to generated
|
|
73
|
+
`__init__` / `model_construct` / constructor signatures, so mypy reports unexpected keyword arguments.
|
|
74
|
+
- **`warn_untyped_fields`** (default: `true`): When `true`, emit error code `sqlmodel-field` for untyped
|
|
75
|
+
declarations like `x = Field(...)` / `x = Relationship(...)` (use `x: T = ...` instead).
|
|
76
|
+
- **`debug_dataclass_transform`** (default: `false`): **Advanced/debug-only**. When `true`, keep SQLModel’s
|
|
77
|
+
`__dataclass_transform__` handling enabled in mypy (useful for debugging plugin interactions; not recommended
|
|
78
|
+
for normal use).
|
|
79
|
+
|
|
64
80
|
`mypy.ini`:
|
|
65
81
|
|
|
66
82
|
```ini
|
|
@@ -68,6 +84,7 @@ plugins = sqlmodel_mypy.plugin, pydantic.mypy
|
|
|
68
84
|
init_typed = false
|
|
69
85
|
init_forbid_extra = false
|
|
70
86
|
warn_untyped_fields = true
|
|
87
|
+
debug_dataclass_transform = false
|
|
71
88
|
```
|
|
72
89
|
|
|
73
90
|
`pyproject.toml`:
|
|
@@ -77,6 +94,7 @@ warn_untyped_fields = true
|
|
|
77
94
|
init_typed = false
|
|
78
95
|
init_forbid_extra = false
|
|
79
96
|
warn_untyped_fields = true
|
|
97
|
+
debug_dataclass_transform = false
|
|
80
98
|
```
|
|
81
99
|
|
|
82
100
|
## Error codes
|
|
@@ -104,6 +122,18 @@ You can also use SQLAlchemy table metadata attributes without `attr-defined` noi
|
|
|
104
122
|
tbl = User.__table__
|
|
105
123
|
```
|
|
106
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
|
+
|
|
107
137
|
### Relationship comparator typing
|
|
108
138
|
|
|
109
139
|
Relationship attributes declared via `Relationship(...)` are typed as SQLAlchemy expressions at class level,
|
|
@@ -117,9 +147,11 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
|
|
|
117
147
|
|
|
118
148
|
### Out of scope
|
|
119
149
|
|
|
120
|
-
-
|
|
121
|
-
|
|
122
|
-
`
|
|
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.
|
|
123
155
|
- **Pydantic `@computed_field`**: not touched by this plugin (it should type-check via normal property typing / Pydantic
|
|
124
156
|
typing).
|
|
125
157
|
|
|
@@ -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,85 @@ 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.13 (SQLModel docs parity suite) - TODO
|
|
216
|
+
|
|
217
|
+
**Motivation**: the north star is “SQLModel as documented works under `mypy --strict` with minimal casts/ignores”.
|
|
218
|
+
We should lock this down with a dedicated docs-parity integration suite (beyond the current focused unit-style
|
|
219
|
+
mypy modules).
|
|
220
|
+
|
|
221
|
+
- **Add docs-derived mypy integration modules**
|
|
222
|
+
- Create minimal, type-focused modules under `tests/mypy/modules/` that mirror official docs patterns (as close
|
|
223
|
+
as possible, but stripped of runtime/demo noise).
|
|
224
|
+
- Add them to `tests/mypy/test_mypy.py` and commit golden outputs under `tests/mypy/outputs/**`.
|
|
225
|
+
- Target doc pages / patterns:
|
|
226
|
+
- Connected-data joins and outer-joins:
|
|
227
|
+
- Docs: `https://sqlmodel.tiangolo.com/tutorial/connect/read-connected-data/`
|
|
228
|
+
- Relationship attribute creation/update patterns (constructor kwargs + mutation):
|
|
229
|
+
- Docs: `https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/create-and-update-relationships/`
|
|
230
|
+
- Many-to-many relationship creation patterns:
|
|
231
|
+
- Docs: `https://sqlmodel.tiangolo.com/tutorial/many-to-many/create-data/`
|
|
232
|
+
- Update patterns using `sqlmodel_update()` / `model_dump(exclude_unset=True)`:
|
|
233
|
+
- Docs: `https://sqlmodel.tiangolo.com/tutorial/fastapi/update/`
|
|
234
|
+
- Source: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py` (`SQLModel.sqlmodel_update`)
|
|
235
|
+
- Ensure strict-mode expectations are explicit (via `# MYPY:` comments and/or `reveal_type(...)` assertions).
|
|
236
|
+
|
|
237
|
+
## v0.14 (`typing.Annotated[...]` field metadata support) - TODO
|
|
238
|
+
|
|
239
|
+
**Motivation**: SQLModel (Pydantic v2) supports `Annotated`-driven typing patterns, and users will naturally try to
|
|
240
|
+
use them to reduce `= Field(...)` assignment noise. Under `--strict`, we should still generate correct constructor
|
|
241
|
+
signatures (required/optional + alias kwargs) when `Field(...)` metadata lives inside `Annotated[...]`.
|
|
242
|
+
|
|
243
|
+
- **Support `Annotated[T, Field(...)]` for constructor/signature generation**
|
|
244
|
+
- Examples we should handle (at least best-effort):
|
|
245
|
+
- `id: Annotated[int | None, Field(default=None, primary_key=True)]`
|
|
246
|
+
- `name: Annotated[str, Field(alias="full_name")]`
|
|
247
|
+
- The historical SQLModel edge-case: `Optional[Annotated[T, ...]]` (see upstream release notes).
|
|
248
|
+
- Implementation sketch:
|
|
249
|
+
- Extend semantic-phase collection in `src/sqlmodel_mypy/transform.py` to extract Field metadata from the
|
|
250
|
+
annotation when there is no `= Field(...)` call in the class body.
|
|
251
|
+
- Mirror the same logic in checker-time signature collection in `src/sqlmodel_mypy/plugin.py`
|
|
252
|
+
(`_collect_member_from_stmt`) so class-call signatures stay consistent with generated `__init__`.
|
|
253
|
+
- Source references:
|
|
254
|
+
- SQLModel `Field()` overloads + alias precedence: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py`
|
|
255
|
+
- `Annotated` handling in SQLModel’s type parsing: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py`
|
|
256
|
+
- Release note: “Fix support for types with `Optional[Annotated[x, f()]]`…” (PR #1093):
|
|
257
|
+
`https://github.com/fastapi/sqlmodel/pull/1093`
|
|
258
|
+
|
|
259
|
+
## v0.15 (Outer-join `None` propagation for relationship-attribute joins) - TODO
|
|
260
|
+
|
|
261
|
+
**Motivation**: we currently propagate `None` on outer-joins only when the join target is a direct model class
|
|
262
|
+
(e.g. `.join(Team, isouter=True)`). A common ORM pattern is to join via relationship attributes
|
|
263
|
+
(e.g. `.join(Hero.team, isouter=True)`), and the result tuple should still reflect that the joined entity may be
|
|
264
|
+
`None`.
|
|
265
|
+
|
|
266
|
+
- **Expand join-target detection**
|
|
267
|
+
- Extend `src/sqlmodel_mypy/plugin.py` (`_sqlalchemy_select_join_like_return_type`) to also detect join targets
|
|
268
|
+
expressed as relationship attributes (e.g. `Hero.team`, `Team.heroes`) and map them to the target model type
|
|
269
|
+
for `None` propagation.
|
|
270
|
+
- References:
|
|
271
|
+
- SQLModel outer-join behavior (baseline example): `https://sqlmodel.tiangolo.com/tutorial/connect/read-connected-data/`
|
|
272
|
+
- SQLAlchemy relationship-attribute joins (`select(User).join(User.addresses)`): `https://docs.sqlalchemy.org/en/20/orm/queryguide/select.html#simple-relationship-joins`
|
|
273
|
+
- Add mypy integration coverage under `tests/mypy/modules/` for relationship-attribute joins, including the SQLModel
|
|
274
|
+
docs pattern of `Team: None` on outer-joins.
|
|
275
|
+
|
|
276
|
+
## v0.16 (Strict typing ergonomics add-ons, opt-in) - TODO
|
|
277
|
+
|
|
278
|
+
**Motivation**: strict typing + SQLModel often means the database populates values “later” (after flush/commit/
|
|
279
|
+
refresh), especially primary keys (`id: int | None`). Mypy can’t infer those runtime effects, and users end up with
|
|
280
|
+
repeated `assert obj.id is not None` (type clutter).
|
|
281
|
+
|
|
282
|
+
- **Provide a tiny opt-in narrowing helper**
|
|
283
|
+
- Docs context (why `id` starts as `None` and is populated after commit/refresh):
|
|
284
|
+
- `https://sqlmodel.tiangolo.com/tutorial/automatic-id-none-refresh/`
|
|
285
|
+
- Ship a small helper in this package (runtime-free or near-zero runtime) that helps users narrow “persisted” models
|
|
286
|
+
without `cast(...)`/`# type: ignore`, e.g.:
|
|
287
|
+
- `assert_persisted(obj)` / `require_id(obj)` / `ensure_persisted(obj)` returning a `TypeGuard[...]` or using
|
|
288
|
+
overloads to narrow `id` to `int`.
|
|
289
|
+
- Keep it optional and well-documented; default plugin behavior should remain conservative.
|