sqlmodel-mypy-plugin 0.19.1__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.1 → sqlmodel_mypy_plugin-0.20.0}/.pre-commit-config.yaml +1 -1
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/PKG-INFO +34 -2
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/README.md +31 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/pyproject.toml +2 -2
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/plugin.py +1 -1
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/transform.py +191 -60
- 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.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/sqlmodel_reexported_sequence.py +25 -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/sqlmodel_reexported_sequence.py +25 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/test_mypy.py +4 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/test_transform_unit.py +290 -0
- 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.20.0}/.gitignore +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/AGENTS.md +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/LICENSE +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/__init__.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/persisted.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/src/sqlmodel_mypy/py.typed +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/conftest.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-strict-typed-execute.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/mypy-plugin.ini +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/configs/pyproject-plugin.toml +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/aliased_joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/annotated_field_metadata.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_many_to_many_create_data.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_read_connected_data.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_relationship_attributes.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/docs_parity_update_patterns.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/expression_typing_strict.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/field_sa_type.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/field_validation_alias.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/getattr_column_property.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/label.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/selectinload.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/session_exec.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/session_execute_typed.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/session_get.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/table_dunders.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/table_via_model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/tuple_expression.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/modules/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-pydantic-first_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-pydantic-last_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → 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.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/aliased_joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/annotated_field_metadata.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → 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.1 → sqlmodel_mypy_plugin-0.20.0}/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.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_relationship_attributes.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → 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.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → 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.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/session_exec.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → 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.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin-strict_ini/tuple_expression.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/aliased_joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/session_exec.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/smoke_test.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → sqlmodel_mypy_plugin-0.20.0}/tests/test_persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.19.1 → 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
|
|
@@ -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.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",
|
|
@@ -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
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import (
|
|
4
|
+
BigInteger,
|
|
5
|
+
Column,
|
|
6
|
+
Computed,
|
|
7
|
+
FetchedValue,
|
|
8
|
+
Identity,
|
|
9
|
+
Integer,
|
|
10
|
+
Sequence,
|
|
11
|
+
String,
|
|
12
|
+
text,
|
|
13
|
+
)
|
|
14
|
+
from sqlmodel import Field, SQLModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Model(SQLModel, table=True):
|
|
18
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
19
|
+
|
|
20
|
+
# Required field (no default, non-nullable).
|
|
21
|
+
required: str = Field()
|
|
22
|
+
|
|
23
|
+
# Nullable fields without explicit defaults should be optional constructor kwargs.
|
|
24
|
+
note: str = Field(nullable=True)
|
|
25
|
+
|
|
26
|
+
optional_sa: str = Field(sa_column=Column("optional_sa", String(), nullable=True))
|
|
27
|
+
|
|
28
|
+
search_vector: str | None = Field(
|
|
29
|
+
sa_column=Column("search_vector", String(), Computed("1", persisted=True))
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
created_by: str = Field(sa_column=Column("created_by", String(), server_default=text("'x'")))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
Model()
|
|
36
|
+
Model(required="x")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class GeneratedDefaults(SQLModel, table=True):
|
|
40
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
41
|
+
|
|
42
|
+
required: str = Field()
|
|
43
|
+
|
|
44
|
+
# Positional `Sequence(...)` becomes `Column.default`, exactly like `default=Sequence(...)`.
|
|
45
|
+
seq_number: int = Field(
|
|
46
|
+
sa_column=Column(
|
|
47
|
+
"seq_number",
|
|
48
|
+
BigInteger,
|
|
49
|
+
Sequence("generated_defaults_seq_number_seq", start=1, increment=1),
|
|
50
|
+
index=True,
|
|
51
|
+
nullable=False,
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Positional `Identity(...)` becomes `Column.server_default` (GENERATED ... AS IDENTITY).
|
|
56
|
+
identity_number: int = Field(
|
|
57
|
+
sa_column=Column("identity_number", Integer, Identity(always=True))
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# Positional `FetchedValue()` becomes `Column.server_default`.
|
|
61
|
+
fetched: str = Field(sa_column=Column("fetched", String(), FetchedValue()))
|
|
62
|
+
|
|
63
|
+
fetched_kw: str = Field(sa_column=Column("fetched_kw", String(), server_default=FetchedValue()))
|
|
64
|
+
|
|
65
|
+
inserted: str = Field(sa_column=Column("inserted", String(), insert_default="x"))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
GeneratedDefaults()
|
|
69
|
+
GeneratedDefaults(required="x")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class AutoIncrementPk(SQLModel, table=True):
|
|
73
|
+
# `autoincrement=True` on a primary key renders as SERIAL/IDENTITY: value comes from the DB.
|
|
74
|
+
auto_pk: int = Field(sa_column=Column("auto_pk", Integer, primary_key=True, autoincrement=True))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
AutoIncrementPk()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ManualPk(SQLModel, table=True):
|
|
81
|
+
# A bare `primary_key=True` says nothing about who supplies the value.
|
|
82
|
+
manual_pk: int = Field(sa_column=Column("manual_pk", Integer, primary_key=True))
|
|
83
|
+
|
|
84
|
+
# `autoincrement=True` off a primary key is ignored by SQLAlchemy (plain INTEGER column).
|
|
85
|
+
counter: int = Field(sa_column=Column("counter", Integer, autoincrement=True))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
ManualPk()
|
|
89
|
+
ManualPk(manual_pk=1, counter=0)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
|
|
5
|
+
import sqlalchemy as sa
|
|
6
|
+
from sqlalchemy import JSON, BigInteger, Column
|
|
7
|
+
from sqlmodel import Field, SQLModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Order(SQLModel, table=True):
|
|
11
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
12
|
+
|
|
13
|
+
# SQLAlchemy's `Sequence`, reached through a module alias: database-generated -> optional.
|
|
14
|
+
number: int = Field(
|
|
15
|
+
sa_column=Column(
|
|
16
|
+
"number",
|
|
17
|
+
BigInteger,
|
|
18
|
+
sa.Sequence("example_number_seq", start=1, increment=1),
|
|
19
|
+
index=True,
|
|
20
|
+
nullable=False,
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# The bare name `Sequence` in this module is `collections.abc.Sequence`, not SQLAlchemy's.
|
|
25
|
+
# It must not turn the field into an optional constructor kwarg.
|
|
26
|
+
tags: Sequence[str] = Field(sa_column=Column("tags", JSON, nullable=False))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
Order()
|
|
30
|
+
Order(tags=["a"])
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sqlmodel import BigInteger, Column, Field, Sequence, SQLModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Order(SQLModel, table=True):
|
|
7
|
+
id: int | None = Field(default=None, primary_key=True)
|
|
8
|
+
|
|
9
|
+
# `sqlmodel.Sequence` is `sqlalchemy.schema.Sequence` re-exported; passed positionally it
|
|
10
|
+
# becomes `Column.default`, so the database assigns the value.
|
|
11
|
+
number: int = Field(
|
|
12
|
+
sa_column=Column(
|
|
13
|
+
BigInteger,
|
|
14
|
+
Sequence("example_number_seq", start=1, increment=1),
|
|
15
|
+
index=True,
|
|
16
|
+
nullable=False,
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
title: str = Field()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
Order()
|
|
24
|
+
Order(title="x")
|