test_plugin.py 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. from __future__ import annotations
  2. import os.path
  3. import shutil
  4. import subprocess
  5. import sys
  6. import tempfile
  7. import pytest
  8. def _fill_init_pyi(tmpdir: str, path: str) -> str:
  9. os.makedirs(os.path.join(tmpdir, path))
  10. for part in path.split(os.sep):
  11. tmpdir = os.path.join(tmpdir, part)
  12. open(os.path.join(tmpdir, "__init__.pyi"), "a").close()
  13. return tmpdir
  14. def call_mypy(src: str, *, plugins: list[str] | None = None) -> tuple[int, str]:
  15. if plugins is None:
  16. plugins = ["tools.mypy_helpers.plugin"]
  17. with tempfile.TemporaryDirectory() as tmpdir:
  18. cfg = os.path.join(tmpdir, "mypy.toml")
  19. with open(cfg, "w") as f:
  20. f.write(f"[tool.mypy]\nplugins = {plugins!r}\n")
  21. # we stub several files in order to test our plugin
  22. # the tests cannot depend on sentry being importable (it isn't!)
  23. here = os.path.dirname(__file__)
  24. # stubs for lazy_service_wrapper
  25. utils_dir = _fill_init_pyi(tmpdir, "sentry/utils")
  26. sentry_src = os.path.join(here, "../../../src/sentry/utils/lazy_service_wrapper.py")
  27. shutil.copy(sentry_src, utils_dir)
  28. with open(os.path.join(utils_dir, "__init__.pyi"), "w") as f:
  29. f.write("from typing import Any\ndef __getattr__(k: str) -> Any: ...\n")
  30. # stubs for auth types
  31. auth_dir = _fill_init_pyi(tmpdir, "sentry/auth/services/auth")
  32. with open(os.path.join(auth_dir, "model.pyi"), "w") as f:
  33. f.write("class AuthenticatedToken: ...")
  34. ret = subprocess.run(
  35. (
  36. *(sys.executable, "-m", "mypy"),
  37. *("--config", cfg),
  38. *("-c", src),
  39. "--show-traceback",
  40. # we only stub out limited parts of the sentry source tree
  41. "--ignore-missing-imports",
  42. ),
  43. env={**os.environ, "MYPYPATH": tmpdir},
  44. capture_output=True,
  45. encoding="UTF-8",
  46. )
  47. assert not ret.stderr
  48. return ret.returncode, ret.stdout
  49. def test_invalid_get_connection_call():
  50. code = """
  51. from django.db.transaction import get_connection
  52. with get_connection() as cursor:
  53. cursor.execute("SELECT 1")
  54. """
  55. expected = """\
  56. <string>:4: error: Missing positional argument "using" in call to "get_connection" [call-arg]
  57. Found 1 error in 1 file (checked 1 source file)
  58. """
  59. ret, out = call_mypy(code)
  60. assert ret
  61. assert out == expected
  62. def test_ok_get_connection():
  63. code = """
  64. from django.db.transaction import get_connection
  65. with get_connection("default") as cursor:
  66. cursor.execute("SELECT 1")
  67. """
  68. ret, out = call_mypy(code)
  69. assert ret == 0
  70. def test_invalid_transaction_atomic():
  71. code = """
  72. from django.db import transaction
  73. with transaction.atomic():
  74. value = 10 / 2
  75. """
  76. expected = """\
  77. <string>:4: error: All overload variants of "atomic" require at least one argument [call-overload]
  78. <string>:4: note: Possible overload variants:
  79. <string>:4: note: def [_C: Callable[..., Any]] atomic(using: _C) -> _C
  80. <string>:4: note: def atomic(using: str, savepoint: bool = ..., durable: bool = ...) -> Atomic
  81. Found 1 error in 1 file (checked 1 source file)
  82. """
  83. ret, out = call_mypy(code)
  84. assert ret
  85. assert out == expected
  86. def test_ok_transaction_atomic():
  87. code = """
  88. from django.db import transaction
  89. with transaction.atomic("default"):
  90. value = 10 / 2
  91. """
  92. ret, _ = call_mypy(code)
  93. assert ret == 0
  94. def test_ok_transaction_on_commit():
  95. code = """
  96. from django.db import transaction
  97. def completed():
  98. pass
  99. transaction.on_commit(completed, "default")
  100. """
  101. ret, _ = call_mypy(code)
  102. assert ret == 0
  103. def test_invalid_transaction_on_commit():
  104. code = """
  105. from django.db import transaction
  106. def completed():
  107. pass
  108. transaction.on_commit(completed)
  109. """
  110. expected = """\
  111. <string>:7: error: Missing positional argument "using" in call to "on_commit" [call-arg]
  112. Found 1 error in 1 file (checked 1 source file)
  113. """
  114. ret, out = call_mypy(code)
  115. assert ret
  116. assert out == expected
  117. def test_invalid_transaction_set_rollback():
  118. code = """
  119. from django.db import transaction
  120. transaction.set_rollback(True)
  121. """
  122. expected = """\
  123. <string>:4: error: Missing positional argument "using" in call to "set_rollback" [call-arg]
  124. Found 1 error in 1 file (checked 1 source file)
  125. """
  126. ret, out = call_mypy(code)
  127. assert ret
  128. assert out == expected
  129. def test_ok_transaction_set_rollback():
  130. code = """
  131. from django.db import transaction
  132. transaction.set_rollback(True, "default")
  133. """
  134. ret, _ = call_mypy(code)
  135. assert ret == 0
  136. @pytest.mark.parametrize(
  137. "attr",
  138. (
  139. pytest.param("access", id="access from sentry.api.base"),
  140. pytest.param("auth", id="auth from sentry.middleware.auth"),
  141. pytest.param("csp_nonce", id="csp_nonce from csp.middleware"),
  142. pytest.param("is_sudo", id="is_sudo from sudo.middleware"),
  143. pytest.param("subdomain", id="subdomain from sentry.middleware.subdomain"),
  144. pytest.param("superuser", id="superuser from sentry.middleware.superuser"),
  145. ),
  146. )
  147. def test_added_http_request_attribute(attr: str) -> None:
  148. src = f"""\
  149. from django.http.request import HttpRequest
  150. x: HttpRequest
  151. x.{attr}
  152. """
  153. ret, out = call_mypy(src, plugins=[])
  154. assert ret
  155. ret, out = call_mypy(src)
  156. assert ret == 0, (ret, out)
  157. def test_adjusted_drf_request_auth() -> None:
  158. src = """\
  159. from rest_framework.request import Request
  160. x: Request
  161. reveal_type(x.auth)
  162. """
  163. expected_no_plugins = """\
  164. <string>:3: note: Revealed type is "Union[rest_framework.authtoken.models.Token, Any]"
  165. Success: no issues found in 1 source file
  166. """
  167. expected_plugins = """\
  168. <string>:3: note: Revealed type is "Union[sentry.auth.services.auth.model.AuthenticatedToken, None]"
  169. Success: no issues found in 1 source file
  170. """
  171. ret, out = call_mypy(src, plugins=[])
  172. assert ret == 0
  173. assert out == expected_no_plugins
  174. ret, out = call_mypy(src)
  175. assert ret == 0
  176. assert out == expected_plugins
  177. def test_lazy_service_wrapper() -> None:
  178. src = """\
  179. from typing import assert_type, Literal
  180. from sentry.utils.lazy_service_wrapper import LazyServiceWrapper, Service, _EmptyType
  181. class MyService(Service):
  182. X = "hello world"
  183. def f(self) -> int:
  184. return 5
  185. backend = LazyServiceWrapper(MyService, "some.path", {})
  186. # should proxy attributes properly
  187. assert_type(backend.X, str)
  188. assert_type(backend.f(), int)
  189. # should represent self types properly
  190. assert_type(backend._backend, str)
  191. assert_type(backend._wrapped, _EmptyType | MyService)
  192. """
  193. expected = """\
  194. <string>:12: error: Expression is of type "Any", not "str" [assert-type]
  195. <string>:13: error: Expression is of type "Any", not "int" [assert-type]
  196. Found 2 errors in 1 file (checked 1 source file)
  197. """
  198. ret, out = call_mypy(src, plugins=[])
  199. assert ret
  200. assert out == expected
  201. ret, out = call_mypy(src)
  202. assert ret == 0