plugin.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. from __future__ import annotations
  2. from typing import Callable
  3. from mypy.nodes import ARG_POS
  4. from mypy.plugin import ClassDefContext, FunctionSigContext, Plugin
  5. from mypy.plugins.common import add_attribute_to_class
  6. from mypy.types import AnyType, CallableType, FunctionLike, Instance, NoneType, TypeOfAny, UnionType
  7. def _make_using_required_str(ctx: FunctionSigContext) -> CallableType:
  8. sig = ctx.default_signature
  9. using_arg = sig.argument_by_name("using")
  10. if using_arg is None or using_arg.pos is None:
  11. ctx.api.fail("The using parameter is required", ctx.context)
  12. return sig
  13. for kind in sig.arg_kinds[: using_arg.pos]:
  14. if kind != ARG_POS:
  15. ctx.api.fail("Expected using to be the first optional", ctx.context)
  16. return sig
  17. str_type = ctx.api.named_generic_type("builtins.str", [])
  18. arg_kinds = [*sig.arg_kinds[: using_arg.pos], ARG_POS, *sig.arg_kinds[using_arg.pos + 1 :]]
  19. arg_types = [*sig.arg_types[: using_arg.pos], str_type, *sig.arg_types[using_arg.pos + 1 :]]
  20. return sig.copy_modified(arg_kinds=arg_kinds, arg_types=arg_types)
  21. def replace_transaction_atomic_sig_callback(ctx: FunctionSigContext) -> CallableType:
  22. sig = ctx.default_signature
  23. if not sig.argument_by_name("using"):
  24. # No using arg in the signature, bail
  25. return sig
  26. # We care about context managers.
  27. if not isinstance(sig.ret_type, Instance):
  28. return sig
  29. return _make_using_required_str(ctx)
  30. _FUNCTION_SIGNATURE_HOOKS = {
  31. "django.db.transaction.atomic": replace_transaction_atomic_sig_callback,
  32. "django.db.transaction.get_connection": _make_using_required_str,
  33. "django.db.transaction.on_commit": _make_using_required_str,
  34. "django.db.transaction.set_rollback": _make_using_required_str,
  35. }
  36. def _adjust_http_request_members(ctx: ClassDefContext) -> None:
  37. if ctx.cls.name == "HttpRequest":
  38. # added by sentry.api.base and sentry.web.frontend.base
  39. # TODO: idk why I can't use the real type here :/
  40. add_attribute_to_class(ctx.api, ctx.cls, "access", AnyType(TypeOfAny.explicit))
  41. # added by sentry.middleware.auth
  42. # TODO: figure out how to get the real types here
  43. add_attribute_to_class(ctx.api, ctx.cls, "auth", AnyType(TypeOfAny.explicit))
  44. # added by csp.middleware.CSPMiddleware
  45. add_attribute_to_class(ctx.api, ctx.cls, "csp_nonce", ctx.api.named_type("builtins.str"))
  46. # added by sudo.middleware.SudoMiddleware
  47. # this is slightly better than a method returning bool for overriding
  48. returns_bool = CallableType(
  49. arg_types=[],
  50. arg_kinds=[],
  51. arg_names=[],
  52. ret_type=ctx.api.named_type("builtins.bool"),
  53. fallback=ctx.api.named_type("builtins.function"),
  54. name="is_sudo",
  55. )
  56. add_attribute_to_class(ctx.api, ctx.cls, "is_sudo", returns_bool)
  57. # added by sentry.middleware.subdomain
  58. subdomain_tp = UnionType([NoneType(), ctx.api.named_type("builtins.str")])
  59. add_attribute_to_class(ctx.api, ctx.cls, "subdomain", subdomain_tp)
  60. # added by sentry.middleware.superuser
  61. # TODO: figure out how to get the real types here
  62. add_attribute_to_class(ctx.api, ctx.cls, "superuser", AnyType(TypeOfAny.explicit))
  63. class SentryMypyPlugin(Plugin):
  64. def get_function_signature_hook(
  65. self, fullname: str
  66. ) -> Callable[[FunctionSigContext], FunctionLike] | None:
  67. return _FUNCTION_SIGNATURE_HOOKS.get(fullname)
  68. def get_base_class_hook(self, fullname: str) -> Callable[[ClassDefContext], None] | None:
  69. # XXX: this is a hack -- I don't know if there's a better callback to modify a class
  70. if fullname == "io.BytesIO":
  71. return _adjust_http_request_members
  72. else:
  73. return None
  74. def plugin(version: str) -> type[SentryMypyPlugin]:
  75. return SentryMypyPlugin