/test/ext/mypy/test_mypy_plugin_py3k.py

https://bitbucket.org/zzzeek/sqlalchemy · Python · 210 lines · 171 code · 32 blank · 7 comment · 44 complexity · fd3950848b14bca4ba7b1468e21301a2 MD5 · raw file

  1. import os
  2. import re
  3. import shutil
  4. import sys
  5. import tempfile
  6. from sqlalchemy import testing
  7. from sqlalchemy.testing import config
  8. from sqlalchemy.testing import eq_
  9. from sqlalchemy.testing import fixtures
  10. class MypyPluginTest(fixtures.TestBase):
  11. __requires__ = ("sqlalchemy2_stubs",)
  12. @testing.fixture(scope="function")
  13. def per_func_cachedir(self):
  14. for item in self._cachedir():
  15. yield item
  16. @testing.fixture(scope="class")
  17. def cachedir(self):
  18. for item in self._cachedir():
  19. yield item
  20. def _cachedir(self):
  21. with tempfile.TemporaryDirectory() as cachedir:
  22. with open(
  23. os.path.join(cachedir, "sqla_mypy_config.cfg"), "w"
  24. ) as config_file:
  25. config_file.write(
  26. """
  27. [mypy]\n
  28. plugins = sqlalchemy.ext.mypy.plugin\n
  29. """
  30. )
  31. with open(
  32. os.path.join(cachedir, "plain_mypy_config.cfg"), "w"
  33. ) as config_file:
  34. config_file.write(
  35. """
  36. [mypy]\n
  37. """
  38. )
  39. yield cachedir
  40. @testing.fixture()
  41. def mypy_runner(self, cachedir):
  42. from mypy import api
  43. def run(path, use_plugin=True, incremental=False):
  44. args = [
  45. "--strict",
  46. "--raise-exceptions",
  47. "--cache-dir",
  48. cachedir,
  49. "--config-file",
  50. os.path.join(
  51. cachedir,
  52. "sqla_mypy_config.cfg"
  53. if use_plugin
  54. else "plain_mypy_config.cfg",
  55. ),
  56. ]
  57. args.append(path)
  58. return api.run(args)
  59. return run
  60. def _incremental_dirs():
  61. path = os.path.join(os.path.dirname(__file__), "incremental")
  62. files = []
  63. for d in os.listdir(path):
  64. if os.path.isdir(os.path.join(path, d)):
  65. files.append(
  66. os.path.join(os.path.dirname(__file__), "incremental", d)
  67. )
  68. for extra_dir in testing.config.options.mypy_extra_test_paths:
  69. if extra_dir and os.path.isdir(extra_dir):
  70. for d in os.listdir(os.path.join(extra_dir, "incremental")):
  71. if os.path.isdir(os.path.join(path, d)):
  72. files.append(os.path.join(extra_dir, "incremental", d))
  73. return files
  74. @testing.combinations(
  75. *[(pathname,) for pathname in _incremental_dirs()], argnames="pathname"
  76. )
  77. @testing.requires.patch_library
  78. def test_incremental(self, mypy_runner, per_func_cachedir, pathname):
  79. import patch
  80. cachedir = per_func_cachedir
  81. dest = os.path.join(cachedir, "mymodel")
  82. os.mkdir(dest)
  83. patches = set()
  84. print("incremental test: %s" % pathname)
  85. for fname in os.listdir(pathname):
  86. if fname.endswith(".py"):
  87. shutil.copy(
  88. os.path.join(pathname, fname), os.path.join(dest, fname)
  89. )
  90. print("copying to: %s" % os.path.join(dest, fname))
  91. elif fname.endswith(".testpatch"):
  92. patches.add(fname)
  93. for patchfile in [None] + sorted(patches):
  94. if patchfile is not None:
  95. print("Applying patchfile %s" % patchfile)
  96. patch_obj = patch.fromfile(os.path.join(pathname, patchfile))
  97. assert patch_obj.apply(1, dest), (
  98. "pathfile %s failed" % patchfile
  99. )
  100. print("running mypy against %s" % dest)
  101. result = mypy_runner(
  102. dest,
  103. use_plugin=True,
  104. incremental=True,
  105. )
  106. eq_(
  107. result[2],
  108. 0,
  109. msg="Failure after applying patch %s: %s"
  110. % (patchfile, result[0]),
  111. )
  112. def _file_combinations():
  113. path = os.path.join(os.path.dirname(__file__), "files")
  114. files = []
  115. for f in os.listdir(path):
  116. if f.endswith(".py"):
  117. files.append(
  118. os.path.join(os.path.dirname(__file__), "files", f)
  119. )
  120. for extra_dir in testing.config.options.mypy_extra_test_paths:
  121. if extra_dir and os.path.isdir(extra_dir):
  122. for f in os.listdir(os.path.join(extra_dir, "files")):
  123. if f.endswith(".py"):
  124. files.append(os.path.join(extra_dir, "files", f))
  125. return files
  126. @testing.combinations(
  127. *[(filename,) for filename in _file_combinations()], argnames="path"
  128. )
  129. def test_mypy(self, mypy_runner, path):
  130. filename = os.path.basename(path)
  131. use_plugin = True
  132. expected_errors = []
  133. expected_re = re.compile(r"\s*# EXPECTED(_MYPY)?: (.+)")
  134. py_ver_re = re.compile(r"^#\s*PYTHON_VERSION\s?>=\s?(\d+\.\d+)")
  135. with open(path) as file_:
  136. for num, line in enumerate(file_, 1):
  137. m = py_ver_re.match(line)
  138. if m:
  139. major, _, minor = m.group(1).partition(".")
  140. if sys.version_info < (int(major), int(minor)):
  141. config.skip_test(
  142. "Requires python >= %s" % (m.group(1))
  143. )
  144. continue
  145. if line.startswith("# NOPLUGINS"):
  146. use_plugin = False
  147. continue
  148. m = expected_re.match(line)
  149. if m:
  150. is_mypy = bool(m.group(1))
  151. expected_msg = m.group(2)
  152. expected_msg = re.sub(r"# noqa ?.*", "", m.group(2))
  153. expected_errors.append(
  154. (num, is_mypy, expected_msg.strip())
  155. )
  156. result = mypy_runner(path, use_plugin=use_plugin)
  157. if expected_errors:
  158. eq_(result[2], 1, msg=result)
  159. print(result[0])
  160. errors = []
  161. for e in result[0].split("\n"):
  162. if re.match(r".+\.py:\d+: error: .*", e):
  163. errors.append(e)
  164. for num, is_mypy, msg in expected_errors:
  165. msg = msg.replace("'", '"')
  166. prefix = "[SQLAlchemy Mypy plugin] " if not is_mypy else ""
  167. for idx, errmsg in enumerate(errors):
  168. if (
  169. f"{filename}:{num + 1}: error: {prefix}{msg}"
  170. in errmsg.replace("'", '"')
  171. ):
  172. break
  173. else:
  174. continue
  175. del errors[idx]
  176. assert not errors, "errors remain: %s" % "\n".join(errors)
  177. else:
  178. eq_(result[2], 0, msg=result)