sqlmodel-mypy-plugin 0.12.0__tar.gz → 0.12.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.12.0 → sqlmodel_mypy_plugin-0.12.2}/PKG-INFO +23 -5
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/README.md +22 -4
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/pyproject.toml +1 -1
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/src/sqlmodel_mypy/__init__.py +1 -1
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/src/sqlmodel_mypy/plugin.py +202 -33
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/src/sqlmodel_mypy/transform.py +90 -7
- sqlmodel_mypy_plugin-0.12.2/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +20 -0
- sqlmodel_mypy_plugin-0.12.2/tests/mypy/modules/expression_typing_strict.py +20 -0
- sqlmodel_mypy_plugin-0.12.2/tests/mypy/modules/nullable_defaults.py +26 -0
- {sqlmodel_mypy_plugin-0.12.0/tests/mypy/outputs/mypy-plugin-pydantic-last_ini → sqlmodel_mypy_plugin-0.12.2/tests/mypy/outputs/mypy-plugin-pydantic-first_ini}/relationships.py +0 -1
- {sqlmodel_mypy_plugin-0.12.0/tests/mypy/outputs/mypy-plugin-pydantic-first_ini → sqlmodel_mypy_plugin-0.12.2/tests/mypy/outputs/mypy-plugin-pydantic-last_ini}/relationships.py +1 -1
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +2 -2
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +7 -7
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +1 -1
- sqlmodel_mypy_plugin-0.12.2/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +27 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +2 -2
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +4 -4
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +3 -3
- sqlmodel_mypy_plugin-0.12.2/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +27 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +1 -1
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +4 -4
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/test_mypy.py +3 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/test_plugin_unit.py +125 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/test_transform_unit.py +94 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/.gitignore +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/.pre-commit-config.yaml +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/AGENTS.md +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/LICENSE +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/ROADMAP.md +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/src/sqlmodel_mypy/py.typed +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/conftest.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/mypy-plugin.ini +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/pyproject-plugin.toml +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/field_sa_type.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/label.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/selectinload.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/session_exec.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/session_get.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/table_dunders.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/smoke_test.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-mypy-plugin
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.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
|
|
@@ -71,7 +71,7 @@ Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
|
|
|
71
71
|
- Accept SQLAlchemy TypeEngine instances in `Field(sa_type=...)` (e.g. `DateTime(timezone=True)`, `String(50)`)
|
|
72
72
|
without `# type: ignore` in strict mode.
|
|
73
73
|
- Accept `model_config = ConfigDict(...)` overrides on SQLModel subclasses in strict mode.
|
|
74
|
-
- Compatible with `pydantic.mypy`
|
|
74
|
+
- Compatible with `pydantic.mypy` **when `sqlmodel_mypy.plugin` is listed first**.
|
|
75
75
|
|
|
76
76
|
## Install (dev)
|
|
77
77
|
|
|
@@ -96,17 +96,33 @@ make check
|
|
|
96
96
|
plugins = sqlmodel_mypy.plugin
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
If you also use `pydantic.mypy`,
|
|
99
|
+
If you also use `pydantic.mypy`, **`sqlmodel_mypy.plugin` must be listed first**:
|
|
100
100
|
|
|
101
101
|
```ini
|
|
102
102
|
[mypy]
|
|
103
103
|
plugins = sqlmodel_mypy.plugin, pydantic.mypy
|
|
104
|
-
# or:
|
|
105
|
-
# plugins = pydantic.mypy, sqlmodel_mypy.plugin
|
|
106
104
|
```
|
|
107
105
|
|
|
106
|
+
This is not a preference: **mypy picks the first plugin that claims a hook**. If `pydantic.mypy` runs first, it
|
|
107
|
+
can treat SQLModel classes as plain Pydantic models and you’ll get incorrect typing (e.g. missing `__table__`,
|
|
108
|
+
broken SQLAlchemy expression types, and incorrect/empty constructor signatures).
|
|
109
|
+
|
|
108
110
|
## Plugin configuration
|
|
109
111
|
|
|
112
|
+
All options are **boolean**.
|
|
113
|
+
|
|
114
|
+
Supported options:
|
|
115
|
+
|
|
116
|
+
- **`init_typed`** (default: `false`): Generate `__init__` / constructor signatures using the declared field types.
|
|
117
|
+
When `false`, the plugin uses `Any` for parameter types (but still enforces required/optional fields).
|
|
118
|
+
- **`init_forbid_extra`** (default: `false`): When `true`, do **not** add `**kwargs: Any` to generated
|
|
119
|
+
`__init__` / `model_construct` / constructor signatures, so mypy reports unexpected keyword arguments.
|
|
120
|
+
- **`warn_untyped_fields`** (default: `true`): When `true`, emit error code `sqlmodel-field` for untyped
|
|
121
|
+
declarations like `x = Field(...)` / `x = Relationship(...)` (use `x: T = ...` instead).
|
|
122
|
+
- **`debug_dataclass_transform`** (default: `false`): **Advanced/debug-only**. When `true`, keep SQLModel’s
|
|
123
|
+
`__dataclass_transform__` handling enabled in mypy (useful for debugging plugin interactions; not recommended
|
|
124
|
+
for normal use).
|
|
125
|
+
|
|
110
126
|
`mypy.ini`:
|
|
111
127
|
|
|
112
128
|
```ini
|
|
@@ -114,6 +130,7 @@ plugins = sqlmodel_mypy.plugin, pydantic.mypy
|
|
|
114
130
|
init_typed = false
|
|
115
131
|
init_forbid_extra = false
|
|
116
132
|
warn_untyped_fields = true
|
|
133
|
+
debug_dataclass_transform = false
|
|
117
134
|
```
|
|
118
135
|
|
|
119
136
|
`pyproject.toml`:
|
|
@@ -123,6 +140,7 @@ warn_untyped_fields = true
|
|
|
123
140
|
init_typed = false
|
|
124
141
|
init_forbid_extra = false
|
|
125
142
|
warn_untyped_fields = true
|
|
143
|
+
debug_dataclass_transform = false
|
|
126
144
|
```
|
|
127
145
|
|
|
128
146
|
## Error codes
|
|
@@ -25,7 +25,7 @@ Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
|
|
|
25
25
|
- Accept SQLAlchemy TypeEngine instances in `Field(sa_type=...)` (e.g. `DateTime(timezone=True)`, `String(50)`)
|
|
26
26
|
without `# type: ignore` in strict mode.
|
|
27
27
|
- Accept `model_config = ConfigDict(...)` overrides on SQLModel subclasses in strict mode.
|
|
28
|
-
- Compatible with `pydantic.mypy`
|
|
28
|
+
- Compatible with `pydantic.mypy` **when `sqlmodel_mypy.plugin` is listed first**.
|
|
29
29
|
|
|
30
30
|
## Install (dev)
|
|
31
31
|
|
|
@@ -50,17 +50,33 @@ make check
|
|
|
50
50
|
plugins = sqlmodel_mypy.plugin
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
If you also use `pydantic.mypy`,
|
|
53
|
+
If you also use `pydantic.mypy`, **`sqlmodel_mypy.plugin` must be listed first**:
|
|
54
54
|
|
|
55
55
|
```ini
|
|
56
56
|
[mypy]
|
|
57
57
|
plugins = sqlmodel_mypy.plugin, pydantic.mypy
|
|
58
|
-
# or:
|
|
59
|
-
# plugins = pydantic.mypy, sqlmodel_mypy.plugin
|
|
60
58
|
```
|
|
61
59
|
|
|
60
|
+
This is not a preference: **mypy picks the first plugin that claims a hook**. If `pydantic.mypy` runs first, it
|
|
61
|
+
can treat SQLModel classes as plain Pydantic models and you’ll get incorrect typing (e.g. missing `__table__`,
|
|
62
|
+
broken SQLAlchemy expression types, and incorrect/empty constructor signatures).
|
|
63
|
+
|
|
62
64
|
## Plugin configuration
|
|
63
65
|
|
|
66
|
+
All options are **boolean**.
|
|
67
|
+
|
|
68
|
+
Supported options:
|
|
69
|
+
|
|
70
|
+
- **`init_typed`** (default: `false`): Generate `__init__` / constructor signatures using the declared field types.
|
|
71
|
+
When `false`, the plugin uses `Any` for parameter types (but still enforces required/optional fields).
|
|
72
|
+
- **`init_forbid_extra`** (default: `false`): When `true`, do **not** add `**kwargs: Any` to generated
|
|
73
|
+
`__init__` / `model_construct` / constructor signatures, so mypy reports unexpected keyword arguments.
|
|
74
|
+
- **`warn_untyped_fields`** (default: `true`): When `true`, emit error code `sqlmodel-field` for untyped
|
|
75
|
+
declarations like `x = Field(...)` / `x = Relationship(...)` (use `x: T = ...` instead).
|
|
76
|
+
- **`debug_dataclass_transform`** (default: `false`): **Advanced/debug-only**. When `true`, keep SQLModel’s
|
|
77
|
+
`__dataclass_transform__` handling enabled in mypy (useful for debugging plugin interactions; not recommended
|
|
78
|
+
for normal use).
|
|
79
|
+
|
|
64
80
|
`mypy.ini`:
|
|
65
81
|
|
|
66
82
|
```ini
|
|
@@ -68,6 +84,7 @@ plugins = sqlmodel_mypy.plugin, pydantic.mypy
|
|
|
68
84
|
init_typed = false
|
|
69
85
|
init_forbid_extra = false
|
|
70
86
|
warn_untyped_fields = true
|
|
87
|
+
debug_dataclass_transform = false
|
|
71
88
|
```
|
|
72
89
|
|
|
73
90
|
`pyproject.toml`:
|
|
@@ -77,6 +94,7 @@ warn_untyped_fields = true
|
|
|
77
94
|
init_typed = false
|
|
78
95
|
init_forbid_extra = false
|
|
79
96
|
warn_untyped_fields = true
|
|
97
|
+
debug_dataclass_transform = false
|
|
80
98
|
```
|
|
81
99
|
|
|
82
100
|
## Error codes
|
|
@@ -5,8 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
import sys
|
|
6
6
|
from collections.abc import Callable, Iterator
|
|
7
7
|
from configparser import ConfigParser
|
|
8
|
-
from typing import Any, NamedTuple
|
|
8
|
+
from typing import Any, NamedTuple, cast
|
|
9
9
|
|
|
10
|
+
from mypy.errorcodes import ErrorCode
|
|
10
11
|
from mypy.nodes import (
|
|
11
12
|
ARG_NAMED,
|
|
12
13
|
ARG_NAMED_OPT,
|
|
@@ -19,7 +20,9 @@ from mypy.nodes import (
|
|
|
19
20
|
AssignmentStmt,
|
|
20
21
|
Block,
|
|
21
22
|
CallExpr,
|
|
23
|
+
Decorator,
|
|
22
24
|
Expression,
|
|
25
|
+
FuncDef,
|
|
23
26
|
IfStmt,
|
|
24
27
|
MemberExpr,
|
|
25
28
|
NameExpr,
|
|
@@ -58,6 +61,7 @@ from mypy.typevars import fill_typevars
|
|
|
58
61
|
|
|
59
62
|
from .transform import (
|
|
60
63
|
ERROR_FIELD,
|
|
64
|
+
METADATA_KEY,
|
|
61
65
|
SQLALCHEMY_MAPPED_FULLNAMES,
|
|
62
66
|
SQLMODEL_FIELD_FULLNAME,
|
|
63
67
|
SQLMODEL_RELATIONSHIP_FULLNAME,
|
|
@@ -71,6 +75,12 @@ CONFIGFILE_KEY = "sqlmodel-mypy"
|
|
|
71
75
|
SQLMODEL_BASEMODEL_FULLNAME = "sqlmodel.main.SQLModel"
|
|
72
76
|
SQLMODEL_METACLASS_FULLNAME = "sqlmodel.main.SQLModelMetaclass"
|
|
73
77
|
|
|
78
|
+
ERROR_PLUGIN_ORDER = ErrorCode(
|
|
79
|
+
"sqlmodel-plugin-order",
|
|
80
|
+
"SQLModel plugin order error",
|
|
81
|
+
"SQLModel",
|
|
82
|
+
)
|
|
83
|
+
|
|
74
84
|
SQLMODEL_COL_FULLNAME = "sqlmodel.sql.expression.col"
|
|
75
85
|
# SQLModel's `select()` is implemented in a generated module and re-exported.
|
|
76
86
|
SQLMODEL_SELECT_GEN_FULLNAME = "sqlmodel.sql._expression_select_gen.select"
|
|
@@ -134,7 +144,7 @@ SQLMODEL_SESSION_EXEC_FULLNAME = "sqlmodel.orm.session.Session.exec"
|
|
|
134
144
|
SQLMODEL_ASYNC_SESSION_EXEC_FULLNAME = "sqlmodel.ext.asyncio.session.AsyncSession.exec"
|
|
135
145
|
|
|
136
146
|
# Increment when plugin changes should invalidate mypy cache.
|
|
137
|
-
__version__ =
|
|
147
|
+
__version__ = 16
|
|
138
148
|
|
|
139
149
|
|
|
140
150
|
class _CollectedField(NamedTuple):
|
|
@@ -234,12 +244,114 @@ def _lookup_typeinfo(plugin: Plugin, fullname: str) -> TypeInfo | None:
|
|
|
234
244
|
return None
|
|
235
245
|
|
|
236
246
|
|
|
247
|
+
def _plugin_any() -> AnyType:
|
|
248
|
+
"""Return an Any that should not trigger `disallow_any_explicit`."""
|
|
249
|
+
return AnyType(TypeOfAny.implementation_artifact)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _named_type_or_none(api: Any, fullname: str) -> Instance | None:
|
|
253
|
+
"""Best-effort `api.named_type`/`api.named_generic_type` wrapper.
|
|
254
|
+
|
|
255
|
+
Prefer `named_type` (semantic analyzer) but fall back to `named_generic_type` (checker).
|
|
256
|
+
"""
|
|
257
|
+
try:
|
|
258
|
+
if hasattr(api, "named_type"):
|
|
259
|
+
return cast(Instance, api.named_type(fullname))
|
|
260
|
+
if hasattr(api, "named_generic_type"):
|
|
261
|
+
return cast(Instance, api.named_generic_type(fullname, []))
|
|
262
|
+
except Exception:
|
|
263
|
+
return None
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _named_generic_type_or_none(api: Any, fullname: str, args: list[Type]) -> Instance | None:
|
|
268
|
+
"""Best-effort wrapper for generic named types across mypy plugin APIs."""
|
|
269
|
+
try:
|
|
270
|
+
if hasattr(api, "named_generic_type"):
|
|
271
|
+
return cast(Instance, api.named_generic_type(fullname, args))
|
|
272
|
+
if hasattr(api, "named_type"):
|
|
273
|
+
return cast(Instance, api.named_type(fullname, args))
|
|
274
|
+
except Exception:
|
|
275
|
+
return None
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _plugins_from_config_file(config_file: str) -> list[str] | None:
|
|
280
|
+
"""Best-effort extraction of `[mypy] plugins = ...` from mypy config files."""
|
|
281
|
+
toml_config = parse_toml(config_file)
|
|
282
|
+
if toml_config is not None:
|
|
283
|
+
plugins = toml_config.get("tool", {}).get("mypy", {}).get("plugins")
|
|
284
|
+
if isinstance(plugins, str):
|
|
285
|
+
return [p.strip() for p in plugins.split(",") if p.strip()]
|
|
286
|
+
if isinstance(plugins, list) and all(isinstance(p, str) for p in plugins):
|
|
287
|
+
return plugins
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
parser = ConfigParser()
|
|
291
|
+
parser.read(config_file)
|
|
292
|
+
if not parser.has_section("mypy"):
|
|
293
|
+
return None
|
|
294
|
+
if not parser.has_option("mypy", "plugins"):
|
|
295
|
+
return None
|
|
296
|
+
raw = parser.get("mypy", "plugins")
|
|
297
|
+
return [p.strip() for p in raw.split(",") if p.strip()]
|
|
298
|
+
|
|
299
|
+
|
|
237
300
|
class SQLModelMypyPlugin(Plugin):
|
|
238
301
|
def __init__(self, options: Options) -> None:
|
|
239
302
|
self.plugin_config = SQLModelPluginConfig(options)
|
|
240
303
|
self._plugin_data = {"__version__": __version__, **self.plugin_config.to_data()}
|
|
304
|
+
self._plugin_order_ok = True
|
|
305
|
+
self._reported_plugin_order = False
|
|
306
|
+
self._warmed_sqlalchemy_typing = False
|
|
307
|
+
plugins: list[str] | None = None
|
|
308
|
+
if options.config_file is not None:
|
|
309
|
+
plugins = _plugins_from_config_file(options.config_file)
|
|
310
|
+
if plugins is None:
|
|
311
|
+
raw_plugins = getattr(options, "plugins", None)
|
|
312
|
+
if isinstance(raw_plugins, (list, tuple)):
|
|
313
|
+
plugins = list(raw_plugins)
|
|
314
|
+
if plugins is not None:
|
|
315
|
+
try:
|
|
316
|
+
sqlmodel_idx = plugins.index("sqlmodel_mypy.plugin")
|
|
317
|
+
pydantic_idx = plugins.index("pydantic.mypy")
|
|
318
|
+
except ValueError:
|
|
319
|
+
pass
|
|
320
|
+
else:
|
|
321
|
+
self._plugin_order_ok = sqlmodel_idx < pydantic_idx
|
|
241
322
|
super().__init__(options)
|
|
242
323
|
|
|
324
|
+
def _warm_sqlalchemy_typing(self, api: Any) -> None:
|
|
325
|
+
"""Best-effort preloading of SQLAlchemy typing symbols.
|
|
326
|
+
|
|
327
|
+
Some mypy plugin APIs only resolve types from already-loaded modules. Ensure
|
|
328
|
+
core SQLAlchemy types are loaded before type-checking hooks run.
|
|
329
|
+
"""
|
|
330
|
+
if self._warmed_sqlalchemy_typing:
|
|
331
|
+
return
|
|
332
|
+
self._warmed_sqlalchemy_typing = True
|
|
333
|
+
|
|
334
|
+
any_t = _plugin_any()
|
|
335
|
+
bool_t = api.named_type("builtins.bool")
|
|
336
|
+
for fullname, args in (
|
|
337
|
+
("sqlalchemy.orm.attributes.InstrumentedAttribute", [any_t]),
|
|
338
|
+
("sqlalchemy.orm.InstrumentedAttribute", [any_t]),
|
|
339
|
+
("sqlalchemy.orm.attributes.QueryableAttribute", [any_t]),
|
|
340
|
+
("sqlalchemy.orm.base.Mapped", [any_t]),
|
|
341
|
+
("sqlalchemy.orm.Mapped", [any_t]),
|
|
342
|
+
("sqlalchemy.sql.elements.ColumnElement", [bool_t]),
|
|
343
|
+
("sqlalchemy.sql.schema.Table", None),
|
|
344
|
+
("sqlalchemy.sql.schema.TableClause", None),
|
|
345
|
+
("sqlalchemy.sql.selectable.FromClause", None),
|
|
346
|
+
):
|
|
347
|
+
try:
|
|
348
|
+
if args is None:
|
|
349
|
+
api.named_type(fullname)
|
|
350
|
+
else:
|
|
351
|
+
api.named_type(fullname, args)
|
|
352
|
+
except Exception:
|
|
353
|
+
continue
|
|
354
|
+
|
|
243
355
|
def get_function_signature_hook(
|
|
244
356
|
self, fullname: str
|
|
245
357
|
) -> Callable[[FunctionSigContext], FunctionLike] | None:
|
|
@@ -331,7 +443,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
331
443
|
return None
|
|
332
444
|
|
|
333
445
|
if attr_name == "__table__":
|
|
334
|
-
return lambda
|
|
446
|
+
return lambda ctx: self._sqlalchemy_table_type(ctx.api)
|
|
335
447
|
|
|
336
448
|
if not self._declares_sqlmodel_member(owner_info, attr_name):
|
|
337
449
|
return None
|
|
@@ -352,6 +464,18 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
352
464
|
|
|
353
465
|
def _declares_sqlmodel_member(self, info: TypeInfo, name: str) -> bool:
|
|
354
466
|
"""Return True if `name` is declared as a field/relationship on `info`."""
|
|
467
|
+
# Preferred: metadata-driven. This avoids relying on class-body AST shape at hook time.
|
|
468
|
+
md = info.metadata.get(METADATA_KEY)
|
|
469
|
+
if isinstance(md, dict):
|
|
470
|
+
fields = md.get("fields")
|
|
471
|
+
if isinstance(fields, dict) and name in fields:
|
|
472
|
+
return True
|
|
473
|
+
rels = md.get("relationships")
|
|
474
|
+
if isinstance(rels, dict) and name in rels:
|
|
475
|
+
return True
|
|
476
|
+
if isinstance(rels, list) and name in rels:
|
|
477
|
+
return True
|
|
478
|
+
|
|
355
479
|
# Fast path: if it isn't even a Var on the class, it's not a model member.
|
|
356
480
|
sym = info.names.get(name)
|
|
357
481
|
if sym is None or sym.node is None or not isinstance(sym.node, Var) or sym.node.is_classvar:
|
|
@@ -509,6 +633,32 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
509
633
|
def _sqlmodel_constructor_signature_callback(
|
|
510
634
|
self, ctx: FunctionSigContext, info: TypeInfo
|
|
511
635
|
) -> FunctionLike:
|
|
636
|
+
# Preferred: derive the class-call signature from the plugin-generated `__init__`.
|
|
637
|
+
# This avoids re-scanning the class body AST at type-check time (which is brittle,
|
|
638
|
+
# especially with compiled mypy) and matches what users see in `reveal_type(Model.__init__)`.
|
|
639
|
+
init_sym = info.names.get("__init__")
|
|
640
|
+
if init_sym is not None and init_sym.plugin_generated:
|
|
641
|
+
init_node = init_sym.node
|
|
642
|
+
init_type: CallableType | None = None
|
|
643
|
+
if isinstance(init_node, Var):
|
|
644
|
+
if isinstance(init_node.type, CallableType):
|
|
645
|
+
init_type = init_node.type
|
|
646
|
+
elif isinstance(init_node, FuncDef):
|
|
647
|
+
if isinstance(init_node.type, CallableType):
|
|
648
|
+
init_type = init_node.type
|
|
649
|
+
elif isinstance(init_node, Decorator):
|
|
650
|
+
if isinstance(init_node.func.type, CallableType):
|
|
651
|
+
init_type = init_node.func.type
|
|
652
|
+
|
|
653
|
+
if init_type is not None and len(init_type.arg_types) >= 1:
|
|
654
|
+
return init_type.copy_modified(
|
|
655
|
+
arg_types=init_type.arg_types[1:],
|
|
656
|
+
arg_kinds=init_type.arg_kinds[1:],
|
|
657
|
+
arg_names=init_type.arg_names[1:],
|
|
658
|
+
ret_type=ctx.default_signature.ret_type,
|
|
659
|
+
fallback=ctx.default_signature.fallback,
|
|
660
|
+
)
|
|
661
|
+
|
|
512
662
|
fields, relationships = self._collect_members_for_signature(info, ctx.api)
|
|
513
663
|
|
|
514
664
|
arg_types: list[Type] = []
|
|
@@ -530,7 +680,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
530
680
|
t = f.type.accept(ForceInvariantTypeVars())
|
|
531
681
|
t = _unwrap_mapped_type(t)
|
|
532
682
|
else:
|
|
533
|
-
t =
|
|
683
|
+
t = _plugin_any()
|
|
534
684
|
field_aliases = [
|
|
535
685
|
alias for alias in f.aliases if alias != f.name and alias not in canonical_names
|
|
536
686
|
]
|
|
@@ -545,7 +695,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
545
695
|
t = rel.type.accept(ForceInvariantTypeVars())
|
|
546
696
|
t = _unwrap_mapped_type(t)
|
|
547
697
|
else:
|
|
548
|
-
t =
|
|
698
|
+
t = _plugin_any()
|
|
549
699
|
arg_types.append(t)
|
|
550
700
|
arg_names.append(rel.name)
|
|
551
701
|
arg_kinds.append(ARG_NAMED_OPT)
|
|
@@ -561,7 +711,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
561
711
|
arg_kinds.append(ARG_NAMED_OPT)
|
|
562
712
|
|
|
563
713
|
if not self.plugin_config.init_forbid_extra:
|
|
564
|
-
kw =
|
|
714
|
+
kw = _plugin_any()
|
|
565
715
|
arg_types.append(kw)
|
|
566
716
|
arg_names.append("kwargs")
|
|
567
717
|
arg_kinds.append(ARG_STAR2)
|
|
@@ -609,7 +759,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
609
759
|
if _contains_instance_fullname(old_sa_type, type_engine_info.fullname):
|
|
610
760
|
return default
|
|
611
761
|
|
|
612
|
-
any_t =
|
|
762
|
+
any_t = _plugin_any()
|
|
613
763
|
type_engine_args = [any_t] * len(type_engine_info.defn.type_vars)
|
|
614
764
|
type_engine_instance = Instance(type_engine_info, type_engine_args)
|
|
615
765
|
|
|
@@ -668,7 +818,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
668
818
|
if select_info is None:
|
|
669
819
|
return ctx.default_signature
|
|
670
820
|
|
|
671
|
-
any_t =
|
|
821
|
+
any_t = _plugin_any()
|
|
672
822
|
tuple_fallback = ctx.api.named_generic_type("builtins.tuple", [any_t])
|
|
673
823
|
row_type = TupleType([any_t] * positional_count, tuple_fallback)
|
|
674
824
|
ret_type = Instance(select_info, [row_type])
|
|
@@ -721,11 +871,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
721
871
|
|
|
722
872
|
field_params: list[tuple[Type, list[str]]] = []
|
|
723
873
|
for f in fields:
|
|
724
|
-
t = (
|
|
725
|
-
f.type.accept(ForceInvariantTypeVars())
|
|
726
|
-
if f.type is not None
|
|
727
|
-
else AnyType(TypeOfAny.explicit)
|
|
728
|
-
)
|
|
874
|
+
t = f.type.accept(ForceInvariantTypeVars()) if f.type is not None else _plugin_any()
|
|
729
875
|
t = _unwrap_mapped_type(t)
|
|
730
876
|
field_aliases = [
|
|
731
877
|
alias for alias in f.aliases if alias != f.name and alias not in canonical_names
|
|
@@ -740,7 +886,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
740
886
|
t = (
|
|
741
887
|
rel.type.accept(ForceInvariantTypeVars())
|
|
742
888
|
if rel.type is not None
|
|
743
|
-
else
|
|
889
|
+
else _plugin_any()
|
|
744
890
|
)
|
|
745
891
|
t = _unwrap_mapped_type(t)
|
|
746
892
|
arg_types.append(t)
|
|
@@ -758,7 +904,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
758
904
|
arg_kinds.append(ARG_NAMED_OPT)
|
|
759
905
|
|
|
760
906
|
if not self.plugin_config.init_forbid_extra:
|
|
761
|
-
kw =
|
|
907
|
+
kw = _plugin_any()
|
|
762
908
|
arg_types.append(kw)
|
|
763
909
|
arg_names.append("kwargs")
|
|
764
910
|
arg_kinds.append(ARG_STAR2)
|
|
@@ -902,7 +1048,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
902
1048
|
try:
|
|
903
1049
|
return ctx.api.named_generic_type("sqlalchemy.sql.elements.ColumnElement", [bool_t])
|
|
904
1050
|
except Exception:
|
|
905
|
-
return
|
|
1051
|
+
return _plugin_any()
|
|
906
1052
|
|
|
907
1053
|
def _sqlalchemy_select_join_like_return_type(
|
|
908
1054
|
self, ctx: MethodContext, *, target_positional_index: int, isouter_always: bool
|
|
@@ -971,13 +1117,24 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
971
1117
|
if proper.type.fullname in SQLALCHEMY_INSTRUMENTED_ATTRIBUTE_FULLNAMES:
|
|
972
1118
|
return default
|
|
973
1119
|
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1120
|
+
for fullname in (
|
|
1121
|
+
"sqlalchemy.orm.attributes.InstrumentedAttribute",
|
|
1122
|
+
"sqlalchemy.orm.InstrumentedAttribute",
|
|
1123
|
+
):
|
|
1124
|
+
info = _lookup_typeinfo(self, fullname)
|
|
1125
|
+
if info is not None:
|
|
1126
|
+
return Instance(info, [default])
|
|
979
1127
|
|
|
980
|
-
|
|
1128
|
+
# Fallback: if ORM attribute types aren't available, prefer a Core expression wrapper.
|
|
1129
|
+
# This still enables common operator methods like `.in_(...)` and `.like(...)`.
|
|
1130
|
+
for fullname in (
|
|
1131
|
+
"sqlalchemy.sql.elements.ColumnElement",
|
|
1132
|
+
"sqlalchemy.sql.expression.ColumnElement",
|
|
1133
|
+
):
|
|
1134
|
+
info = _lookup_typeinfo(self, fullname)
|
|
1135
|
+
if info is not None:
|
|
1136
|
+
return Instance(info, [default])
|
|
1137
|
+
return default
|
|
981
1138
|
|
|
982
1139
|
def _sqlmodel_col_return_type_callback(self, ctx: FunctionContext) -> Type:
|
|
983
1140
|
# `col()` is effectively a typed identity/cast helper.
|
|
@@ -996,13 +1153,11 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
996
1153
|
if value_type is None:
|
|
997
1154
|
return ctx.default_return_type
|
|
998
1155
|
|
|
999
|
-
|
|
1000
|
-
self,
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
return Instance(mapped_info, [value_type])
|
|
1156
|
+
for fullname in ("sqlalchemy.orm.base.Mapped", "sqlalchemy.orm.Mapped"):
|
|
1157
|
+
info = _lookup_typeinfo(self, fullname)
|
|
1158
|
+
if info is not None:
|
|
1159
|
+
return Instance(info, [value_type])
|
|
1160
|
+
return ctx.default_return_type
|
|
1006
1161
|
|
|
1007
1162
|
def _sqlmodel_model_class_callback(self, ctx: ClassDefContext) -> None:
|
|
1008
1163
|
# SQLModel is built on Pydantic v2; users commonly override `model_config`
|
|
@@ -1011,6 +1166,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1011
1166
|
# errors. Widen to a compatible supertype so `model_config = ConfigDict(...)`
|
|
1012
1167
|
# is accepted without `# type: ignore`.
|
|
1013
1168
|
self._widen_model_config_type(ctx)
|
|
1169
|
+
self._warm_sqlalchemy_typing(ctx.api)
|
|
1014
1170
|
transformer = SQLModelTransformer(ctx.cls, ctx.reason, ctx.api, self.plugin_config)
|
|
1015
1171
|
transformer.transform()
|
|
1016
1172
|
self._add_table_dunders(ctx)
|
|
@@ -1023,7 +1179,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1023
1179
|
if "__table__" in info.names:
|
|
1024
1180
|
return
|
|
1025
1181
|
|
|
1026
|
-
table_t = self._sqlalchemy_table_type()
|
|
1182
|
+
table_t = self._sqlalchemy_table_type(ctx.api)
|
|
1027
1183
|
v = Var("__table__", table_t)
|
|
1028
1184
|
v.info = info
|
|
1029
1185
|
v._fullname = f"{info.fullname}.__table__"
|
|
@@ -1032,7 +1188,7 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1032
1188
|
sym.plugin_generated = True
|
|
1033
1189
|
info.names["__table__"] = sym
|
|
1034
1190
|
|
|
1035
|
-
def _sqlalchemy_table_type(self) -> Type:
|
|
1191
|
+
def _sqlalchemy_table_type(self, api: Any | None = None) -> Type:
|
|
1036
1192
|
"""Return the best available `sqlalchemy` Table-ish type.
|
|
1037
1193
|
|
|
1038
1194
|
Prefer `sqlalchemy.sql.schema.Table`, but fall back to broader interfaces if the
|
|
@@ -1043,15 +1199,19 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1043
1199
|
"sqlalchemy.sql.schema.TableClause",
|
|
1044
1200
|
"sqlalchemy.sql.selectable.FromClause",
|
|
1045
1201
|
):
|
|
1202
|
+
if api is not None:
|
|
1203
|
+
inst = _named_type_or_none(api, fullname)
|
|
1204
|
+
if inst is not None:
|
|
1205
|
+
return inst
|
|
1046
1206
|
info = _lookup_typeinfo(self, fullname)
|
|
1047
1207
|
if info is not None:
|
|
1048
1208
|
return Instance(info, [])
|
|
1049
|
-
return
|
|
1209
|
+
return _plugin_any()
|
|
1050
1210
|
|
|
1051
1211
|
def _widen_model_config_type(self, ctx: ClassDefContext) -> None:
|
|
1052
1212
|
mapping_str_any = ctx.api.named_type(
|
|
1053
1213
|
"typing.Mapping",
|
|
1054
|
-
[ctx.api.named_type("builtins.str"),
|
|
1214
|
+
[ctx.api.named_type("builtins.str"), _plugin_any()],
|
|
1055
1215
|
)
|
|
1056
1216
|
|
|
1057
1217
|
# Widen the base SQLModel attribute (used as the expected type for overrides).
|
|
@@ -1072,6 +1232,15 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1072
1232
|
SQLModel decorates `SQLModelMetaclass` with `__dataclass_transform__`, but we want this plugin
|
|
1073
1233
|
(not mypy's generic dataclass-transform logic) to generate the model signatures.
|
|
1074
1234
|
"""
|
|
1235
|
+
if not self._plugin_order_ok and not self._reported_plugin_order:
|
|
1236
|
+
self._reported_plugin_order = True
|
|
1237
|
+
ctx.api.fail(
|
|
1238
|
+
"Plugin order matters: list 'sqlmodel_mypy.plugin' before 'pydantic.mypy' "
|
|
1239
|
+
"(see sqlmodel-mypy-plugin README). With 'pydantic.mypy' first, mypy can "
|
|
1240
|
+
"claim SQLModel classes as plain Pydantic models and SQLModel typing becomes incorrect.",
|
|
1241
|
+
ctx.cls,
|
|
1242
|
+
code=ERROR_PLUGIN_ORDER,
|
|
1243
|
+
)
|
|
1075
1244
|
if self.plugin_config.debug_dataclass_transform:
|
|
1076
1245
|
return
|
|
1077
1246
|
info_metaclass = ctx.cls.info.declared_metaclass
|