sphinx.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. # coding: utf-8
  2. """
  3. Sphinx directive integration
  4. ============================
  5. We usually need to document the life-cycle of functions and classes:
  6. when they are created, modified or deprecated.
  7. To do that, `Sphinx <http://www.sphinx-doc.org>`_ has a set
  8. of `Paragraph-level markups <http://www.sphinx-doc.org/en/stable/markup/para.html>`_:
  9. - ``versionadded``: to document the version of the project which added the described feature to the library,
  10. - ``versionchanged``: to document changes of a feature,
  11. - ``deprecated``: to document a deprecated feature.
  12. The purpose of this module is to defined decorators which adds this Sphinx directives
  13. to the docstring of your function and classes.
  14. Of course, the ``@deprecated`` decorator will emit a deprecation warning
  15. when the function/method is called or the class is constructed.
  16. """
  17. import re
  18. import textwrap
  19. from deprecated.classic import ClassicAdapter
  20. from deprecated.classic import deprecated as _classic_deprecated
  21. class SphinxAdapter(ClassicAdapter):
  22. """
  23. Sphinx adapter -- *for advanced usage only*
  24. This adapter override the :class:`~deprecated.classic.ClassicAdapter`
  25. in order to add the Sphinx directives to the end of the function/class docstring.
  26. Such a directive is a `Paragraph-level markup <http://www.sphinx-doc.org/en/stable/markup/para.html>`_
  27. - The directive can be one of "versionadded", "versionchanged" or "deprecated".
  28. - The version number is added if provided.
  29. - The reason message is obviously added in the directive block if not empty.
  30. """
  31. def __init__(
  32. self,
  33. directive,
  34. reason="",
  35. version="",
  36. action=None,
  37. category=DeprecationWarning,
  38. extra_stacklevel=0,
  39. line_length=70,
  40. ):
  41. """
  42. Construct a wrapper adapter.
  43. :type directive: str
  44. :param directive:
  45. Sphinx directive: can be one of "versionadded", "versionchanged" or "deprecated".
  46. :type reason: str
  47. :param reason:
  48. Reason message which documents the deprecation in your library (can be omitted).
  49. :type version: str
  50. :param version:
  51. Version of your project which deprecates this feature.
  52. If you follow the `Semantic Versioning <https://semver.org/>`_,
  53. the version number has the format "MAJOR.MINOR.PATCH".
  54. :type action: Literal["default", "error", "ignore", "always", "module", "once"]
  55. :param action:
  56. A warning filter used to activate or not the deprecation warning.
  57. Can be one of "error", "ignore", "always", "default", "module", or "once".
  58. If ``None`` or empty, the global filtering mechanism is used.
  59. See: `The Warnings Filter`_ in the Python documentation.
  60. :type category: Type[Warning]
  61. :param category:
  62. The warning category to use for the deprecation warning.
  63. By default, the category class is :class:`~DeprecationWarning`,
  64. you can inherit this class to define your own deprecation warning category.
  65. :type extra_stacklevel: int
  66. :param extra_stacklevel:
  67. Number of additional stack levels to consider instrumentation rather than user code.
  68. With the default value of 0, the warning refers to where the class was instantiated
  69. or the function was called.
  70. :type line_length: int
  71. :param line_length:
  72. Max line length of the directive text. If non nul, a long text is wrapped in several lines.
  73. .. versionchanged:: 1.2.15
  74. Add the *extra_stacklevel* parameter.
  75. """
  76. if not version:
  77. # https://github.com/laurent-laporte-pro/deprecated/issues/40
  78. raise ValueError("'version' argument is required in Sphinx directives")
  79. self.directive = directive
  80. self.line_length = line_length
  81. super(SphinxAdapter, self).__init__(
  82. reason=reason, version=version, action=action, category=category, extra_stacklevel=extra_stacklevel
  83. )
  84. def __call__(self, wrapped):
  85. """
  86. Add the Sphinx directive to your class or function.
  87. :param wrapped: Wrapped class or function.
  88. :return: the decorated class or function.
  89. """
  90. # -- build the directive division
  91. fmt = ".. {directive}:: {version}" if self.version else ".. {directive}::"
  92. div_lines = [fmt.format(directive=self.directive, version=self.version)]
  93. width = self.line_length - 3 if self.line_length > 3 else 2**16
  94. reason = textwrap.dedent(self.reason).strip()
  95. for paragraph in reason.splitlines():
  96. if paragraph:
  97. div_lines.extend(
  98. textwrap.fill(
  99. paragraph,
  100. width=width,
  101. initial_indent=" ",
  102. subsequent_indent=" ",
  103. ).splitlines()
  104. )
  105. else:
  106. div_lines.append("")
  107. # -- get the docstring, normalize the trailing newlines
  108. # keep a consistent behaviour if the docstring starts with newline or directly on the first one
  109. docstring = wrapped.__doc__ or ""
  110. lines = docstring.splitlines(True) or [""]
  111. docstring = textwrap.dedent("".join(lines[1:])) if len(lines) > 1 else ""
  112. docstring = lines[0] + docstring
  113. if docstring:
  114. # An empty line must separate the original docstring and the directive.
  115. docstring = re.sub(r"\n+$", "", docstring, flags=re.DOTALL) + "\n\n"
  116. else:
  117. # Avoid "Explicit markup ends without a blank line" when the decorated function has no docstring
  118. docstring = "\n"
  119. # -- append the directive division to the docstring
  120. docstring += "".join("{}\n".format(line) for line in div_lines)
  121. wrapped.__doc__ = docstring
  122. if self.directive in {"versionadded", "versionchanged"}:
  123. return wrapped
  124. return super(SphinxAdapter, self).__call__(wrapped)
  125. def get_deprecated_msg(self, wrapped, instance):
  126. """
  127. Get the deprecation warning message (without Sphinx cross-referencing syntax) for the user.
  128. :param wrapped: Wrapped class or function.
  129. :param instance: The object to which the wrapped function was bound when it was called.
  130. :return: The warning message.
  131. .. versionadded:: 1.2.12
  132. Strip Sphinx cross-referencing syntax from warning message.
  133. """
  134. msg = super(SphinxAdapter, self).get_deprecated_msg(wrapped, instance)
  135. # Strip Sphinx cross-reference syntax (like ":function:", ":py:func:" and ":py:meth:")
  136. # Possible values are ":role:`foo`", ":domain:role:`foo`"
  137. # where ``role`` and ``domain`` should match "[a-zA-Z]+"
  138. msg = re.sub(r"(?: : [a-zA-Z]+ )? : [a-zA-Z]+ : (`[^`]*`)", r"\1", msg, flags=re.X)
  139. return msg
  140. def versionadded(reason="", version="", line_length=70):
  141. """
  142. This decorator can be used to insert a "versionadded" directive
  143. in your function/class docstring in order to document the
  144. version of the project which adds this new functionality in your library.
  145. :param str reason:
  146. Reason message which documents the addition in your library (can be omitted).
  147. :param str version:
  148. Version of your project which adds this feature.
  149. If you follow the `Semantic Versioning <https://semver.org/>`_,
  150. the version number has the format "MAJOR.MINOR.PATCH", and,
  151. in the case of a new functionality, the "PATCH" component should be "0".
  152. :type line_length: int
  153. :param line_length:
  154. Max line length of the directive text. If non nul, a long text is wrapped in several lines.
  155. :return: the decorated function.
  156. """
  157. adapter = SphinxAdapter(
  158. 'versionadded',
  159. reason=reason,
  160. version=version,
  161. line_length=line_length,
  162. )
  163. return adapter
  164. def versionchanged(reason="", version="", line_length=70):
  165. """
  166. This decorator can be used to insert a "versionchanged" directive
  167. in your function/class docstring in order to document the
  168. version of the project which modifies this functionality in your library.
  169. :param str reason:
  170. Reason message which documents the modification in your library (can be omitted).
  171. :param str version:
  172. Version of your project which modifies this feature.
  173. If you follow the `Semantic Versioning <https://semver.org/>`_,
  174. the version number has the format "MAJOR.MINOR.PATCH".
  175. :type line_length: int
  176. :param line_length:
  177. Max line length of the directive text. If non nul, a long text is wrapped in several lines.
  178. :return: the decorated function.
  179. """
  180. adapter = SphinxAdapter(
  181. 'versionchanged',
  182. reason=reason,
  183. version=version,
  184. line_length=line_length,
  185. )
  186. return adapter
  187. def deprecated(reason="", version="", line_length=70, **kwargs):
  188. """
  189. This decorator can be used to insert a "deprecated" directive
  190. in your function/class docstring in order to document the
  191. version of the project which deprecates this functionality in your library.
  192. :param str reason:
  193. Reason message which documents the deprecation in your library (can be omitted).
  194. :param str version:
  195. Version of your project which deprecates this feature.
  196. If you follow the `Semantic Versioning <https://semver.org/>`_,
  197. the version number has the format "MAJOR.MINOR.PATCH".
  198. :type line_length: int
  199. :param line_length:
  200. Max line length of the directive text. If non nul, a long text is wrapped in several lines.
  201. Keyword arguments can be:
  202. - "action":
  203. A warning filter used to activate or not the deprecation warning.
  204. Can be one of "error", "ignore", "always", "default", "module", or "once".
  205. If ``None``, empty or missing, the global filtering mechanism is used.
  206. - "category":
  207. The warning category to use for the deprecation warning.
  208. By default, the category class is :class:`~DeprecationWarning`,
  209. you can inherit this class to define your own deprecation warning category.
  210. - "extra_stacklevel":
  211. Number of additional stack levels to consider instrumentation rather than user code.
  212. With the default value of 0, the warning refers to where the class was instantiated
  213. or the function was called.
  214. :return: a decorator used to deprecate a function.
  215. .. versionchanged:: 1.2.13
  216. Change the signature of the decorator to reflect the valid use cases.
  217. .. versionchanged:: 1.2.15
  218. Add the *extra_stacklevel* parameter.
  219. """
  220. directive = kwargs.pop('directive', 'deprecated')
  221. adapter_cls = kwargs.pop('adapter_cls', SphinxAdapter)
  222. kwargs["reason"] = reason
  223. kwargs["version"] = version
  224. kwargs["line_length"] = line_length
  225. return _classic_deprecated(directive=directive, adapter_cls=adapter_cls, **kwargs)