docs.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. # -*- coding: utf-8 -*-
  2. """
  3. tasks.docstrings
  4. ~~~~~~~~~~~~~~~~
  5. Check salt code base for for missing or wrong docstrings
  6. """
  7. import ast
  8. import collections
  9. import os
  10. import pathlib
  11. import re
  12. from invoke import task # pylint: disable=3rd-party-module-not-gated
  13. from tasks import utils
  14. CODE_DIR = pathlib.Path(__file__).resolve().parent.parent
  15. DOCS_DIR = CODE_DIR / "doc"
  16. SALT_CODE_DIR = CODE_DIR / "salt"
  17. os.chdir(str(CODE_DIR))
  18. python_module_to_doc_path = {}
  19. doc_path_to_python_module = {}
  20. check_paths = (
  21. "salt/auth",
  22. "salt/beacons",
  23. "salt/cache",
  24. "salt/cloud",
  25. "salt/engine",
  26. "salt/executors",
  27. "salt/fileserver",
  28. "salt/grains",
  29. "salt/modules",
  30. "salt/netapi",
  31. "salt/output",
  32. "salt/pillar",
  33. "salt/proxy",
  34. "salt/queues",
  35. "salt/renderers",
  36. "salt/returners",
  37. "salt/roster",
  38. "salt/runners",
  39. "salt/sdb",
  40. "salt/serializers",
  41. "salt/states",
  42. "salt/thorium",
  43. "salt/tokens",
  44. "salt/tops",
  45. "salt/wheel",
  46. )
  47. exclude_paths = (
  48. "salt/cloud/cli.py",
  49. "salt/cloud/exceptions.py",
  50. "salt/cloud/libcloudfuncs.py",
  51. )
  52. def build_path_cache():
  53. """
  54. Build a python module to doc module cache
  55. """
  56. for path in SALT_CODE_DIR.rglob("*.py"):
  57. path = path.resolve().relative_to(CODE_DIR)
  58. strpath = str(path)
  59. if strpath.endswith("__init__.py"):
  60. continue
  61. if not strpath.startswith(check_paths):
  62. continue
  63. if strpath.startswith(exclude_paths):
  64. continue
  65. parts = list(path.parts)
  66. stub_path = DOCS_DIR / "ref"
  67. # Remove salt from parts
  68. parts.pop(0)
  69. # Remove the package from parts
  70. package = parts.pop(0)
  71. # Remove the module from parts
  72. module = parts.pop()
  73. if package == "cloud":
  74. package = "clouds"
  75. if package == "fileserver":
  76. package = "file_server"
  77. if package == "netapi":
  78. # These are handled differently
  79. if not parts:
  80. # This is rest_wsgi
  81. stub_path = (
  82. stub_path
  83. / package
  84. / "all"
  85. / str(path).replace(".py", ".rst").replace(os.sep, ".")
  86. )
  87. else:
  88. # rest_cherrypy, rest_tornado
  89. subpackage = parts.pop(0)
  90. stub_path = (
  91. stub_path
  92. / package
  93. / "all"
  94. / "salt.netapi.{}.rst".format(subpackage)
  95. )
  96. else:
  97. stub_path = (
  98. stub_path
  99. / package
  100. / "all"
  101. / str(path).replace(".py", ".rst").replace(os.sep, ".")
  102. )
  103. stub_path = stub_path.relative_to(CODE_DIR)
  104. python_module_to_doc_path[path] = stub_path
  105. doc_path_to_python_module[stub_path] = path
  106. build_path_cache()
  107. def build_file_list(files, extension):
  108. # Unfortunately invoke does not support nargs.
  109. # We migth have been passed --files="foo.py bar.py"
  110. # Turn that into a list of paths
  111. _files = []
  112. for path in files:
  113. if not path:
  114. continue
  115. for spath in path.split():
  116. if not spath.endswith(extension):
  117. continue
  118. _files.append(spath)
  119. if not _files:
  120. _files = CODE_DIR.rglob("*{}".format(extension))
  121. else:
  122. _files = [pathlib.Path(fname).resolve() for fname in _files]
  123. _files = [path.relative_to(CODE_DIR) for path in _files]
  124. return _files
  125. def build_python_module_paths(files):
  126. _files = []
  127. for path in build_file_list(files, ".py"):
  128. strpath = str(path)
  129. if strpath.endswith("__init__.py"):
  130. continue
  131. if not strpath.startswith(check_paths):
  132. continue
  133. if strpath.startswith(exclude_paths):
  134. continue
  135. _files.append(path)
  136. return _files
  137. def build_docs_paths(files):
  138. return build_file_list(files, ".rst")
  139. @task(iterable=["files"], positional=["files"])
  140. def check_inline_markup(ctx, files):
  141. """
  142. Check docstring for :doc: usage
  143. We should not be using the ``:doc:`` inline markup option when
  144. cross-referencing locations. Use ``:ref:`` or ``:mod:`` instead.
  145. This task checks for reference to ``:doc:`` usage.
  146. See Issue #12788 for more information.
  147. https://github.com/saltstack/salt/issues/12788
  148. """
  149. # CD into Salt's repo root directory
  150. ctx.cd(CODE_DIR)
  151. files = build_python_module_paths(files)
  152. exitcode = 0
  153. for path in files:
  154. module = ast.parse(path.read_text(), filename=str(path))
  155. funcdefs = [node for node in module.body if isinstance(node, ast.FunctionDef)]
  156. for funcdef in funcdefs:
  157. docstring = ast.get_docstring(funcdef, clean=True)
  158. if not docstring:
  159. continue
  160. if ":doc:" in docstring:
  161. utils.error(
  162. "The {} function in {} contains ':doc:' usage", funcdef.name, path
  163. )
  164. exitcode += 1
  165. utils.exit_invoke(exitcode)
  166. @task(iterable=["files"])
  167. def check_stubs(ctx, files):
  168. # CD into Salt's repo root directory
  169. ctx.cd(CODE_DIR)
  170. files = build_python_module_paths(files)
  171. exitcode = 0
  172. for path in files:
  173. strpath = str(path)
  174. if strpath.endswith("__init__.py"):
  175. continue
  176. if not strpath.startswith(check_paths):
  177. continue
  178. if strpath.startswith(exclude_paths):
  179. continue
  180. stub_path = python_module_to_doc_path[path]
  181. if not stub_path.exists():
  182. exitcode += 1
  183. utils.error(
  184. "The module at {} does not have a sphinx stub at {}", path, stub_path
  185. )
  186. utils.exit_invoke(exitcode)
  187. @task(iterable=["files"])
  188. def check_virtual(ctx, files):
  189. """
  190. Check if .rst files for each module contains the text ".. _virtual"
  191. indicating it is a virtual doc page, and, in case a module exists by
  192. the same name, it's going to be shaddowed and not accessible
  193. """
  194. exitcode = 0
  195. files = build_docs_paths(files)
  196. for path in files:
  197. if path.name == "index.rst":
  198. continue
  199. contents = path.read_text()
  200. if ".. _virtual-" in contents:
  201. try:
  202. python_module = doc_path_to_python_module[path]
  203. utils.error(
  204. "The doc file at {} indicates that it's virtual, yet, there's a python module "
  205. "at {} that will shaddow it.",
  206. path,
  207. python_module,
  208. )
  209. exitcode += 1
  210. except KeyError:
  211. # This is what we're expecting
  212. continue
  213. utils.exit_invoke(exitcode)
  214. @task(iterable=["files"])
  215. def check_module_indexes(ctx, files):
  216. exitcode = 0
  217. files = build_docs_paths(files)
  218. for path in files:
  219. if path.name != "index.rst":
  220. continue
  221. contents = path.read_text()
  222. if ".. autosummary::" not in contents:
  223. continue
  224. module_index_block = re.search(
  225. r"""
  226. \.\.\s+autosummary::\s*\n
  227. (\s+:[a-z]+:.*\n)*
  228. (\s*\n)+
  229. (?P<mods>(\s*[a-z0-9_\.]+\s*\n)+)
  230. """,
  231. contents,
  232. flags=re.VERBOSE,
  233. )
  234. if not module_index_block:
  235. continue
  236. module_index = re.findall(
  237. r"""\s*([a-z0-9_\.]+)\s*\n""", module_index_block.group("mods")
  238. )
  239. if module_index != sorted(module_index):
  240. exitcode += 1
  241. utils.error(
  242. "The autosummary mods in {} are not properly sorted. Please sort them.",
  243. path,
  244. )
  245. module_index_duplicates = [
  246. mod for mod, count in collections.Counter(module_index).items() if count > 1
  247. ]
  248. if module_index_duplicates:
  249. exitcode += 1
  250. utils.error(
  251. "Module index {} contains duplicates: {}", path, module_index_duplicates
  252. )
  253. # Let's check if all python modules are included in the index
  254. path_parts = list(path.parts)
  255. # drop doc
  256. path_parts.pop(0)
  257. # drop ref
  258. path_parts.pop(0)
  259. # drop "index.rst"
  260. path_parts.pop()
  261. # drop "all"
  262. path_parts.pop()
  263. package = path_parts.pop(0)
  264. if package == "clouds":
  265. package = "cloud"
  266. if package == "file_server":
  267. package = "fileserver"
  268. if package == "configuration":
  269. package = "log"
  270. path_parts = ["handlers"]
  271. python_package = SALT_CODE_DIR.joinpath(package, *path_parts).relative_to(
  272. CODE_DIR
  273. )
  274. modules = set()
  275. for module in python_package.rglob("*.py"):
  276. if package == "netapi":
  277. if module.stem == "__init__":
  278. continue
  279. if len(module.parts) > 4:
  280. continue
  281. if len(module.parts) > 3:
  282. modules.add(module.parent.stem)
  283. else:
  284. modules.add(module.stem)
  285. elif package == "cloud":
  286. if len(module.parts) < 4:
  287. continue
  288. if module.name == "__init__.py":
  289. continue
  290. modules.add(module.stem)
  291. elif package == "modules":
  292. if len(module.parts) > 3:
  293. # salt.modules.inspeclib
  294. if module.name == "__init__.py":
  295. modules.add(module.parent.stem)
  296. continue
  297. modules.add("{}.{}".format(module.parent.stem, module.stem))
  298. continue
  299. if module.name == "__init__.py":
  300. continue
  301. modules.add(module.stem)
  302. elif module.name == "__init__.py":
  303. continue
  304. elif module.name != "__init__.py":
  305. modules.add(module.stem)
  306. missing_modules_in_index = set(modules) - set(module_index)
  307. if missing_modules_in_index:
  308. exitcode += 1
  309. utils.error(
  310. "The module index at {} is missing the following modules: {}",
  311. path,
  312. ", ".join(missing_modules_in_index),
  313. )
  314. extra_modules_in_index = set(module_index) - set(modules)
  315. if extra_modules_in_index:
  316. exitcode += 1
  317. utils.error(
  318. "The module index at {} has extra modules(non existing): {}",
  319. path,
  320. ", ".join(extra_modules_in_index),
  321. )
  322. utils.exit_invoke(exitcode)
  323. @task(iterable=["files"])
  324. def check_stray(ctx, files):
  325. exitcode = 0
  326. exclude_paths = (
  327. DOCS_DIR / "_inc",
  328. DOCS_DIR / "ref" / "cli" / "_includes",
  329. DOCS_DIR / "ref" / "cli",
  330. DOCS_DIR / "ref" / "configuration",
  331. DOCS_DIR / "ref" / "file_server" / "backends.rst",
  332. DOCS_DIR / "ref" / "file_server" / "environments.rst",
  333. DOCS_DIR / "ref" / "file_server" / "file_roots.rst",
  334. DOCS_DIR / "ref" / "internals",
  335. DOCS_DIR / "ref" / "modules" / "all" / "salt.modules.inspectlib.rst",
  336. DOCS_DIR / "ref" / "peer.rst",
  337. DOCS_DIR / "ref" / "publisheracl.rst",
  338. DOCS_DIR / "ref" / "python-api.rst",
  339. DOCS_DIR / "ref" / "states" / "aggregate.rst",
  340. DOCS_DIR / "ref" / "states" / "altering_states.rst",
  341. DOCS_DIR / "ref" / "states" / "backup_mode.rst",
  342. DOCS_DIR / "ref" / "states" / "compiler_ordering.rst",
  343. DOCS_DIR / "ref" / "states" / "extend.rst",
  344. DOCS_DIR / "ref" / "states" / "failhard.rst",
  345. DOCS_DIR / "ref" / "states" / "global_state_arguments.rst",
  346. DOCS_DIR / "ref" / "states" / "highstate.rst",
  347. DOCS_DIR / "ref" / "states" / "include.rst",
  348. DOCS_DIR / "ref" / "states" / "layers.rst",
  349. DOCS_DIR / "ref" / "states" / "master_side.rst",
  350. DOCS_DIR / "ref" / "states" / "ordering.rst",
  351. DOCS_DIR / "ref" / "states" / "parallel.rst",
  352. DOCS_DIR / "ref" / "states" / "providers.rst",
  353. DOCS_DIR / "ref" / "states" / "requisites.rst",
  354. DOCS_DIR / "ref" / "states" / "startup.rst",
  355. DOCS_DIR / "ref" / "states" / "testing.rst",
  356. DOCS_DIR / "ref" / "states" / "top.rst",
  357. DOCS_DIR / "ref" / "states" / "vars.rst",
  358. DOCS_DIR / "ref" / "states" / "writing.rst",
  359. DOCS_DIR / "topics",
  360. )
  361. exclude_paths = tuple([str(p.relative_to(CODE_DIR)) for p in exclude_paths])
  362. files = build_docs_paths(files)
  363. for path in files:
  364. if not str(path).startswith(str((DOCS_DIR / "ref").relative_to(CODE_DIR))):
  365. continue
  366. if str(path).startswith(exclude_paths):
  367. continue
  368. if path.name in ("index.rst", "glossary.rst", "faq.rst", "README.rst"):
  369. continue
  370. try:
  371. python_module = doc_path_to_python_module[path]
  372. except KeyError:
  373. contents = path.read_text()
  374. if ".. _virtual-" in contents:
  375. continue
  376. exitcode += 1
  377. utils.error(
  378. "The doc at {} doesn't have a corresponding python module an is considered a stray "
  379. "doc. Please remove it.",
  380. path,
  381. )
  382. utils.exit_invoke(exitcode)
  383. @task(iterable=["files"])
  384. def check(ctx, files):
  385. try:
  386. utils.info("Checking inline :doc: markup")
  387. check_inline_markup(ctx, files)
  388. except SystemExit as exc:
  389. if exc.code != 0:
  390. raise
  391. try:
  392. utils.info("Checking python module stubs")
  393. check_stubs(ctx, files)
  394. except SystemExit as exc:
  395. if exc.code != 0:
  396. raise
  397. try:
  398. utils.info("Checking virtual modules")
  399. check_virtual(ctx, files)
  400. except SystemExit as exc:
  401. if exc.code != 0:
  402. raise
  403. try:
  404. utils.info("Checking doc module indexes")
  405. check_module_indexes(ctx, files)
  406. except SystemExit as exc:
  407. if exc.code != 0:
  408. raise
  409. try:
  410. utils.info("Checking stray docs")
  411. check_stray(ctx, files)
  412. except SystemExit as exc:
  413. if exc.code != 0:
  414. raise