_check_docs_utils.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  1. # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  2. # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
  3. # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
  4. """Utility methods for docstring checking."""
  5. from __future__ import annotations
  6. import re
  7. import astroid
  8. from astroid import nodes
  9. from astroid.util import UninferableBase
  10. from pylint.checkers import utils
  11. def space_indentation(s: str) -> int:
  12. """The number of leading spaces in a string.
  13. :param str s: input string
  14. :rtype: int
  15. :return: number of leading spaces
  16. """
  17. return len(s) - len(s.lstrip(" "))
  18. def get_setters_property_name(node: nodes.FunctionDef) -> str | None:
  19. """Get the name of the property that the given node is a setter for.
  20. :param node: The node to get the property name for.
  21. :type node: str
  22. :rtype: str or None
  23. :returns: The name of the property that the node is a setter for,
  24. or None if one could not be found.
  25. """
  26. decorators = node.decorators.nodes if node.decorators else []
  27. for decorator in decorators:
  28. if (
  29. isinstance(decorator, nodes.Attribute)
  30. and decorator.attrname == "setter"
  31. and isinstance(decorator.expr, nodes.Name)
  32. ):
  33. return decorator.expr.name # type: ignore[no-any-return]
  34. return None
  35. def get_setters_property(node: nodes.FunctionDef) -> nodes.FunctionDef | None:
  36. """Get the property node for the given setter node.
  37. :param node: The node to get the property for.
  38. :type node: nodes.FunctionDef
  39. :rtype: nodes.FunctionDef or None
  40. :returns: The node relating to the property of the given setter node,
  41. or None if one could not be found.
  42. """
  43. property_ = None
  44. property_name = get_setters_property_name(node)
  45. class_node = utils.node_frame_class(node)
  46. if property_name and class_node:
  47. class_attrs: list[nodes.FunctionDef] = class_node.getattr(node.name)
  48. for attr in class_attrs:
  49. if utils.decorated_with_property(attr):
  50. property_ = attr
  51. break
  52. return property_
  53. def returns_something(return_node: nodes.Return) -> bool:
  54. """Check if a return node returns a value other than None.
  55. :param return_node: The return node to check.
  56. :type return_node: astroid.Return
  57. :rtype: bool
  58. :return: True if the return node returns a value other than None,
  59. False otherwise.
  60. """
  61. returns = return_node.value
  62. if returns is None:
  63. return False
  64. return not (isinstance(returns, nodes.Const) and returns.value is None)
  65. def _get_raise_target(node: nodes.NodeNG) -> nodes.NodeNG | UninferableBase | None:
  66. if isinstance(node.exc, nodes.Call):
  67. func = node.exc.func
  68. if isinstance(func, (nodes.Name, nodes.Attribute)):
  69. return utils.safe_infer(func)
  70. return None
  71. def _split_multiple_exc_types(target: str) -> list[str]:
  72. delimiters = r"(\s*,(?:\s*or\s)?\s*|\s+or\s+)"
  73. return re.split(delimiters, target)
  74. def possible_exc_types(node: nodes.NodeNG) -> set[nodes.ClassDef]:
  75. """Gets all the possible raised exception types for the given raise node.
  76. .. note::
  77. Caught exception types are ignored.
  78. :param node: The raise node to find exception types for.
  79. :returns: A list of exception types possibly raised by :param:`node`.
  80. """
  81. exceptions = []
  82. if isinstance(node.exc, nodes.Name):
  83. inferred = utils.safe_infer(node.exc)
  84. if inferred:
  85. exceptions = [inferred]
  86. elif node.exc is None:
  87. handler = node.parent
  88. while handler and not isinstance(handler, nodes.ExceptHandler):
  89. handler = handler.parent
  90. if handler and handler.type:
  91. try:
  92. for exception in astroid.unpack_infer(handler.type):
  93. if not isinstance(exception, UninferableBase):
  94. exceptions.append(exception)
  95. except astroid.InferenceError:
  96. pass
  97. else:
  98. target = _get_raise_target(node)
  99. if isinstance(target, nodes.ClassDef):
  100. exceptions = [target]
  101. elif isinstance(target, nodes.FunctionDef):
  102. for ret in target.nodes_of_class(nodes.Return):
  103. if ret.value is None:
  104. continue
  105. if ret.frame(future=True) != target:
  106. # return from inner function - ignore it
  107. continue
  108. val = utils.safe_infer(ret.value)
  109. if val and utils.inherit_from_std_ex(val):
  110. if isinstance(val, nodes.ClassDef):
  111. exceptions.append(val)
  112. elif isinstance(val, astroid.Instance):
  113. exceptions.append(val.getattr("__class__")[0])
  114. try:
  115. return {
  116. exc
  117. for exc in exceptions
  118. if not utils.node_ignores_exception(node, exc.name)
  119. }
  120. except astroid.InferenceError:
  121. return set()
  122. def docstringify(
  123. docstring: nodes.Const | None, default_type: str = "default"
  124. ) -> Docstring:
  125. best_match = (0, DOCSTRING_TYPES.get(default_type, Docstring)(docstring))
  126. for docstring_type in (
  127. SphinxDocstring,
  128. EpytextDocstring,
  129. GoogleDocstring,
  130. NumpyDocstring,
  131. ):
  132. instance = docstring_type(docstring)
  133. matching_sections = instance.matching_sections()
  134. if matching_sections > best_match[0]:
  135. best_match = (matching_sections, instance)
  136. return best_match[1]
  137. class Docstring:
  138. re_for_parameters_see = re.compile(
  139. r"""
  140. For\s+the\s+(other)?\s*parameters\s*,\s+see
  141. """,
  142. re.X | re.S,
  143. )
  144. supports_yields: bool = False
  145. """True if the docstring supports a "yield" section.
  146. False if the docstring uses the returns section to document generators.
  147. """
  148. # These methods are designed to be overridden
  149. def __init__(self, doc: nodes.Const | None) -> None:
  150. docstring: str = doc.value if doc else ""
  151. self.doc = docstring.expandtabs()
  152. def __repr__(self) -> str:
  153. return f"<{self.__class__.__name__}:'''{self.doc}'''>"
  154. def matching_sections(self) -> int:
  155. """Returns the number of matching docstring sections."""
  156. return 0
  157. def exceptions(self) -> set[str]:
  158. return set()
  159. def has_params(self) -> bool:
  160. return False
  161. def has_returns(self) -> bool:
  162. return False
  163. def has_rtype(self) -> bool:
  164. return False
  165. def has_property_returns(self) -> bool:
  166. return False
  167. def has_property_type(self) -> bool:
  168. return False
  169. def has_yields(self) -> bool:
  170. return False
  171. def has_yields_type(self) -> bool:
  172. return False
  173. def match_param_docs(self) -> tuple[set[str], set[str]]:
  174. return set(), set()
  175. def params_documented_elsewhere(self) -> bool:
  176. return self.re_for_parameters_see.search(self.doc) is not None
  177. class SphinxDocstring(Docstring):
  178. re_type = r"""
  179. [~!.]? # Optional link style prefix
  180. \w(?:\w|\.[^\.])* # Valid python name
  181. """
  182. re_simple_container_type = rf"""
  183. {re_type} # a container type
  184. [\(\[] [^\n\s]+ [\)\]] # with the contents of the container
  185. """
  186. re_multiple_simple_type = r"""
  187. (?:{container_type}|{type})
  188. (?:(?:\s+(?:of|or)\s+|\s*,\s*|\s+\|\s+)(?:{container_type}|{type}))*
  189. """.format(
  190. type=re_type, container_type=re_simple_container_type
  191. )
  192. re_xref = rf"""
  193. (?::\w+:)? # optional tag
  194. `{re_type}` # what to reference
  195. """
  196. re_param_raw = rf"""
  197. : # initial colon
  198. (?: # Sphinx keywords
  199. param|parameter|
  200. arg|argument|
  201. key|keyword
  202. )
  203. \s+ # whitespace
  204. (?: # optional type declaration
  205. ({re_type}|{re_simple_container_type})
  206. \s+
  207. )?
  208. ((\\\*{{0,2}}\w+)|(\w+)) # Parameter name with potential asterisks
  209. \s* # whitespace
  210. : # final colon
  211. """
  212. re_param_in_docstring = re.compile(re_param_raw, re.X | re.S)
  213. re_type_raw = rf"""
  214. :type # Sphinx keyword
  215. \s+ # whitespace
  216. ({re_multiple_simple_type}) # Parameter name
  217. \s* # whitespace
  218. : # final colon
  219. """
  220. re_type_in_docstring = re.compile(re_type_raw, re.X | re.S)
  221. re_property_type_raw = rf"""
  222. :type: # Sphinx keyword
  223. \s+ # whitespace
  224. {re_multiple_simple_type} # type declaration
  225. """
  226. re_property_type_in_docstring = re.compile(re_property_type_raw, re.X | re.S)
  227. re_raise_raw = rf"""
  228. : # initial colon
  229. (?: # Sphinx keyword
  230. raises?|
  231. except|exception
  232. )
  233. \s+ # whitespace
  234. ({re_multiple_simple_type}) # exception type
  235. \s* # whitespace
  236. : # final colon
  237. """
  238. re_raise_in_docstring = re.compile(re_raise_raw, re.X | re.S)
  239. re_rtype_in_docstring = re.compile(r":rtype:")
  240. re_returns_in_docstring = re.compile(r":returns?:")
  241. supports_yields = False
  242. def matching_sections(self) -> int:
  243. """Returns the number of matching docstring sections."""
  244. return sum(
  245. bool(i)
  246. for i in (
  247. self.re_param_in_docstring.search(self.doc),
  248. self.re_raise_in_docstring.search(self.doc),
  249. self.re_rtype_in_docstring.search(self.doc),
  250. self.re_returns_in_docstring.search(self.doc),
  251. self.re_property_type_in_docstring.search(self.doc),
  252. )
  253. )
  254. def exceptions(self) -> set[str]:
  255. types: set[str] = set()
  256. for match in re.finditer(self.re_raise_in_docstring, self.doc):
  257. raise_type = match.group(1)
  258. types.update(_split_multiple_exc_types(raise_type))
  259. return types
  260. def has_params(self) -> bool:
  261. if not self.doc:
  262. return False
  263. return self.re_param_in_docstring.search(self.doc) is not None
  264. def has_returns(self) -> bool:
  265. if not self.doc:
  266. return False
  267. return bool(self.re_returns_in_docstring.search(self.doc))
  268. def has_rtype(self) -> bool:
  269. if not self.doc:
  270. return False
  271. return bool(self.re_rtype_in_docstring.search(self.doc))
  272. def has_property_returns(self) -> bool:
  273. if not self.doc:
  274. return False
  275. # The summary line is the return doc,
  276. # so the first line must not be a known directive.
  277. return not self.doc.lstrip().startswith(":")
  278. def has_property_type(self) -> bool:
  279. if not self.doc:
  280. return False
  281. return bool(self.re_property_type_in_docstring.search(self.doc))
  282. def match_param_docs(self) -> tuple[set[str], set[str]]:
  283. params_with_doc = set()
  284. params_with_type = set()
  285. for match in re.finditer(self.re_param_in_docstring, self.doc):
  286. name = match.group(2)
  287. # Remove escape characters necessary for asterisks
  288. name = name.replace("\\", "")
  289. params_with_doc.add(name)
  290. param_type = match.group(1)
  291. if param_type is not None:
  292. params_with_type.add(name)
  293. params_with_type.update(re.findall(self.re_type_in_docstring, self.doc))
  294. return params_with_doc, params_with_type
  295. class EpytextDocstring(SphinxDocstring):
  296. """Epytext is similar to Sphinx.
  297. See the docs:
  298. http://epydoc.sourceforge.net/epytext.html
  299. http://epydoc.sourceforge.net/fields.html#fields
  300. It's used in PyCharm:
  301. https://www.jetbrains.com/help/pycharm/2016.1/creating-documentation-comments.html#d848203e314
  302. https://www.jetbrains.com/help/pycharm/2016.1/using-docstrings-to-specify-types.html
  303. """
  304. re_param_in_docstring = re.compile(
  305. SphinxDocstring.re_param_raw.replace(":", "@", 1), re.X | re.S
  306. )
  307. re_type_in_docstring = re.compile(
  308. SphinxDocstring.re_type_raw.replace(":", "@", 1), re.X | re.S
  309. )
  310. re_property_type_in_docstring = re.compile(
  311. SphinxDocstring.re_property_type_raw.replace(":", "@", 1), re.X | re.S
  312. )
  313. re_raise_in_docstring = re.compile(
  314. SphinxDocstring.re_raise_raw.replace(":", "@", 1), re.X | re.S
  315. )
  316. re_rtype_in_docstring = re.compile(
  317. r"""
  318. @ # initial "at" symbol
  319. (?: # Epytext keyword
  320. rtype|returntype
  321. )
  322. : # final colon
  323. """,
  324. re.X | re.S,
  325. )
  326. re_returns_in_docstring = re.compile(r"@returns?:")
  327. def has_property_returns(self) -> bool:
  328. if not self.doc:
  329. return False
  330. # If this is a property docstring, the summary is the return doc.
  331. if self.has_property_type():
  332. # The summary line is the return doc,
  333. # so the first line must not be a known directive.
  334. return not self.doc.lstrip().startswith("@")
  335. return False
  336. class GoogleDocstring(Docstring):
  337. re_type = SphinxDocstring.re_type
  338. re_xref = SphinxDocstring.re_xref
  339. re_container_type = rf"""
  340. (?:{re_type}|{re_xref}) # a container type
  341. [\(\[] [^\n]+ [\)\]] # with the contents of the container
  342. """
  343. re_multiple_type = r"""
  344. (?:{container_type}|{type}|{xref})
  345. (?:(?:\s+(?:of|or)\s+|\s*,\s*|\s+\|\s+)(?:{container_type}|{type}|{xref}))*
  346. """.format(
  347. type=re_type, xref=re_xref, container_type=re_container_type
  348. )
  349. _re_section_template = r"""
  350. ^([ ]*) {0} \s*: \s*$ # Google parameter header
  351. ( .* ) # section
  352. """
  353. re_param_section = re.compile(
  354. _re_section_template.format(r"(?:Args|Arguments|Parameters)"),
  355. re.X | re.S | re.M,
  356. )
  357. re_keyword_param_section = re.compile(
  358. _re_section_template.format(r"Keyword\s(?:Args|Arguments|Parameters)"),
  359. re.X | re.S | re.M,
  360. )
  361. re_param_line = re.compile(
  362. rf"""
  363. \s* ((?:\\?\*{{0,2}})?[\w\\]+) # identifier potentially with asterisks or escaped `\`
  364. \s* ( [(]
  365. {re_multiple_type}
  366. (?:,\s+optional)?
  367. [)] )? \s* : # optional type declaration
  368. \s* (.*) # beginning of optional description
  369. """,
  370. re.X | re.S | re.M,
  371. )
  372. re_raise_section = re.compile(
  373. _re_section_template.format(r"Raises"), re.X | re.S | re.M
  374. )
  375. re_raise_line = re.compile(
  376. rf"""
  377. \s* ({re_multiple_type}) \s* : # identifier
  378. \s* (.*) # beginning of optional description
  379. """,
  380. re.X | re.S | re.M,
  381. )
  382. re_returns_section = re.compile(
  383. _re_section_template.format(r"Returns?"), re.X | re.S | re.M
  384. )
  385. re_returns_line = re.compile(
  386. rf"""
  387. \s* ({re_multiple_type}:)? # identifier
  388. \s* (.*) # beginning of description
  389. """,
  390. re.X | re.S | re.M,
  391. )
  392. re_property_returns_line = re.compile(
  393. rf"""
  394. ^{re_multiple_type}: # identifier
  395. \s* (.*) # Summary line / description
  396. """,
  397. re.X | re.S | re.M,
  398. )
  399. re_yields_section = re.compile(
  400. _re_section_template.format(r"Yields?"), re.X | re.S | re.M
  401. )
  402. re_yields_line = re_returns_line
  403. supports_yields = True
  404. def matching_sections(self) -> int:
  405. """Returns the number of matching docstring sections."""
  406. return sum(
  407. bool(i)
  408. for i in (
  409. self.re_param_section.search(self.doc),
  410. self.re_raise_section.search(self.doc),
  411. self.re_returns_section.search(self.doc),
  412. self.re_yields_section.search(self.doc),
  413. self.re_property_returns_line.search(self._first_line()),
  414. )
  415. )
  416. def has_params(self) -> bool:
  417. if not self.doc:
  418. return False
  419. return self.re_param_section.search(self.doc) is not None
  420. def has_returns(self) -> bool:
  421. if not self.doc:
  422. return False
  423. entries = self._parse_section(self.re_returns_section)
  424. for entry in entries:
  425. match = self.re_returns_line.match(entry)
  426. if not match:
  427. continue
  428. return_desc = match.group(2)
  429. if return_desc:
  430. return True
  431. return False
  432. def has_rtype(self) -> bool:
  433. if not self.doc:
  434. return False
  435. entries = self._parse_section(self.re_returns_section)
  436. for entry in entries:
  437. match = self.re_returns_line.match(entry)
  438. if not match:
  439. continue
  440. return_type = match.group(1)
  441. if return_type:
  442. return True
  443. return False
  444. def has_property_returns(self) -> bool:
  445. # The summary line is the return doc,
  446. # so the first line must not be a known directive.
  447. first_line = self._first_line()
  448. return not bool(
  449. self.re_param_section.search(first_line)
  450. or self.re_raise_section.search(first_line)
  451. or self.re_returns_section.search(first_line)
  452. or self.re_yields_section.search(first_line)
  453. )
  454. def has_property_type(self) -> bool:
  455. if not self.doc:
  456. return False
  457. return bool(self.re_property_returns_line.match(self._first_line()))
  458. def has_yields(self) -> bool:
  459. if not self.doc:
  460. return False
  461. entries = self._parse_section(self.re_yields_section)
  462. for entry in entries:
  463. match = self.re_yields_line.match(entry)
  464. if not match:
  465. continue
  466. yield_desc = match.group(2)
  467. if yield_desc:
  468. return True
  469. return False
  470. def has_yields_type(self) -> bool:
  471. if not self.doc:
  472. return False
  473. entries = self._parse_section(self.re_yields_section)
  474. for entry in entries:
  475. match = self.re_yields_line.match(entry)
  476. if not match:
  477. continue
  478. yield_type = match.group(1)
  479. if yield_type:
  480. return True
  481. return False
  482. def exceptions(self) -> set[str]:
  483. types: set[str] = set()
  484. entries = self._parse_section(self.re_raise_section)
  485. for entry in entries:
  486. match = self.re_raise_line.match(entry)
  487. if not match:
  488. continue
  489. exc_type = match.group(1)
  490. exc_desc = match.group(2)
  491. if exc_desc:
  492. types.update(_split_multiple_exc_types(exc_type))
  493. return types
  494. def match_param_docs(self) -> tuple[set[str], set[str]]:
  495. params_with_doc: set[str] = set()
  496. params_with_type: set[str] = set()
  497. entries = self._parse_section(self.re_param_section)
  498. entries.extend(self._parse_section(self.re_keyword_param_section))
  499. for entry in entries:
  500. match = self.re_param_line.match(entry)
  501. if not match:
  502. continue
  503. param_name = match.group(1)
  504. # Remove escape characters necessary for asterisks
  505. param_name = param_name.replace("\\", "")
  506. param_type = match.group(2)
  507. param_desc = match.group(3)
  508. if param_type:
  509. params_with_type.add(param_name)
  510. if param_desc:
  511. params_with_doc.add(param_name)
  512. return params_with_doc, params_with_type
  513. def _first_line(self) -> str:
  514. return self.doc.lstrip().split("\n", 1)[0]
  515. @staticmethod
  516. def min_section_indent(section_match: re.Match[str]) -> int:
  517. return len(section_match.group(1)) + 1
  518. @staticmethod
  519. def _is_section_header(_: str) -> bool:
  520. # Google parsing does not need to detect section headers,
  521. # because it works off of indentation level only
  522. return False
  523. def _parse_section(self, section_re: re.Pattern[str]) -> list[str]:
  524. section_match = section_re.search(self.doc)
  525. if section_match is None:
  526. return []
  527. min_indentation = self.min_section_indent(section_match)
  528. entries: list[str] = []
  529. entry: list[str] = []
  530. is_first = True
  531. for line in section_match.group(2).splitlines():
  532. if not line.strip():
  533. continue
  534. indentation = space_indentation(line)
  535. if indentation < min_indentation:
  536. break
  537. # The first line after the header defines the minimum
  538. # indentation.
  539. if is_first:
  540. min_indentation = indentation
  541. is_first = False
  542. if indentation == min_indentation:
  543. if self._is_section_header(line):
  544. break
  545. # Lines with minimum indentation must contain the beginning
  546. # of a new parameter documentation.
  547. if entry:
  548. entries.append("\n".join(entry))
  549. entry = []
  550. entry.append(line)
  551. if entry:
  552. entries.append("\n".join(entry))
  553. return entries
  554. class NumpyDocstring(GoogleDocstring):
  555. _re_section_template = r"""
  556. ^([ ]*) {0} \s*?$ # Numpy parameters header
  557. \s* [-=]+ \s*?$ # underline
  558. ( .* ) # section
  559. """
  560. re_param_section = re.compile(
  561. _re_section_template.format(r"(?:Args|Arguments|Parameters)"),
  562. re.X | re.S | re.M,
  563. )
  564. re_default_value = r"""((['"]\w+\s*['"])|(\d+)|(True)|(False)|(None))"""
  565. re_param_line = re.compile(
  566. rf"""
  567. \s* (?P<param_name>\*{{0,2}}\w+)(\s?(:|\n)) # identifier with potential asterisks
  568. \s*
  569. (?P<param_type>
  570. (
  571. ({GoogleDocstring.re_multiple_type}) # default type declaration
  572. (,\s+optional)? # optional 'optional' indication
  573. )?
  574. (
  575. {{({re_default_value},?\s*)+}} # set of default values
  576. )?
  577. (?:$|\n)
  578. )?
  579. (
  580. \s* (?P<param_desc>.*) # optional description
  581. )?
  582. """,
  583. re.X | re.S,
  584. )
  585. re_raise_section = re.compile(
  586. _re_section_template.format(r"Raises"), re.X | re.S | re.M
  587. )
  588. re_raise_line = re.compile(
  589. rf"""
  590. \s* ({GoogleDocstring.re_type})$ # type declaration
  591. \s* (.*) # optional description
  592. """,
  593. re.X | re.S | re.M,
  594. )
  595. re_returns_section = re.compile(
  596. _re_section_template.format(r"Returns?"), re.X | re.S | re.M
  597. )
  598. re_returns_line = re.compile(
  599. rf"""
  600. \s* (?:\w+\s+:\s+)? # optional name
  601. ({GoogleDocstring.re_multiple_type})$ # type declaration
  602. \s* (.*) # optional description
  603. """,
  604. re.X | re.S | re.M,
  605. )
  606. re_yields_section = re.compile(
  607. _re_section_template.format(r"Yields?"), re.X | re.S | re.M
  608. )
  609. re_yields_line = re_returns_line
  610. supports_yields = True
  611. def match_param_docs(self) -> tuple[set[str], set[str]]:
  612. """Matches parameter documentation section to parameter documentation rules."""
  613. params_with_doc = set()
  614. params_with_type = set()
  615. entries = self._parse_section(self.re_param_section)
  616. entries.extend(self._parse_section(self.re_keyword_param_section))
  617. for entry in entries:
  618. match = self.re_param_line.match(entry)
  619. if not match:
  620. continue
  621. # check if parameter has description only
  622. re_only_desc = re.match(r"\s*(\*{0,2}\w+)\s*:?\n\s*\w*$", entry)
  623. if re_only_desc:
  624. param_name = match.group("param_name")
  625. param_desc = match.group("param_type")
  626. param_type = None
  627. else:
  628. param_name = match.group("param_name")
  629. param_type = match.group("param_type")
  630. param_desc = match.group("param_desc")
  631. # The re_param_line pattern needs to match multi-line which removes the ability
  632. # to match a single line description like 'arg : a number type.'
  633. # We are not trying to determine whether 'a number type' is correct typing
  634. # but we do accept it as typing as it is in the place where typing
  635. # should be
  636. if param_type is None and re.match(r"\s*(\*{0,2}\w+)\s*:.+$", entry):
  637. param_type = param_desc
  638. # If the description is "" but we have a type description
  639. # we consider the description to be the type
  640. if not param_desc and param_type:
  641. param_desc = param_type
  642. if param_type:
  643. params_with_type.add(param_name)
  644. if param_desc:
  645. params_with_doc.add(param_name)
  646. return params_with_doc, params_with_type
  647. @staticmethod
  648. def min_section_indent(section_match: re.Match[str]) -> int:
  649. return len(section_match.group(1))
  650. @staticmethod
  651. def _is_section_header(line: str) -> bool:
  652. return bool(re.match(r"\s*-+$", line))
  653. DOCSTRING_TYPES = {
  654. "sphinx": SphinxDocstring,
  655. "epytext": EpytextDocstring,
  656. "google": GoogleDocstring,
  657. "numpy": NumpyDocstring,
  658. "default": Docstring,
  659. }
  660. """A map of the name of the docstring type to its class.
  661. :type: dict(str, type)
  662. """