sqlmodel-mypy-plugin 0.18.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 (111) hide show
  1. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/.gitignore +2 -0
  2. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/LICENSE +1 -1
  3. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/PKG-INFO +4 -6
  4. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/README.md +0 -2
  5. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/pyproject.toml +3 -3
  6. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/src/sqlmodel_mypy/__init__.py +1 -1
  7. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/src/sqlmodel_mypy/plugin.py +97 -12
  8. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/src/sqlmodel_mypy/transform.py +22 -4
  9. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/session_exec.py +9 -2
  10. sqlmodel_mypy_plugin-0.19.1/tests/mypy/modules/table_dunders.py +24 -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.1/tests/mypy/outputs/mypy-plugin_ini/session_exec.py +43 -0
  13. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/test_plugin_unit.py +25 -0
  14. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/test_transform_unit.py +52 -1
  15. sqlmodel_mypy_plugin-0.18.0/ROADMAP.md +0 -349
  16. sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/table_dunders.py +0 -12
  17. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/.pre-commit-config.yaml +0 -0
  18. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/AGENTS.md +0 -0
  19. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/src/sqlmodel_mypy/persisted.py +0 -0
  20. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/src/sqlmodel_mypy/py.typed +0 -0
  21. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/conftest.py +0 -0
  22. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
  23. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
  24. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
  25. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +0 -0
  26. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-strict-typed-execute.ini +0 -0
  27. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
  28. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/mypy-plugin.ini +0 -0
  29. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/configs/pyproject-plugin.toml +0 -0
  30. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/aliased_joins.py +0 -0
  31. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/aliases.py +0 -0
  32. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/annotated_field_metadata.py +0 -0
  33. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/constructor.py +0 -0
  34. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/docs_parity_many_to_many_create_data.py +0 -0
  35. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/docs_parity_read_connected_data.py +0 -0
  36. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/docs_parity_relationship_attributes.py +0 -0
  37. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/docs_parity_update_patterns.py +0 -0
  38. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/execution_options.py +0 -0
  39. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/expression_typing_strict.py +0 -0
  40. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/expressions.py +0 -0
  41. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/field_sa_type.py +0 -0
  42. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/field_validation_alias.py +0 -0
  43. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/generics.py +0 -0
  44. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/getattr_column_property.py +0 -0
  45. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/inheritance.py +0 -0
  46. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/joins.py +0 -0
  47. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/label.py +0 -0
  48. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/many_to_many_relationships.py +0 -0
  49. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/mapped_fields.py +0 -0
  50. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/model_config.py +0 -0
  51. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/model_construct.py +0 -0
  52. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/nullable_defaults.py +0 -0
  53. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/persisted_helpers.py +0 -0
  54. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/relationship_comparators.py +0 -0
  55. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/relationship_expressions.py +0 -0
  56. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/relationships.py +0 -0
  57. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/select_varargs.py +0 -0
  58. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/selectinload.py +0 -0
  59. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/session_execute_typed.py +0 -0
  60. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/session_get.py +0 -0
  61. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/table_model_awareness.py +0 -0
  62. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/table_via_model_config.py +0 -0
  63. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/tuple_expression.py +0 -0
  64. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/modules/untyped_fields.py +0 -0
  65. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-pydantic-first_ini/relationships.py +0 -0
  66. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-pydantic-last_ini/relationships.py +0 -0
  67. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict-typed-execute_ini/session_execute_typed.py +0 -0
  68. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/aliased_joins.py +0 -0
  69. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +0 -0
  70. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/annotated_field_metadata.py +0 -0
  71. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +0 -0
  72. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_many_to_many_create_data.py +0 -0
  73. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_read_connected_data.py +0 -0
  74. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_relationship_attributes.py +0 -0
  75. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_update_patterns.py +0 -0
  76. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/execution_options.py +0 -0
  77. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
  78. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -0
  79. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
  80. {sqlmodel_mypy_plugin-0.18.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.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +0 -0
  82. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
  83. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +0 -0
  84. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/persisted_helpers.py +0 -0
  85. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
  86. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +0 -0
  87. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/select_varargs.py +0 -0
  88. {sqlmodel_mypy_plugin-0.18.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.18.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.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin-strict_ini/tuple_expression.py +0 -0
  91. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/aliased_joins.py +0 -0
  92. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
  93. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +0 -0
  94. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/execution_options.py +0 -0
  95. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
  96. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +0 -0
  97. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -0
  98. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
  99. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
  100. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
  101. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +0 -0
  102. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
  103. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
  104. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +0 -0
  105. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/select_varargs.py +0 -0
  106. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
  107. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
  108. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +0 -0
  109. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/mypy/test_mypy.py +0 -0
  110. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/smoke_test.py +0 -0
  111. {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.1}/tests/test_persisted_helpers.py +0 -0
@@ -21,3 +21,5 @@ ENV/
21
21
 
22
22
  dist/
23
23
  build/
24
+
25
+ excl-*
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Artem
3
+ Copyright (c) 2026 sertione
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlmodel-mypy-plugin
3
- Version: 0.18.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
7
7
  Project-URL: Issues, https://github.com/sertione/sqlmodel-mypy-plugin/issues
8
8
  Project-URL: Changelog, https://github.com/sertione/sqlmodel-mypy-plugin/releases
9
- Author: Artem
9
+ Author: sertione
10
10
  License: MIT License
11
11
 
12
- Copyright (c) 2026 Artem
12
+ Copyright (c) 2026 sertione
13
13
 
14
14
  Permission is hereby granted, free of charge, to any person obtaining a copy
15
15
  of this software and associated documentation files (the "Software"), to deal
@@ -30,7 +30,7 @@ License: MIT License
30
30
  SOFTWARE.
31
31
  License-File: LICENSE
32
32
  Keywords: mypy,plugin,pydantic,sqlalchemy,sqlmodel,typing
33
- Classifier: Development Status :: 3 - Alpha
33
+ Classifier: Development Status :: 4 - Beta
34
34
  Classifier: Intended Audience :: Developers
35
35
  Classifier: License :: OSI Approved :: MIT License
36
36
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -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,15 +4,15 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sqlmodel-mypy-plugin"
7
- version = "0.18.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"
11
11
  license = { file = "LICENSE" }
12
- authors = [{ name = "Artem" }]
12
+ authors = [{ name = "sertione" }]
13
13
  keywords = ["mypy", "plugin", "sqlmodel", "pydantic", "sqlalchemy", "typing"]
14
14
  classifiers = [
15
- "Development Status :: 3 - Alpha",
15
+ "Development Status :: 4 - Beta",
16
16
  "Intended Audience :: Developers",
17
17
  "License :: OSI Approved :: MIT License",
18
18
  "Programming Language :: Python :: 3 :: Only",
@@ -1,4 +1,4 @@
1
- __version__ = "0.18.0"
1
+ __version__ = "0.19.1"
2
2
 
3
3
  from .persisted import HasId, HasOptionalId, has_id, require_id
4
4
 
@@ -163,8 +163,15 @@ SQLMODEL_ASYNC_SESSION_EXECUTE_FULLNAME = "sqlmodel.ext.asyncio.session.AsyncSes
163
163
 
164
164
  SQLMODEL_SELECT_OF_SCALAR_CLS_FULLNAME = "sqlmodel.sql._expression_select_cls.SelectOfScalar"
165
165
 
166
+ # `inspect` is re-exported from `sqlalchemy` and `sqlmodel`.
167
+ SQLALCHEMY_INSPECT_FULLNAMES = {
168
+ "sqlalchemy.inspect",
169
+ "sqlalchemy.inspection.inspect",
170
+ "sqlmodel.inspect",
171
+ }
172
+
166
173
  # Increment when plugin changes should invalidate mypy cache.
167
- __version__ = 23
174
+ __version__ = 26
168
175
 
169
176
 
170
177
  class _CollectedField(NamedTuple):
@@ -554,6 +561,8 @@ class SQLModelMypyPlugin(Plugin):
554
561
  return self._sqlmodel_select_return_type_callback
555
562
  if fullname == SQLMODEL_COL_FULLNAME:
556
563
  return self._sqlmodel_col_return_type_callback
564
+ if fullname in SQLALCHEMY_INSPECT_FULLNAMES:
565
+ return self._sqlalchemy_inspect_return_type_callback
557
566
  if fullname in SQLMODEL_TUPLE_FULLNAMES:
558
567
  return self._sqlmodel_tuple_return_type_callback
559
568
  if fullname == BUILTINS_GETATTR_FULLNAME:
@@ -1751,6 +1760,48 @@ class SQLModelMypyPlugin(Plugin):
1751
1760
 
1752
1761
  return self._sqlalchemy_tuple_expr_type(ctx.api)
1753
1762
 
1763
+ def _sqlalchemy_inspect_return_type_callback(self, ctx: FunctionContext) -> Type:
1764
+ """Type `inspect(Model)` / `inspect(model)` for SQLModel table models.
1765
+
1766
+ SQLAlchemy's overloads for `inspect()` rely on the inspected object implementing typing-only
1767
+ protocols (`_sa_inspect_type` / `_sa_inspect_instance`). SQLModel models are valid ORM entities
1768
+ at runtime, but the required typing surface is not always present for strict type checkers.
1769
+
1770
+ Here we conservatively support the two common ORM introspection patterns:
1771
+ - `inspect(User)` -> `Mapper[User]`
1772
+ - `inspect(user)` -> `InstanceState[User]`
1773
+ """
1774
+ if not ctx.arg_types or not ctx.arg_types[0]:
1775
+ return ctx.default_return_type
1776
+
1777
+ arg0 = get_proper_type(ctx.arg_types[0][0])
1778
+
1779
+ model_inst: Instance | None = None
1780
+ is_type: bool = False
1781
+ if isinstance(arg0, TypeType):
1782
+ item = get_proper_type(arg0.item)
1783
+ if isinstance(item, Instance):
1784
+ model_inst = item
1785
+ is_type = True
1786
+ elif isinstance(arg0, Instance):
1787
+ model_inst = arg0
1788
+ is_type = False
1789
+
1790
+ if model_inst is None:
1791
+ return ctx.default_return_type
1792
+
1793
+ info = model_inst.type
1794
+ if info.fullname == SQLMODEL_BASEMODEL_FULLNAME:
1795
+ return ctx.default_return_type
1796
+ if not info.has_base(SQLMODEL_BASEMODEL_FULLNAME):
1797
+ return ctx.default_return_type
1798
+ if not _is_table_model(info):
1799
+ return ctx.default_return_type
1800
+
1801
+ if is_type:
1802
+ return self._sqlalchemy_mapper_type(ctx.api, model_inst)
1803
+ return self._sqlalchemy_instance_state_type(ctx.api, model_inst)
1804
+
1754
1805
  @staticmethod
1755
1806
  def _lookup_var_in_mro(info: TypeInfo, name: str) -> Var | None:
1756
1807
  """Find a `Var` named `name` in `info` or its MRO (best-effort)."""
@@ -1992,17 +2043,25 @@ class SQLModelMypyPlugin(Plugin):
1992
2043
  if not _is_table_model(info):
1993
2044
  return
1994
2045
 
1995
- if "__table__" in info.names:
1996
- return
1997
-
1998
- table_t = self._sqlalchemy_table_type(ctx.api)
1999
- v = Var("__table__", table_t)
2000
- v.info = info
2001
- v._fullname = f"{info.fullname}.__table__"
2002
- v.is_classvar = True
2003
- sym = SymbolTableNode(MDEF, v)
2004
- sym.plugin_generated = True
2005
- info.names["__table__"] = sym
2046
+ if "__table__" not in info.names:
2047
+ table_t = self._sqlalchemy_table_type(ctx.api)
2048
+ v = Var("__table__", table_t)
2049
+ v.info = info
2050
+ v._fullname = f"{info.fullname}.__table__"
2051
+ v.is_classvar = True
2052
+ sym = SymbolTableNode(MDEF, v)
2053
+ sym.plugin_generated = True
2054
+ info.names["__table__"] = sym
2055
+
2056
+ if "__mapper__" not in info.names:
2057
+ mapper_t = self._sqlalchemy_mapper_type(ctx.api, fill_typevars(info))
2058
+ v = Var("__mapper__", mapper_t)
2059
+ v.info = info
2060
+ v._fullname = f"{info.fullname}.__mapper__"
2061
+ v.is_classvar = True
2062
+ sym = SymbolTableNode(MDEF, v)
2063
+ sym.plugin_generated = True
2064
+ info.names["__mapper__"] = sym
2006
2065
 
2007
2066
  def _sqlalchemy_table_type(self, api: Any | None = None) -> Type:
2008
2067
  """Return the best available `sqlalchemy` Table-ish type.
@@ -2024,6 +2083,32 @@ class SQLModelMypyPlugin(Plugin):
2024
2083
  return Instance(info, [])
2025
2084
  return _plugin_any()
2026
2085
 
2086
+ def _sqlalchemy_mapper_type(self, api: Any | None, model_type: Type) -> Type:
2087
+ """Return `sqlalchemy.orm.Mapper[model_type]` (best-effort)."""
2088
+ for fullname in ("sqlalchemy.orm.Mapper", "sqlalchemy.orm.mapper.Mapper"):
2089
+ if api is not None:
2090
+ inst = _named_generic_type_or_none(api, fullname, [model_type])
2091
+ if inst is not None:
2092
+ return inst
2093
+ info = _lookup_typeinfo(self, fullname)
2094
+ if info is not None:
2095
+ args = [model_type] * len(info.defn.type_vars)
2096
+ return Instance(info, args)
2097
+ return _plugin_any()
2098
+
2099
+ def _sqlalchemy_instance_state_type(self, api: Any | None, model_type: Type) -> Type:
2100
+ """Return `sqlalchemy.orm.InstanceState[model_type]` (best-effort)."""
2101
+ for fullname in ("sqlalchemy.orm.InstanceState", "sqlalchemy.orm.state.InstanceState"):
2102
+ if api is not None:
2103
+ inst = _named_generic_type_or_none(api, fullname, [model_type])
2104
+ if inst is not None:
2105
+ return inst
2106
+ info = _lookup_typeinfo(self, fullname)
2107
+ if info is not None:
2108
+ args = [model_type] * len(info.defn.type_vars)
2109
+ return Instance(info, args)
2110
+ return _plugin_any()
2111
+
2027
2112
  def _widen_model_config_type(self, ctx: ClassDefContext) -> None:
2028
2113
  mapping_str_any = ctx.api.named_type(
2029
2114
  "typing.Mapping",
@@ -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)
@@ -2,9 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any
4
4
 
5
- from sqlalchemy import text
5
+ from sqlalchemy import select, text
6
6
  from sqlalchemy.engine.cursor import CursorResult
7
- from sqlalchemy.engine.result import Result
7
+ from sqlalchemy.engine.result import Result, TupleResult
8
8
  from sqlmodel import Field, Session, SQLModel, create_engine, delete
9
9
  from sqlmodel.ext.asyncio.session import AsyncSession
10
10
 
@@ -21,6 +21,10 @@ with Session(engine) as session:
21
21
  result = session.exec(text("SELECT 1"))
22
22
  ok_result: Result[Any] = result
23
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
+
24
28
  # Keep update/delete statements working (select correct overload).
25
29
  delete_result = session.exec(delete(Hero))
26
30
  ok_delete_result: CursorResult[Any] = delete_result
@@ -30,5 +34,8 @@ async def async_exec(session: AsyncSession) -> None:
30
34
  result2 = await session.exec(text("SELECT 1"))
31
35
  _ok_result2: Result[Any] = result2
32
36
 
37
+ select_result2 = await session.exec(select(Hero))
38
+ _ok_select_result2: TupleResult[tuple[Hero]] = select_result2
39
+
33
40
  delete_result2 = await session.exec(delete(Hero))
34
41
  _ok_delete_result2: CursorResult[Any] = delete_result2
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy import inspect as sa_inspect
4
+ from sqlalchemy.orm import InstanceState, Mapper
5
+ from sqlalchemy.sql.schema import Table
6
+ from sqlmodel import Field, SQLModel
7
+ from sqlmodel import inspect as sm_inspect
8
+
9
+
10
+ class User(SQLModel, table=True):
11
+ id: int | None = Field(default=None, primary_key=True)
12
+ name: str = Field()
13
+
14
+
15
+ ok_table: Table = User.__table__
16
+ ok_mapper: Mapper[User] = User.__mapper__
17
+
18
+ # `inspect(...)` should be typed for SQLModel table models (SQLAlchemy + SQLModel re-export).
19
+ ok_mapper2: Mapper[User] = sa_inspect(User)
20
+ ok_mapper3: Mapper[User] = sm_inspect(User)
21
+
22
+ user = User(name="x")
23
+ ok_state: InstanceState[User] = sa_inspect(user)
24
+ ok_state2: InstanceState[User] = sm_inspect(user)
@@ -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
@@ -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
@@ -2941,3 +2941,28 @@ def test_select_signature_hook_is_idempotent_for_star_overload() -> None:
2941
2941
  hook(FunctionSigContext(args=[], default_signature=default_sig, context=call, api=api))
2942
2942
  is default_sig
2943
2943
  )
2944
+
2945
+
2946
+ def test_sqlalchemy_typing_helpers_fallback_to_any(monkeypatch: pytest.MonkeyPatch) -> None:
2947
+ """Cover SQLAlchemy typing helper fallbacks (best-effort robustness)."""
2948
+ p = plugin_mod.SQLModelMypyPlugin(Options())
2949
+ any_t = AnyType(TypeOfAny.explicit)
2950
+
2951
+ def _no_lookup(_plugin: object, _fullname: str) -> None:
2952
+ return None
2953
+
2954
+ monkeypatch.setattr(plugin_mod, "_lookup_typeinfo", _no_lookup)
2955
+
2956
+ assert isinstance(get_proper_type(p._sqlalchemy_table_type(None)), AnyType)
2957
+ assert isinstance(get_proper_type(p._sqlalchemy_mapper_type(None, any_t)), AnyType)
2958
+ assert isinstance(get_proper_type(p._sqlalchemy_instance_state_type(None, any_t)), AnyType)
2959
+
2960
+
2961
+ def test_sqlalchemy_instance_state_type_uses_named_generic_type() -> None:
2962
+ """Cover the fast-path branch for InstanceState typing."""
2963
+ p = plugin_mod.SQLModelMypyPlugin(Options())
2964
+ api = DummyCheckerAPI()
2965
+ any_t = AnyType(TypeOfAny.explicit)
2966
+
2967
+ out = p._sqlalchemy_instance_state_type(api, any_t)
2968
+ assert isinstance(get_proper_type(out), Instance)
@@ -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: