sqlmodel-mypy-plugin 0.19.0__tar.gz → 0.19.1__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 (109) hide show
  1. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/PKG-INFO +1 -3
  2. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/README.md +0 -2
  3. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/pyproject.toml +1 -1
  4. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/src/sqlmodel_mypy/__init__.py +1 -1
  5. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/src/sqlmodel_mypy/plugin.py +1 -29
  6. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/src/sqlmodel_mypy/transform.py +22 -4
  7. {sqlmodel_mypy_plugin-0.19.0/tests/mypy/outputs/mypy-plugin_ini → sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin-strict_ini}/aliased_joins.py +1 -0
  8. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_read_connected_data.py +1 -1
  9. {sqlmodel_mypy_plugin-0.19.0/tests/mypy/outputs/mypy-plugin_ini → sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin-strict_ini}/execution_options.py +1 -0
  10. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +3 -0
  11. sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin-strict_ini/session_exec.py +43 -0
  12. {sqlmodel_mypy_plugin-0.19.0/tests/mypy/outputs/mypy-plugin-strict_ini → sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin_ini}/aliased_joins.py +1 -0
  13. {sqlmodel_mypy_plugin-0.19.0/tests/mypy/outputs/mypy-plugin-strict_ini → sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin_ini}/execution_options.py +1 -0
  14. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/joins.py +3 -0
  15. sqlmodel_mypy_plugin-0.19.1/tests/mypy/outputs/mypy-plugin_ini/session_exec.py +43 -0
  16. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/test_transform_unit.py +52 -1
  17. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/.gitignore +0 -0
  18. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/.pre-commit-config.yaml +0 -0
  19. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/AGENTS.md +0 -0
  20. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/LICENSE +0 -0
  21. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/src/sqlmodel_mypy/persisted.py +0 -0
  22. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/src/sqlmodel_mypy/py.typed +0 -0
  23. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/conftest.py +0 -0
  24. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
  25. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
  26. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
  27. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +0 -0
  28. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-strict-typed-execute.ini +0 -0
  29. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
  30. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin.ini +0 -0
  31. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/pyproject-plugin.toml +0 -0
  32. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/aliased_joins.py +0 -0
  33. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/aliases.py +0 -0
  34. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/annotated_field_metadata.py +0 -0
  35. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/constructor.py +0 -0
  36. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/docs_parity_many_to_many_create_data.py +0 -0
  37. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/docs_parity_read_connected_data.py +0 -0
  38. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/docs_parity_relationship_attributes.py +0 -0
  39. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/docs_parity_update_patterns.py +0 -0
  40. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/execution_options.py +0 -0
  41. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/expression_typing_strict.py +0 -0
  42. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/expressions.py +0 -0
  43. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/field_sa_type.py +0 -0
  44. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/field_validation_alias.py +0 -0
  45. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/generics.py +0 -0
  46. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/getattr_column_property.py +0 -0
  47. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/inheritance.py +0 -0
  48. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/joins.py +0 -0
  49. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/label.py +0 -0
  50. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/many_to_many_relationships.py +0 -0
  51. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/mapped_fields.py +0 -0
  52. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/model_config.py +0 -0
  53. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/model_construct.py +0 -0
  54. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/nullable_defaults.py +0 -0
  55. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/persisted_helpers.py +0 -0
  56. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/relationship_comparators.py +0 -0
  57. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/relationship_expressions.py +0 -0
  58. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/relationships.py +0 -0
  59. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/select_varargs.py +0 -0
  60. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/selectinload.py +0 -0
  61. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/session_exec.py +0 -0
  62. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/session_execute_typed.py +0 -0
  63. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/session_get.py +0 -0
  64. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/table_dunders.py +0 -0
  65. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/table_model_awareness.py +0 -0
  66. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/table_via_model_config.py +0 -0
  67. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/tuple_expression.py +0 -0
  68. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/untyped_fields.py +0 -0
  69. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-pydantic-first_ini/relationships.py +0 -0
  70. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-pydantic-last_ini/relationships.py +0 -0
  71. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict-typed-execute_ini/session_execute_typed.py +0 -0
  72. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +0 -0
  73. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/annotated_field_metadata.py +0 -0
  74. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +0 -0
  75. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_many_to_many_create_data.py +0 -0
  76. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_relationship_attributes.py +0 -0
  77. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_update_patterns.py +0 -0
  78. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
  79. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
  80. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
  81. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +0 -0
  82. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
  83. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +0 -0
  84. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/persisted_helpers.py +0 -0
  85. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
  86. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +0 -0
  87. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/select_varargs.py +0 -0
  88. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
  89. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/table_via_model_config.py +0 -0
  90. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/tuple_expression.py +0 -0
  91. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
  92. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +0 -0
  93. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
  94. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +0 -0
  95. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
  96. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
  97. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
  98. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +0 -0
  99. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
  100. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
  101. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +0 -0
  102. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/select_varargs.py +0 -0
  103. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
  104. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
  105. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +0 -0
  106. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/test_mypy.py +0 -0
  107. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/smoke_test.py +0 -0
  108. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/test_persisted_helpers.py +0 -0
  109. {sqlmodel_mypy_plugin-0.19.0 → sqlmodel_mypy_plugin-0.19.1}/tests/test_plugin_unit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlmodel-mypy-plugin
