sqlmodel-mypy-plugin 0.19.1__tar.gz → 0.21.2__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.1 → sqlmodel_mypy_plugin-0.21.2}/.pre-commit-config.yaml +1 -1
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/PKG-INFO +34 -2
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/README.md +31 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/pyproject.toml +5 -2
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/src/sqlmodel_mypy/plugin.py +33 -13
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/src/sqlmodel_mypy/transform.py +194 -62
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/configs/mypy-plugin-recursive.ini +10 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/execution_options.py +1 -2
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/modules/nullable_defaults.py +89 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/modules/recursive_aliases.py +35 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/modules/recursive_aliases_pep695.py +26 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/modules/row_generics.py +41 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/select_varargs.py +1 -2
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/modules/sequence_name_collision.py +30 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/session_execute_typed.py +12 -2
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/modules/sqlmodel_reexported_sequence.py +24 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin-recursive_ini/recursive_aliases_pep695.py +30 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict-typed-execute_ini/session_execute_typed.py +12 -2
- {sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin_ini → sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin-strict_ini}/execution_options.py +1 -2
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +93 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin-strict_ini/recursive_aliases.py +39 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin-strict_ini/select_varargs.py +35 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin-strict_ini/sequence_name_collision.py +31 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin-strict_ini/sqlmodel_reexported_sequence.py +25 -0
- {sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin-strict_ini → sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin_ini}/execution_options.py +1 -2
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +93 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin_ini/select_varargs.py +35 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin_ini/sequence_name_collision.py +31 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin_ini/sqlmodel_reexported_sequence.py +25 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin-strict-typed-execute_ini/session_execute_typed.py +48 -0
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin-strict_ini/row_generics.py +47 -0
- {sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin_ini → sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin-strict_ini}/select_varargs.py +2 -3
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin-strict_ini/session_exec.py +43 -0
- {sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin-strict_ini → sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin_ini}/select_varargs.py +2 -3
- sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin_ini/session_exec.py +43 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/test_mypy.py +40 -5
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/test_plugin_unit.py +37 -10
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/test_transform_unit.py +323 -1
- sqlmodel_mypy_plugin-0.19.1/tests/mypy/modules/nullable_defaults.py +0 -26
- sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +0 -27
- sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +0 -27
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/.gitignore +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/AGENTS.md +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/LICENSE +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/src/sqlmodel_mypy/__init__.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/src/sqlmodel_mypy/persisted.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/src/sqlmodel_mypy/py.typed +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/conftest.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-strict-typed-execute.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/pyproject-plugin.toml +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/aliased_joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/annotated_field_metadata.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/docs_parity_many_to_many_create_data.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/docs_parity_read_connected_data.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/docs_parity_relationship_attributes.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/docs_parity_update_patterns.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/expression_typing_strict.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/field_sa_type.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/field_validation_alias.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/getattr_column_property.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/label.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/selectinload.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/session_exec.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/session_get.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/table_dunders.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/table_via_model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/tuple_expression.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-pydantic-first_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-pydantic-last_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/aliased_joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/annotated_field_metadata.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_many_to_many_create_data.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_read_connected_data.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_relationship_attributes.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_update_patterns.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/session_exec.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/table_via_model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/tuple_expression.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/aliased_joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/session_exec.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/smoke_test.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/test_persisted_helpers.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.21.2
|
|
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
|
|
@@ -167,10 +168,12 @@ expressions, so you can write queries without `col()`:
|
|
|
167
168
|
```py
|
|
168
169
|
from sqlmodel import Field, SQLModel, select
|
|
169
170
|
|
|
171
|
+
|
|
170
172
|
class User(SQLModel, table=True):
|
|
171
173
|
id: int = Field(primary_key=True)
|
|
172
174
|
name: str = Field()
|
|
173
175
|
|
|
176
|
+
|
|
174
177
|
stmt = select(User).where(User.name.like("%x%"))
|
|
175
178
|
```
|
|
176
179
|
|
|
@@ -228,6 +231,35 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
|
|
|
228
231
|
- **SQLAlchemy typing**: relied upon for core SQL/ORM typing wherever possible.
|
|
229
232
|
- **Stub overlays**: only for upstream gaps that can’t be addressed cleanly via hooks (prefer upstream fixes first).
|
|
230
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
|
+
|
|
231
263
|
## Field aliases in constructor kwargs
|
|
232
264
|
|
|
233
265
|
If you use `Field(alias=...)` (or `validation_alias=...` / `schema_extra={"validation_alias": ...}`), the plugin
|
|
@@ -121,10 +121,12 @@ expressions, so you can write queries without `col()`:
|
|
|
121
121
|
```py
|
|
122
122
|
from sqlmodel import Field, SQLModel, select
|
|
123
123
|
|
|
124
|
+
|
|
124
125
|
class User(SQLModel, table=True):
|
|
125
126
|
id: int = Field(primary_key=True)
|
|
126
127
|
name: str = Field()
|
|
127
128
|
|
|
129
|
+
|
|
128
130
|
stmt = select(User).where(User.name.like("%x%"))
|
|
129
131
|
```
|
|
130
132
|
|
|
@@ -182,6 +184,35 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
|
|
|
182
184
|
- **SQLAlchemy typing**: relied upon for core SQL/ORM typing wherever possible.
|
|
183
185
|
- **Stub overlays**: only for upstream gaps that can’t be addressed cleanly via hooks (prefer upstream fixes first).
|
|
184
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
|
+
|
|
185
216
|
## Field aliases in constructor kwargs
|
|
186
217
|
|
|
187
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.21.2"
|
|
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",
|
|
@@ -64,6 +64,9 @@ line-length = 100
|
|
|
64
64
|
target-version = "py310"
|
|
65
65
|
exclude = ["tests/mypy/outputs"]
|
|
66
66
|
|
|
67
|
+
[tool.ruff.per-file-target-version]
|
|
68
|
+
"tests/mypy/modules/recursive_aliases_pep695.py" = "py312"
|
|
69
|
+
|
|
67
70
|
[tool.ruff.lint]
|
|
68
71
|
select = [
|
|
69
72
|
"E", # pycodestyle errors
|
|
@@ -58,6 +58,7 @@ from mypy.types import (
|
|
|
58
58
|
TypeOfAny,
|
|
59
59
|
TypeType,
|
|
60
60
|
UnionType,
|
|
61
|
+
UnpackType,
|
|
61
62
|
get_proper_type,
|
|
62
63
|
)
|
|
63
64
|
from mypy.typevars import fill_typevars
|
|
@@ -171,7 +172,7 @@ SQLALCHEMY_INSPECT_FULLNAMES = {
|
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
# Increment when plugin changes should invalidate mypy cache.
|
|
174
|
-
__version__ =
|
|
175
|
+
__version__ = 29
|
|
175
176
|
|
|
176
177
|
|
|
177
178
|
class _CollectedField(NamedTuple):
|
|
@@ -926,7 +927,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
926
927
|
|
|
927
928
|
SQLModel's generated stubs traditionally cap `select()` overloads at 4 entities.
|
|
928
929
|
For 5+ entities, fall back to a conservative signature that accepts the call and
|
|
929
|
-
returns
|
|
930
|
+
returns a Select with one unknown value per selected entity.
|
|
930
931
|
"""
|
|
931
932
|
call = ctx.context
|
|
932
933
|
if not isinstance(call, CallExpr):
|
|
@@ -977,7 +978,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
977
978
|
any_t = _plugin_any()
|
|
978
979
|
tuple_fallback = ctx.api.named_generic_type("builtins.tuple", [any_t])
|
|
979
980
|
row_type = TupleType([any_t] * positional_count, tuple_fallback)
|
|
980
|
-
ret_type =
|
|
981
|
+
ret_type = self._instance_with_row_type(select_info, row_type)
|
|
981
982
|
|
|
982
983
|
fallback = default_items[0].fallback if default_items else tuple_fallback
|
|
983
984
|
arg_types = [any_t] * positional_count
|
|
@@ -990,7 +991,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
990
991
|
|
|
991
992
|
Our signature hook for 5+ args deliberately uses `Any` to avoid "no overload matches"
|
|
992
993
|
errors. Here, once mypy has inferred argument expression types, we recover a more precise
|
|
993
|
-
|
|
994
|
+
row element types for the common cases (models + column expressions).
|
|
994
995
|
"""
|
|
995
996
|
call = ctx.context
|
|
996
997
|
if not isinstance(call, CallExpr):
|
|
@@ -1012,7 +1013,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1012
1013
|
proper_default = get_proper_type(default)
|
|
1013
1014
|
if not isinstance(proper_default, Instance) or not proper_default.args:
|
|
1014
1015
|
return default
|
|
1015
|
-
proper_row = get_proper_type(proper_default.
|
|
1016
|
+
proper_row = get_proper_type(self._instance_row_type(proper_default, ctx.api))
|
|
1016
1017
|
if not isinstance(proper_row, TupleType):
|
|
1017
1018
|
return default
|
|
1018
1019
|
|
|
@@ -1036,7 +1037,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1036
1037
|
any_t = _plugin_any()
|
|
1037
1038
|
tuple_fallback = ctx.api.named_generic_type("builtins.tuple", [any_t])
|
|
1038
1039
|
row_type = TupleType(items, tuple_fallback)
|
|
1039
|
-
return
|
|
1040
|
+
return self._instance_with_row_type(proper_default.type, row_type)
|
|
1040
1041
|
|
|
1041
1042
|
def _sqlmodel_select_entity_result_type(self, typ: Type) -> Type:
|
|
1042
1043
|
"""Return the value type produced by an entity passed to `select(...)`.
|
|
@@ -1054,6 +1055,9 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1054
1055
|
return UnionType.make_union(
|
|
1055
1056
|
[self._sqlmodel_select_entity_result_type(item) for item in proper.items]
|
|
1056
1057
|
)
|
|
1058
|
+
if isinstance(proper, FunctionLike) and proper.is_type_obj():
|
|
1059
|
+
# Mypy represents a model class expression by its constructor overloads.
|
|
1060
|
+
return UnionType.make_union([item.ret_type for item in proper.items])
|
|
1057
1061
|
if isinstance(proper, TypeType):
|
|
1058
1062
|
item = get_proper_type(proper.item)
|
|
1059
1063
|
if isinstance(item, Instance):
|
|
@@ -1371,6 +1375,23 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1371
1375
|
coro = _named_generic_type_or_none(ctx.api, "typing.Coroutine", [any_t, any_t, result_type])
|
|
1372
1376
|
return coro or default
|
|
1373
1377
|
|
|
1378
|
+
@staticmethod
|
|
1379
|
+
def _instance_row_type(instance: Instance, api: Any) -> Type:
|
|
1380
|
+
"""Read a row from SQLAlchemy 2.0's tuple or 2.1's variadic arguments."""
|
|
1381
|
+
if instance.type.has_type_var_tuple_type:
|
|
1382
|
+
tuple_fallback = api.named_generic_type("builtins.tuple", [_plugin_any()])
|
|
1383
|
+
return TupleType(list(instance.args), tuple_fallback)
|
|
1384
|
+
return instance.args[0]
|
|
1385
|
+
|
|
1386
|
+
@staticmethod
|
|
1387
|
+
def _instance_with_row_type(info: TypeInfo, row_type: Type) -> Instance:
|
|
1388
|
+
"""Use the installed class's generic shape, without guessing package versions."""
|
|
1389
|
+
if info.has_type_var_tuple_type:
|
|
1390
|
+
proper_row = get_proper_type(row_type)
|
|
1391
|
+
args = proper_row.items if isinstance(proper_row, TupleType) else [UnpackType(row_type)]
|
|
1392
|
+
return Instance(info, args)
|
|
1393
|
+
return Instance(info, [row_type])
|
|
1394
|
+
|
|
1374
1395
|
def _row_tuple_type_from_typed_statement(self, statement: Type, api: Any) -> Type | None:
|
|
1375
1396
|
"""Infer the row tuple type produced by a SQL statement (best-effort)."""
|
|
1376
1397
|
proper = get_proper_type(statement)
|
|
@@ -1385,12 +1406,12 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1385
1406
|
tuple_fallback = api.named_generic_type("builtins.tuple", [any_t])
|
|
1386
1407
|
return TupleType([scalar_t], tuple_fallback)
|
|
1387
1408
|
|
|
1388
|
-
#
|
|
1409
|
+
# SQLAlchemy 2.1 and SQLModel 0.0.48 use variadic row parameters.
|
|
1389
1410
|
if proper.type.fullname in {
|
|
1390
1411
|
SQLMODEL_SELECT_CLS_FULLNAME,
|
|
1391
1412
|
SQLALCHEMY_SELECT_FULLNAME,
|
|
1392
1413
|
} or proper.type.has_base(SQLALCHEMY_SELECT_FULLNAME):
|
|
1393
|
-
return proper
|
|
1414
|
+
return self._instance_row_type(proper, api)
|
|
1394
1415
|
|
|
1395
1416
|
return None
|
|
1396
1417
|
|
|
@@ -1399,11 +1420,10 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1399
1420
|
for fullname in ("sqlalchemy.engine.Result", "sqlalchemy.engine.result.Result"):
|
|
1400
1421
|
inst = _named_generic_type_or_none(api, fullname, [row_tuple_type])
|
|
1401
1422
|
if inst is not None:
|
|
1402
|
-
return inst
|
|
1423
|
+
return self._instance_with_row_type(inst.type, row_tuple_type)
|
|
1403
1424
|
info = _lookup_typeinfo(self, fullname)
|
|
1404
1425
|
if info is not None:
|
|
1405
|
-
|
|
1406
|
-
return Instance(info, args)
|
|
1426
|
+
return self._instance_with_row_type(info, row_tuple_type)
|
|
1407
1427
|
return None
|
|
1408
1428
|
|
|
1409
1429
|
def _sqlalchemy_select_join_return_type_callback(self, ctx: MethodContext) -> Type:
|
|
@@ -1661,7 +1681,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1661
1681
|
if not receiver.args:
|
|
1662
1682
|
return ctx.default_return_type
|
|
1663
1683
|
|
|
1664
|
-
tp = get_proper_type(receiver.
|
|
1684
|
+
tp = get_proper_type(self._instance_row_type(receiver, ctx.api))
|
|
1665
1685
|
if not isinstance(tp, TupleType):
|
|
1666
1686
|
return ctx.default_return_type
|
|
1667
1687
|
|
|
@@ -1682,7 +1702,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1682
1702
|
return ctx.default_return_type
|
|
1683
1703
|
|
|
1684
1704
|
new_tp = tp.copy_modified(items=new_items)
|
|
1685
|
-
return
|
|
1705
|
+
return self._instance_with_row_type(receiver.type, new_tp)
|
|
1686
1706
|
|
|
1687
1707
|
def _sqlmodel_class_attr_type_callback(self, ctx: AttributeContext) -> Type:
|
|
1688
1708
|
if ctx.is_lvalue:
|
|
@@ -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
|
|
@@ -320,8 +448,9 @@ class ForceInvariantTypeVars(TypeTranslator):
|
|
|
320
448
|
return modified
|
|
321
449
|
|
|
322
450
|
def visit_type_alias_type(self, t: TypeAliasType) -> Type: # noqa: D102
|
|
323
|
-
#
|
|
324
|
-
|
|
451
|
+
# Alias targets can refer back to themselves. Preserve their graph and
|
|
452
|
+
# translate only the supplied arguments, like mypy's ExpandTypeVisitor.
|
|
453
|
+
return t.copy_modified(args=[arg.accept(self) for arg in t.args])
|
|
325
454
|
|
|
326
455
|
|
|
327
456
|
class SQLModelField:
|
|
@@ -717,10 +846,8 @@ class SQLModelTransformer:
|
|
|
717
846
|
def _column_expr_is_defaultish(value: Expression) -> bool:
|
|
718
847
|
"""Best-effort detection of SQLAlchemy-generated/defaultable columns.
|
|
719
848
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
- server_default / default / insert_default
|
|
723
|
-
- Computed(...) columns
|
|
849
|
+
Shares `_column_args_are_defaultish` with the `Annotated[...]` `ast` reader; see
|
|
850
|
+
that function for what counts as "the caller need not supply this value".
|
|
724
851
|
"""
|
|
725
852
|
if not isinstance(value, CallExpr):
|
|
726
853
|
return False
|
|
@@ -730,38 +857,28 @@ class SQLModelTransformer:
|
|
|
730
857
|
# Be conservative: avoid interpreting non-Column calls.
|
|
731
858
|
return False
|
|
732
859
|
|
|
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
|
|
860
|
+
return _column_args_are_defaultish(
|
|
861
|
+
zip(value.arg_names, value.args, strict=True),
|
|
862
|
+
is_marker_call=_is_sqlalchemy_marker_call,
|
|
863
|
+
is_true=lambda e: _is_bool_nameexpr(e, True),
|
|
864
|
+
is_none=_is_none_expr,
|
|
865
|
+
)
|
|
750
866
|
|
|
751
867
|
def _sa_column_kwargs_are_defaultish(value: Expression) -> bool:
|
|
752
868
|
"""Best-effort for `Field(sa_column_kwargs={...})`."""
|
|
753
869
|
if not isinstance(value, DictExpr):
|
|
754
870
|
return False
|
|
755
871
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
872
|
+
return _column_args_are_defaultish(
|
|
873
|
+
[
|
|
874
|
+
(key_expr.value, val_expr)
|
|
875
|
+
for key_expr, val_expr in value.items
|
|
876
|
+
if isinstance(key_expr, StrExpr)
|
|
877
|
+
],
|
|
878
|
+
is_marker_call=_is_sqlalchemy_marker_call,
|
|
879
|
+
is_true=lambda e: _is_bool_nameexpr(e, True),
|
|
880
|
+
is_none=_is_none_expr,
|
|
881
|
+
)
|
|
765
882
|
|
|
766
883
|
saw_nullable_true = False
|
|
767
884
|
sa_column_expr: Expression | None = None
|
|
@@ -1057,6 +1174,21 @@ def _callee_fullname(call: CallExpr) -> str | None:
|
|
|
1057
1174
|
return None
|
|
1058
1175
|
|
|
1059
1176
|
|
|
1177
|
+
def _is_sqlalchemy_marker_call(value: Expression) -> bool:
|
|
1178
|
+
"""True for a call to a SQLAlchemy schema construct that installs a column default.
|
|
1179
|
+
|
|
1180
|
+
Matching is by resolved fullname, so `Sequence` imported from `typing` /
|
|
1181
|
+
`collections.abc` does not qualify. An unresolved callee is not treated as a marker.
|
|
1182
|
+
"""
|
|
1183
|
+
if not isinstance(value, CallExpr):
|
|
1184
|
+
return False
|
|
1185
|
+
fullname = _callee_fullname(value)
|
|
1186
|
+
if fullname is None:
|
|
1187
|
+
return False
|
|
1188
|
+
module, _, attr = fullname.rpartition(".")
|
|
1189
|
+
return attr in SQLALCHEMY_COLUMN_DEFAULT_MARKERS and _is_sqlalchemy_module(module)
|
|
1190
|
+
|
|
1191
|
+
|
|
1060
1192
|
def _unwrap_mapped_type(typ: Type) -> Type:
|
|
1061
1193
|
"""Convert SQLAlchemy `Mapped[T]` annotations to `T` for constructor signatures.
|
|
1062
1194
|
|
{sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/execution_options.py
RENAMED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from sqlmodel import Field, Session, SQLModel, create_engine, select
|
|
4
|
-
from sqlmodel.sql._expression_select_cls import Select as SQLModelSelect
|
|
5
4
|
|
|
6
5
|
|
|
7
6
|
class Team(SQLModel, table=True):
|
|
@@ -21,7 +20,7 @@ stmt = select(Hero, Team).join(Team, isouter=True).execution_options(populate_ex
|
|
|
21
20
|
|
|
22
21
|
# `execution_options()` should preserve the `Select[...]` generic, including outer-join `None`
|
|
23
22
|
# propagation for the joined entity.
|
|
24
|
-
ok_stmt
|
|
23
|
+
ok_stmt = stmt
|
|
25
24
|
|
|
26
25
|
with Session(engine) as session:
|
|
27
26
|
for hero, team in session.exec(stmt):
|