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.
Files changed (80) hide show
  1. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/PKG-INFO +23 -5
  2. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/README.md +22 -4
  3. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/pyproject.toml +1 -1
  4. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/src/sqlmodel_mypy/__init__.py +1 -1
  5. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/src/sqlmodel_mypy/plugin.py +202 -33
  6. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/src/sqlmodel_mypy/transform.py +90 -7
  7. sqlmodel_mypy_plugin-0.12.2/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +20 -0
  8. sqlmodel_mypy_plugin-0.12.2/tests/mypy/modules/expression_typing_strict.py +20 -0
  9. sqlmodel_mypy_plugin-0.12.2/tests/mypy/modules/nullable_defaults.py +26 -0
  10. {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
  11. {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
  12. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +2 -2
  13. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +7 -7
  14. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +1 -1
  15. sqlmodel_mypy_plugin-0.12.2/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +27 -0
  16. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +2 -2
  17. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +4 -4
  18. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +3 -3
  19. sqlmodel_mypy_plugin-0.12.2/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +27 -0
  20. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +1 -1
  21. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +4 -4
  22. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/test_mypy.py +3 -0
  23. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/test_plugin_unit.py +125 -0
  24. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/test_transform_unit.py +94 -0
  25. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/.gitignore +0 -0
  26. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/.pre-commit-config.yaml +0 -0
  27. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/AGENTS.md +0 -0
  28. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/LICENSE +0 -0
  29. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/ROADMAP.md +0 -0
  30. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/src/sqlmodel_mypy/py.typed +0 -0
  31. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/conftest.py +0 -0
  32. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
  33. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
  34. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
  35. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
  36. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/mypy-plugin.ini +0 -0
  37. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/configs/pyproject-plugin.toml +0 -0
  38. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/aliases.py +0 -0
  39. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/constructor.py +0 -0
  40. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/execution_options.py +0 -0
  41. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/expressions.py +0 -0
  42. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/field_sa_type.py +0 -0
  43. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/generics.py +0 -0
  44. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/inheritance.py +0 -0
  45. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/joins.py +0 -0
  46. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/label.py +0 -0
  47. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/many_to_many_relationships.py +0 -0
  48. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/mapped_fields.py +0 -0
  49. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/model_config.py +0 -0
  50. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/model_construct.py +0 -0
  51. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/relationship_comparators.py +0 -0
  52. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/relationship_expressions.py +0 -0
  53. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/relationships.py +0 -0
  54. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/select_varargs.py +0 -0
  55. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/selectinload.py +0 -0
  56. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/session_exec.py +0 -0
  57. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/session_get.py +0 -0
  58. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/table_dunders.py +0 -0
  59. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/table_model_awareness.py +0 -0
  60. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/modules/untyped_fields.py +0 -0
  61. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/execution_options.py +0 -0
  62. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
  63. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -0
  64. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
  65. {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
  66. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
  67. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
  68. {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
  69. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
  70. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/execution_options.py +0 -0
  71. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
  72. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -0
  73. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
  74. {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
  75. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
  76. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
  77. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
  78. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
  79. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.2}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
  80. {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.0
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` in either plugin order.
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`, you can list plugins in either order:
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` in either plugin order.
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`, you can list plugins in either order:
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlmodel-mypy-plugin"
7
- version = "0.12.0"
7
+ version = "0.12.2"
8
8
  description = "Mypy plugin for SQLModel (better constructor/Field typing)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,3 +1,3 @@
1
1
  __all__ = ["__version__"]
2
2
 
3
- __version__ = "0.12.0"
3
+ __version__ = "0.12.2"
@@ -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__ = 14
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 _ctx: self._sqlalchemy_table_type()
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 = AnyType(TypeOfAny.explicit)
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 = AnyType(TypeOfAny.explicit)
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 = AnyType(TypeOfAny.explicit)
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 = AnyType(TypeOfAny.explicit)
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 = AnyType(TypeOfAny.explicit)
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 AnyType(TypeOfAny.explicit)
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 = AnyType(TypeOfAny.explicit)
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 AnyType(TypeOfAny.explicit)
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
- inst_attr_info = _lookup_typeinfo(
975
- self, "sqlalchemy.orm.attributes.InstrumentedAttribute"
976
- ) or _lookup_typeinfo(self, "sqlalchemy.orm.InstrumentedAttribute")
977
- if inst_attr_info is None:
978
- return default
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
- return Instance(inst_attr_info, [default])
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
- mapped_info = _lookup_typeinfo(self, "sqlalchemy.orm.base.Mapped") or _lookup_typeinfo(
1000
- self, "sqlalchemy.orm.Mapped"
1001
- )
1002
- if mapped_info is None:
1003
- return ctx.default_return_type
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 AnyType(TypeOfAny.explicit)
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"), AnyType(TypeOfAny.explicit)],
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