3
- Version: 0.19.0
3
+ Version: 0.19.1
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
@@ -52,8 +52,6 @@ Mypy plugin that improves type checking for [`SQLModel`](https://github.com/fast
52
52
 
53
53
  Early/experimental. Implemented scope (supported today):
54
54
 
55
- Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
56
-
57
55
  - Generate correct `__init__` / `model_construct` signatures for SQLModel models:
58
56
  - treat `sqlmodel.Field(...)` required/optional correctly
59
57
  - accept `sqlmodel.Relationship(...)` kwargs for `table=True` models
@@ -6,8 +6,6 @@ Mypy plugin that improves type checking for [`SQLModel`](https://github.com/fast
6
6
 
7
7
  Early/experimental. Implemented scope (supported today):
8
8
 
9
- Planned work is tracked in [`ROADMAP.md`](ROADMAP.md).
10
-
11
9
  - Generate correct `__init__` / `model_construct` signatures for SQLModel models:
12
10
  - treat `sqlmodel.Field(...)` required/optional correctly
13
11
  - accept `sqlmodel.Relationship(...)` kwargs for `table=True` models
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlmodel-mypy-plugin"
7
- version = "0.19.0"
7
+ version = "0.19.1"
8
8
  description = "Mypy plugin for SQLModel (better constructor/Field typing)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,4 +1,4 @@
1
- __version__ = "0.19.0"
1
+ __version__ = "0.19.1"
2
2
 
3
3
  from .persisted import HasId, HasOptionalId, has_id, require_id
4
4
 
@@ -171,7 +171,7 @@ SQLALCHEMY_INSPECT_FULLNAMES = {
171
171
  }
172
172
 
173
173
  # Increment when plugin changes should invalidate mypy cache.
174
- __version__ = 25
174
+ __version__ = 26
175
175
 
176
176
 
177
177
  class _CollectedField(NamedTuple):
@@ -1275,34 +1275,6 @@ class SQLModelMypyPlugin(Plugin):
1275
1275
  return any(_contains_instance_fullname(item, fullname) for item in proper.items)
1276
1276
  return False
1277
1277
 
1278
- # Accept SQLAlchemy `Select[...]` in `Session.exec(...)`.
1279
- #
1280
- # SQLModel defines its own `Select[...]` wrapper class and types `Session.exec` to accept it,
1281
- # but users often import and use SQLAlchemy's `select()` directly. Runtime supports it, and
1282
- # with this adjustment we preserve a correctly-shaped `TupleResult[...]` return type.
1283
- sqlmodel_select_info = _lookup_typeinfo(self, SQLMODEL_SELECT_CLS_FULLNAME)
1284
- sqlalchemy_select_info = _lookup_typeinfo(self, SQLALCHEMY_SELECT_FULLNAME)
1285
- if sqlmodel_select_info is not None and sqlalchemy_select_info is not None:
1286
- if _contains_instance_fullname(default.arg_types[0], sqlmodel_select_info.fullname):
1287
- # Idempotency: if upstream stubs already accept SQLAlchemy Select, keep unchanged.
1288
- if not _contains_instance_fullname(
1289
- default.arg_types[0], sqlalchemy_select_info.fullname
1290
- ):
1291
- proper_arg0 = get_proper_type(default.arg_types[0])
1292
- if isinstance(proper_arg0, Instance) and proper_arg0.args:
1293
- row_type = proper_arg0.args[0]
1294
- else:
1295
- row_type = _plugin_any()
1296
- sqlalchemy_select = Instance(
1297
- sqlalchemy_select_info,
1298
- [row_type] * len(sqlalchemy_select_info.defn.type_vars),
1299
- )
1300
- new_arg_types = list(default.arg_types)
1301
- new_arg_types[0] = UnionType.make_union(
1302
- [default.arg_types[0], sqlalchemy_select]
1303
- )
1304
- return default.copy_modified(arg_types=new_arg_types)
1305
-
1306
1278
  # Only adjust the broad overload; keep Select/SelectOfScalar overloads precise.
1307
1279
  broad = False
1308
1280
  if sqlmodel_executable_info is not None and _contains_instance_fullname(
@@ -1198,11 +1198,25 @@ def add_method(
1198
1198
  """
1199
1199
  info = cls.info
1200
1200
 
1201
- # Remove previously generated methods.
1201
+ def _remove_stmt(stmt: Statement) -> None:
1202
+ # `list.remove(...)` raises ValueError if not present; ignore.
1203
+ try:
1204
+ cls.defs.body.remove(stmt)
1205
+ except ValueError:
1206
+ return
1207
+
1208
+ # Remove previously generated methods (handle both FuncDef and Decorator nodes).
1202
1209
  if name in info.names:
1203
1210
  sym = info.names[name]
1204
- if sym.plugin_generated and isinstance(sym.node, FuncDef):
1205
- cls.defs.body.remove(sym.node)
1211
+ if sym.plugin_generated:
1212
+ node = sym.node
1213
+ if isinstance(node, Decorator):
1214
+ # NOTE: older buggy versions appended the underlying FuncDef instead of the
1215
+ # Decorator; remove both to be safe.
1216
+ _remove_stmt(node)
1217
+ _remove_stmt(node.func)
1218
+ elif isinstance(node, FuncDef):
1219
+ _remove_stmt(node)
1206
1220
 
1207
1221
  # Keep existing definition for semantic analysis by renaming it.
1208
1222
  if name in info.names:
@@ -1248,9 +1262,13 @@ def add_method(
1248
1262
  dec = Decorator(func, [NameExpr("classmethod")], v)
1249
1263
  dec.line = info.line
1250
1264
  sym = SymbolTableNode(MDEF, dec)
1265
+ # NOTE: keep the class body node as a plain FuncDef (this matches the
1266
+ # previous plugin behavior and avoids override-check churn).
1267
+ body_node: Statement = func
1251
1268
  else:
1252
1269
  sym = SymbolTableNode(MDEF, func)
1270
+ body_node = func
1253
1271
 
1254
1272
  sym.plugin_generated = True
1255
1273
  info.names[name] = sym
1256
- info.defn.defs.body.append(func)
1274
+ info.defn.defs.body.append(body_node)
@@ -28,5 +28,6 @@ with Session(engine) as session:
28
28
  ok_team: Team | None = team
29
29
 
30
30
  bad_team: Team = team
31
+ # MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
31
32
  if team is not None:
32
33
  ok_team_name: str = team.name
@@ -37,7 +37,6 @@ with Session(engine) as session:
37
37
  stmt_heroes_only = select(Hero).join(Team).where(Team.name == "Preventers")
38
38
  for hero3 in session.exec(stmt_heroes_only):
39
39
  ok_hero3: Hero = hero3
40
- # MYPY: error: Incompatible types in assignment (expression has type "tuple[Hero]", variable has type "Hero") [assignment]
41
40
 
42
41
  # Docs pattern: LEFT OUTER join via isouter=True.
43
42
  stmt_outer = select(Hero, Team).join(Team, isouter=True)
@@ -47,3 +46,4 @@ with Session(engine) as session:
47
46
 
48
47
  # Outer join may yield `None` for the right-hand entity.
49
48
  bad_team4: Team = team4
49
+ # MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
@@ -30,3 +30,4 @@ with Session(engine) as session:
30
30
 
31
31
  # Outer join may yield `None` for the right-hand entity.
32
32
  bad_team: Team = team
33
+ # MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
@@ -32,6 +32,7 @@ with Session(engine) as session:
32
32
 
33
33
  # Outer join may yield `None` for the right-hand entity.
34
34
  bad_team: Team = team2
35
+ # MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
35
36
  if team2 is not None:
36
37
  ok_team_name2: str = team2.name
37
38
 
@@ -41,6 +42,7 @@ with Session(engine) as session:
41
42
  ok_hero3: Hero = hero3
42
43
  ok_team3: Team | None = team3
43
44
  bad_team3: Team = team3
45
+ # MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
44
46
 
45
47
  # One-to-many join via relationship attribute target should also propagate `None`.
46
48
  stmt_outer_rel_many = select(Team, Hero).join(Team.heroes, isouter=True)
@@ -48,5 +50,6 @@ with Session(engine) as session:
48
50
  ok_team4: Team = team4
49
51
  ok_hero4: Hero | None = hero4
50
52
  bad_hero4: Hero = hero4
53
+ # MYPY: error: Incompatible types in assignment (expression has type "Hero | None", variable has type "Hero") [assignment]
51
54
  if hero4 is not None:
52
55
  ok_hero4_name: str = hero4.name
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from sqlalchemy import select, text
6
+ from sqlalchemy.engine.cursor import CursorResult
7
+ from sqlalchemy.engine.result import Result, TupleResult
8
+ from sqlmodel import Field, Session, SQLModel, create_engine, delete
9
+ from sqlmodel.ext.asyncio.session import AsyncSession
10
+
11
+
12
+ class Hero(SQLModel, table=True):
13
+ id: int | None = Field(default=None, primary_key=True)
14
+ name: str = Field()
15
+
16
+
17
+ engine = create_engine("sqlite://")
18
+
19
+ with Session(engine) as session:
20
+ # accept SQLAlchemy (non-SQLModel) Executable statements, e.g. text(...)
21
+ result = session.exec(text("SELECT 1"))
22
+ ok_result: Result[Any] = result
23
+
24
+ # accept SQLAlchemy `select(...)` statements (commonly used with SQLModel)
25
+ select_result = session.exec(select(Hero))
26
+ _ok_select_result: TupleResult[tuple[Hero]] = select_result
27
+ # MYPY: error: Incompatible types in assignment (expression has type "CursorResult[Any]", variable has type "TupleResult[tuple[Hero]]") [assignment]
28
+
29
+ # Keep update/delete statements working (select correct overload).
30
+ delete_result = session.exec(delete(Hero))
31
+ ok_delete_result: CursorResult[Any] = delete_result
32
+
33
+
34
+ async def async_exec(session: AsyncSession) -> None:
35
+ result2 = await session.exec(text("SELECT 1"))
36
+ _ok_result2: Result[Any] = result2
37
+
38
+ select_result2 = await session.exec(select(Hero))
39
+ _ok_select_result2: TupleResult[tuple[Hero]] = select_result2
40
+ # MYPY: error: Incompatible types in assignment (expression has type "CursorResult[Any]", variable has type "TupleResult[tuple[Hero]]") [assignment]
41
+
42
+ delete_result2 = await session.exec(delete(Hero))
43
+ _ok_delete_result2: CursorResult[Any] = delete_result2
@@ -28,5 +28,6 @@ with Session(engine) as session:
28
28
  ok_team: Team | None = team
29
29
 
30
30
  bad_team: Team = team
31
+ # MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
31
32
  if team is not None:
32
33
  ok_team_name: str = team.name
@@ -30,3 +30,4 @@ with Session(engine) as session:
30
30
 
31
31
  # Outer join may yield `None` for the right-hand entity.
32
32
  bad_team: Team = team
33
+ # MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
@@ -32,6 +32,7 @@ with Session(engine) as session:
32
32
 
33
33
  # Outer join may yield `None` for the right-hand entity.
34
34
  bad_team: Team = team2
35
+ # MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
35
36
  if team2 is not None:
36
37
  ok_team_name2: str = team2.name
37
38
 
@@ -41,6 +42,7 @@ with Session(engine) as session:
41
42
  ok_hero3: Hero = hero3
42
43
  ok_team3: Team | None = team3
43
44
  bad_team3: Team = team3
45
+ # MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
44
46
 
45
47
  # One-to-many join via relationship attribute target should also propagate `None`.
46
48
  stmt_outer_rel_many = select(Team, Hero).join(Team.heroes, isouter=True)
@@ -48,5 +50,6 @@ with Session(engine) as session:
48
50
  ok_team4: Team = team4
49
51
  ok_hero4: Hero | None = hero4
50
52
  bad_hero4: Hero = hero4
53
+ # MYPY: error: Incompatible types in assignment (expression has type "Hero | None", variable has type "Hero") [assignment]
51
54
  if hero4 is not None:
52
55
  ok_hero4_name: str = hero4.name
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from sqlalchemy import select, text
6
+ from sqlalchemy.engine.cursor import CursorResult
7
+ from sqlalchemy.engine.result import Result, TupleResult
8
+ from sqlmodel import Field, Session, SQLModel, create_engine, delete
9
+ from sqlmodel.ext.asyncio.session import AsyncSession
10
+
11
+
12
+ class Hero(SQLModel, table=True):
13
+ id: int | None = Field(default=None, primary_key=True)
14
+ name: str = Field()
15
+
16
+
17
+ engine = create_engine("sqlite://")
18
+
19
+ with Session(engine) as session:
20
+ # accept SQLAlchemy (non-SQLModel) Executable statements, e.g. text(...)
21
+ result = session.exec(text("SELECT 1"))
22
+ ok_result: Result[Any] = result
23
+
24
+ # accept SQLAlchemy `select(...)` statements (commonly used with SQLModel)
25
+ select_result = session.exec(select(Hero))
26
+ _ok_select_result: TupleResult[tuple[Hero]] = select_result
27
+ # MYPY: error: Incompatible types in assignment (expression has type "CursorResult[Any]", variable has type "TupleResult[tuple[Hero]]") [assignment]
28
+
29
+ # Keep update/delete statements working (select correct overload).
30
+ delete_result = session.exec(delete(Hero))
31
+ ok_delete_result: CursorResult[Any] = delete_result
32
+
33
+
34
+ async def async_exec(session: AsyncSession) -> None:
35
+ result2 = await session.exec(text("SELECT 1"))
36
+ _ok_result2: Result[Any] = result2
37
+
38
+ select_result2 = await session.exec(select(Hero))
39
+ _ok_select_result2: TupleResult[tuple[Hero]] = select_result2
40
+ # MYPY: error: Incompatible types in assignment (expression has type "CursorResult[Any]", variable has type "TupleResult[tuple[Hero]]") [assignment]
41
+
42
+ delete_result2 = await session.exec(delete(Hero))
43
+ _ok_delete_result2: CursorResult[Any] = delete_result2
@@ -657,7 +657,58 @@ def test_add_method_generates_and_replaces_methods() -> None:
657
657
  api, cls, "cm", args=[arg], return_type=AnyType(TypeOfAny.explicit), is_classmethod=True
658
658
  )
659
659
  assert "cm" in cls.info.names
660
- assert isinstance(cls.info.names["cm"].node, Decorator)
660
+ cm_node = cls.info.names["cm"].node
661
+ assert isinstance(cm_node, Decorator)
662
+ assert cm_node.func.is_class is True
663
+ assert cm_node not in cls.defs.body
664
+ assert cm_node.func in cls.defs.body
665
+
666
+
667
+ def test_add_method_classmethod_reentry_is_idempotent_and_handles_legacy_body() -> None:
668
+ api = DummyAPI()
669
+ cls = make_sqlmodel_class()
670
+
671
+ any_t = AnyType(TypeOfAny.explicit)
672
+ arg = Argument(Var("x", any_t), any_t, None, ARG_NAMED)
673
+
674
+ # First generation
675
+ add_method(
676
+ api, cls, "cm", args=[arg], return_type=AnyType(TypeOfAny.explicit), is_classmethod=True
677
+ )
678
+ node1 = cls.info.names["cm"].node
679
+ assert isinstance(node1, Decorator)
680
+ assert node1 not in cls.defs.body
681
+ assert node1.func in cls.defs.body
682
+ assert node1.func.is_class is True
683
+
684
+ # Re-entry: previously generated Decorator should be removed, no duplicates.
685
+ add_method(
686
+ api, cls, "cm", args=[arg], return_type=AnyType(TypeOfAny.explicit), is_classmethod=True
687
+ )
688
+ node2 = cls.info.names["cm"].node
689
+ assert isinstance(node2, Decorator)
690
+ assert node2 not in cls.defs.body
691
+ assert node2.func in cls.defs.body
692
+ assert node1.func not in cls.defs.body
693
+ assert node2.func.is_class is True
694
+ assert sum(1 for s in cls.defs.body if isinstance(s, FuncDef) and s.name == "cm") == 1
695
+
696
+ # Simulate legacy buggy state: body contains both Decorator and FuncDef.
697
+ cls.defs.body.append(node2)
698
+ assert node2 in cls.defs.body
699
+ assert node2.func in cls.defs.body
700
+
701
+ add_method(
702
+ api, cls, "cm", args=[arg], return_type=AnyType(TypeOfAny.explicit), is_classmethod=True
703
+ )
704
+ node3 = cls.info.names["cm"].node
705
+ assert isinstance(node3, Decorator)
706
+ assert node3 not in cls.defs.body
707
+ assert node3.func in cls.defs.body
708
+ assert node2 not in cls.defs.body
709
+ assert node2.func not in cls.defs.body
710
+ assert node3.func.is_class is True
711
+ assert sum(1 for s in cls.defs.body if isinstance(s, FuncDef) and s.name == "cm") == 1
661
712
 
662
713
 
663
714
  def test_transform_defer_paths() -> None: