sqlmodel-mypy-plugin 0.18.0__tar.gz → 0.19.0__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.
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/.gitignore +2 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/LICENSE +1 -1
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/PKG-INFO +4 -4
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/pyproject.toml +3 -3
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/src/sqlmodel_mypy/__init__.py +1 -1
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/src/sqlmodel_mypy/plugin.py +125 -12
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/session_exec.py +9 -2
- sqlmodel_mypy_plugin-0.19.0/tests/mypy/modules/table_dunders.py +24 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/aliased_joins.py +0 -1
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_read_connected_data.py +1 -1
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/execution_options.py +0 -1
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/joins.py +0 -3
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/aliased_joins.py +0 -1
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/execution_options.py +0 -1
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/joins.py +0 -3
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/test_plugin_unit.py +25 -0
- sqlmodel_mypy_plugin-0.18.0/ROADMAP.md +0 -349
- sqlmodel_mypy_plugin-0.18.0/tests/mypy/modules/table_dunders.py +0 -12
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/.pre-commit-config.yaml +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/AGENTS.md +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/README.md +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/src/sqlmodel_mypy/persisted.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/src/sqlmodel_mypy/py.typed +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/src/sqlmodel_mypy/transform.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/conftest.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/configs/mypy-plugin-no-untyped.ini +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/configs/mypy-plugin-pydantic-first.ini +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/configs/mypy-plugin-pydantic-last.ini +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/configs/mypy-plugin-strict-no-explicit-any.ini +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/configs/mypy-plugin-strict-typed-execute.ini +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/configs/mypy-plugin-strict.ini +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/configs/mypy-plugin.ini +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/configs/pyproject-plugin.toml +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/aliased_joins.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/annotated_field_metadata.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/docs_parity_many_to_many_create_data.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/docs_parity_read_connected_data.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/docs_parity_relationship_attributes.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/docs_parity_update_patterns.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/execution_options.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/expression_typing_strict.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/field_sa_type.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/field_validation_alias.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/getattr_column_property.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/joins.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/label.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/nullable_defaults.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/selectinload.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/session_execute_typed.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/session_get.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/table_via_model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/tuple_expression.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-pydantic-first_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-pydantic-last_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict-typed-execute_ini/session_execute_typed.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/annotated_field_metadata.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_many_to_many_create_data.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_relationship_attributes.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/docs_parity_update_patterns.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/generics.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/mapped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/nullable_defaults.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/table_via_model_config.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin-strict_ini/tuple_expression.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/aliases.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/inheritance.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/label.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/many_to_many_relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/model_construct.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/nullable_defaults.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_comparators.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/relationship_expressions.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/relationships.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/select_varargs.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/table_model_awareness.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/mypy-plugin_ini/untyped_fields.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/outputs/pyproject-plugin_toml/constructor.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/test_mypy.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/smoke_test.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/test_persisted_helpers.py +0 -0
- {sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/test_transform_unit.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sqlmodel-mypy-plugin
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
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:
|
|
9
|
+
Author: sertione
|
|
10
10
|
License: MIT License
|
|
11
11
|
|
|
12
|
-
Copyright (c) 2026
|
|
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 ::
|
|
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
|
|
@@ -4,15 +4,15 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sqlmodel-mypy-plugin"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.19.0"
|
|
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 = "
|
|
12
|
+
authors = [{ name = "sertione" }]
|
|
13
13
|
keywords = ["mypy", "plugin", "sqlmodel", "pydantic", "sqlalchemy", "typing"]
|
|
14
14
|
classifiers = [
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"License :: OSI Approved :: MIT License",
|
|
18
18
|
"Programming Language :: Python :: 3 :: Only",
|
|
@@ -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__ =
|
|
174
|
+
__version__ = 25
|
|
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:
|
|
@@ -1266,6 +1275,34 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1266
1275
|
return any(_contains_instance_fullname(item, fullname) for item in proper.items)
|
|
1267
1276
|
return False
|
|
1268
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
|
+
|
|
1269
1306
|
# Only adjust the broad overload; keep Select/SelectOfScalar overloads precise.
|
|
1270
1307
|
broad = False
|
|
1271
1308
|
if sqlmodel_executable_info is not None and _contains_instance_fullname(
|
|
@@ -1751,6 +1788,48 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1751
1788
|
|
|
1752
1789
|
return self._sqlalchemy_tuple_expr_type(ctx.api)
|
|
1753
1790
|
|
|
1791
|
+
def _sqlalchemy_inspect_return_type_callback(self, ctx: FunctionContext) -> Type:
|
|
1792
|
+
"""Type `inspect(Model)` / `inspect(model)` for SQLModel table models.
|
|
1793
|
+
|
|
1794
|
+
SQLAlchemy's overloads for `inspect()` rely on the inspected object implementing typing-only
|
|
1795
|
+
protocols (`_sa_inspect_type` / `_sa_inspect_instance`). SQLModel models are valid ORM entities
|
|
1796
|
+
at runtime, but the required typing surface is not always present for strict type checkers.
|
|
1797
|
+
|
|
1798
|
+
Here we conservatively support the two common ORM introspection patterns:
|
|
1799
|
+
- `inspect(User)` -> `Mapper[User]`
|
|
1800
|
+
- `inspect(user)` -> `InstanceState[User]`
|
|
1801
|
+
"""
|
|
1802
|
+
if not ctx.arg_types or not ctx.arg_types[0]:
|
|
1803
|
+
return ctx.default_return_type
|
|
1804
|
+
|
|
1805
|
+
arg0 = get_proper_type(ctx.arg_types[0][0])
|
|
1806
|
+
|
|
1807
|
+
model_inst: Instance | None = None
|
|
1808
|
+
is_type: bool = False
|
|
1809
|
+
if isinstance(arg0, TypeType):
|
|
1810
|
+
item = get_proper_type(arg0.item)
|
|
1811
|
+
if isinstance(item, Instance):
|
|
1812
|
+
model_inst = item
|
|
1813
|
+
is_type = True
|
|
1814
|
+
elif isinstance(arg0, Instance):
|
|
1815
|
+
model_inst = arg0
|
|
1816
|
+
is_type = False
|
|
1817
|
+
|
|
1818
|
+
if model_inst is None:
|
|
1819
|
+
return ctx.default_return_type
|
|
1820
|
+
|
|
1821
|
+
info = model_inst.type
|
|
1822
|
+
if info.fullname == SQLMODEL_BASEMODEL_FULLNAME:
|
|
1823
|
+
return ctx.default_return_type
|
|
1824
|
+
if not info.has_base(SQLMODEL_BASEMODEL_FULLNAME):
|
|
1825
|
+
return ctx.default_return_type
|
|
1826
|
+
if not _is_table_model(info):
|
|
1827
|
+
return ctx.default_return_type
|
|
1828
|
+
|
|
1829
|
+
if is_type:
|
|
1830
|
+
return self._sqlalchemy_mapper_type(ctx.api, model_inst)
|
|
1831
|
+
return self._sqlalchemy_instance_state_type(ctx.api, model_inst)
|
|
1832
|
+
|
|
1754
1833
|
@staticmethod
|
|
1755
1834
|
def _lookup_var_in_mro(info: TypeInfo, name: str) -> Var | None:
|
|
1756
1835
|
"""Find a `Var` named `name` in `info` or its MRO (best-effort)."""
|
|
@@ -1992,17 +2071,25 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
1992
2071
|
if not _is_table_model(info):
|
|
1993
2072
|
return
|
|
1994
2073
|
|
|
1995
|
-
if "__table__" in info.names:
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2074
|
+
if "__table__" not in info.names:
|
|
2075
|
+
table_t = self._sqlalchemy_table_type(ctx.api)
|
|
2076
|
+
v = Var("__table__", table_t)
|
|
2077
|
+
v.info = info
|
|
2078
|
+
v._fullname = f"{info.fullname}.__table__"
|
|
2079
|
+
v.is_classvar = True
|
|
2080
|
+
sym = SymbolTableNode(MDEF, v)
|
|
2081
|
+
sym.plugin_generated = True
|
|
2082
|
+
info.names["__table__"] = sym
|
|
2083
|
+
|
|
2084
|
+
if "__mapper__" not in info.names:
|
|
2085
|
+
mapper_t = self._sqlalchemy_mapper_type(ctx.api, fill_typevars(info))
|
|
2086
|
+
v = Var("__mapper__", mapper_t)
|
|
2087
|
+
v.info = info
|
|
2088
|
+
v._fullname = f"{info.fullname}.__mapper__"
|
|
2089
|
+
v.is_classvar = True
|
|
2090
|
+
sym = SymbolTableNode(MDEF, v)
|
|
2091
|
+
sym.plugin_generated = True
|
|
2092
|
+
info.names["__mapper__"] = sym
|
|
2006
2093
|
|
|
2007
2094
|
def _sqlalchemy_table_type(self, api: Any | None = None) -> Type:
|
|
2008
2095
|
"""Return the best available `sqlalchemy` Table-ish type.
|
|
@@ -2024,6 +2111,32 @@ class SQLModelMypyPlugin(Plugin):
|
|
|
2024
2111
|
return Instance(info, [])
|
|
2025
2112
|
return _plugin_any()
|
|
2026
2113
|
|
|
2114
|
+
def _sqlalchemy_mapper_type(self, api: Any | None, model_type: Type) -> Type:
|
|
2115
|
+
"""Return `sqlalchemy.orm.Mapper[model_type]` (best-effort)."""
|
|
2116
|
+
for fullname in ("sqlalchemy.orm.Mapper", "sqlalchemy.orm.mapper.Mapper"):
|
|
2117
|
+
if api is not None:
|
|
2118
|
+
inst = _named_generic_type_or_none(api, fullname, [model_type])
|
|
2119
|
+
if inst is not None:
|
|
2120
|
+
return inst
|
|
2121
|
+
info = _lookup_typeinfo(self, fullname)
|
|
2122
|
+
if info is not None:
|
|
2123
|
+
args = [model_type] * len(info.defn.type_vars)
|
|
2124
|
+
return Instance(info, args)
|
|
2125
|
+
return _plugin_any()
|
|
2126
|
+
|
|
2127
|
+
def _sqlalchemy_instance_state_type(self, api: Any | None, model_type: Type) -> Type:
|
|
2128
|
+
"""Return `sqlalchemy.orm.InstanceState[model_type]` (best-effort)."""
|
|
2129
|
+
for fullname in ("sqlalchemy.orm.InstanceState", "sqlalchemy.orm.state.InstanceState"):
|
|
2130
|
+
if api is not None:
|
|
2131
|
+
inst = _named_generic_type_or_none(api, fullname, [model_type])
|
|
2132
|
+
if inst is not None:
|
|
2133
|
+
return inst
|
|
2134
|
+
info = _lookup_typeinfo(self, fullname)
|
|
2135
|
+
if info is not None:
|
|
2136
|
+
args = [model_type] * len(info.defn.type_vars)
|
|
2137
|
+
return Instance(info, args)
|
|
2138
|
+
return _plugin_any()
|
|
2139
|
+
|
|
2027
2140
|
def _widen_model_config_type(self, ctx: ClassDefContext) -> None:
|
|
2028
2141
|
mapping_str_any = ctx.api.named_type(
|
|
2029
2142
|
"typing.Mapping",
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/session_exec.py
RENAMED
|
@@ -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)
|
|
@@ -28,6 +28,5 @@ 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]
|
|
32
31
|
if team is not None:
|
|
33
32
|
ok_team_name: str = team.name
|
|
@@ -37,6 +37,7 @@ 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]
|
|
40
41
|
|
|
41
42
|
# Docs pattern: LEFT OUTER join via isouter=True.
|
|
42
43
|
stmt_outer = select(Hero, Team).join(Team, isouter=True)
|
|
@@ -46,4 +47,3 @@ with Session(engine) as session:
|
|
|
46
47
|
|
|
47
48
|
# Outer join may yield `None` for the right-hand entity.
|
|
48
49
|
bad_team4: Team = team4
|
|
49
|
-
# MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
|
|
@@ -32,7 +32,6 @@ 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]
|
|
36
35
|
if team2 is not None:
|
|
37
36
|
ok_team_name2: str = team2.name
|
|
38
37
|
|
|
@@ -42,7 +41,6 @@ with Session(engine) as session:
|
|
|
42
41
|
ok_hero3: Hero = hero3
|
|
43
42
|
ok_team3: Team | None = team3
|
|
44
43
|
bad_team3: Team = team3
|
|
45
|
-
# MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
|
|
46
44
|
|
|
47
45
|
# One-to-many join via relationship attribute target should also propagate `None`.
|
|
48
46
|
stmt_outer_rel_many = select(Team, Hero).join(Team.heroes, isouter=True)
|
|
@@ -50,6 +48,5 @@ with Session(engine) as session:
|
|
|
50
48
|
ok_team4: Team = team4
|
|
51
49
|
ok_hero4: Hero | None = hero4
|
|
52
50
|
bad_hero4: Hero = hero4
|
|
53
|
-
# MYPY: error: Incompatible types in assignment (expression has type "Hero | None", variable has type "Hero") [assignment]
|
|
54
51
|
if hero4 is not None:
|
|
55
52
|
ok_hero4_name: str = hero4.name
|
|
@@ -28,6 +28,5 @@ 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]
|
|
32
31
|
if team is not None:
|
|
33
32
|
ok_team_name: str = team.name
|
|
@@ -32,7 +32,6 @@ 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]
|
|
36
35
|
if team2 is not None:
|
|
37
36
|
ok_team_name2: str = team2.name
|
|
38
37
|
|
|
@@ -42,7 +41,6 @@ with Session(engine) as session:
|
|
|
42
41
|
ok_hero3: Hero = hero3
|
|
43
42
|
ok_team3: Team | None = team3
|
|
44
43
|
bad_team3: Team = team3
|
|
45
|
-
# MYPY: error: Incompatible types in assignment (expression has type "Team | None", variable has type "Team") [assignment]
|
|
46
44
|
|
|
47
45
|
# One-to-many join via relationship attribute target should also propagate `None`.
|
|
48
46
|
stmt_outer_rel_many = select(Team, Hero).join(Team.heroes, isouter=True)
|
|
@@ -50,6 +48,5 @@ with Session(engine) as session:
|
|
|
50
48
|
ok_team4: Team = team4
|
|
51
49
|
ok_hero4: Hero | None = hero4
|
|
52
50
|
bad_hero4: Hero = hero4
|
|
53
|
-
# MYPY: error: Incompatible types in assignment (expression has type "Hero | None", variable has type "Hero") [assignment]
|
|
54
51
|
if hero4 is not None:
|
|
55
52
|
ok_hero4_name: str = hero4.name
|
|
@@ -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)
|
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
# Roadmap
|
|
2
|
-
|
|
3
|
-
## v0.1 (MVP) - DONE
|
|
4
|
-
|
|
5
|
-
- Correct required/optional kwargs for `sqlmodel.Field(...)` when generating `__init__`.
|
|
6
|
-
- Exclude `sqlmodel.Relationship(...)` from generated `__init__` kwargs.
|
|
7
|
-
- Minimal plugin config + cache invalidation via `report_config_data()`.
|
|
8
|
-
- Mypy integration test suite with golden outputs.
|
|
9
|
-
|
|
10
|
-
## v0.2 - DONE
|
|
11
|
-
|
|
12
|
-
- Config parity with Pydantic plugin (more knobs; documented):
|
|
13
|
-
- `init_typed`
|
|
14
|
-
- `init_forbid_extra`
|
|
15
|
-
- Better handling of inherited fields (including generics/typevars) and overrides.
|
|
16
|
-
|
|
17
|
-
## v0.3 - DONE
|
|
18
|
-
|
|
19
|
-
- Relationship typing improvements (forward refs, `Mapped[...]` wrapping behavior).
|
|
20
|
-
- Typed SQL expression building (reduce need for `col()`, `table.c.*`, etc.):
|
|
21
|
-
- Improve class/instance attribute typing so `User.id`, `User.name`, etc. are well-typed in expressions.
|
|
22
|
-
- Ensure common SQLModel re-exports (`select`, `and_`, `or_`, `col`, etc.) preserve SQLAlchemy typing.
|
|
23
|
-
- Decide approach: plugin hooks vs stub overlays vs relying on SQLAlchemy typing (documented trade-offs).
|
|
24
|
-
- Compatibility matrix CI across mypy/SQLModel versions (and pin policy).
|
|
25
|
-
- **Plugin ordering guardrails (mypy limitation)**
|
|
26
|
-
- **Reality check**: mypy generally uses the **first plugin that claims a hook**, so true "order independence"
|
|
27
|
-
between `sqlmodel_mypy.plugin` and `pydantic.mypy` is not realistically achievable today.
|
|
28
|
-
- Enforce and clearly explain that `sqlmodel_mypy.plugin` must be listed **before** `pydantic.mypy`:
|
|
29
|
-
- Source: `src/sqlmodel_mypy/plugin.py` (`SQLModelMypyPlugin.__init__`, `_sqlmodel_metaclass_callback()`).
|
|
30
|
-
- User-facing docs: `README.md` (plugin ordering section).
|
|
31
|
-
|
|
32
|
-
## v0.4 (Docs-aligned relationship constructors) - DONE
|
|
33
|
-
|
|
34
|
-
**Motivation**: SQLModel's official docs pass relationship attributes in constructors (e.g. `Hero(team=team)`,
|
|
35
|
-
`Team(heroes=[...])`). v0.4 accepts relationship kwargs in generated `__init__` / `model_construct` for
|
|
36
|
-
`table=True` models, so `init_forbid_extra=true` no longer causes false-positive "unexpected keyword argument"
|
|
37
|
-
errors for documented patterns.
|
|
38
|
-
|
|
39
|
-
- **Accept relationship kwargs in generated `__init__` / `model_construct` (table models)**
|
|
40
|
-
- Allow many-to-one and one-to-many patterns from the docs:
|
|
41
|
-
- `Hero(..., team=team_obj)` / `hero.team = team_obj`
|
|
42
|
-
- `Team(..., heroes=[hero1, hero2])` / `team.heroes.append(hero)`
|
|
43
|
-
- Docs: `https://sqlmodel.tiangolo.com/tutorial/relationship-attributes/create-and-update-relationships/`
|
|
44
|
-
- Allow many-to-many relationship kwargs (lists) from the docs:
|
|
45
|
-
- `Hero(..., teams=[team1, team2])` with `link_model=...`
|
|
46
|
-
- Docs: `https://sqlmodel.tiangolo.com/tutorial/many-to-many/create-data/`
|
|
47
|
-
- Keep behavior **scoped to `table=True` models** (relationship kwargs are applied after construction in SQLModel):
|
|
48
|
-
- Source: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py` (`sqlmodel_table_construct()`,
|
|
49
|
-
`sqlmodel_validate()` assign relationship keys after fields).
|
|
50
|
-
- Add/extend mypy integration tests covering relationship kwargs under both configs:
|
|
51
|
-
- `init_forbid_extra=false` and `init_forbid_extra=true`
|
|
52
|
-
- `init_typed=false` and `init_typed=true`
|
|
53
|
-
|
|
54
|
-
- **Join/outer-join query typing coverage (SQLModel docs patterns)**
|
|
55
|
-
- Ensure expression typing works for doc examples using joins:
|
|
56
|
-
- `select(Hero, Team).join(Team)` and `select(Hero, Team).join(Team, isouter=True)`
|
|
57
|
-
- Docs: `https://sqlmodel.tiangolo.com/tutorial/connect/read-connected-data/`
|
|
58
|
-
|
|
59
|
-
## v0.4.1 (Outer-join `None` propagation) - DONE
|
|
60
|
-
|
|
61
|
-
- Decide how to model **outer-join `None` propagation** in result tuples (docs show `Team: None`):
|
|
62
|
-
- Either type `Team | None` on the right-hand side when `isouter=True`, or document a recommended narrowing
|
|
63
|
-
pattern (and keep types conservative).
|
|
64
|
-
- Update/add mypy integration tests to match the chosen strategy.
|
|
65
|
-
|
|
66
|
-
## v0.5 (Model-kind awareness + reduce "surprising" typing) - DONE
|
|
67
|
-
|
|
68
|
-
- **Table-model awareness for SQLAlchemy expression typing**
|
|
69
|
-
- Only treat class attributes as SQLAlchemy expressions (e.g. wrap to `InstrumentedAttribute[T]`) for `table=True`
|
|
70
|
-
SQLModel models.
|
|
71
|
-
- Avoid changing attribute types for "data models" (non-table SQLModel subclasses) used for FastAPI schemas, e.g.
|
|
72
|
-
`TeamBase`, `HeroCreate`, `HeroPublic`.
|
|
73
|
-
- Docs: `https://sqlmodel.tiangolo.com/tutorial/fastapi/relationships/`
|
|
74
|
-
|
|
75
|
-
- **Pydantic/Field alias & config parity checkpoints**
|
|
76
|
-
- Verify our generated signatures stay compatible with SQLModel's `Field(...)` alias behavior (alias propagation
|
|
77
|
-
and precedence in `validation_alias` / `serialization_alias`).
|
|
78
|
-
- Source: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py` (`Field()` implementation).
|
|
79
|
-
- Document what is supported vs intentionally out of scope when users rely on aliases in constructor kwargs.
|
|
80
|
-
|
|
81
|
-
## v0.6 (Session.exec / AsyncSession.exec typing gaps) - DONE
|
|
82
|
-
|
|
83
|
-
- **Broaden `Session.exec()` / `AsyncSession.exec()` accepted statement types**
|
|
84
|
-
- Fix common mypy failures when passing non-`Select` statements (e.g. `delete(...)`) to `exec()`.
|
|
85
|
-
- Implement via upstream PRs and/or stub overlays (keep runtime behavior unchanged):
|
|
86
|
-
- Source: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/orm/session.py`
|
|
87
|
-
- Source: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/ext/asyncio/session.py`
|
|
88
|
-
- Known gap example: `https://github.com/fastapi/sqlmodel/discussions/831`
|
|
89
|
-
|
|
90
|
-
## v0.7 (Advanced SQL expression + relationship comparator typing) - DONE
|
|
91
|
-
|
|
92
|
-
- **Relationship comparator typing**
|
|
93
|
-
- `.any()`, `.has()`, `.contains()`, etc. on relationship attributes in query expressions.
|
|
94
|
-
- **More query-shape coverage**
|
|
95
|
-
- Multi-entity selects, joins/outer-joins, and relationship-based filters across common SQLModel tutorial patterns.
|
|
96
|
-
- **Document trade-offs (and pick a default strategy)**
|
|
97
|
-
- Plugin hooks vs stub overlays vs relying purely on SQLAlchemy typing (with "when to use what" guidance).
|
|
98
|
-
|
|
99
|
-
## v0.8 (Field `sa_type=` supports SQLAlchemy TypeEngine instances) - DONE
|
|
100
|
-
|
|
101
|
-
**Motivation**: SQLModel's `Field(sa_type=...)` is a supported API, but real-world usage commonly passes
|
|
102
|
-
**instantiated SQLAlchemy types** (e.g. `DateTime(timezone=True)`, `String(50)`), which triggers mypy
|
|
103
|
-
`call-overload` errors under `--strict`. Users currently work around this with `sa_column=Column(...)` or
|
|
104
|
-
`# type: ignore`, which is exactly the "types clutter" we want to remove.
|
|
105
|
-
|
|
106
|
-
- **Fix mypy `call-overload` for `Field(sa_type=<TypeEngine instance>)`**
|
|
107
|
-
- Upstream issue reports:
|
|
108
|
-
- `https://github.com/fastapi/sqlmodel/discussions/955`
|
|
109
|
-
- `https://github.com/fastapi/sqlmodel/discussions/1228`
|
|
110
|
-
- Upstream pending fix (adjust `sa_type` annotation to match SQLAlchemy's `Column` type argument rules):
|
|
111
|
-
- `https://github.com/fastapi/sqlmodel/pull/1345`
|
|
112
|
-
- Source references:
|
|
113
|
-
- SQLModel `Field()` signature: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py`
|
|
114
|
-
- SQLAlchemy `Column` accepts both a **TypeEngine class** and a **TypeEngine instance** (see PR #1345 write-up).
|
|
115
|
-
|
|
116
|
-
- **Implementation options (pick default; document trade-offs)**
|
|
117
|
-
- **Preferred**: upstream-first — rely on SQLModel merging PR #1345; in our plugin add a compatibility layer for
|
|
118
|
-
older SQLModel releases and/or for users stuck on versions without the fix.
|
|
119
|
-
- **Plugin signature hook**: add a `Field()` signature hook that widens `sa_type` to accept
|
|
120
|
-
`sqlalchemy.sql.type_api.TypeEngine[Any] | type[Any]` when present.
|
|
121
|
-
- **Stub overlay**: ship a small `.pyi` overlay (or `typing`-only shim) to widen the `Field()` overload(s) without
|
|
122
|
-
touching runtime behavior.
|
|
123
|
-
|
|
124
|
-
- **Tests**
|
|
125
|
-
- Add a mypy integration module that reproduces the failure in strict mode and asserts it's fixed without
|
|
126
|
-
`# type: ignore`, e.g.:
|
|
127
|
-
- `Field(sa_type=DateTime(timezone=True))`
|
|
128
|
-
- `Field(sa_type=String(50))`
|
|
129
|
-
- Ensure the fix is **idempotent**: if upstream SQLModel already accepts instances, our hook/overlay must not
|
|
130
|
-
change behavior or introduce new ambiguities.
|
|
131
|
-
|
|
132
|
-
## v0.9 (`model_config = ConfigDict(...)` compatibility in strict mode) - DONE
|
|
133
|
-
|
|
134
|
-
**Motivation**: SQLModel is built on Pydantic v2, and users often want to set things like `extra="forbid"` on
|
|
135
|
-
schema-like models (e.g. `HeroCreate`, `HeroUpdate`) via `model_config = ConfigDict(...)`. Today this can fail mypy
|
|
136
|
-
strict with an "incompatible types in assignment" error (`ConfigDict` vs `SQLModelConfig`), forcing users into
|
|
137
|
-
`# type: ignore` or `cast(...)`.
|
|
138
|
-
|
|
139
|
-
- **Fix mypy incompatibility for `model_config` overrides**
|
|
140
|
-
- Upstream discussion / reproduction:
|
|
141
|
-
- `https://github.com/fastapi/sqlmodel/discussions/855`
|
|
142
|
-
- Source references:
|
|
143
|
-
- SQLModel config typing helpers: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py`
|
|
144
|
-
- SQLModel base class `model_config` annotation: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py`
|
|
145
|
-
|
|
146
|
-
- **Implementation options (pick default; document trade-offs)**
|
|
147
|
-
- **Stub overlay (likely simplest)**: type `SQLModel.model_config` as `pydantic.ConfigDict` (or `Mapping[str, Any]`)
|
|
148
|
-
so assignment from `ConfigDict(...)` is always accepted.
|
|
149
|
-
- **Plugin adjustment**: during semantic analysis, rewrite the inferred type of `model_config` on SQLModel subclasses
|
|
150
|
-
to a compatible supertype (while keeping runtime untouched).
|
|
151
|
-
- Ensure we don't regress table-model behavior: SQLModel uses `model_config["table"]` and `model_config["registry"]`
|
|
152
|
-
at runtime (see `SQLModelMetaclass.__new__`).
|
|
153
|
-
|
|
154
|
-
- **Tests**
|
|
155
|
-
- Add a mypy integration module proving strict-mode acceptance without ignores, e.g.:
|
|
156
|
-
- `from pydantic import ConfigDict`
|
|
157
|
-
- `class HeroCreate(SQLModel): model_config = ConfigDict(extra="forbid")`
|
|
158
|
-
- Include a minimal table model + data model inheritance case (common in SQLModel docs) to ensure no false positives.
|
|
159
|
-
|
|
160
|
-
## v0.10 (`select()` varargs: avoid overload ceiling) - DONE
|
|
161
|
-
|
|
162
|
-
**Motivation**: SQLModel provides its own typed `select()` overloads, but they are generated only up to **4**
|
|
163
|
-
entities. In real code it's common to select more than 4 columns/entities (especially with joins/aggregates), which
|
|
164
|
-
can trigger "No overload variant of `select` matches argument types …" even though runtime works.
|
|
165
|
-
|
|
166
|
-
- **Fix overload ceiling for `sqlmodel.select(...)`**
|
|
167
|
-
- Source reference (generated overloads; note the 4-entity cap):
|
|
168
|
-
- `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/sql/_expression_select_gen.py`
|
|
169
|
-
- Upstream issue reports:
|
|
170
|
-
- `https://github.com/fastapi/sqlmodel/issues/92`
|
|
171
|
-
- `https://github.com/fastapi/sqlmodel/issues/271`
|
|
172
|
-
|
|
173
|
-
- **Implementation options (pick default; document trade-offs)**
|
|
174
|
-
- **Stub overlay (likely simplest)**: add a final "varargs fallback" overload, e.g.:
|
|
175
|
-
- `@overload`
|
|
176
|
-
- `def select(__ent0: Any, __ent1: Any, __ent2: Any, __ent3: Any, __ent4: Any, *entities: Any) -> Select[tuple[Any, ...]]: ...`
|
|
177
|
-
so mypy always has a matching overload beyond 4 args.
|
|
178
|
-
- **Plugin signature hook**: provide a mypy hook for `sqlmodel.sql.expression.select` that adds the same fallback
|
|
179
|
-
overload, keeping the existing 1–4 entity overload precision.
|
|
180
|
-
- Optionally, generate more precise overloads (e.g. up to 8) if we're willing to carry the maintenance cost.
|
|
181
|
-
|
|
182
|
-
- **Tests**
|
|
183
|
-
- Add a mypy integration module that selects 5+ entities and asserts no `call-overload` errors under strict mode.
|
|
184
|
-
- Decide and lock down the fallback return type (likely `Select[tuple[Any, ...]]`) to keep behavior predictable.
|
|
185
|
-
|
|
186
|
-
## v0.11 (SQLAlchemy "extras" coverage + SQLModel `Select` join parity) - DONE
|
|
187
|
-
|
|
188
|
-
- **Lock down common SQLAlchemy helpers used with SQLModel**
|
|
189
|
-
- `.label(...)` on `table=True` model fields typed as SQLAlchemy expressions.
|
|
190
|
-
- ORM loader options like `selectinload(Model.relationship)` on `table=True` relationships.
|
|
191
|
-
- `Select.execution_options(...)` chaining without losing the `Select[...]` generic (including outer-join `None` propagation).
|
|
192
|
-
- **Fix outer-join `None` propagation for SQLModel's own `Select` wrapper**
|
|
193
|
-
- Apply the join/outerjoin return-type adjustment to both SQLAlchemy `Select` and SQLModel's `sqlmodel.sql._expression_select_cls.Select`.
|
|
194
|
-
- **Tests**
|
|
195
|
-
- Add/extend mypy integration coverage and commit golden outputs for the new cases.
|
|
196
|
-
|
|
197
|
-
## v0.12 (`__table__` dunder typing + `Session.get`/`AsyncSession.get` coverage) - DONE
|
|
198
|
-
|
|
199
|
-
**Motivation**: SQLAlchemy APIs and patterns commonly rely on ORM models exposing `Model.__table__`. SQLModel
|
|
200
|
-
models do provide this at runtime for `table=True`, but type checkers can miss it and report `attr-defined`.
|
|
201
|
-
|
|
202
|
-
- **Expose and type `__table__` on `table=True` SQLModel models**
|
|
203
|
-
- Provide a plugin-generated `__table__` attribute typed as `sqlalchemy.sql.schema.Table` (with safe fallbacks
|
|
204
|
-
when stubs don't expose `Table`).
|
|
205
|
-
- **Tests**
|
|
206
|
-
- Add mypy integration coverage for `Model.__table__` and `Session.get` / `AsyncSession.get` with SQLModel
|
|
207
|
-
table models.
|
|
208
|
-
|
|
209
|
-
## v0.12.4 (Dynamic query-builder helpers) - DONE
|
|
210
|
-
|
|
211
|
-
- Type `getattr(Model, "field")` like `Model.field` for `table=True` SQLModel models (string-literal names only).
|
|
212
|
-
- Best-effort typing for `column_property(...)` return type (infer `Mapped[T]`) to support
|
|
213
|
-
`Model._foo = column_property(...)` patterns.
|
|
214
|
-
|
|
215
|
-
## v0.14 (`typing.Annotated[...]` field metadata support) - DONE
|
|
216
|
-
|
|
217
|
-
**Motivation**: SQLModel (Pydantic v2) supports `Annotated`-driven typing patterns, and users will naturally try to
|
|
218
|
-
use them to reduce `= Field(...)` assignment noise. Under `--strict`, we should still generate correct constructor
|
|
219
|
-
signatures (required/optional + alias kwargs) when `Field(...)` metadata lives inside `Annotated[...]`.
|
|
220
|
-
|
|
221
|
-
- **Support `Annotated[T, Field(...)]` for constructor/signature generation**
|
|
222
|
-
- Examples we should handle (at least best-effort):
|
|
223
|
-
- `id: Annotated[int | None, Field(default=None, primary_key=True)]`
|
|
224
|
-
- `name: Annotated[str, Field(alias="full_name")]`
|
|
225
|
-
- The historical SQLModel edge-case: `Optional[Annotated[T, ...]]` (see upstream release notes).
|
|
226
|
-
- Implementation sketch:
|
|
227
|
-
- Extend semantic-phase collection in `src/sqlmodel_mypy/transform.py` to extract Field metadata from the
|
|
228
|
-
annotation when there is no `= Field(...)` call in the class body.
|
|
229
|
-
- Mirror the same logic in checker-time signature collection in `src/sqlmodel_mypy/plugin.py`
|
|
230
|
-
(`_collect_member_from_stmt`) so class-call signatures stay consistent with generated `__init__`.
|
|
231
|
-
- Source references:
|
|
232
|
-
- SQLModel `Field()` overloads + alias precedence: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py`
|
|
233
|
-
- `Annotated` handling in SQLModel's type parsing: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py`
|
|
234
|
-
- Release note: "Fix support for types with `Optional[Annotated[x, f()]]`…" (PR #1093):
|
|
235
|
-
`https://github.com/fastapi/sqlmodel/pull/1093`
|
|
236
|
-
|
|
237
|
-
## v0.15 (Outer-join `None` propagation for relationship-attribute joins) - DONE
|
|
238
|
-
|
|
239
|
-
**Motivation**: we currently propagate `None` on outer-joins only when the join target is a direct model class
|
|
240
|
-
(e.g. `.join(Team, isouter=True)`). A common ORM pattern is to join via relationship attributes
|
|
241
|
-
(e.g. `.join(Hero.team, isouter=True)`), and the result tuple should still reflect that the joined entity may be
|
|
242
|
-
`None`.
|
|
243
|
-
|
|
244
|
-
- **Expand join-target detection**
|
|
245
|
-
- Extend `src/sqlmodel_mypy/plugin.py` (`_sqlalchemy_select_join_like_return_type`) to also detect join targets
|
|
246
|
-
expressed as relationship attributes (e.g. `Hero.team`, `Team.heroes`) and map them to the target model type
|
|
247
|
-
for `None` propagation.
|
|
248
|
-
- References:
|
|
249
|
-
- SQLModel outer-join behavior (baseline example): `https://sqlmodel.tiangolo.com/tutorial/connect/read-connected-data/`
|
|
250
|
-
- SQLAlchemy relationship-attribute joins (`select(User).join(User.addresses)`): `https://docs.sqlalchemy.org/en/20/orm/queryguide/select.html#simple-relationship-joins`
|
|
251
|
-
- Add mypy integration coverage under `tests/mypy/modules/` for relationship-attribute joins, including the SQLModel
|
|
252
|
-
docs pattern of `Team: None` on outer-joins.
|
|
253
|
-
|
|
254
|
-
## v0.16 (Strict typing ergonomics add-ons, opt-in) - DONE
|
|
255
|
-
|
|
256
|
-
**Motivation**: strict typing + SQLModel often means the database populates values "later" (after flush/commit/
|
|
257
|
-
refresh), especially primary keys (`id: int | None`). Mypy can't infer those runtime effects, and users end up with
|
|
258
|
-
repeated `assert obj.id is not None` (type clutter).
|
|
259
|
-
|
|
260
|
-
- **Provide a tiny opt-in narrowing helper**
|
|
261
|
-
- Docs context (why `id` starts as `None` and is populated after commit/refresh):
|
|
262
|
-
- `https://sqlmodel.tiangolo.com/tutorial/automatic-id-none-refresh/`
|
|
263
|
-
- Ship a small helper in this package (runtime-free or near-zero runtime) that helps users narrow "persisted" models
|
|
264
|
-
without `cast(...)`/`# type: ignore`, e.g.:
|
|
265
|
-
- `assert_persisted(obj)` / `require_id(obj)` / `ensure_persisted(obj)` returning a `TypeGuard[...]` or using
|
|
266
|
-
overloads to narrow `id` to `int`.
|
|
267
|
-
- Keep it optional and well-documented; default plugin behavior should remain conservative.
|
|
268
|
-
|
|
269
|
-
## v0.17 (`tuple_()` should return a SQL expression, not a Python tuple) - DONE
|
|
270
|
-
|
|
271
|
-
**Motivation**: SQLModel re-exports `tuple_` from `sqlmodel.sql.expression`, but its typing currently says it returns a
|
|
272
|
-
built-in `tuple[Any, ...]`. In SQLAlchemy, `tuple_()` returns a SQL expression object (`sqlalchemy.sql.elements.Tuple`)
|
|
273
|
-
which supports expression methods like `.in_(...)`. Under `--strict` this mismatch forces `cast(...)` / `# type: ignore`
|
|
274
|
-
for otherwise normal SQLAlchemy patterns.
|
|
275
|
-
|
|
276
|
-
- **Fix `sqlmodel.tuple_` / `sqlmodel.sql.expression.tuple_` return type**
|
|
277
|
-
- Add a plugin return-type hook for:
|
|
278
|
-
- `sqlmodel.sql.expression.tuple_`
|
|
279
|
-
- `sqlmodel.tuple_` (re-export)
|
|
280
|
-
- Prefer returning `sqlalchemy.sql.elements.Tuple` (or fall back to `ColumnElement[tuple[Any, ...]]` if stubs vary).
|
|
281
|
-
- Source references:
|
|
282
|
-
- SQLModel wrapper: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/sql/expression.py`
|
|
283
|
-
- SQLAlchemy `tuple_` constructor: `https://github.com/sqlalchemy/sqlalchemy/blob/main/lib/sqlalchemy/sql/_elements_constructors.py`
|
|
284
|
-
- SQLAlchemy `Tuple` expression class: `https://github.com/sqlalchemy/sqlalchemy/blob/main/lib/sqlalchemy/sql/elements.py`
|
|
285
|
-
- **Tests**
|
|
286
|
-
- Add a mypy integration module demonstrating composite IN typing without ignores, e.g.:
|
|
287
|
-
- `tuple_(User.id, User.team_id).in_([(1, 2)])`
|
|
288
|
-
|
|
289
|
-
## v0.18 (Comprehensive typing polish) - DONE
|
|
290
|
-
|
|
291
|
-
This release consolidates several enhancements to round out SQLModel/mypy strict-mode ergonomics.
|
|
292
|
-
|
|
293
|
-
### Recognize `table=True` via `model_config["table"]`
|
|
294
|
-
|
|
295
|
-
SQLModel's runtime `is_table_model_class()` checks `model_config.get("table")`, so a class can be a table model even
|
|
296
|
-
without `class Model(SQLModel, table=True)`.
|
|
297
|
-
|
|
298
|
-
- Extend semantic analysis in `src/sqlmodel_mypy/transform.py` to detect `model_config = ConfigDict(table=True)`.
|
|
299
|
-
- Persist the result in `TypeInfo.metadata["sqlmodel-mypy-metadata"]`, then update `_is_table_model(...)` in both
|
|
300
|
-
`plugin.py` and `transform.py` to consult metadata first.
|
|
301
|
-
- Source references:
|
|
302
|
-
- SQLModel metaclass: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/main.py` (`SQLModelMetaclass.__new__`)
|
|
303
|
-
- SQLModel runtime table detection: `https://github.com/fastapi/sqlmodel/blob/main/sqlmodel/_compat.py`
|
|
304
|
-
|
|
305
|
-
### `select(5+)`: recover tuple element types
|
|
306
|
-
|
|
307
|
-
v0.10 fixed the overload ceiling by accepting `select(5+)`, but the fallback return type was `Select[tuple[Any, ...]]`.
|
|
308
|
-
|
|
309
|
-
- Add a best-effort return-type hook that recovers element types from inferred argument expression types.
|
|
310
|
-
- Code: `_sqlmodel_select_signature_callback()`, `_sqlmodel_select_return_type_callback()`, `_sqlmodel_select_entity_result_type()`
|
|
311
|
-
- Tests: `tests/mypy/modules/select_varargs.py`
|
|
312
|
-
|
|
313
|
-
### Outer-join `None` propagation for aliased/variable join targets
|
|
314
|
-
|
|
315
|
-
Outer-join `None` propagation previously only worked when the join target was a direct model ref or relationship
|
|
316
|
-
attribute, not when users join via an alias variable (e.g. `team_alias = aliased(Team)`).
|
|
317
|
-
|
|
318
|
-
- Extend join target type inference to handle typed variables (including `aliased(Model)` results).
|
|
319
|
-
- Code: `_typeinfo_from_join_target_expr()`, `_typeinfo_from_typed_join_target_expr()`, `_typeinfo_from_typed_join_target_type()`
|
|
320
|
-
- Tests: `tests/mypy/modules/aliased_joins.py`
|
|
321
|
-
|
|
322
|
-
### `getattr(Model, NAME)`: support `Final`/`Literal[...]` names
|
|
323
|
-
|
|
324
|
-
Dynamic query builders often use constants for attribute names; without this, `getattr(...)` falls back to `Any`.
|
|
325
|
-
|
|
326
|
-
- Extend `getattr(Model, NAME)` typing beyond string literals by accepting name args typed as string `Literal[...]`
|
|
327
|
-
(and best-effort `Final` constants where mypy retains the literal).
|
|
328
|
-
- Code: `_sqlmodel_getattr_return_type_callback()`
|
|
329
|
-
- Tests: `tests/mypy/modules/getattr_column_property.py`
|
|
330
|
-
|
|
331
|
-
### `Field(validation_alias=...)`: Pydantic v2 alias parity
|
|
332
|
-
|
|
333
|
-
Pydantic v2 supports `validation_alias=AliasPath(...)` / `AliasChoices(...)`, but SQLModel's `Field(...)` typing is
|
|
334
|
-
narrower; strict mypy users otherwise need `cast(...)`/`# type: ignore`.
|
|
335
|
-
|
|
336
|
-
- Widen `Field(validation_alias=...)` to accept `pydantic.aliases.AliasPath | AliasChoices` (idempotent if upstream
|
|
337
|
-
is already widened).
|
|
338
|
-
- Code: `_sqlmodel_field_signature_callback()`
|
|
339
|
-
- Tests: `tests/mypy/modules/field_validation_alias.py`
|
|
340
|
-
|
|
341
|
-
### Optional typed `Session.execute()` / `AsyncSession.execute()`
|
|
342
|
-
|
|
343
|
-
SQLModel overrides `execute()` and types it as `Result[Any]`, shadowing SQLAlchemy's typed overloads. Even though
|
|
344
|
-
`execute()` is deprecated in SQLModel docs, users still rely on it during migrations.
|
|
345
|
-
|
|
346
|
-
- Add an opt-in mypy plugin behavior that types `execute(select(...))` as `Result[...]` (and preserves async wrapping).
|
|
347
|
-
- Config: `typed_execute = true` under `[sqlmodel-mypy]`
|
|
348
|
-
- Code: `SQLModelPluginConfig.typed_execute`, `_sqlmodel_session_execute_return_type_callback()`, `_row_tuple_type_from_typed_statement()`
|
|
349
|
-
- Tests: `tests/mypy/configs/mypy-plugin-strict-typed-execute.ini`, `tests/mypy/modules/session_execute_typed.py`
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from sqlalchemy.sql.schema import Table
|
|
4
|
-
from sqlmodel import Field, SQLModel
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class User(SQLModel, table=True):
|
|
8
|
-
id: int | None = Field(default=None, primary_key=True)
|
|
9
|
-
name: str = Field()
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
ok_table: Table = User.__table__
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/configs/mypy-plugin.ini
RENAMED
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/configs/pyproject-plugin.toml
RENAMED
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/aliased_joins.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/constructor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/execution_options.py
RENAMED
|
File without changes
|
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/expressions.py
RENAMED
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/field_sa_type.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/inheritance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/mapped_fields.py
RENAMED
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/model_config.py
RENAMED
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/model_construct.py
RENAMED
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/nullable_defaults.py
RENAMED
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/persisted_helpers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/relationships.py
RENAMED
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/select_varargs.py
RENAMED
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/selectinload.py
RENAMED
|
File without changes
|
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/session_get.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/tuple_expression.py
RENAMED
|
File without changes
|
{sqlmodel_mypy_plugin-0.18.0 → sqlmodel_mypy_plugin-0.19.0}/tests/mypy/modules/untyped_fields.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|