sqlmodel-mypy-plugin 0.20.0__tar.gz → 0.21.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 (127) hide show
  1. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/PKG-INFO +1 -1
  2. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/pyproject.toml +4 -1
  3. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/src/sqlmodel_mypy/plugin.py +33 -13
  4. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/src/sqlmodel_mypy/transform.py +3 -2
  5. sqlmodel_mypy_plugin-0.21.2/tests/mypy/configs/mypy-plugin-recursive.ini +10 -0
  6. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/execution_options.py +1 -2
  7. sqlmodel_mypy_plugin-0.21.2/tests/mypy/modules/recursive_aliases.py +35 -0
  8. sqlmodel_mypy_plugin-0.21.2/tests/mypy/modules/recursive_aliases_pep695.py +26 -0
  9. sqlmodel_mypy_plugin-0.21.2/tests/mypy/modules/row_generics.py +41 -0
  10. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/select_varargs.py +1 -2
  11. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/session_execute_typed.py +12 -2
  12. sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin-recursive_ini/recursive_aliases_pep695.py +30 -0
  13. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict-typed-execute_ini/session_execute_typed.py +12 -2
  14. {sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin_ini → sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin-strict_ini}/execution_options.py +1 -2
  15. sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin-strict_ini/recursive_aliases.py +39 -0
  16. sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin-strict_ini/select_varargs.py +35 -0
  17. {sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin-strict_ini → sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin_ini}/execution_options.py +1 -2
  18. sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/mypy-plugin_ini/select_varargs.py +35 -0
  19. sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin-strict-typed-execute_ini/session_execute_typed.py +48 -0
  20. sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin-strict_ini/row_generics.py +47 -0
  21. {sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin_ini → sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin-strict_ini}/select_varargs.py +2 -3
  22. sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin-strict_ini/session_exec.py +43 -0
  23. {sqlmodel_mypy_plugin-0.20.0/tests/mypy/outputs/mypy-plugin-strict_ini → sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin_ini}/select_varargs.py +2 -3
  24. sqlmodel_mypy_plugin-0.21.2/tests/mypy/outputs/sqlalchemy-2.1/mypy-plugin_ini/session_exec.py +43 -0
  25. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/test_mypy.py +36 -5
  26. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/test_plugin_unit.py +37 -10
  27. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/test_transform_unit.py +33 -1
  28. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/.gitignore +0 -0
  29. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/.pre-commit-config.yaml +0 -0
  30. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/AGENTS.md +0 -0
  31. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/LICENSE +0 -0
  32. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/README.md +0 -0
  33. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/src/sqlmodel_mypy/__init__.py +0 -0
  34. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/src/sqlmodel_mypy/persisted.py +0 -0
  35. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/src/sqlmodel_mypy/py.typed +0 -0
  36. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/conftest.py +0 -0
  37. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
  38. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
  39. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
  40. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +0 -0
  41. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-strict-typed-execute.ini +0 -0
  42. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
  43. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/mypy-plugin.ini +0 -0
  44. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/configs/pyproject-plugin.toml +0 -0
  45. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/aliased_joins.py +0 -0
  46. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/aliases.py +0 -0
  47. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/annotated_field_metadata.py +0 -0
  48. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/constructor.py +0 -0
  49. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/docs_parity_many_to_many_create_data.py +0 -0
  50. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/docs_parity_read_connected_data.py +0 -0
  51. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/docs_parity_relationship_attributes.py +0 -0
  52. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/docs_parity_update_patterns.py +0 -0
  53. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/expression_typing_strict.py +0 -0
  54. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/expressions.py +0 -0
  55. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/field_sa_type.py +0 -0
  56. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/field_validation_alias.py +0 -0
  57. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/generics.py +0 -0
  58. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/getattr_column_property.py +0 -0
  59. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/inheritance.py +0 -0
  60. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/joins.py +0 -0
  61. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/label.py +0 -0
  62. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/many_to_many_relationships.py +0 -0
  63. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/mapped_fields.py +0 -0
  64. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/model_config.py +0 -0
  65. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/model_construct.py +0 -0
  66. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/nullable_defaults.py +0 -0
  67. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/persisted_helpers.py +0 -0
  68. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/relationship_comparators.py +0 -0
  69. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/relationship_expressions.py +0 -0
  70. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/relationships.py +0 -0
  71. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/selectinload.py +0 -0
  72. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/sequence_name_collision.py +0 -0
  73. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/session_exec.py +0 -0
  74. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/session_get.py +0 -0
  75. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/sqlmodel_reexported_sequence.py +0 -0
  76. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/table_dunders.py +0 -0
  77. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/table_model_awareness.py +0 -0
  78. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/table_via_model_config.py +0 -0
  79. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/tuple_expression.py +0 -0
  80. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/modules/untyped_fields.py +0 -0
  81. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-pydantic-first_ini/relationships.py +0 -0
  82. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-pydantic-last_ini/relationships.py +0 -0
  83. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/aliased_joins.py +0 -0
  84. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +0 -0
  85. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/annotated_field_metadata.py +0 -0
  86. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +0 -0
  87. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_many_to_many_create_data.py +0 -0
  88. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_read_connected_data.py +0 -0
  89. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_relationship_attributes.py +0 -0
  90. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_update_patterns.py +0 -0
  91. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
  92. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -0
  93. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
  94. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
  95. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +0 -0
  96. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
  97. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +0 -0
  98. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/persisted_helpers.py +0 -0
  99. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
  100. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +0 -0
  101. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/sequence_name_collision.py +0 -0
  102. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/session_exec.py +0 -0
  103. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/sqlmodel_reexported_sequence.py +0 -0
  104. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
  105. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/table_via_model_config.py +0 -0
  106. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin-strict_ini/tuple_expression.py +0 -0
  107. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/aliased_joins.py +0 -0
  108. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
  109. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +0 -0
  110. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
  111. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +0 -0
  112. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -0
  113. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
  114. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
  115. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
  116. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +0 -0
  117. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
  118. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
  119. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +0 -0
  120. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/sequence_name_collision.py +0 -0
  121. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/session_exec.py +0 -0
  122. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/sqlmodel_reexported_sequence.py +0 -0
  123. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
  124. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
  125. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +0 -0
  126. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/smoke_test.py +0 -0
  127. {sqlmodel_mypy_plugin-0.20.0 → sqlmodel_mypy_plugin-0.21.2}/tests/test_persisted_helpers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sqlmodel-mypy-plugin
3
- Version: 0.20.0
3
+ Version: 0.21.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlmodel-mypy-plugin"
7
- version = "0.20.0"
7
+ version = "0.21.2"
8
8
  description = "Mypy plugin for SQLModel (better constructor/Field typing)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -64,6 +64,9 @@ line-length = 100
64
64
  target-version = "py310"
65
65
  exclude = ["tests/mypy/outputs"]
66
66
 
67
+ [tool.ruff.per-file-target-version]
68
+ "tests/mypy/modules/recursive_aliases_pep695.py" = "py312"
69
+
67
70
  [tool.ruff.lint]
68
71
  select = [
69
72
  "E", # pycodestyle errors
@@ -58,6 +58,7 @@ from mypy.types import (
58
58
  TypeOfAny,
59
59
  TypeType,
60
60
  UnionType,
61
+ UnpackType,
61
62
  get_proper_type,
62
63
  )
63
64
  from mypy.typevars import fill_typevars
@@ -171,7 +172,7 @@ SQLALCHEMY_INSPECT_FULLNAMES = {
171
172
  }
172
173
 
173
174
  # Increment when plugin changes should invalidate mypy cache.
174
- __version__ = 27
175
+ __version__ = 29
175
176
 
176
177
 
177
178
  class _CollectedField(NamedTuple):
@@ -926,7 +927,7 @@ class SQLModelMypyPlugin(Plugin):
926
927
 
927
928
  SQLModel's generated stubs traditionally cap `select()` overloads at 4 entities.
928
929
  For 5+ entities, fall back to a conservative signature that accepts the call and
929
- returns `Select[tuple[Any, ...]]` (represented as a fixed-length tuple of `Any`s).
930
+ returns a Select with one unknown value per selected entity.
930
931
  """
931
932
  call = ctx.context
932
933
  if not isinstance(call, CallExpr):
@@ -977,7 +978,7 @@ class SQLModelMypyPlugin(Plugin):
977
978
  any_t = _plugin_any()
978
979
  tuple_fallback = ctx.api.named_generic_type("builtins.tuple", [any_t])
979
980
  row_type = TupleType([any_t] * positional_count, tuple_fallback)
980
- ret_type = Instance(select_info, [row_type])
981
+ ret_type = self._instance_with_row_type(select_info, row_type)
981
982
 
982
983
  fallback = default_items[0].fallback if default_items else tuple_fallback
983
984
  arg_types = [any_t] * positional_count
@@ -990,7 +991,7 @@ class SQLModelMypyPlugin(Plugin):
990
991
 
991
992
  Our signature hook for 5+ args deliberately uses `Any` to avoid "no overload matches"
992
993
  errors. Here, once mypy has inferred argument expression types, we recover a more precise
993
- `Select[tuple[...]]` element type for the common cases (models + column expressions).
994
+ row element types for the common cases (models + column expressions).
994
995
  """
995
996
  call = ctx.context
996
997
  if not isinstance(call, CallExpr):
@@ -1012,7 +1013,7 @@ class SQLModelMypyPlugin(Plugin):
1012
1013
  proper_default = get_proper_type(default)
1013
1014
  if not isinstance(proper_default, Instance) or not proper_default.args:
1014
1015
  return default
1015
- proper_row = get_proper_type(proper_default.args[0])
1016
+ proper_row = get_proper_type(self._instance_row_type(proper_default, ctx.api))
1016
1017
  if not isinstance(proper_row, TupleType):
1017
1018
  return default
1018
1019
 
@@ -1036,7 +1037,7 @@ class SQLModelMypyPlugin(Plugin):
1036
1037
  any_t = _plugin_any()
1037
1038
  tuple_fallback = ctx.api.named_generic_type("builtins.tuple", [any_t])
1038
1039
  row_type = TupleType(items, tuple_fallback)
1039
- return Instance(proper_default.type, [row_type])
1040
+ return self._instance_with_row_type(proper_default.type, row_type)
1040
1041
 
1041
1042
  def _sqlmodel_select_entity_result_type(self, typ: Type) -> Type:
1042
1043
  """Return the value type produced by an entity passed to `select(...)`.
@@ -1054,6 +1055,9 @@ class SQLModelMypyPlugin(Plugin):
1054
1055
  return UnionType.make_union(
1055
1056
  [self._sqlmodel_select_entity_result_type(item) for item in proper.items]
1056
1057
  )
1058
+ if isinstance(proper, FunctionLike) and proper.is_type_obj():
1059
+ # Mypy represents a model class expression by its constructor overloads.
1060
+ return UnionType.make_union([item.ret_type for item in proper.items])
1057
1061
  if isinstance(proper, TypeType):
1058
1062
  item = get_proper_type(proper.item)
1059
1063
  if isinstance(item, Instance):
@@ -1371,6 +1375,23 @@ class SQLModelMypyPlugin(Plugin):
1371
1375
  coro = _named_generic_type_or_none(ctx.api, "typing.Coroutine", [any_t, any_t, result_type])
1372
1376
  return coro or default
1373
1377
 
1378
+ @staticmethod
1379
+ def _instance_row_type(instance: Instance, api: Any) -> Type:
1380
+ """Read a row from SQLAlchemy 2.0's tuple or 2.1's variadic arguments."""
1381
+ if instance.type.has_type_var_tuple_type:
1382
+ tuple_fallback = api.named_generic_type("builtins.tuple", [_plugin_any()])
1383
+ return TupleType(list(instance.args), tuple_fallback)
1384
+ return instance.args[0]
1385
+
1386
+ @staticmethod
1387
+ def _instance_with_row_type(info: TypeInfo, row_type: Type) -> Instance:
1388
+ """Use the installed class's generic shape, without guessing package versions."""
1389
+ if info.has_type_var_tuple_type:
1390
+ proper_row = get_proper_type(row_type)
1391
+ args = proper_row.items if isinstance(proper_row, TupleType) else [UnpackType(row_type)]
1392
+ return Instance(info, args)
1393
+ return Instance(info, [row_type])
1394
+
1374
1395
  def _row_tuple_type_from_typed_statement(self, statement: Type, api: Any) -> Type | None:
1375
1396
  """Infer the row tuple type produced by a SQL statement (best-effort)."""
1376
1397
  proper = get_proper_type(statement)
@@ -1385,12 +1406,12 @@ class SQLModelMypyPlugin(Plugin):
1385
1406
  tuple_fallback = api.named_generic_type("builtins.tuple", [any_t])
1386
1407
  return TupleType([scalar_t], tuple_fallback)
1387
1408
 
1388
- # SQLModel Select[T] and SQLAlchemy Select[_TP] both store the row parameter in the first type argument.
1409
+ # SQLAlchemy 2.1 and SQLModel 0.0.48 use variadic row parameters.
1389
1410
  if proper.type.fullname in {
1390
1411
  SQLMODEL_SELECT_CLS_FULLNAME,
1391
1412
  SQLALCHEMY_SELECT_FULLNAME,
1392
1413
  } or proper.type.has_base(SQLALCHEMY_SELECT_FULLNAME):
1393
- return proper.args[0]
1414
+ return self._instance_row_type(proper, api)
1394
1415
 
1395
1416
  return None
1396
1417
 
@@ -1399,11 +1420,10 @@ class SQLModelMypyPlugin(Plugin):
1399
1420
  for fullname in ("sqlalchemy.engine.Result", "sqlalchemy.engine.result.Result"):
1400
1421
  inst = _named_generic_type_or_none(api, fullname, [row_tuple_type])
1401
1422
  if inst is not None:
1402
- return inst
1423
+ return self._instance_with_row_type(inst.type, row_tuple_type)
1403
1424
  info = _lookup_typeinfo(self, fullname)
1404
1425
  if info is not None:
1405
- args = [row_tuple_type] * len(info.defn.type_vars)
1406
- return Instance(info, args)
1426
+ return self._instance_with_row_type(info, row_tuple_type)
1407
1427
  return None
1408
1428
 
1409
1429
  def _sqlalchemy_select_join_return_type_callback(self, ctx: MethodContext) -> Type:
@@ -1661,7 +1681,7 @@ class SQLModelMypyPlugin(Plugin):
1661
1681
  if not receiver.args:
1662
1682
  return ctx.default_return_type
1663
1683
 
1664
- tp = get_proper_type(receiver.args[0])
1684
+ tp = get_proper_type(self._instance_row_type(receiver, ctx.api))
1665
1685
  if not isinstance(tp, TupleType):
1666
1686
  return ctx.default_return_type
1667
1687
 
@@ -1682,7 +1702,7 @@ class SQLModelMypyPlugin(Plugin):
1682
1702
  return ctx.default_return_type
1683
1703
 
1684
1704
  new_tp = tp.copy_modified(items=new_items)
1685
- return Instance(receiver.type, [new_tp])
1705
+ return self._instance_with_row_type(receiver.type, new_tp)
1686
1706
 
1687
1707
  def _sqlmodel_class_attr_type_callback(self, ctx: AttributeContext) -> Type:
1688
1708
  if ctx.is_lvalue:
@@ -448,8 +448,9 @@ class ForceInvariantTypeVars(TypeTranslator):
448
448
  return modified
449
449
 
450
450
  def visit_type_alias_type(self, t: TypeAliasType) -> Type: # noqa: D102
451
- # Expand type aliases and then continue translation.
452
- return get_proper_type(t).accept(self)
451
+ # Alias targets can refer back to themselves. Preserve their graph and
452
+ # translate only the supplied arguments, like mypy's ExpandTypeVisitor.
453
+ return t.copy_modified(args=[arg.accept(self) for arg in t.args])
453
454
 
454
455
 
455
456
  class SQLModelField:
@@ -0,0 +1,10 @@
1
+ [mypy]
2
+ plugins = sqlmodel_mypy.plugin
3
+ follow_imports = silent
4
+ strict = true
5
+ python_version = 3.12
6
+
7
+ [sqlmodel-mypy]
8
+ init_forbid_extra = true
9
+ init_typed = true
10
+ warn_untyped_fields = true
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from sqlmodel import Field, Session, SQLModel, create_engine, select
4
- from sqlmodel.sql._expression_select_cls import Select as SQLModelSelect
5
4
 
6
5
 
7
6
  class Team(SQLModel, table=True):
@@ -21,7 +20,7 @@ stmt = select(Hero, Team).join(Team, isouter=True).execution_options(populate_ex
21
20
 
22
21
  # `execution_options()` should preserve the `Select[...]` generic, including outer-join `None`
23
22
  # propagation for the joined entity.
24
- ok_stmt: SQLModelSelect[tuple[Hero, Team | None]] = stmt
23
+ ok_stmt = stmt
25
24
 
26
25
  with Session(engine) as session:
27
26
  for hero, team in session.exec(stmt):
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Generic, TypeAlias, TypeVar
4
+
5
+ from sqlmodel import SQLModel
6
+
7
+ JsonObject: TypeAlias = list["JsonObject"]
8
+ JsonValue: TypeAlias = str | int | None | list["JsonValue"] | dict[str, "JsonValue"]
9
+
10
+
11
+ class RecursiveTable(SQLModel, table=True):
12
+ field: JsonObject
13
+
14
+
15
+ class Payload(SQLModel):
16
+ value: JsonValue
17
+
18
+
19
+ RecursiveTable(field=[[], [[]]])
20
+ RecursiveTable(field="invalid")
21
+ RecursiveTable.model_construct(field=[[]])
22
+ RecursiveTable.model_construct(field=123)
23
+ Payload(value={"nested": ["text", 1, None]})
24
+ Payload(value=object())
25
+
26
+ T_co = TypeVar("T_co", covariant=True)
27
+ Values: TypeAlias = list[T_co]
28
+
29
+
30
+ class Box(SQLModel, Generic[T_co]):
31
+ values: Values[T_co]
32
+
33
+
34
+ Box[int](values=[1])
35
+ Box[int](values=["invalid"])
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlmodel import SQLModel
4
+
5
+ type JsonObject = list[JsonObject]
6
+ type JsonValue = str | int | None | list[JsonValue] | dict[str, JsonValue]
7
+ type Left = list[Right]
8
+ type Right = dict[str, Left]
9
+
10
+
11
+ class RecursiveTable(SQLModel, table=True):
12
+ field: JsonObject
13
+
14
+
15
+ class Payload(SQLModel):
16
+ value: JsonValue
17
+ mutual: Left
18
+
19
+
20
+ RecursiveTable(field=[[], [[]]])
21
+ RecursiveTable(field="invalid")
22
+ RecursiveTable.model_construct(field=[[]])
23
+ RecursiveTable.model_construct(field=123)
24
+ Payload(value={"nested": ["text", 1, None]}, mutual=[{"child": []}])
25
+ Payload(value=object(), mutual=[])
26
+ Payload(value=None, mutual=[123])
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy import select as sa_select
4
+ from sqlmodel import Field, Session, SQLModel, create_engine, select
5
+
6
+
7
+ class Team(SQLModel, table=True):
8
+ id: int | None = Field(default=None, primary_key=True)
9
+ name: str
10
+
11
+
12
+ class Hero(SQLModel, table=True):
13
+ id: int | None = Field(default=None, primary_key=True)
14
+ name: str
15
+ team_id: int | None = Field(default=None, foreign_key="team.id")
16
+
17
+
18
+ engine = create_engine("sqlite://")
19
+ # The 5+ entity fallback and outer-join nullability must compose.
20
+ statement = select(Hero, Team, Hero.id, Team.id, Hero.name).outerjoin(Team)
21
+ with Session(engine) as session:
22
+ for hero, team, hero_id, _team_id, name in session.exec(statement):
23
+ ok_hero: Hero = hero
24
+ ok_team: Team | None = team
25
+ ok_id: int | None = hero_id
26
+ ok_name: str = name
27
+ bad_team: Team = team
28
+ bad_id: int = hero_id
29
+ bad_name: int = name
30
+
31
+ # A SQLModel select must preserve column arity when executed on a Core connection.
32
+ with engine.connect() as connection:
33
+ for hero_id2, name2, _team_id2, _team_name2, _repeated_id in connection.execute(
34
+ select(Hero.id, Hero.name, Hero.team_id, Team.name, Hero.id)
35
+ ):
36
+ ok_name2: str = name2
37
+ bad_id2: int = hero_id2
38
+ bad_name2: int = name2
39
+ for hero3, team3 in connection.execute(sa_select(Hero, Team).outerjoin(Team)):
40
+ ok_hero3: Hero = hero3
41
+ bad_team3: Team = team3
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from sqlmodel import Field, Session, SQLModel, create_engine, select
4
- from sqlmodel.sql._expression_select_cls import Select as SQLModelSelect
5
4
 
6
5
 
7
6
  class Team(SQLModel, table=True):
@@ -19,7 +18,7 @@ class Hero(SQLModel, table=True):
19
18
  stmt = select(Hero, Team, Hero.id, Team.id, Hero.name)
20
19
 
21
20
  # Prefer returning SQLModel's own `Select[...]` wrapper, with best-effort element typing.
22
- ok_stmt: SQLModelSelect[tuple[Hero, Team, int | None, int | None, str]] = stmt
21
+ ok_stmt = stmt
23
22
 
24
23
  engine = create_engine("sqlite://")
25
24
 
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from sqlalchemy.engine.result import Result
4
3
  from sqlmodel import Field, Session, SQLModel, create_engine, select
5
4
  from sqlmodel.ext.asyncio.session import AsyncSession
6
5
 
@@ -16,7 +15,9 @@ with Session(engine) as session:
16
15
  result = session.execute(select(Hero))
17
16
 
18
17
  # When `typed_execute=true`, `execute(select(Model))` should not degrade to `Result[Any]`.
19
- ok_result: Result[tuple[Hero]] = result
18
+ row = result.one()
19
+ ok_row_hero: Hero = row[0]
20
+ bad_row_bool: bool = row[0]
20
21
 
21
22
  ok_hero: Hero = result.scalars().one()
22
23
  bad_bool: bool = result.scalars().one()
@@ -30,3 +31,12 @@ async def async_execute(session: AsyncSession) -> None:
30
31
  result3 = await session.execute(select(Hero))
31
32
  _ok_hero2: Hero = result3.scalars().one()
32
33
  _bad_bool2: bool = result3.scalars().one()
34
+
35
+
36
+ async def async_execute_columns(session: AsyncSession) -> None:
37
+ result = await session.execute(select(Hero.id, Hero.name))
38
+ for hero_id, hero_name in result:
39
+ _ok_id: int | None = hero_id
40
+ _ok_name: str = hero_name
41
+ _bad_id: int = hero_id
42
+ _bad_name: int = hero_name
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlmodel import SQLModel
4
+
5
+ type JsonObject = list[JsonObject]
6
+ type JsonValue = str | int | None | list[JsonValue] | dict[str, JsonValue]
7
+ type Left = list[Right]
8
+ type Right = dict[str, Left]
9
+
10
+
11
+ class RecursiveTable(SQLModel, table=True):
12
+ field: JsonObject
13
+
14
+
15
+ class Payload(SQLModel):
16
+ value: JsonValue
17
+ mutual: Left
18
+
19
+
20
+ RecursiveTable(field=[[], [[]]])
21
+ RecursiveTable(field="invalid")
22
+ # MYPY: error: Argument "field" to "__init__" of "RecursiveTable" has incompatible type "str"; expected "JsonObject" [arg-type]
23
+ RecursiveTable.model_construct(field=[[]])
24
+ RecursiveTable.model_construct(field=123)
25
+ # MYPY: error: Argument "field" to "model_construct" of "RecursiveTable" has incompatible type "int"; expected "JsonObject" [arg-type]
26
+ Payload(value={"nested": ["text", 1, None]}, mutual=[{"child": []}])
27
+ Payload(value=object(), mutual=[])
28
+ # MYPY: error: Argument "value" to "__init__" of "Payload" has incompatible type "object"; expected "JsonValue" [arg-type]
29
+ Payload(value=None, mutual=[123])
30
+ # MYPY: error: List item 0 has incompatible type "int"; expected "dict[str, Left]" [list-item]
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from sqlalchemy.engine.result import Result
4
3
  from sqlmodel import Field, Session, SQLModel, create_engine, select
5
4
  from sqlmodel.ext.asyncio.session import AsyncSession
6
5
 
@@ -16,7 +15,9 @@ with Session(engine) as session:
16
15
  result = session.execute(select(Hero))
17
16
 
18
17
  # When `typed_execute=true`, `execute(select(Model))` should not degrade to `Result[Any]`.
19
- ok_result: Result[tuple[Hero]] = result
18
+ row = result.one()
19
+ ok_row_hero: Hero = row[0]
20
+ bad_row_bool: bool = row[0]
20
21
 
21
22
  ok_hero: Hero = result.scalars().one()
22
23
  bad_bool: bool = result.scalars().one()
@@ -33,3 +34,12 @@ async def async_execute(session: AsyncSession) -> None:
33
34
  _ok_hero2: Hero = result3.scalars().one()
34
35
  _bad_bool2: bool = result3.scalars().one()
35
36
  # MYPY: error: Incompatible types in assignment (expression has type "Hero", variable has type "bool") [assignment]
37
+
38
+
39
+ async def async_execute_columns(session: AsyncSession) -> None:
40
+ result = await session.execute(select(Hero.id, Hero.name))
41
+ for hero_id, hero_name in result:
42
+ _ok_id: int | None = hero_id
43
+ _ok_name: str = hero_name
44
+ _bad_id: int = hero_id
45
+ _bad_name: int = hero_name
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from sqlmodel import Field, Session, SQLModel, create_engine, select
4
- from sqlmodel.sql._expression_select_cls import Select as SQLModelSelect
5
4
 
6
5
 
7
6
  class Team(SQLModel, table=True):
@@ -21,7 +20,7 @@ stmt = select(Hero, Team).join(Team, isouter=True).execution_options(populate_ex
21
20
 
22
21
  # `execution_options()` should preserve the `Select[...]` generic, including outer-join `None`
23
22
  # propagation for the joined entity.
24
- ok_stmt: SQLModelSelect[tuple[Hero, Team | None]] = stmt
23
+ ok_stmt = stmt
25
24
 
26
25
  with Session(engine) as session:
27
26
  for hero, team in session.exec(stmt):
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Generic, TypeAlias, TypeVar
4
+
5
+ from sqlmodel import SQLModel
6
+
7
+ JsonObject: TypeAlias = list["JsonObject"]
8
+ JsonValue: TypeAlias = str | int | None | list["JsonValue"] | dict[str, "JsonValue"]
9
+
10
+
11
+ class RecursiveTable(SQLModel, table=True):
12
+ field: JsonObject
13
+
14
+
15
+ class Payload(SQLModel):
16
+ value: JsonValue
17
+
18
+
19
+ RecursiveTable(field=[[], [[]]])
20
+ RecursiveTable(field="invalid")
21
+ # MYPY: error: Argument "field" to "__init__" of "RecursiveTable" has incompatible type "str"; expected "JsonObject" [arg-type]
22
+ RecursiveTable.model_construct(field=[[]])
23
+ RecursiveTable.model_construct(field=123)
24
+ # MYPY: error: Argument "field" to "model_construct" of "RecursiveTable" has incompatible type "int"; expected "JsonObject" [arg-type]
25
+ Payload(value={"nested": ["text", 1, None]})
26
+ Payload(value=object())
27
+ # MYPY: error: Argument "value" to "__init__" of "Payload" has incompatible type "object"; expected "JsonValue" [arg-type]
28
+
29
+ T_co = TypeVar("T_co", covariant=True)
30
+ Values: TypeAlias = list[T_co]
31
+
32
+
33
+ class Box(SQLModel, Generic[T_co]):
34
+ values: Values[T_co]
35
+
36
+
37
+ Box[int](values=[1])
38
+ Box[int](values=["invalid"])
39
+ # MYPY: error: List item 0 has incompatible type "str"; expected "int" [list-item]
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlmodel import Field, Session, SQLModel, create_engine, select
4
+
5
+
6
+ class Team(SQLModel, table=True):
7
+ id: int | None = Field(default=None, primary_key=True)
8
+ name: str = Field()
9
+
10
+
11
+ class Hero(SQLModel, table=True):
12
+ id: int | None = Field(default=None, primary_key=True)
13
+ name: str = Field()
14
+ team_id: int | None = Field(default=None, foreign_key="team.id")
15
+
16
+
17
+ # SQLModel's generated `select()` overloads cap at 4 entities; ensure 5+ works.
18
+ stmt = select(Hero, Team, Hero.id, Team.id, Hero.name)
19
+
20
+ # Prefer returning SQLModel's own `Select[...]` wrapper, with best-effort element typing.
21
+ ok_stmt = stmt
22
+
23
+ engine = create_engine("sqlite://")
24
+
25
+ with Session(engine) as session:
26
+ for hero, team, hero_id, team_id, hero_name in session.exec(stmt):
27
+ ok_hero: Hero = hero
28
+ ok_team: Team = team
29
+ ok_team_id: int | None = team_id
30
+ ok_hero_name: str = hero_name
31
+
32
+ # `Hero.id` is optional at the type level.
33
+ bad_hero_id: int = hero_id
34
+ if hero_id is not None:
35
+ ok_hero_id: int = hero_id
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from sqlmodel import Field, Session, SQLModel, create_engine, select
4
- from sqlmodel.sql._expression_select_cls import Select as SQLModelSelect
5
4
 
6
5
 
7
6
  class Team(SQLModel, table=True):
@@ -21,7 +20,7 @@ stmt = select(Hero, Team).join(Team, isouter=True).execution_options(populate_ex
21
20
 
22
21
  # `execution_options()` should preserve the `Select[...]` generic, including outer-join `None`
23
22
  # propagation for the joined entity.
24
- ok_stmt: SQLModelSelect[tuple[Hero, Team | None]] = stmt
23
+ ok_stmt = stmt
25
24
 
26
25
  with Session(engine) as session:
27
26
  for hero, team in session.exec(stmt):
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlmodel import Field, Session, SQLModel, create_engine, select
4
+
5
+
6
+ class Team(SQLModel, table=True):
7
+ id: int | None = Field(default=None, primary_key=True)
8
+ name: str = Field()
9
+
10
+
11
+ class Hero(SQLModel, table=True):
12
+ id: int | None = Field(default=None, primary_key=True)
13
+ name: str = Field()
14
+ team_id: int | None = Field(default=None, foreign_key="team.id")
15
+
16
+
17
+ # SQLModel's generated `select()` overloads cap at 4 entities; ensure 5+ works.
18
+ stmt = select(Hero, Team, Hero.id, Team.id, Hero.name)
19
+
20
+ # Prefer returning SQLModel's own `Select[...]` wrapper, with best-effort element typing.
21
+ ok_stmt = stmt
22
+
23
+ engine = create_engine("sqlite://")
24
+
25
+ with Session(engine) as session:
26
+ for hero, team, hero_id, team_id, hero_name in session.exec(stmt):
27
+ ok_hero: Hero = hero
28
+ ok_team: Team = team
29
+ ok_team_id: int | None = team_id
30
+ ok_hero_name: str = hero_name
31
+
32
+ # `Hero.id` is optional at the type level.
33
+ bad_hero_id: int = hero_id
34
+ if hero_id is not None:
35
+ ok_hero_id: int = hero_id
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlmodel import Field, Session, SQLModel, create_engine, select
4
+ from sqlmodel.ext.asyncio.session import AsyncSession
5
+
6
+
7
+ class Hero(SQLModel, table=True):
8
+ id: int | None = Field(default=None, primary_key=True)
9
+ name: str = Field()
10
+
11
+
12
+ engine = create_engine("sqlite://")
13
+
14
+ with Session(engine) as session:
15
+ result = session.execute(select(Hero))
16
+
17
+ # When `typed_execute=true`, `execute(select(Model))` should not degrade to `Result[Any]`.
18
+ row = result.one()
19
+ ok_row_hero: Hero = row[0]
20
+ bad_row_bool: bool = row[0]
21
+ # MYPY: error: Incompatible types in assignment (expression has type "Hero", variable has type "bool") [assignment]
22
+
23
+ ok_hero: Hero = result.scalars().one()
24
+ bad_bool: bool = result.scalars().one()
25
+ # MYPY: error: Incompatible types in assignment (expression has type "Hero", variable has type "bool") [assignment]
26
+
27
+ result2 = session.execute(select(Hero.id))
28
+ ok_id: int | None = result2.scalars().one()
29
+ bad_id: int = result2.scalars().one()
30
+ # MYPY: error: Incompatible types in assignment (expression has type "int | None", variable has type "int") [assignment]
31
+
32
+
33
+ async def async_execute(session: AsyncSession) -> None:
34
+ result3 = await session.execute(select(Hero))
35
+ _ok_hero2: Hero = result3.scalars().one()
36
+ _bad_bool2: bool = result3.scalars().one()
37
+ # MYPY: error: Incompatible types in assignment (expression has type "Hero", variable has type "bool") [assignment]
38
+
39
+
40
+ async def async_execute_columns(session: AsyncSession) -> None:
41
+ result = await session.execute(select(Hero.id, Hero.name))
42
+ for hero_id, hero_name in result:
43
+ _ok_id: int | None = hero_id
44
+ _ok_name: str = hero_name
45
+ _bad_id: int = hero_id
46
+ # MYPY: error: Incompatible types in assignment (expression has type "int | None", variable has type "int") [assignment]
47
+ _bad_name: int = hero_name
48
+ # MYPY: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment]