Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

297 rindas
9.4KB

  1. # ext/mypy/plugin.py
  2. # Copyright (C) 2021 the SQLAlchemy authors and contributors
  3. # <see AUTHORS file>
  4. #
  5. # This module is part of SQLAlchemy and is released under
  6. # the MIT License: http://www.opensource.org/licenses/mit-license.php
  7. """
  8. Mypy plugin for SQLAlchemy ORM.
  9. """
  10. from typing import Callable
  11. from typing import List
  12. from typing import Optional
  13. from typing import Tuple
  14. from typing import Type as TypingType
  15. from typing import Union
  16. from mypy import nodes
  17. from mypy.mro import calculate_mro
  18. from mypy.mro import MroError
  19. from mypy.nodes import Block
  20. from mypy.nodes import ClassDef
  21. from mypy.nodes import GDEF
  22. from mypy.nodes import MypyFile
  23. from mypy.nodes import NameExpr
  24. from mypy.nodes import SymbolTable
  25. from mypy.nodes import SymbolTableNode
  26. from mypy.nodes import TypeInfo
  27. from mypy.plugin import AttributeContext
  28. from mypy.plugin import ClassDefContext
  29. from mypy.plugin import DynamicClassDefContext
  30. from mypy.plugin import Plugin
  31. from mypy.plugin import SemanticAnalyzerPluginInterface
  32. from mypy.types import get_proper_type
  33. from mypy.types import Instance
  34. from mypy.types import Type
  35. from . import decl_class
  36. from . import names
  37. from . import util
  38. class SQLAlchemyPlugin(Plugin):
  39. def get_dynamic_class_hook(
  40. self, fullname: str
  41. ) -> Optional[Callable[[DynamicClassDefContext], None]]:
  42. if names._type_id_for_fullname(fullname) is names.DECLARATIVE_BASE:
  43. return _dynamic_class_hook
  44. return None
  45. def get_base_class_hook(
  46. self, fullname: str
  47. ) -> Optional[Callable[[ClassDefContext], None]]:
  48. # kind of a strange relationship between get_metaclass_hook()
  49. # and get_base_class_hook(). the former doesn't fire off for
  50. # subclasses. but then you can just check it here from the "base"
  51. # and get the same effect.
  52. sym = self.lookup_fully_qualified(fullname)
  53. if (
  54. sym
  55. and isinstance(sym.node, TypeInfo)
  56. and sym.node.metaclass_type
  57. and names._type_id_for_named_node(sym.node.metaclass_type.type)
  58. is names.DECLARATIVE_META
  59. ):
  60. return _base_cls_hook
  61. return None
  62. def get_class_decorator_hook(
  63. self, fullname: str
  64. ) -> Optional[Callable[[ClassDefContext], None]]:
  65. sym = self.lookup_fully_qualified(fullname)
  66. if sym is not None and sym.node is not None:
  67. type_id = names._type_id_for_named_node(sym.node)
  68. if type_id is names.MAPPED_DECORATOR:
  69. return _cls_decorator_hook
  70. elif type_id in (
  71. names.AS_DECLARATIVE,
  72. names.AS_DECLARATIVE_BASE,
  73. ):
  74. return _base_cls_decorator_hook
  75. elif type_id is names.DECLARATIVE_MIXIN:
  76. return _declarative_mixin_hook
  77. return None
  78. def get_customize_class_mro_hook(
  79. self, fullname: str
  80. ) -> Optional[Callable[[ClassDefContext], None]]:
  81. return _fill_in_decorators
  82. def get_attribute_hook(
  83. self, fullname: str
  84. ) -> Optional[Callable[[AttributeContext], Type]]:
  85. if fullname.startswith(
  86. "sqlalchemy.orm.attributes.QueryableAttribute."
  87. ):
  88. return _queryable_getattr_hook
  89. return None
  90. def get_additional_deps(
  91. self, file: MypyFile
  92. ) -> List[Tuple[int, str, int]]:
  93. return [
  94. (10, "sqlalchemy.orm.attributes", -1),
  95. (10, "sqlalchemy.orm.decl_api", -1),
  96. ]
  97. def plugin(version: str) -> TypingType[SQLAlchemyPlugin]:
  98. return SQLAlchemyPlugin
  99. def _queryable_getattr_hook(ctx: AttributeContext) -> Type:
  100. # how do I....tell it it has no attribute of a certain name?
  101. # can't find any Type that seems to match that
  102. return ctx.default_attr_type
  103. def _fill_in_decorators(ctx: ClassDefContext) -> None:
  104. for decorator in ctx.cls.decorators:
  105. # set the ".fullname" attribute of a class decorator
  106. # that is a MemberExpr. This causes the logic in
  107. # semanal.py->apply_class_plugin_hooks to invoke the
  108. # get_class_decorator_hook for our "registry.map_class()"
  109. # and "registry.as_declarative_base()" methods.
  110. # this seems like a bug in mypy that these decorators are otherwise
  111. # skipped.
  112. if (
  113. isinstance(decorator, nodes.CallExpr)
  114. and isinstance(decorator.callee, nodes.MemberExpr)
  115. and decorator.callee.name == "as_declarative_base"
  116. ):
  117. target = decorator.callee
  118. elif (
  119. isinstance(decorator, nodes.MemberExpr)
  120. and decorator.name == "mapped"
  121. ):
  122. target = decorator
  123. else:
  124. continue
  125. assert isinstance(target.expr, NameExpr)
  126. sym = ctx.api.lookup_qualified(
  127. target.expr.name, target, suppress_errors=True
  128. )
  129. if sym and sym.node:
  130. sym_type = get_proper_type(sym.type)
  131. if isinstance(sym_type, Instance):
  132. target.fullname = f"{sym_type.type.fullname}.{target.name}"
  133. else:
  134. # if the registry is in the same file as where the
  135. # decorator is used, it might not have semantic
  136. # symbols applied and we can't get a fully qualified
  137. # name or an inferred type, so we are actually going to
  138. # flag an error in this case that they need to annotate
  139. # it. The "registry" is declared just
  140. # once (or few times), so they have to just not use
  141. # type inference for its assignment in this one case.
  142. util.fail(
  143. ctx.api,
  144. "Class decorator called %s(), but we can't "
  145. "tell if it's from an ORM registry. Please "
  146. "annotate the registry assignment, e.g. "
  147. "my_registry: registry = registry()" % target.name,
  148. sym.node,
  149. )
  150. def _add_globals(ctx: Union[ClassDefContext, DynamicClassDefContext]) -> None:
  151. """Add __sa_DeclarativeMeta and __sa_Mapped symbol to the global space
  152. for all class defs
  153. """
  154. util.add_global(
  155. ctx,
  156. "sqlalchemy.orm.decl_api",
  157. "DeclarativeMeta",
  158. "__sa_DeclarativeMeta",
  159. )
  160. util.add_global(ctx, "sqlalchemy.orm.attributes", "Mapped", "__sa_Mapped")
  161. def _cls_metadata_hook(ctx: ClassDefContext) -> None:
  162. _add_globals(ctx)
  163. decl_class._scan_declarative_assignments_and_apply_types(ctx.cls, ctx.api)
  164. def _base_cls_hook(ctx: ClassDefContext) -> None:
  165. _add_globals(ctx)
  166. decl_class._scan_declarative_assignments_and_apply_types(ctx.cls, ctx.api)
  167. def _declarative_mixin_hook(ctx: ClassDefContext) -> None:
  168. _add_globals(ctx)
  169. decl_class._scan_declarative_assignments_and_apply_types(
  170. ctx.cls, ctx.api, is_mixin_scan=True
  171. )
  172. def _cls_decorator_hook(ctx: ClassDefContext) -> None:
  173. _add_globals(ctx)
  174. assert isinstance(ctx.reason, nodes.MemberExpr)
  175. expr = ctx.reason.expr
  176. assert isinstance(expr, nodes.RefExpr) and isinstance(expr.node, nodes.Var)
  177. node_type = get_proper_type(expr.node.type)
  178. assert (
  179. isinstance(node_type, Instance)
  180. and names._type_id_for_named_node(node_type.type) is names.REGISTRY
  181. )
  182. decl_class._scan_declarative_assignments_and_apply_types(ctx.cls, ctx.api)
  183. def _base_cls_decorator_hook(ctx: ClassDefContext) -> None:
  184. _add_globals(ctx)
  185. cls = ctx.cls
  186. _make_declarative_meta(ctx.api, cls)
  187. decl_class._scan_declarative_assignments_and_apply_types(
  188. cls, ctx.api, is_mixin_scan=True
  189. )
  190. def _dynamic_class_hook(ctx: DynamicClassDefContext) -> None:
  191. """Generate a declarative Base class when the declarative_base() function
  192. is encountered."""
  193. _add_globals(ctx)
  194. cls = ClassDef(ctx.name, Block([]))
  195. cls.fullname = ctx.api.qualified_name(ctx.name)
  196. info = TypeInfo(SymbolTable(), cls, ctx.api.cur_mod_id)
  197. cls.info = info
  198. _make_declarative_meta(ctx.api, cls)
  199. cls_arg = util._get_callexpr_kwarg(ctx.call, "cls", expr_types=(NameExpr,))
  200. if cls_arg is not None and isinstance(cls_arg.node, TypeInfo):
  201. decl_class._scan_declarative_assignments_and_apply_types(
  202. cls_arg.node.defn, ctx.api, is_mixin_scan=True
  203. )
  204. info.bases = [Instance(cls_arg.node, [])]
  205. else:
  206. obj = ctx.api.named_type("__builtins__.object")
  207. info.bases = [obj]
  208. try:
  209. calculate_mro(info)
  210. except MroError:
  211. util.fail(
  212. ctx.api, "Not able to calculate MRO for declarative base", ctx.call
  213. )
  214. obj = ctx.api.named_type("__builtins__.object")
  215. info.bases = [obj]
  216. info.fallback_to_any = True
  217. ctx.api.add_symbol_table_node(ctx.name, SymbolTableNode(GDEF, info))
  218. def _make_declarative_meta(
  219. api: SemanticAnalyzerPluginInterface, target_cls: ClassDef
  220. ) -> None:
  221. declarative_meta_name: NameExpr = NameExpr("__sa_DeclarativeMeta")
  222. declarative_meta_name.kind = GDEF
  223. declarative_meta_name.fullname = "sqlalchemy.orm.decl_api.DeclarativeMeta"
  224. # installed by _add_globals
  225. sym = api.lookup_qualified("__sa_DeclarativeMeta", target_cls)
  226. assert sym is not None and isinstance(sym.node, nodes.TypeInfo)
  227. declarative_meta_typeinfo = sym.node
  228. declarative_meta_name.node = declarative_meta_typeinfo
  229. target_cls.metaclass = declarative_meta_name
  230. declarative_meta_instance = Instance(declarative_meta_typeinfo, [])
  231. info = target_cls.info
  232. info.declared_metaclass = info.metaclass_type = declarative_meta_instance