--- /dev/null
+.. change::
+ :tags: bug, mypy
+ :tickets: 7321
+
+ Fixed Mypy crash which would occur when using Mypy plugin against code
+ which made use of :class:`_orm.declared_attr` methods for non-mapped names
+ like ``__mapper_args__``, ``__table_args__``, or other dunder names, as the
+ plugin would try to interpret these as mapped attributes which would then
+ be later mis-handled. As part of this change, the decorated function is
+ still converted by the plugin into a generic assignment statement (e.g.
+ ``__mapper_args__: Any``) so that the argument signature can continue to be
+ annotated in the same way one would for any other ``@classmethod`` without
+ Mypy complaining about the wrong argument type for a method that isn't
+ explicitly ``@classmethod``.
+
+
left_hand_explicit_type: Optional[ProperType] = None
- if isinstance(stmt.func.type, CallableType):
+ if util.name_is_dunder(stmt.name):
+ # for dunder names like __table_args__, __tablename__,
+ # __mapper_args__ etc., rewrite these as simple assignment
+ # statements; otherwise mypy doesn't like if the decorated
+ # function has an annotation like ``cls: Type[Foo]`` because
+ # it isn't @classmethod
+ any_ = AnyType(TypeOfAny.special_form)
+ left_node = NameExpr(stmt.var.name)
+ left_node.node = stmt.var
+ new_stmt = AssignmentStmt([left_node], TempNode(any_))
+ new_stmt.type = left_node.node.type
+ cls.defs.body[dec_index] = new_stmt
+ return
+ elif isinstance(stmt.func.type, CallableType):
func_type = stmt.func.type.ret_type
if isinstance(func_type, UnboundType):
type_id = names.type_id_for_unbound_type(func_type, cls, api)
+import re
from typing import Any
from typing import Iterable
from typing import Iterator
return cls(typ=typ, info=info, **data)
+def name_is_dunder(name):
+ return bool(re.match(r"^__.+?__$", name))
+
+
def _set_info_metadata(info: TypeInfo, key: str, data: Any) -> None:
info.metadata.setdefault("sqlalchemy", {})[key] = data
--- /dev/null
+from typing import Any
+
+from sqlalchemy.orm import declarative_base
+from sqlalchemy.orm import declared_attr
+
+
+Base = declarative_base()
+
+
+class Foo(Base):
+ @declared_attr
+ def __tablename__(cls) -> str:
+ return "name"
+
+ @declared_attr
+ def __mapper_args__(cls) -> dict[Any, Any]:
+ return {}
+
+ @declared_attr
+ def __table_args__(cls) -> dict[Any, Any]:
+ return {}
--- /dev/null
+from typing import Any
+from typing import Type
+
+from sqlalchemy.orm import declarative_base
+from sqlalchemy.orm import declared_attr
+
+
+Base = declarative_base()
+
+
+class Foo(Base):
+ # no mypy error emitted regarding the
+ # Type[Foo] part
+ @declared_attr
+ def __tablename__(cls: Type["Foo"]) -> str:
+ return "name"
+
+ @declared_attr
+ def __mapper_args__(cls: Type["Foo"]) -> dict[Any, Any]:
+ return {}
+
+ # this was a workaround that works if there's no plugin present, make
+ # sure that doesn't crash anything
+ @classmethod
+ @declared_attr
+ def __table_args__(cls: Type["Foo"]) -> dict[Any, Any]:
+ return {}