]> 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>
Tue, 17 Jan 2023 19:02:36 +0000 (14:02 -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
(cherry picked from commit cf64582f61b15716228302f669322d7efa1003c1)
(cherry picked from commit 36285760238314f70eed4532ca2c2c0c2d684609)

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/files/issue_9102.py [new file with mode: 0644]
test/ext/mypy/files/issue_9102_workaround.py [new file with mode: 0644]
test/ext/mypy/test_mypy_plugin_py3k.py

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 99be194cdccc9d9d2f94d6cc36d487f12c97a11b..ad81c15b1d80d71d803e455ab6aaa26cd3b663f7 100644 (file)
@@ -164,7 +164,10 @@ def re_apply_declarative_assignments(
 
                 update_cls_metadata = True
 
-            if python_type_for_type is not None:
+            if python_type_for_type is not None and (
+                not isinstance(left_node.type, Instance)
+                or left_node.type.type.fullname != NAMED_TYPE_SQLA_MAPPED
+            ):
                 left_node.type = api.named_type(
                     NAMED_TYPE_SQLA_MAPPED, [python_type_for_type]
                 )
@@ -201,15 +204,23 @@ 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:
         left_node.type = api.named_type(
             NAMED_TYPE_SQLA_MAPPED, [left_hand_explicit_type]
         )
     else:
         lvalue.is_inferred_def = False
-        left_node.type = api.named_type(
+        left_node.type = stmt.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:
@@ -226,6 +237,11 @@ def apply_type_to_mapped_statement(
     # internally
     stmt.rvalue = util.expr_to_mapped_constructor(stmt.rvalue)
 
+    if stmt.type is None or python_type_for_type is None:
+        stmt.type = api.named_type(
+            NAMED_TYPE_SQLA_MAPPED, [AnyType(TypeOfAny.special_form)]
+        )
+
 
 def add_additional_orm_attributes(
     cls: ClassDef,
index 8687012a1e45e874a3ac0c02236b14799c44b071..bd2dd79d62ae90cf8776d427dfda4bedd0e94558 100644 (file)
@@ -184,10 +184,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/files/issue_9102.py b/test/ext/mypy/files/issue_9102.py
new file mode 100644 (file)
index 0000000..a9eea7c
--- /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[Integer]", variable has type "int")  # noqa: E501
+    id: int = Column(Integer(), primary_key=True)
diff --git a/test/ext/mypy/files/issue_9102_workaround.py b/test/ext/mypy/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)
index 3df758c56db1097a2f70716d631c9b2cb2cfe185..cb04d1c739a901adefaea50043cda5bfdc1162d0 100644 (file)
@@ -76,6 +76,10 @@ class MypyPluginTest(fixtures.TestBase):
                 shutil.copyfile(path, test_program)
                 args.append(test_program)
 
+            # I set this locally but for the suite here needs to be
+            # disabled
+            os.environ.pop("MYPY_FORCE_COLOR", None)
+
             result = api.run(args)
             return result