test_find_sources.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. from __future__ import annotations
  2. import os
  3. import shutil
  4. import tempfile
  5. import unittest
  6. import pytest
  7. from mypy.find_sources import InvalidSourceList, SourceFinder, create_source_list
  8. from mypy.fscache import FileSystemCache
  9. from mypy.modulefinder import BuildSource
  10. from mypy.options import Options
  11. class FakeFSCache(FileSystemCache):
  12. def __init__(self, files: set[str]) -> None:
  13. self.files = {os.path.abspath(f) for f in files}
  14. def isfile(self, file: str) -> bool:
  15. return file in self.files
  16. def isdir(self, dir: str) -> bool:
  17. if not dir.endswith(os.sep):
  18. dir += os.sep
  19. return any(f.startswith(dir) for f in self.files)
  20. def listdir(self, dir: str) -> list[str]:
  21. if not dir.endswith(os.sep):
  22. dir += os.sep
  23. return list({f[len(dir) :].split(os.sep)[0] for f in self.files if f.startswith(dir)})
  24. def init_under_package_root(self, file: str) -> bool:
  25. return False
  26. def normalise_path(path: str) -> str:
  27. path = os.path.splitdrive(path)[1]
  28. path = path.replace(os.sep, "/")
  29. return path
  30. def normalise_build_source_list(sources: list[BuildSource]) -> list[tuple[str, str | None]]:
  31. return sorted(
  32. (s.module, (normalise_path(s.base_dir) if s.base_dir is not None else None))
  33. for s in sources
  34. )
  35. def crawl(finder: SourceFinder, f: str) -> tuple[str, str]:
  36. module, base_dir = finder.crawl_up(f)
  37. return module, normalise_path(base_dir)
  38. def find_sources_in_dir(finder: SourceFinder, f: str) -> list[tuple[str, str | None]]:
  39. return normalise_build_source_list(finder.find_sources_in_dir(os.path.abspath(f)))
  40. def find_sources(
  41. paths: list[str], options: Options, fscache: FileSystemCache
  42. ) -> list[tuple[str, str | None]]:
  43. paths = [os.path.abspath(p) for p in paths]
  44. return normalise_build_source_list(create_source_list(paths, options, fscache))
  45. class SourceFinderSuite(unittest.TestCase):
  46. def setUp(self) -> None:
  47. self.tempdir = tempfile.mkdtemp()
  48. self.oldcwd = os.getcwd()
  49. os.chdir(self.tempdir)
  50. def tearDown(self) -> None:
  51. os.chdir(self.oldcwd)
  52. shutil.rmtree(self.tempdir)
  53. def test_crawl_no_namespace(self) -> None:
  54. options = Options()
  55. options.namespace_packages = False
  56. finder = SourceFinder(FakeFSCache({"/setup.py"}), options)
  57. assert crawl(finder, "/setup.py") == ("setup", "/")
  58. finder = SourceFinder(FakeFSCache({"/a/setup.py"}), options)
  59. assert crawl(finder, "/a/setup.py") == ("setup", "/a")
  60. finder = SourceFinder(FakeFSCache({"/a/b/setup.py"}), options)
  61. assert crawl(finder, "/a/b/setup.py") == ("setup", "/a/b")
  62. finder = SourceFinder(FakeFSCache({"/a/setup.py", "/a/__init__.py"}), options)
  63. assert crawl(finder, "/a/setup.py") == ("a.setup", "/")
  64. finder = SourceFinder(FakeFSCache({"/a/invalid-name/setup.py", "/a/__init__.py"}), options)
  65. assert crawl(finder, "/a/invalid-name/setup.py") == ("setup", "/a/invalid-name")
  66. finder = SourceFinder(FakeFSCache({"/a/b/setup.py", "/a/__init__.py"}), options)
  67. assert crawl(finder, "/a/b/setup.py") == ("setup", "/a/b")
  68. finder = SourceFinder(
  69. FakeFSCache({"/a/b/c/setup.py", "/a/__init__.py", "/a/b/c/__init__.py"}), options
  70. )
  71. assert crawl(finder, "/a/b/c/setup.py") == ("c.setup", "/a/b")
  72. def test_crawl_namespace(self) -> None:
  73. options = Options()
  74. options.namespace_packages = True
  75. finder = SourceFinder(FakeFSCache({"/setup.py"}), options)
  76. assert crawl(finder, "/setup.py") == ("setup", "/")
  77. finder = SourceFinder(FakeFSCache({"/a/setup.py"}), options)
  78. assert crawl(finder, "/a/setup.py") == ("setup", "/a")
  79. finder = SourceFinder(FakeFSCache({"/a/b/setup.py"}), options)
  80. assert crawl(finder, "/a/b/setup.py") == ("setup", "/a/b")
  81. finder = SourceFinder(FakeFSCache({"/a/setup.py", "/a/__init__.py"}), options)
  82. assert crawl(finder, "/a/setup.py") == ("a.setup", "/")
  83. finder = SourceFinder(FakeFSCache({"/a/invalid-name/setup.py", "/a/__init__.py"}), options)
  84. assert crawl(finder, "/a/invalid-name/setup.py") == ("setup", "/a/invalid-name")
  85. finder = SourceFinder(FakeFSCache({"/a/b/setup.py", "/a/__init__.py"}), options)
  86. assert crawl(finder, "/a/b/setup.py") == ("a.b.setup", "/")
  87. finder = SourceFinder(
  88. FakeFSCache({"/a/b/c/setup.py", "/a/__init__.py", "/a/b/c/__init__.py"}), options
  89. )
  90. assert crawl(finder, "/a/b/c/setup.py") == ("a.b.c.setup", "/")
  91. def test_crawl_namespace_explicit_base(self) -> None:
  92. options = Options()
  93. options.namespace_packages = True
  94. options.explicit_package_bases = True
  95. finder = SourceFinder(FakeFSCache({"/setup.py"}), options)
  96. assert crawl(finder, "/setup.py") == ("setup", "/")
  97. finder = SourceFinder(FakeFSCache({"/a/setup.py"}), options)
  98. assert crawl(finder, "/a/setup.py") == ("setup", "/a")
  99. finder = SourceFinder(FakeFSCache({"/a/b/setup.py"}), options)
  100. assert crawl(finder, "/a/b/setup.py") == ("setup", "/a/b")
  101. finder = SourceFinder(FakeFSCache({"/a/setup.py", "/a/__init__.py"}), options)
  102. assert crawl(finder, "/a/setup.py") == ("a.setup", "/")
  103. finder = SourceFinder(FakeFSCache({"/a/invalid-name/setup.py", "/a/__init__.py"}), options)
  104. assert crawl(finder, "/a/invalid-name/setup.py") == ("setup", "/a/invalid-name")
  105. finder = SourceFinder(FakeFSCache({"/a/b/setup.py", "/a/__init__.py"}), options)
  106. assert crawl(finder, "/a/b/setup.py") == ("a.b.setup", "/")
  107. finder = SourceFinder(
  108. FakeFSCache({"/a/b/c/setup.py", "/a/__init__.py", "/a/b/c/__init__.py"}), options
  109. )
  110. assert crawl(finder, "/a/b/c/setup.py") == ("a.b.c.setup", "/")
  111. # set mypy path, so we actually have some explicit base dirs
  112. options.mypy_path = ["/a/b"]
  113. finder = SourceFinder(FakeFSCache({"/a/b/c/setup.py"}), options)
  114. assert crawl(finder, "/a/b/c/setup.py") == ("c.setup", "/a/b")
  115. finder = SourceFinder(
  116. FakeFSCache({"/a/b/c/setup.py", "/a/__init__.py", "/a/b/c/__init__.py"}), options
  117. )
  118. assert crawl(finder, "/a/b/c/setup.py") == ("c.setup", "/a/b")
  119. options.mypy_path = ["/a/b", "/a/b/c"]
  120. finder = SourceFinder(FakeFSCache({"/a/b/c/setup.py"}), options)
  121. assert crawl(finder, "/a/b/c/setup.py") == ("setup", "/a/b/c")
  122. def test_crawl_namespace_multi_dir(self) -> None:
  123. options = Options()
  124. options.namespace_packages = True
  125. options.explicit_package_bases = True
  126. options.mypy_path = ["/a", "/b"]
  127. finder = SourceFinder(FakeFSCache({"/a/pkg/a.py", "/b/pkg/b.py"}), options)
  128. assert crawl(finder, "/a/pkg/a.py") == ("pkg.a", "/a")
  129. assert crawl(finder, "/b/pkg/b.py") == ("pkg.b", "/b")
  130. def test_find_sources_in_dir_no_namespace(self) -> None:
  131. options = Options()
  132. options.namespace_packages = False
  133. files = {
  134. "/pkg/a1/b/c/d/e.py",
  135. "/pkg/a1/b/f.py",
  136. "/pkg/a2/__init__.py",
  137. "/pkg/a2/b/c/d/e.py",
  138. "/pkg/a2/b/f.py",
  139. }
  140. finder = SourceFinder(FakeFSCache(files), options)
  141. assert find_sources_in_dir(finder, "/") == [
  142. ("a2", "/pkg"),
  143. ("e", "/pkg/a1/b/c/d"),
  144. ("e", "/pkg/a2/b/c/d"),
  145. ("f", "/pkg/a1/b"),
  146. ("f", "/pkg/a2/b"),
  147. ]
  148. def test_find_sources_in_dir_namespace(self) -> None:
  149. options = Options()
  150. options.namespace_packages = True
  151. files = {
  152. "/pkg/a1/b/c/d/e.py",
  153. "/pkg/a1/b/f.py",
  154. "/pkg/a2/__init__.py",
  155. "/pkg/a2/b/c/d/e.py",
  156. "/pkg/a2/b/f.py",
  157. }
  158. finder = SourceFinder(FakeFSCache(files), options)
  159. assert find_sources_in_dir(finder, "/") == [
  160. ("a2", "/pkg"),
  161. ("a2.b.c.d.e", "/pkg"),
  162. ("a2.b.f", "/pkg"),
  163. ("e", "/pkg/a1/b/c/d"),
  164. ("f", "/pkg/a1/b"),
  165. ]
  166. def test_find_sources_in_dir_namespace_explicit_base(self) -> None:
  167. options = Options()
  168. options.namespace_packages = True
  169. options.explicit_package_bases = True
  170. options.mypy_path = ["/"]
  171. files = {
  172. "/pkg/a1/b/c/d/e.py",
  173. "/pkg/a1/b/f.py",
  174. "/pkg/a2/__init__.py",
  175. "/pkg/a2/b/c/d/e.py",
  176. "/pkg/a2/b/f.py",
  177. }
  178. finder = SourceFinder(FakeFSCache(files), options)
  179. assert find_sources_in_dir(finder, "/") == [
  180. ("pkg.a1.b.c.d.e", "/"),
  181. ("pkg.a1.b.f", "/"),
  182. ("pkg.a2", "/"),
  183. ("pkg.a2.b.c.d.e", "/"),
  184. ("pkg.a2.b.f", "/"),
  185. ]
  186. options.mypy_path = ["/pkg"]
  187. finder = SourceFinder(FakeFSCache(files), options)
  188. assert find_sources_in_dir(finder, "/") == [
  189. ("a1.b.c.d.e", "/pkg"),
  190. ("a1.b.f", "/pkg"),
  191. ("a2", "/pkg"),
  192. ("a2.b.c.d.e", "/pkg"),
  193. ("a2.b.f", "/pkg"),
  194. ]
  195. def test_find_sources_in_dir_namespace_multi_dir(self) -> None:
  196. options = Options()
  197. options.namespace_packages = True
  198. options.explicit_package_bases = True
  199. options.mypy_path = ["/a", "/b"]
  200. finder = SourceFinder(FakeFSCache({"/a/pkg/a.py", "/b/pkg/b.py"}), options)
  201. assert find_sources_in_dir(finder, "/") == [("pkg.a", "/a"), ("pkg.b", "/b")]
  202. def test_find_sources_exclude(self) -> None:
  203. options = Options()
  204. options.namespace_packages = True
  205. # default
  206. for excluded_dir in ["site-packages", ".whatever", "node_modules", ".x/.z"]:
  207. fscache = FakeFSCache({"/dir/a.py", f"/dir/venv/{excluded_dir}/b.py"})
  208. assert find_sources(["/"], options, fscache) == [("a", "/dir")]
  209. with pytest.raises(InvalidSourceList):
  210. find_sources(["/dir/venv/"], options, fscache)
  211. assert find_sources([f"/dir/venv/{excluded_dir}"], options, fscache) == [
  212. ("b", f"/dir/venv/{excluded_dir}")
  213. ]
  214. assert find_sources([f"/dir/venv/{excluded_dir}/b.py"], options, fscache) == [
  215. ("b", f"/dir/venv/{excluded_dir}")
  216. ]
  217. files = {
  218. "/pkg/a1/b/c/d/e.py",
  219. "/pkg/a1/b/f.py",
  220. "/pkg/a2/__init__.py",
  221. "/pkg/a2/b/c/d/e.py",
  222. "/pkg/a2/b/f.py",
  223. }
  224. # file name
  225. options.exclude = [r"/f\.py$"]
  226. fscache = FakeFSCache(files)
  227. assert find_sources(["/"], options, fscache) == [
  228. ("a2", "/pkg"),
  229. ("a2.b.c.d.e", "/pkg"),
  230. ("e", "/pkg/a1/b/c/d"),
  231. ]
  232. assert find_sources(["/pkg/a1/b/f.py"], options, fscache) == [("f", "/pkg/a1/b")]
  233. assert find_sources(["/pkg/a2/b/f.py"], options, fscache) == [("a2.b.f", "/pkg")]
  234. # directory name
  235. options.exclude = ["/a1/"]
  236. fscache = FakeFSCache(files)
  237. assert find_sources(["/"], options, fscache) == [
  238. ("a2", "/pkg"),
  239. ("a2.b.c.d.e", "/pkg"),
  240. ("a2.b.f", "/pkg"),
  241. ]
  242. with pytest.raises(InvalidSourceList):
  243. find_sources(["/pkg/a1"], options, fscache)
  244. with pytest.raises(InvalidSourceList):
  245. find_sources(["/pkg/a1/"], options, fscache)
  246. with pytest.raises(InvalidSourceList):
  247. find_sources(["/pkg/a1/b"], options, fscache)
  248. options.exclude = ["/a1/$"]
  249. assert find_sources(["/pkg/a1"], options, fscache) == [
  250. ("e", "/pkg/a1/b/c/d"),
  251. ("f", "/pkg/a1/b"),
  252. ]
  253. # paths
  254. options.exclude = ["/pkg/a1/"]
  255. fscache = FakeFSCache(files)
  256. assert find_sources(["/"], options, fscache) == [
  257. ("a2", "/pkg"),
  258. ("a2.b.c.d.e", "/pkg"),
  259. ("a2.b.f", "/pkg"),
  260. ]
  261. with pytest.raises(InvalidSourceList):
  262. find_sources(["/pkg/a1"], options, fscache)
  263. # OR two patterns together
  264. for orred in [["/(a1|a3)/"], ["a1", "a3"], ["a3", "a1"]]:
  265. options.exclude = orred
  266. fscache = FakeFSCache(files)
  267. assert find_sources(["/"], options, fscache) == [
  268. ("a2", "/pkg"),
  269. ("a2.b.c.d.e", "/pkg"),
  270. ("a2.b.f", "/pkg"),
  271. ]
  272. options.exclude = ["b/c/"]
  273. fscache = FakeFSCache(files)
  274. assert find_sources(["/"], options, fscache) == [
  275. ("a2", "/pkg"),
  276. ("a2.b.f", "/pkg"),
  277. ("f", "/pkg/a1/b"),
  278. ]
  279. # nothing should be ignored as a result of this
  280. big_exclude1 = [
  281. "/pkg/a/",
  282. "/2",
  283. "/1",
  284. "/pk/",
  285. "/kg",
  286. "/g.py",
  287. "/bc",
  288. "/xxx/pkg/a2/b/f.py",
  289. "xxx/pkg/a2/b/f.py",
  290. ]
  291. big_exclude2 = ["|".join(big_exclude1)]
  292. for big_exclude in [big_exclude1, big_exclude2]:
  293. options.exclude = big_exclude
  294. fscache = FakeFSCache(files)
  295. assert len(find_sources(["/"], options, fscache)) == len(files)
  296. files = {
  297. "pkg/a1/b/c/d/e.py",
  298. "pkg/a1/b/f.py",
  299. "pkg/a2/__init__.py",
  300. "pkg/a2/b/c/d/e.py",
  301. "pkg/a2/b/f.py",
  302. }
  303. fscache = FakeFSCache(files)
  304. assert len(find_sources(["."], options, fscache)) == len(files)