sqlmodel-mypy-plugin 0.12.0__tar.gz → 0.12.3__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 (81) hide show
  1. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/PKG-INFO +40 -8
  2. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/README.md +39 -7
  3. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/ROADMAP.md +88 -1
  4. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/pyproject.toml +1 -1
  5. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/src/sqlmodel_mypy/__init__.py +1 -1
  6. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/src/sqlmodel_mypy/plugin.py +404 -31
  7. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/src/sqlmodel_mypy/transform.py +90 -7
  8. sqlmodel_mypy_plugin-0.12.3/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +20 -0
  9. sqlmodel_mypy_plugin-0.12.3/tests/mypy/modules/expression_typing_strict.py +20 -0
  10. sqlmodel_mypy_plugin-0.12.3/tests/mypy/modules/getattr_column_property.py +45 -0
  11. sqlmodel_mypy_plugin-0.12.3/tests/mypy/modules/nullable_defaults.py +26 -0
  12. {sqlmodel_mypy_plugin-0.12.0/tests/mypy/outputs/mypy-plugin-pydantic-last_ini → sqlmodel_mypy_plugin-0.12.3/tests/mypy/outputs/mypy-plugin-pydantic-first_ini}/relationships.py +0 -1
  13. {sqlmodel_mypy_plugin-0.12.0/tests/mypy/outputs/mypy-plugin-pydantic-first_ini → sqlmodel_mypy_plugin-0.12.3/tests/mypy/outputs/mypy-plugin-pydantic-last_ini}/relationships.py +1 -1
  14. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +2 -2
  15. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +7 -7
  16. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +1 -1
  17. sqlmodel_mypy_plugin-0.12.3/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +27 -0
  18. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +2 -2
  19. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +4 -4
  20. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +3 -3
  21. sqlmodel_mypy_plugin-0.12.3/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +27 -0
  22. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +1 -1
  23. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +4 -4
  24. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/test_mypy.py +5 -0
  25. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/test_plugin_unit.py +418 -0
  26. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/test_transform_unit.py +94 -0
  27. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/.gitignore +0 -0
  28. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/.pre-commit-config.yaml +0 -0
  29. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/AGENTS.md +0 -0
  30. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/LICENSE +0 -0
  31. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/src/sqlmodel_mypy/py.typed +0 -0
  32. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/conftest.py +0 -0
  33. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
  34. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
  35. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
  36. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
  37. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/mypy-plugin.ini +0 -0
  38. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/configs/pyproject-plugin.toml +0 -0
  39. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/aliases.py +0 -0
  40. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/constructor.py +0 -0
  41. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/execution_options.py +0 -0
  42. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/expressions.py +0 -0
  43. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/field_sa_type.py +0 -0
  44. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/generics.py +0 -0
  45. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/inheritance.py +0 -0
  46. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/joins.py +0 -0
  47. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/label.py +0 -0
  48. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/many_to_many_relationships.py +0 -0
  49. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/mapped_fields.py +0 -0
  50. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/model_config.py +0 -0
  51. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/model_construct.py +0 -0
  52. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/relationship_comparators.py +0 -0
  53. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/relationship_expressions.py +0 -0
  54. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/relationships.py +0 -0
  55. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/select_varargs.py +0 -0
  56. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/selectinload.py +0 -0
  57. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/session_exec.py +0 -0
  58. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/session_get.py +0 -0
  59. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/table_dunders.py +0 -0
  60. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/table_model_awareness.py +0 -0
  61. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/modules/untyped_fields.py +0 -0
  62. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/execution_options.py +0 -0
  63. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
  64. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -0
  65. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
  66. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
  67. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
  68. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
  69. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
  70. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
  71. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/execution_options.py +0 -0
  72. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
  73. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -0
  74. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
  75. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
  76. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
  77. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
  78. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
  79. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
  80. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
  81. {sqlmodel_mypy_plugin-0.12.0 → sqlmodel_mypy_plugin-0.12.3}/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.3
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
@@ -150,6 +168,18 @@ You can also use SQLAlchemy table metadata attributes without `attr-defined` noi
150
168
  tbl = User.__table__
