sqlmodel-mypy-plugin 0.19.0__tar.gz → 0.20.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.19.0 → sqlmodel_mypy_plugin-0.20.0}/.pre-commit-config.yaml +1 -1
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/PKG-INFO +34 -4
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/README.md +31 -2
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/pyproject.toml +2 -2
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/__init__.py +1 -1
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/plugin.py +1 -29
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/transform.py +213 -64
- sqlmodel_mypy_plugin-0.20.0/tests/mypy/modules/nullable_defaults.py +89 -0
- sqlmodel_mypy_plugin-0.20.0/tests/mypy/modules/sequence_name_collision.py +30 -0
- sqlmodel_mypy_plugin-0.20.0/tests/mypy/modules/sqlmodel_reexported_sequence.py +24 -0
- {sqlmodel_mypy_plugin-0.19.0/tests/mypy/outputs/mypy-plugin_ini → sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin-strict_ini}/aliased_joins.py +1 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_read_connected_data.py +1 -1
- {sqlmodel_mypy_plugin-0.19.0/tests/mypy/outputs/mypy-plugin_ini → sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin-strict_ini}/execution_options.py +1 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +3 -0
- sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +93 -0
- sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin-strict_ini/sequence_name_collision.py +31 -0
- sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin-strict_ini/session_exec.py +43 -0
- sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin-strict_ini/sqlmodel_reexported_sequence.py +25 -0
- {sqlmodel_mypy_plugin-0.19.0/tests/mypy/outputs/mypy-plugin-strict_ini → sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin_ini}/aliased_joins.py +1 -0
- {sqlmodel_mypy_plugin-0.19.0/tests/mypy/outputs/mypy-plugin-strict_ini → sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin_ini}/execution_options.py +1 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/joins.py +3 -0
- sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +93 -0
- sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin_ini/sequence_name_collision.py +31 -0
- sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin_ini/session_exec.py +43 -0
- sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin_ini/sqlmodel_reexported_sequence.py +25 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/test_mypy.py +4 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/test_transform_unit.py +342 -1
- sqlmodel_mypy_plugin-0.19.0/tests/mypy/modules/nullable_defaults.py +0 -26
- sqlmodel_mypy_plugin-0.19.0/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +0 -27
- sqlmodel_mypy_plugin-0.19.0/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +0 -27
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/.gitignore +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/AGENTS.md +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/LICENSE +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/persisted.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/py.typed +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/conftest.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-strict-typed-execute.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/pyproject-plugin.toml +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/aliased_joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/annotated_field_metadata.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_many_to_many_create_data.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_read_connected_data.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_relationship_attributes.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_update_patterns.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/expression_typing_strict.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/field_sa_type.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/field_validation_alias.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/getattr_column_property.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/label.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/selectinload.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/session_exec.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/session_execute_typed.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/session_get.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/table_dunders.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/table_via_model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/tuple_expression.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-pydantic-first_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-pydantic-last_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict-typed-execute_ini/session_execute_typed.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/annotated_field_metadata.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_many_to_many_create_data.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_relationship_attributes.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_update_patterns.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/table_via_model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/tuple_expression.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/smoke_test.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/test_persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.20.0}/tests/test_plugin_unit.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: sqlmodel-mypy-plugin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.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
|
|
@@ -38,6 +38,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
38
38
|
Classifier: Programming Language :: Python :: 3.11
|
|
39
39
|
Classifier: Programming Language :: Python :: 3.12
|
|
40
40
|
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
41
42
|
Classifier: Typing :: Typed
|
|
42
43
|
Requires-Python: >=3.10
|
|
43
44
|
Requires-Dist: mypy>=1.19.1
|
|
@@ -52,8 +53,6 @@ Mypy plugin that improves type checking for [`SQLModel`](https://github.com/fast
|
|
|
52
53
|
|
|
53
54
|
Early/experimental. Implemented scope (supported today):
|
|
54
55
|
|
|
55
|
-
Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
|
|
56
|
-
|
|
57
56
|
- Generate correct `__init__` / `model_construct` signatures for SQLModel models:
|
|
58
57
|
- treat `sqlmodel.Field(...)` required/optional correctly
|
|
59
58
|
- accept `sqlmodel.Relationship(...)` kwargs for `table=True` models
|
|
@@ -169,10 +168,12 @@ expressions, so you can write queries without `col()`:
|
|
|
169
168
|
```py
|
|
170
169
|
from sqlmodel import Field, SQLModel, select
|
|
171
170
|
|
|
171
|
+
|
|
172
172
|
class User(SQLModel, table=True):
|
|
173
173
|
id: int = Field(primary_key=True)
|
|
174
174
|
name: str = Field()
|
|
175
175
|
|
|
176
|
+
|
|
176
177
|
stmt = select(User).where(User.name.like("%x%"))
|
|
177
178
|
```
|
|
178
179
|
|
|
@@ -230,6 +231,35 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
|
|
|
230
231
|
- **SQLAlchemy typing**: relied upon for core SQL/ORM typing wherever possible.
|
|
231
232
|
- **Stub overlays**: only for upstream gaps that can’t be addressed cleanly via hooks (prefer upstream fixes first).
|
|
232
233
|
|
|
234
|
+
## Database-generated columns are optional kwargs
|
|
235
|
+
|
|
236
|
+
A field whose value the database (or SQLAlchemy) supplies on INSERT is not required in the generated
|
|
237
|
+
`__init__` / `model_construct` signature. The plugin reads this off `Field(...)` syntactically, from
|
|
238
|
+
`nullable=True`, `sa_column=Column(...)` and `sa_column_kwargs={...}`:
|
|
239
|
+
|
|
240
|
+
| Form | Why it counts |
|
|
241
|
+
| --- | --- |
|
|
242
|
+
| `nullable=True` | column accepts `NULL` |
|
|
243
|
+
| `default=` / `insert_default=` / `server_default=` (incl. `server_default=FetchedValue()`) | an explicit default |
|
|
244
|
+
| positional `Computed(...)` | `GENERATED ALWAYS AS`, computed by the database |
|
|
245
|
+
| positional `Sequence(...)` | becomes `Column.default`, identical to `default=Sequence(...)` |
|
|
246
|
+
| positional `Identity(...)` | becomes `Column.server_default` (`GENERATED ... AS IDENTITY`) |
|
|
247
|
+
| positional `FetchedValue()` | becomes `Column.server_default` |
|
|
248
|
+
| `primary_key=True` **and** `autoincrement=True` | renders as `SERIAL`/`IDENTITY` |
|
|
249
|
+
|
|
250
|
+
Markers are matched by their resolved module, so `Sequence` imported from `typing` or
|
|
251
|
+
`collections.abc` is never mistaken for `sqlalchemy.Sequence`; `sqlmodel` re-exports of these
|
|
252
|
+
constructs are recognized. A name the plugin cannot trace back to SQLAlchemy is left alone.
|
|
253
|
+
|
|
254
|
+
Deliberately **not** covered:
|
|
255
|
+
|
|
256
|
+
- a bare `primary_key=True`. SQLAlchemy's default `autoincrement="auto"` only generates a value for
|
|
257
|
+
a *single-column integer* primary key, and neither the column type nor the rest of the table's
|
|
258
|
+
primary key is knowable from one `Column(...)` call — composite or non-integer primary keys would
|
|
259
|
+
silently become optional. Use SQLModel's usual `Field(default=None, primary_key=True)`.
|
|
260
|
+
- `autoincrement=True` **without** `primary_key=True`. SQLAlchemy ignores it there (the column
|
|
261
|
+
compiles to a plain `INTEGER`), so the value really is caller-supplied.
|
|
262
|
+
|
|
233
263
|
## Field aliases in constructor kwargs
|
|
234
264
|
|
|
235
265
|
If you use `Field(alias=...)` (or `validation_alias=...` / `schema_extra={"validation_alias": ...}`), the plugin
|
|
@@ -6,8 +6,6 @@ Mypy plugin that improves type checking for [`SQLModel`](https://github.com/fast
|
|
|
6
6
|
|
|
7
7
|
Early/experimental. Implemented scope (supported today):
|
|
8
8
|
|
|
9
|
-
Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
|
|
10
|
-
|
|
11
9
|
- Generate correct `__init__` / `model_construct` signatures for SQLModel models:
|
|
12
10
|
- treat `sqlmodel.Field(...)` required/optional correctly
|
|
13
11
|
- accept `sqlmodel.Relationship(...)` kwargs for `table=True` models
|
|
@@ -123,10 +121,12 @@ expressions, so you can write queries without `col()`:
|
|
|
123
121
|
```py
|
|
124
122
|
from sqlmodel import Field, SQLModel, select
|
|
125
123
|
|
|
124
|
+
|
|
126
125
|
class User(SQLModel, table=True):
|
|
127
126
|
id: int = Field(primary_key=True)
|
|
128
127
|
name: str = Field()
|
|
129
128
|
|
|
129
|
+
|
|
130
130
|
stmt = select(User).where(User.name.like("%x%"))
|
|
131
131
|
```
|
|
132
132
|
|
|
@@ -184,6 +184,35 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
|
|
|
184
184
|
- **SQLAlchemy typing**: relied upon for core SQL/ORM typing wherever possible.
|
|
185
185
|
- **Stub overlays**: only for upstream gaps that can’t be addressed cleanly via hooks (prefer upstream fixes first).
|
|
186
186
|
|
|
187
|
+
## Database-generated columns are optional kwargs
|
|
188
|
+
|
|
189
|
+
A field whose value the database (or SQLAlchemy) supplies on INSERT is not required in the generated
|
|
190
|
+
`__init__` / `model_construct` signature. The plugin reads this off `Field(...)` syntactically, from
|
|
191
|
+
`nullable=True`, `sa_column=Column(...)` and `sa_column_kwargs={...}`:
|
|
192
|
+
|
|
193
|
+
| Form | Why it counts |
|
|
194
|
+
| --- | --- |
|
|
195
|
+
| `nullable=True` | column accepts `NULL` |
|
|
196
|
+
| `default=` / `insert_default=` / `server_default=` (incl. `server_default=FetchedValue()`) | an explicit default |
|
|
197
|
+
| positional `Computed(...)` | `GENERATED ALWAYS AS`, computed by the database |
|
|
198
|
+
| positional `Sequence(...)` | becomes `Column.default`, identical to `default=Sequence(...)` |
|
|
199
|
+
| positional `Identity(...)` | becomes `Column.server_default` (`GENERATED ... AS IDENTITY`) |
|
|
200
|
+
| positional `FetchedValue()` | becomes `Column.server_default` |
|
|
201
|
+
| `primary_key=True` **and** `autoincrement=True` | renders as `SERIAL`/`IDENTITY` |
|
|
202
|
+
|
|
203
|
+
Markers are matched by their resolved module, so `Sequence` imported from `typing` or
|
|
204
|
+
`collections.abc` is never mistaken for `sqlalchemy.Sequence`; `sqlmodel` re-exports of these
|
|
205
|
+
constructs are recognized. A name the plugin cannot trace back to SQLAlchemy is left alone.
|
|
206
|
+
|
|
207
|
+
Deliberately **not** covered:
|
|
208
|
+
|
|
209
|
+
- a bare `primary_key=True`. SQLAlchemy's default `autoincrement="auto"` only generates a value for
|
|
210
|
+
a *single-column integer* primary key, and neither the column type nor the rest of the table's
|
|
211
|
+
primary key is knowable from one `Column(...)` call — composite or non-integer primary keys would
|
|
212
|
+
silently become optional. Use SQLModel's usual `Field(default=None, primary_key=True)`.
|
|
213
|
+
- `autoincrement=True` **without** `primary_key=True`. SQLAlchemy ignores it there (the column
|
|
214
|
+
compiles to a plain `INTEGER`), so the value really is caller-supplied.
|
|
215
|
+
|
|
187
216
|
## Field aliases in constructor kwargs
|
|
188
217
|
|
|
189
218
|
If you use `Field(alias=...)` (or `validation_alias=...` / `schema_extra={"validation_alias": ...}`), the plugin
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlmodel-mypy-plugin"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.20.0"
|
|
8
8
|
description = "Mypy plugin for SQLModel (better constructor/Field typing)."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -20,6 +20,7 @@ classifiers = [
|
|
|
20
20
|
"Programming Language :: Python :: 3.11",
|
|
21
21
|
"Programming Language :: Python :: 3.12",
|
|
22
22
|
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
23
24
|
"Typing :: Typed",
|
|
24
25
|
]
|
|
25
26
|
dependencies = [
|
|
@@ -52,7 +53,6 @@ include = [
|
|
|
52
53
|
"/src",
|
|
53
54
|
"/tests",
|
|
54
55
|
"/README.md",
|
|
55
|
-
"/ROADMAP.md",
|
|
56
56
|
"/AGENTS.md",
|
|
57
57
|
"/LICENSE",
|
|
58
58
|
"/pyproject.toml",
|
|
@@ -171,7 +171,7 @@ SQLALCHEMY_INSPECT_FULLNAMES = {
|
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
# Increment when plugin changes should invalidate mypy cache.
|
|
174
|
-
__version__ =
|
|
174
|
+
__version__ = 27
|
|
175
175
|
|
|
176
176
|
|
|
177
177
|
class _CollectedField(NamedTuple):
|
|
@@ -1275,34 +1275,6 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1275
1275
|
return any(_contains_instance_fullname(item, fullname) for item in proper.items)
|
|
1276
1276
|
return False
|
|
1277
1277
|
|
|
1278
|
-
# Accept SQLAlchemy `Select[...]` in `Session.exec(...)`.
|
|
1279
|
-
#
|
|
1280
|
-
# SQLModel defines its own `Select[...]` wrapper class and types `Session.exec` to accept it,
|
|
1281
|
-
# but users often import and use SQLAlchemy's `select()` directly. Runtime supports it, and
|
|
1282
|
-
# with this adjustment we preserve a correctly-shaped `TupleResult[...]` return type.
|
|
1283
|
-
sqlmodel_select_info = _lookup_typeinfo(self, SQLMODEL_SELECT_CLS_FULLNAME)
|
|
1284
|
-
sqlalchemy_select_info = _lookup_typeinfo(self, SQLALCHEMY_SELECT_FULLNAME)
|
|
1285
|
-
if sqlmodel_select_info is not None and sqlalchemy_select_info is not None:
|
|
1286
|
-
if _contains_instance_fullname(default.arg_types[0], sqlmodel_select_info.fullname):
|
|
1287
|
-
# Idempotency: if upstream stubs already accept SQLAlchemy Select, keep unchanged.
|
|
1288
|
-
if not _contains_instance_fullname(
|
|
1289
|
-
default.arg_types[0], sqlalchemy_select_info.fullname
|
|
1290
|
-
):
|
|
1291
|
-
proper_arg0 = get_proper_type(default.arg_types[0])
|
|
1292
|
-
if isinstance(proper_arg0, Instance) and proper_arg0.args:
|
|
1293
|
-
row_type = proper_arg0.args[0]
|
|
1294
|
-
else:
|
|
1295
|
-
row_type = _plugin_any()
|
|
1296
|
-
sqlalchemy_select = Instance(
|
|
1297
|
-
sqlalchemy_select_info,
|
|
1298
|
-
[row_type] * len(sqlalchemy_select_info.defn.type_vars),
|
|
1299
|
-
)
|
|
1300
|
-
new_arg_types = list(default.arg_types)
|
|
1301
|
-
new_arg_types[0] = UnionType.make_union(
|
|
1302
|
-
[default.arg_types[0], sqlalchemy_select]
|
|
1303
|
-
)
|
|
1304
|
-
return default.copy_modified(arg_types=new_arg_types)
|
|
1305
|
-
|
|
1306
1278
|
# Only adjust the broad overload; keep Select/SelectOfScalar overloads precise.
|
|
1307
1279
|
broad = False
|
|
1308
1280
|
if sqlmodel_executable_info is not None and _contains_instance_fullname(
|
|
@@ -11,8 +11,8 @@ from __future__ import annotations
|
|
|
11
11
|
|
|
12
12
|
import ast
|
|
13
13
|
import keyword
|
|
14
|
-
from collections.abc import Iterator
|
|
15
|
-
from typing import Any, Protocol
|
|
14
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
15
|
+
from typing import Any, Protocol, TypeVar
|
|
16
16
|
|
|
17
17
|
from mypy.errorcodes import ErrorCode
|
|
18
18
|
from mypy.expandtype import expand_type
|
|
@@ -85,6 +85,19 @@ SQLMODEL_RELATIONSHIP_FULLNAME = "sqlmodel.main.Relationship"
|
|
|
85
85
|
|
|
86
86
|
SQLALCHEMY_MAPPED_FULLNAMES = {"sqlalchemy.orm.Mapped", "sqlalchemy.orm.base.Mapped"}
|
|
87
87
|
|
|
88
|
+
# `Column(...)` keywords whose presence means the value does not have to be supplied by the caller.
|
|
89
|
+
# All of them end up as `Column.default` / `Column.server_default`, i.e. the value is produced by
|
|
90
|
+
# SQLAlchemy or by the database on INSERT.
|
|
91
|
+
SQLALCHEMY_DEFAULTISH_COLUMN_KWARGS = frozenset({"server_default", "default", "insert_default"})
|
|
92
|
+
|
|
93
|
+
# SQLAlchemy schema constructs that are passed to `Column(...)` *positionally* and install a
|
|
94
|
+
# default/server-default on the column via `SchemaEventTarget._set_parent`:
|
|
95
|
+
# - `Computed(...)` -> GENERATED ALWAYS AS, value is computed by the database
|
|
96
|
+
# - `Sequence(...)` -> becomes `Column.default` (identical to `default=Sequence(...)`)
|
|
97
|
+
# - `Identity(...)` -> becomes `Column.server_default` (GENERATED ... AS IDENTITY)
|
|
98
|
+
# - `FetchedValue()` -> becomes `Column.server_default` (value fetched after INSERT)
|
|
99
|
+
SQLALCHEMY_COLUMN_DEFAULT_MARKERS = frozenset({"Computed", "Sequence", "Identity", "FetchedValue"})
|
|
100
|
+
|
|
88
101
|
|
|
89
102
|
def _plugin_any() -> AnyType:
|
|
90
103
|
"""Return an Any that should not trigger `disallow_any_explicit`."""
|
|
@@ -94,6 +107,109 @@ def _plugin_any() -> AnyType:
|
|
|
94
107
|
ERROR_FIELD = ErrorCode("sqlmodel-field", "SQLModel field error", "SQLModel")
|
|
95
108
|
|
|
96
109
|
|
|
110
|
+
_ExprT = TypeVar("_ExprT")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _is_sqlalchemy_module(module: str | None) -> bool:
|
|
114
|
+
"""True for `sqlalchemy` and any of its submodules."""
|
|
115
|
+
if not module:
|
|
116
|
+
return False
|
|
117
|
+
return module.split(".", 1)[0] == "sqlalchemy"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _is_sqlalchemy_marker_source_module(module: str | None) -> bool:
|
|
121
|
+
"""Modules an import may legitimately pull a SQLAlchemy default marker out of.
|
|
122
|
+
|
|
123
|
+
`sqlmodel/__init__.py` re-exports `Computed` / `Sequence` / `Identity` / `FetchedValue`
|
|
124
|
+
straight from `sqlalchemy.schema` (`from sqlalchemy.schema import X as X`), so
|
|
125
|
+
`from sqlmodel import Sequence` binds the very same class.
|
|
126
|
+
"""
|
|
127
|
+
if not module:
|
|
128
|
+
return False
|
|
129
|
+
return module.split(".", 1)[0] in {"sqlalchemy", "sqlmodel"}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _column_args_are_defaultish(
|
|
133
|
+
args: Iterable[tuple[str | None, _ExprT]],
|
|
134
|
+
*,
|
|
135
|
+
is_marker_call: Callable[[_ExprT], bool],
|
|
136
|
+
is_true: Callable[[_ExprT], bool],
|
|
137
|
+
is_none: Callable[[_ExprT], bool],
|
|
138
|
+
) -> bool:
|
|
139
|
+
"""Shared `Column(...)` rule: does this column get its value without the caller supplying one?
|
|
140
|
+
|
|
141
|
+
`args` is the argument list normalised to `(keyword_name_or_None, value)` pairs, so the same
|
|
142
|
+
rule can be driven both from raw `ast` nodes (`Annotated[...]` metadata) and from mypy
|
|
143
|
+
`Expression` nodes (`x: T = Field(...)`), and from `sa_column_kwargs={...}` dicts (which only
|
|
144
|
+
ever produce keyword entries).
|
|
145
|
+
|
|
146
|
+
Deliberately *not* covered: a bare `primary_key=True`. SQLAlchemy's default
|
|
147
|
+
`autoincrement="auto"` only generates a value for a *single-column integer* primary key, and
|
|
148
|
+
neither the column type nor the rest of the table's primary key is knowable from one
|
|
149
|
+
`Column(...)` call, so composite or non-integer primary keys would silently become optional.
|
|
150
|
+
An explicit `autoincrement=True` *on a primary key* is different: it is the author stating that
|
|
151
|
+
the database generates the value, it renders as SERIAL/IDENTITY, and SQLAlchemy rejects it at
|
|
152
|
+
DDL time on an incompatible column type.
|
|
153
|
+
"""
|
|
154
|
+
saw_autoincrement_true = False
|
|
155
|
+
saw_primary_key_true = False
|
|
156
|
+
|
|
157
|
+
for arg_name, arg in args:
|
|
158
|
+
if arg_name is None:
|
|
159
|
+
# Positional schema constructs, e.g. `Column(BigInteger, Sequence("s"))`.
|
|
160
|
+
if is_marker_call(arg):
|
|
161
|
+
return True
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
if arg_name == "nullable" and is_true(arg):
|
|
165
|
+
return True
|
|
166
|
+
if arg_name in SQLALCHEMY_DEFAULTISH_COLUMN_KWARGS and not is_none(arg):
|
|
167
|
+
return True
|
|
168
|
+
if arg_name == "autoincrement" and is_true(arg):
|
|
169
|
+
saw_autoincrement_true = True
|
|
170
|
+
elif arg_name == "primary_key" and is_true(arg):
|
|
171
|
+
saw_primary_key_true = True
|
|
172
|
+
|
|
173
|
+
return saw_autoincrement_true and saw_primary_key_true
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _ast_dotted_name(expr: ast.expr) -> str | None:
|
|
177
|
+
"""Flatten `a.b.C` into `"a.b.C"`; return None for anything that is not a plain dotted name."""
|
|
178
|
+
if isinstance(expr, ast.Name):
|
|
179
|
+
return expr.id
|
|
180
|
+
if isinstance(expr, ast.Attribute):
|
|
181
|
+
base = _ast_dotted_name(expr.value)
|
|
182
|
+
return None if base is None else f"{base}.{expr.attr}"
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _collect_sqlalchemy_marker_bindings(tree: ast.Module) -> tuple[set[str], set[str]]:
|
|
187
|
+
"""Return `(marker names, module aliases)` bound by the module's imports.
|
|
188
|
+
|
|
189
|
+
Import-aware on purpose: `Sequence` is also `typing.Sequence` / `collections.abc.Sequence`, so
|
|
190
|
+
matching the bare name would misread `x: Sequence[int]`-style code as a SQLAlchemy sequence.
|
|
191
|
+
Names we cannot trace back to a `sqlalchemy` / `sqlmodel` import are not treated as markers.
|
|
192
|
+
"""
|
|
193
|
+
marker_names: set[str] = set()
|
|
194
|
+
module_aliases: set[str] = set()
|
|
195
|
+
|
|
196
|
+
for node in ast.walk(tree):
|
|
197
|
+
if isinstance(node, ast.Import):
|
|
198
|
+
for alias in node.names:
|
|
199
|
+
if not _is_sqlalchemy_marker_source_module(alias.name):
|
|
200
|
+
continue
|
|
201
|
+
# `import sqlalchemy.orm` binds the top-level package name.
|
|
202
|
+
module_aliases.add(alias.asname or alias.name.split(".", 1)[0])
|
|
203
|
+
elif isinstance(node, ast.ImportFrom):
|
|
204
|
+
if node.level or not _is_sqlalchemy_marker_source_module(node.module):
|
|
205
|
+
continue
|
|
206
|
+
for alias in node.names:
|
|
207
|
+
if alias.name in SQLALCHEMY_COLUMN_DEFAULT_MARKERS:
|
|
208
|
+
marker_names.add(alias.asname or alias.name)
|
|
209
|
+
|
|
210
|
+
return marker_names, module_aliases
|
|
211
|
+
|
|
212
|
+
|
|
97
213
|
def _parse_annotated_field_metadata_by_line(source: str) -> dict[int, tuple[bool, list[str]]]:
|
|
98
214
|
"""Return mapping of line -> (has_default, aliases) for Annotated Field metadata.
|
|
99
215
|
|
|
@@ -103,6 +219,13 @@ def _parse_annotated_field_metadata_by_line(source: str) -> dict[int, tuple[bool
|
|
|
103
219
|
|
|
104
220
|
Only handles class-body `AnnAssign` nodes with **no assignment value** (i.e. `x: ...`).
|
|
105
221
|
"""
|
|
222
|
+
out: dict[int, tuple[bool, list[str]]] = {}
|
|
223
|
+
try:
|
|
224
|
+
tree = ast.parse(source)
|
|
225
|
+
except SyntaxError:
|
|
226
|
+
return out
|
|
227
|
+
|
|
228
|
+
marker_names, sqlalchemy_module_aliases = _collect_sqlalchemy_marker_bindings(tree)
|
|
106
229
|
|
|
107
230
|
def _call_func_name(expr: ast.expr) -> str | None:
|
|
108
231
|
if isinstance(expr, ast.Name):
|
|
@@ -175,6 +298,20 @@ def _parse_annotated_field_metadata_by_line(source: str) -> dict[int, tuple[bool
|
|
|
175
298
|
aliases.append(candidate)
|
|
176
299
|
return aliases
|
|
177
300
|
|
|
301
|
+
def _is_marker_call(expr: ast.expr) -> bool:
|
|
302
|
+
if not isinstance(expr, ast.Call):
|
|
303
|
+
return False
|
|
304
|
+
name = _ast_dotted_name(expr.func)
|
|
305
|
+
if name is None:
|
|
306
|
+
return False
|
|
307
|
+
head, _, attr = name.rpartition(".")
|
|
308
|
+
if not head:
|
|
309
|
+
return name in marker_names
|
|
310
|
+
return (
|
|
311
|
+
attr in SQLALCHEMY_COLUMN_DEFAULT_MARKERS
|
|
312
|
+
and head.split(".", 1)[0] in sqlalchemy_module_aliases
|
|
313
|
+
)
|
|
314
|
+
|
|
178
315
|
def _column_expr_is_defaultish(expr: ast.expr) -> bool:
|
|
179
316
|
if not isinstance(expr, ast.Call):
|
|
180
317
|
return False
|
|
@@ -183,32 +320,29 @@ def _parse_annotated_field_metadata_by_line(source: str) -> dict[int, tuple[bool
|
|
|
183
320
|
if callee_name is not None and not callee_name.endswith("Column"):
|
|
184
321
|
return False
|
|
185
322
|
|
|
186
|
-
for arg in expr.args
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return True
|
|
195
|
-
if kw.arg in {"server_default", "default", "insert_default"} and not _is_none(kw.value):
|
|
196
|
-
return True
|
|
197
|
-
return False
|
|
323
|
+
args: list[tuple[str | None, ast.expr]] = [(None, arg) for arg in expr.args]
|
|
324
|
+
args += [(kw.arg, kw.value) for kw in expr.keywords if kw.arg is not None]
|
|
325
|
+
return _column_args_are_defaultish(
|
|
326
|
+
args,
|
|
327
|
+
is_marker_call=_is_marker_call,
|
|
328
|
+
is_true=_is_true,
|
|
329
|
+
is_none=_is_none,
|
|
330
|
+
)
|
|
198
331
|
|
|
199
332
|
def _sa_column_kwargs_are_defaultish(expr: ast.expr) -> bool:
|
|
200
333
|
if not isinstance(expr, ast.Dict):
|
|
201
334
|
return False
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if key_expr.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
335
|
+
items: list[tuple[str | None, ast.expr]] = [
|
|
336
|
+
(key_expr.value, val_expr)
|
|
337
|
+
for key_expr, val_expr in zip(expr.keys, expr.values, strict=True)
|
|
338
|
+
if isinstance(key_expr, ast.Constant) and isinstance(key_expr.value, str)
|
|
339
|
+
]
|
|
340
|
+
return _column_args_are_defaultish(
|
|
341
|
+
items,
|
|
342
|
+
is_marker_call=_is_marker_call,
|
|
343
|
+
is_true=_is_true,
|
|
344
|
+
is_none=_is_none,
|
|
345
|
+
)
|
|
212
346
|
|
|
213
347
|
def _field_has_default_from_call(call: ast.Call) -> bool:
|
|
214
348
|
# Explicit default always wins.
|
|
@@ -263,12 +397,6 @@ def _parse_annotated_field_metadata_by_line(source: str) -> dict[int, tuple[bool
|
|
|
263
397
|
|
|
264
398
|
return None
|
|
265
399
|
|
|
266
|
-
out: dict[int, tuple[bool, list[str]]] = {}
|
|
267
|
-
try:
|
|
268
|
-
tree = ast.parse(source)
|
|
269
|
-
except SyntaxError:
|
|
270
|
-
return out
|
|
271
|
-
|
|
272
400
|
class _Visitor(ast.NodeVisitor):
|
|
273
401
|
def __init__(self) -> None:
|
|
274
402
|
self.class_depth = 0
|
|
@@ -717,10 +845,8 @@ class SQLModelTransformer:
|
|
|
717
845
|
def _column_expr_is_defaultish(value: Expression) -> bool:
|
|
718
846
|
"""Best-effort detection of SQLAlchemy-generated/defaultable columns.
|
|
719
847
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
- server_default / default / insert_default
|
|
723
|
-
- Computed(...) columns
|
|
848
|
+
Shares `_column_args_are_defaultish` with the `Annotated[...]` `ast` reader; see
|
|
849
|
+
that function for what counts as "the caller need not supply this value".
|
|
724
850
|
"""
|
|
725
851
|
if not isinstance(value, CallExpr):
|
|
726
852
|
return False
|
|
@@ -730,38 +856,28 @@ class SQLModelTransformer:
|
|
|
730
856
|
# Be conservative: avoid interpreting non-Column calls.
|
|
731
857
|
return False
|
|
732
858
|
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
):
|
|
740
|
-
return True
|
|
741
|
-
|
|
742
|
-
# Keyword hints.
|
|
743
|
-
if c_arg_name == "nullable" and _is_bool_nameexpr(c_arg, True):
|
|
744
|
-
return True
|
|
745
|
-
if c_arg_name in {"server_default", "default", "insert_default"}:
|
|
746
|
-
if not _is_none_expr(c_arg):
|
|
747
|
-
return True
|
|
748
|
-
|
|
749
|
-
return False
|
|
859
|
+
return _column_args_are_defaultish(
|
|
860
|
+
zip(value.arg_names, value.args, strict=True),
|
|
861
|
+
is_marker_call=_is_sqlalchemy_marker_call,
|
|
862
|
+
is_true=lambda e: _is_bool_nameexpr(e, True),
|
|
863
|
+
is_none=_is_none_expr,
|
|
864
|
+
)
|
|
750
865
|
|
|
751
866
|
def _sa_column_kwargs_are_defaultish(value: Expression) -> bool:
|
|
752
867
|
"""Best-effort for `Field(sa_column_kwargs={...})`."""
|
|
753
868
|
if not isinstance(value, DictExpr):
|
|
754
869
|
return False
|
|
755
870
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
871
|
+
return _column_args_are_defaultish(
|
|
872
|
+
[
|
|
873
|
+
(key_expr.value, val_expr)
|
|
874
|
+
for key_expr, val_expr in value.items
|
|
875
|
+
if isinstance(key_expr, StrExpr)
|
|
876
|
+
],
|
|
877
|
+
is_marker_call=_is_sqlalchemy_marker_call,
|
|
878
|
+
is_true=lambda e: _is_bool_nameexpr(e, True),
|
|
879
|
+
is_none=_is_none_expr,
|
|
880
|
+
)
|
|
765
881
|
|
|
766
882
|
saw_nullable_true = False
|
|
767
883
|
sa_column_expr: Expression | None = None
|
|
@@ -1057,6 +1173,21 @@ def _callee_fullname(call: CallExpr) -> str | None:
|
|
|
1057
1173
|
return None
|
|
1058
1174
|
|
|
1059
1175
|
|
|
1176
|
+
def _is_sqlalchemy_marker_call(value: Expression) -> bool:
|
|
1177
|
+
"""True for a call to a SQLAlchemy schema construct that installs a column default.
|
|
1178
|
+
|
|
1179
|
+
Matching is by resolved fullname, so `Sequence` imported from `typing` /
|
|
1180
|
+
`collections.abc` does not qualify. An unresolved callee is not treated as a marker.
|
|
1181
|
+
"""
|
|
1182
|
+
if not isinstance(value, CallExpr):
|
|
1183
|
+
return False
|
|
1184
|
+
fullname = _callee_fullname(value)
|
|
1185
|
+
if fullname is None:
|
|
1186
|
+
return False
|
|
1187
|
+
module, _, attr = fullname.rpartition(".")
|
|
1188
|
+
return attr in SQLALCHEMY_COLUMN_DEFAULT_MARKERS and _is_sqlalchemy_module(module)
|
|
1189
|
+
|
|
1190
|
+
|
|
1060
1191
|
def _unwrap_mapped_type(typ: Type) -> Type:
|
|
1061
1192
|
"""Convert SQLAlchemy `Mapped[T]` annotations to `T` for constructor signatures.
|
|
1062
1193
|
|
|
@@ -1198,11 +1329,25 @@ def add_method(
|
|
|
1198
1329
|
"""
|
|
1199
1330
|
info = cls.info
|
|
1200
1331
|
|
|
1201
|
-
|
|
1332
|
+
def _remove_stmt(stmt: Statement) -> None:
|
|
1333
|
+
# `list.remove(...)` raises ValueError if not present; ignore.
|
|
1334
|
+
try:
|
|
1335
|
+
cls.defs.body.remove(stmt)
|
|
1336
|
+
except ValueError:
|
|
1337
|
+
return
|
|
1338
|
+
|
|
1339
|
+
# Remove previously generated methods (handle both FuncDef and Decorator nodes).
|
|
1202
1340
|
if name in info.names:
|
|
1203
1341
|
sym = info.names[name]
|
|
1204
|
-
if sym.plugin_generated
|
|
1205
|
-
|
|
1342
|
+
if sym.plugin_generated:
|
|
1343
|
+
node = sym.node
|
|
1344
|
+
if isinstance(node, Decorator):
|
|
1345
|
+
# NOTE: older buggy versions appended the underlying FuncDef instead of the
|
|
1346
|
+
# Decorator; remove both to be safe.
|
|
1347
|
+
_remove_stmt(node)
|
|
1348
|
+
_remove_stmt(node.func)
|
|
1349
|
+
elif isinstance(node, FuncDef):
|
|
1350
|
+
_remove_stmt(node)
|
|
1206
1351
|
|
|
1207
1352
|
# Keep existing definition for semantic analysis by renaming it.
|
|
1208
1353
|
if name in info.names:
|
|
@@ -1248,9 +1393,13 @@ def add_method(
|
|
|
1248
1393
|
dec = Decorator(func, [NameExpr("classmethod")], v)
|
|
1249
1394
|
dec.line = info.line
|
|
1250
1395
|
sym = SymbolTableNode(MDEF, dec)
|
|
1396
|
+
# NOTE: keep the class body node as a plain FuncDef (this matches the
|
|
1397
|
+
# previous plugin behavior and avoids override-check churn).
|
|
1398
|
+
body_node: Statement = func
|
|
1251
1399
|
else:
|
|
1252
1400
|
sym = SymbolTableNode(MDEF, func)
|
|
1401
|
+
body_node = func
|
|
1253
1402
|
|
|
1254
1403
|
sym.plugin_generated = True
|
|
1255
1404
|
info.names[name] = sym
|
|
1256
|
-
info.defn.defs.body.append(
|
|
1405
|
+
info.defn.defs.body.append(body_node)
|