]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
mypy plugin fixes
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 17 Jan 2023 01:17:50 +0000 (20:17 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 18 Jan 2023 13:56:17 +0000 (08:56 -0500)
Adjustments made to the mypy plugin to accommodate for some potential
changes being made for issue #236 sqlalchemy2-stubs when using SQLAlchemy
1.4. These changes are being kept in sync within SQLAlchemy 2.0.
The changes are also backwards compatible with older versions of
sqlalchemy2-stubs.

Fixed crash in mypy plugin which could occur on both 1.4 and 2.0 versions
if a decorator for the :func:`_orm.registry.mapped` decorator were used
that was referenced in an expression with more than two components (e.g.
``@Backend.mapper_registry.mapped``). This scenario is now ignored; when
using the plugin, the decorator expression needs to be two components (i.e.
``@reg.mapped``).

References: https://github.com/sqlalchemy/sqlalchemy2-stubs/issues/236
Fixes: #9102
Change-Id: Ieb1bf7bf8184645bcd43253e57f1c267b2640537

doc/build/changelog/unreleased_14/mypy_fix.rst [new file with mode: 0644]
lib/sqlalchemy/ext/mypy/apply.py
lib/sqlalchemy/ext/mypy/plugin.py
test/ext/mypy/plugin_files/issue_9102.py [new file with mode: 0644]
test/ext/mypy/plugin_files/issue_9102_workaround.py [new file with mode: 0644]

diff --git a/doc/build/changelog/unreleased_14/mypy_fix.rst b/doc/build/changelog/unreleased_14/mypy_fix.rst
new file mode 100644 (file)
index 0000000..d383c77
--- /dev/null
@@ -0,0 +1,22 @@
+.. change::
+    :tags: bug, mypy
+    :versions: 2.0.0rc3
+
+    Adjustments made to the mypy plugin to accommodate for some potential
+    changes being made for issue #236 sqlalchemy2-stubs when using SQLAlchemy
+    1.4. These changes are being kept in sync within SQLAlchemy 2.0.
+    The changes are also backwards compatible with older versions of
+    sqlalchemy2-stubs.
+
+
+.. change::
+    :tags: bug, mypy
+    :tickets: 9102
+    :versions: 2.0.0rc3
+
+    Fixed crash in mypy plugin which could occur on both 1.4 and 2.0 versions
+    if a decorator for the :func:`_orm.registry.mapped` decorator were used
+    that was referenced in an expression with more than two components (e.g.
+    ``@Backend.mapper_registry.mapped``). This scenario is now ignored; when
+    using the plugin, the decorator expression needs to be two components (i.e.
+    ``@reg.mapped``).
index f392a85a751f8da7dfecd81dd08de6973643cb84..2211561b1be110caf9edcafe9ba490659776ed92 100644 (file)
@@ -169,9 +169,6 @@ def re_apply_declarative_assignments(
 
                     update_cls_metadata = True
 
-            # for some reason if you have a Mapped type explicitly annotated,
-            # and here you set it again, mypy forgets how to do descriptors.
-            # no idea.  100% feeling around in the dark to see what sticks
             if (
                 not isinstance(left_node.type, Instance)
                 or left_node.type.type.fullname != NAMED_TYPE_SQLA_MAPPED
@@ -213,6 +210,12 @@ def apply_type_to_mapped_statement(
     left_node = lvalue.node
     assert isinstance(left_node, Var)
 
+    # to be completely honest I have no idea what the difference between
+    # left_node.type and stmt.type is, what it means if these are different
+    # vs. the same, why in order to get tests to pass I have to assign
+    # to stmt.type for the second case and not the first.  this is complete
+    # trying every combination until it works stuff.
+
     if left_hand_explicit_type is not None:
         lvalue.is_inferred_def = False
         left_node.type = api.named_type(
@@ -222,7 +225,9 @@ def apply_type_to_mapped_statement(
         lvalue.is_inferred_def = False
         left_node.type = api.named_type(
             NAMED_TYPE_SQLA_MAPPED,
-            [] if python_type_for_type is None else [python_type_for_type],
+            [AnyType(TypeOfAny.special_form)]
+            if python_type_for_type is None
+            else [python_type_for_type],
         )
 
     # so to have it skip the right side totally, we can do this:
@@ -239,6 +244,9 @@ def apply_type_to_mapped_statement(
     # internally
     stmt.rvalue = expr_to_mapped_constructor(stmt.rvalue)
 
+    if stmt.type is not None and python_type_for_type is not None:
+        stmt.type = python_type_for_type
+
 
 def add_additional_orm_attributes(
     cls: ClassDef,
index e21f1fcb099062f6e14e314ae2084562d77fe0ec..22e52838725a63898a05f2a48aa3eed8712d2d34 100644 (file)
@@ -201,10 +201,13 @@ def _fill_in_decorators(ctx: ClassDefContext) -> None:
         else:
             continue
 
-        assert isinstance(target.expr, NameExpr)
-        sym = ctx.api.lookup_qualified(
-            target.expr.name, target, suppress_errors=True
-        )
+        if isinstance(target.expr, NameExpr):
+            sym = ctx.api.lookup_qualified(
+                target.expr.name, target, suppress_errors=True
+            )
+        else:
+            continue
+
         if sym and sym.node:
             sym_type = get_proper_type(sym.type)
             if isinstance(sym_type, Instance):
diff --git a/test/ext/mypy/plugin_files/issue_9102.py b/test/ext/mypy/plugin_files/issue_9102.py
new file mode 100644 (file)
index 0000000..aec8401
--- /dev/null
@@ -0,0 +1,18 @@
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy.orm import registry
+
+
+class BackendMeta:
+    __abstract__ = True
+    mapped_registry: registry = registry()
+    metadata = mapped_registry.metadata
+
+
+# this decorator is not picked up now, but at least it doesn't crash
+@BackendMeta.mapped_registry.mapped
+class User:
+    __tablename__ = "user"
+
+    # EXPECTED_MYPY: Incompatible types in assignment (expression has type "Column[int]", variable has type "int")
+    id: int = Column(Integer(), primary_key=True)
diff --git a/test/ext/mypy/plugin_files/issue_9102_workaround.py b/test/ext/mypy/plugin_files/issue_9102_workaround.py
new file mode 100644 (file)
index 0000000..3682d29
--- /dev/null
@@ -0,0 +1,19 @@
+from sqlalchemy import Column
+from sqlalchemy import Integer
+from sqlalchemy.orm import registry
+
+
+class BackendMeta:
+    __abstract__ = True
+    mapped_registry: registry = registry()
+    metadata = mapped_registry.metadata
+
+
+reg: registry = BackendMeta.mapped_registry
+
+
+@reg.mapped
+class User:
+    __tablename__ = "user"
+
+    id: int = Column(Integer(), primary_key=True)