151
169
  ```
152
170
 
171
+ ### `getattr(Model, "field")` support
172
+
173
+ If you build query filters dynamically, `getattr(Model, "field")` with a **string literal** is typed the same as
174
+ direct `Model.field` access on `table=True` models:
175
+
176
+ ```py
177
+ stmt = select(User).where(getattr(User, "name").like("%x%"))
178
+ ```
179
+
180
+ Non-literal names (runtime strings) are intentionally left as `Any` (you’ll still need a cast or a different
181
+ pattern).
182
+
153
183
  ### Relationship comparator typing
154
184
 
155
185
  Relationship attributes declared via `Relationship(...)` are typed as SQLAlchemy expressions at class level,
@@ -163,9 +193,11 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
163
193
 
164
194
  ### Out of scope
165
195
 
166
- - **`column_property(...)` and similar SQLAlchemy helpers**: this plugin doesn’t try to “fix” the *declaration-site*
167
- typing for SQLAlchemy-only APIs that aren’t part of SQLModel’s surface area. Prefer SQLAlchemy-typed patterns like
168
- `Mapped[...]` / `InstrumentedAttribute[...]` (or local casts/`# type: ignore` if needed).
196
+ - **Most SQLAlchemy-only declaration helpers**: this plugin doesn’t try to “fix” the full *declaration-site* typing
197
+ for SQLAlchemy ORM APIs that aren’t part of SQLModel’s surface area.
198
+ - Exception: best-effort return typing for `column_property(...)` (infers `Mapped[T]` from `ScalarSelect[T]` /
199
+ `ColumnElement[T]`) to support patterns like `Model._foo = column_property(...)` combined with
200
+ `getattr(Model, "_foo")` in query builders.
169
201
  - **Pydantic `@computed_field`**: not touched by this plugin (it should type-check via normal property typing / Pydantic
170
202
  typing).
171
203
 
@@ -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
@@ -104,6 +122,18 @@ You can also use SQLAlchemy table metadata attributes without `attr-defined` noi
104
122
  tbl = User.__table__
105
123
  ```
106
124
 
125
+ ### `getattr(Model, "field")` support
126
+
127
+ If you build query filters dynamically, `getattr(Model, "field")` with a **string literal** is typed the same as
128
+ direct `Model.field` access on `table=True` models:
129
+
130
+ ```py
131
+ stmt = select(User).where(getattr(User, "name").like("%x%"))
132
+ ```
133
+
134
+ Non-literal names (runtime strings) are intentionally left as `Any` (you’ll still need a cast or a different
135
+ pattern).
136
+
107
137
  ### Relationship comparator typing
108
138
 
109
139
  Relationship attributes declared via `Relationship(...)` are typed as SQLAlchemy expressions at class level,
@@ -117,9 +147,11 @@ stmt = select(Team).where(Team.heroes.contains(hero_obj))
117
147
 
118
148
  ### Out of scope
119
149
 
120
- - **`column_property(...)` and similar SQLAlchemy helpers**: this plugin doesn’t try to “fix” the *declaration-site*
121
- typing for SQLAlchemy-only APIs that aren’t part of SQLModel’s surface area. Prefer SQLAlchemy-typed patterns like
122
- `Mapped[...]` / `InstrumentedAttribute[...]` (or local casts/`# type: ignore` if needed).
150
+ - **Most SQLAlchemy-only declaration helpers**: this plugin doesn’t try to “fix” the full *declaration-site* typing
151
+ for SQLAlchemy ORM APIs that aren’t part of SQLModel’s surface area.
152
+ - Exception: best-effort return typing for `column_property(...)` (infers `Mapped[T]` from `ScalarSelect[T]` /
153
+ `ColumnElement[T]`) to support patterns like `Model._foo = column_property(...)` combined with
154
+ `getattr(Model, "_foo")` in query builders.
123
155
  - **Pydantic `@computed_field`**: not touched by this plugin (it should type-check via normal property typing / Pydantic
124
156
  typing).
125
157
 
@@ -22,7 +22,12 @@
22
22
  - Ensure common SQLModel re-exports (`select`, `and_`, `or_`, `col`, etc.) preserve SQLAlchemy typing.
23
23
  - Decide approach: plugin hooks vs stub overlays vs relying on SQLAlchemy typing (documented trade-offs).
24
24
  - Compatibility matrix CI across mypy/SQLModel versions (and pin policy).
25
- - Avoid forcing users to order plugins (`sqlmodel_mypy.plugin` vs `pydantic.mypy`) for correct SQLModel resolution.
25
+ - **Plugin ordering guardrails (mypy limitation)**
26
+ - **Reality check**: mypy generally uses the **first plugin that claims a hook**, so true “order independence”
27
+ between `sqlmodel_mypy.plugin` and `pydantic.mypy` is not realistically achievable today.
28
+ - Enforce and clearly explain that `sqlmodel_mypy.plugin` must be listed **before** `pydantic.mypy`:
29
+ - Source: `src/sqlmodel_mypy/plugin.py` (`SQLModelMypyPlugin.__init__`, `_sqlmodel_metaclass_callback()`).
30
+ - User-facing docs: `README.md` (plugin ordering section).
26
31
 
27
32
  ## v0.4 (Docs-aligned relationship constructors) - DONE
28
33
 
@@ -200,3 +205,85 @@ models do provide this at runtime for `table=True`, but type checkers can miss i
200
205
  - **Tests**
201
206
  - Add mypy integration coverage for `Model.__table__` and `Session.get` / `AsyncSession.get` with SQLModel
202
207
  table models.
208
+
209
+ ## v0.12.4 (Dynamic query-builder helpers) - DONE
210
+
211
+ - Type `getattr(Model, "field")` like `Model.field` for `table=True` SQLModel models (string-literal names only).
212
+ - Best-effort typing for `column_property(...)` return type (infer `Mapped[T]`) to support
213
+ `Model._foo = column_property(...)` patterns.
214
+
215
+ ## v0.13 (SQLModel docs parity suite) - TODO
216
+
217
+ **Motivation**: the north star is “SQLModel as documented works under `mypy --strict` with minimal casts/ignores”.
218
+ We should lock this down with a dedicated docs-parity integration suite (beyond the current focused unit-style
219
+ mypy modules).
220
+
221
+ - **Add docs-derived mypy integration modules**
222
+ - Create minimal, type-focused modules under `tests/mypy/modules/` that mirror official docs patterns (as close
223
+ as possible, but stripped of runtime/demo noise).
224
+ - Add them to `tests/mypy/test_mypy.py` and commit golden outputs under `tests/mypy/outputs/**`.
225
+ - Target doc pages / patterns:
226
+ - Connected-data joins and outer-joins:
227
+ - Docs: `https://sqlmodel.tiangolo.com/tutorial/connect/read-connected-data/`
228
+ - Relationship attribute creation/update patterns (constructor kwargs + mutation):
229
+ - Docs: `https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/create-and-update-relationships/`
230
+ - Many-to-many relationship creation patterns:
231
+ - Docs: `https://sqlmodel.tiangolo.com/tutorial/many-to-many/create-data/`
232
+ - Update patterns using `sqlmodel_update()` / `model_dump(exclude_unset=True)`:
233
+ - Docs: `https://sqlmodel.tiangolo.com/tutorial/fastapi/update/`
234
+ - Source: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py` (`SQLModel.sqlmodel_update`)
235
+ - Ensure strict-mode expectations are explicit (via `# MYPY:` comments and/or `reveal_type(...)` assertions).
236
+
237
+ ## v0.14 (`typing.Annotated[...]` field metadata support) - TODO
238
+
239
+ **Motivation**: SQLModel (Pydantic v2) supports `Annotated`-driven typing patterns, and users will naturally try to
240
+ use them to reduce `= Field(...)` assignment noise. Under `--strict`, we should still generate correct constructor
241
+ signatures (required/optional + alias kwargs) when `Field(...)` metadata lives inside `Annotated[...]`.
242
+
243
+ - **Support `Annotated[T, Field(...)]` for constructor/signature generation**
244
+ - Examples we should handle (at least best-effort):
245
+ - `id: Annotated[int | None, Field(default=None, primary_key=True)]`
246
+ - `name: Annotated[str, Field(alias="full_name")]`
247
+ - The historical SQLModel edge-case: `Optional[Annotated[T, ...]]` (see upstream release notes).
248
+ - Implementation sketch:
249
+ - Extend semantic-phase collection in `src/sqlmodel_mypy/transform.py` to extract Field metadata from the
250
+ annotation when there is no `= Field(...)` call in the class body.
251
+ - Mirror the same logic in checker-time signature collection in `src/sqlmodel_mypy/plugin.py`
252
+ (`_collect_member_from_stmt`) so class-call signatures stay consistent with generated `__init__`.
253
+ - Source references:
254
+ - SQLModel `Field()` overloads + alias precedence: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py`
255
+ - `Annotated` handling in SQLModel’s type parsing: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py`
256
+ - Release note: “Fix support for types with `Optional[Annotated[x, f()]]`…” (PR #1093):
257
+ `https://github.com/fastapi/sqlmodel/pull/1093`
258
+
259
+ ## v0.15 (Outer-join `None` propagation for relationship-attribute joins) - TODO
260
+
261
+ **Motivation**: we currently propagate `None` on outer-joins only when the join target is a direct model class
262
+ (e.g. `.join(Team, isouter=True)`). A common ORM pattern is to join via relationship attributes
263
+ (e.g. `.join(Hero.team, isouter=True)`), and the result tuple should still reflect that the joined entity may be
264
+ `None`.
265
+
266
+ - **Expand join-target detection**
267
+ - Extend `src/sqlmodel_mypy/plugin.py` (`_sqlalchemy_select_join_like_return_type`) to also detect join targets
268
+ expressed as relationship attributes (e.g. `Hero.team`, `Team.heroes`) and map them to the target model type
269
+ for `None` propagation.
270
+ - References:
271
+ - SQLModel outer-join behavior (baseline example): `https://sqlmodel.tiangolo.com/tutorial/connect/read-connected-data/`
272
+ - SQLAlchemy relationship-attribute joins (`select(User).join(User.addresses)`): `https://docs.sqlalchemy.org/en/20/orm/queryguide/select.html#simple-relationship-joins`
273
+ - Add mypy integration coverage under `tests/mypy/modules/` for relationship-attribute joins, including the SQLModel
274
+ docs pattern of `Team: None` on outer-joins.
275
+
276
+ ## v0.16 (Strict typing ergonomics add-ons, opt-in) - TODO
277
+
278
+ **Motivation**: strict typing + SQLModel often means the database populates values “later” (after flush/commit/
279
+ refresh), especially primary keys (`id: int | None`). Mypy can’t infer those runtime effects, and users end up with
280
+ repeated `assert obj.id is not None` (type clutter).
281
+
282
+ - **Provide a tiny opt-in narrowing helper**
283
+ - Docs context (why `id` starts as `None` and is populated after commit/refresh):
284
+ - `https://sqlmodel.tiangolo.com/tutorial/automatic-id-none-refresh/`
285
+ - Ship a small helper in this package (runtime-free or near-zero runtime) that helps users narrow “persisted” models
286
+ without `cast(...)`/`# type: ignore`, e.g.:
287
+ - `assert_persisted(obj)` / `require_id(obj)` / `ensure_persisted(obj)` returning a `TypeGuard[...]` or using
288
+ overloads to narrow `id` to `int`.
289
+ - Keep it optional and well-documented; default plugin behavior should remain conservative.
@@ -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.3"
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.3"