From: Mike Bayer Date: Tue, 17 Jan 2023 01:17:50 +0000 (-0500) Subject: mypy plugin fixes X-Git-Tag: rel_2_0_0rc3~6 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0e2b3aa323f62f46a342a8a8bba35323e214d300;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git mypy plugin fixes 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 --- diff --git a/doc/build/changelog/unreleased_14/mypy_fix.rst b/doc/build/changelog/unreleased_14/mypy_fix.rst new file mode 100644 index 0000000000..d383c77287 --- /dev/null +++ b/doc/build/changelog/unreleased_14/mypy_fix.rst @@ -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``). diff --git a/lib/sqlalchemy/ext/mypy/apply.py b/lib/sqlalchemy/ext/mypy/apply.py index f392a85a75..2211561b1b 100644 --- a/lib/sqlalchemy/ext/mypy/apply.py +++ b/lib/sqlalchemy/ext/mypy/apply.py @@ -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, diff --git a/lib/sqlalchemy/ext/mypy/plugin.py b/lib/sqlalchemy/ext/mypy/plugin.py index e21f1fcb09..22e5283872 100644 --- a/lib/sqlalchemy/ext/mypy/plugin.py +++ b/lib/sqlalchemy/ext/mypy/plugin.py @@ -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 index 0000000000..aec840189c --- /dev/null +++ b/test/ext/mypy/plugin_files/issue_9102.py @@ -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 index 0000000000..3682d29b23 --- /dev/null +++ b/test/ext/mypy/plugin_files/issue_9102_workaround.py @@ -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)