annotations.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. import inspect
  2. import sys
  3. import warnings
  4. from enum import Enum
  5. from functools import wraps
  6. from typing import Any, Callable, Optional, TypeVar, cast, overload
  7. # TypeVar for preserving function/class signatures through decorators.
  8. # Note: These decorators also accept properties, but we use Callable for the
  9. # common case. Properties work at runtime but won't get full type inference.
  10. F = TypeVar("F", bound=Callable[..., Any])
  11. class AnnotationType(Enum):
  12. PUBLIC_API = "PublicAPI"
  13. DEVELOPER_API = "DeveloperAPI"
  14. DEPRECATED = "Deprecated"
  15. UNKNOWN = "Unknown"
  16. @overload
  17. def PublicAPI(obj: F) -> F:
  18. ...
  19. @overload
  20. def PublicAPI(
  21. *, stability: str = "stable", api_group: str = "Others"
  22. ) -> Callable[[F], F]:
  23. ...
  24. def PublicAPI(*args, **kwargs):
  25. """Annotation for documenting public APIs.
  26. Public APIs are classes and methods exposed to end users of Ray.
  27. If ``stability="alpha"``, the API can be used by advanced users who are
  28. tolerant to and expect breaking changes.
  29. If ``stability="beta"``, the API is still public and can be used by early
  30. users, but are subject to change.
  31. If ``stability="stable"``, the APIs will remain backwards compatible across
  32. minor Ray releases (e.g., Ray 1.4 -> 1.8).
  33. For a full definition of the stability levels, please refer to the
  34. :ref:`Ray API Stability definitions <api-stability>`.
  35. Args:
  36. stability: One of {"stable", "beta", "alpha"}.
  37. api_group: Optional. Used only for doc rendering purpose. APIs in the same group
  38. will be grouped together in the API doc pages.
  39. Examples:
  40. >>> from ray.util.annotations import PublicAPI
  41. >>> @PublicAPI
  42. ... def func(x):
  43. ... return x
  44. >>> @PublicAPI(stability="beta")
  45. ... def func(y):
  46. ... return y
  47. """
  48. if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
  49. return PublicAPI(stability="stable", api_group="Others")(args[0])
  50. if "stability" in kwargs:
  51. stability = kwargs["stability"]
  52. assert stability in ["stable", "beta", "alpha"], stability
  53. else:
  54. stability = "stable"
  55. api_group = kwargs.get("api_group", "Others")
  56. def wrap(obj: F) -> F:
  57. if stability in ["alpha", "beta"]:
  58. message = (
  59. f"**PublicAPI ({stability}):** This API is in {stability} "
  60. "and may change before becoming stable."
  61. )
  62. _append_doc(obj, message=message)
  63. _mark_annotated(obj, type=AnnotationType.PUBLIC_API, api_group=api_group)
  64. return obj
  65. return wrap
  66. @overload
  67. def DeveloperAPI(obj: F) -> F:
  68. ...
  69. @overload
  70. def DeveloperAPI() -> Callable[[F], F]:
  71. ...
  72. def DeveloperAPI(*args, **kwargs):
  73. """Annotation for documenting developer APIs.
  74. Developer APIs are lower-level methods explicitly exposed to advanced Ray
  75. users and library developers. Their interfaces may change across minor
  76. Ray releases.
  77. Examples:
  78. >>> from ray.util.annotations import DeveloperAPI
  79. >>> @DeveloperAPI
  80. ... def func(x):
  81. ... return x
  82. """
  83. if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
  84. return DeveloperAPI()(args[0])
  85. def wrap(obj: F) -> F:
  86. _append_doc(
  87. obj,
  88. message="**DeveloperAPI:** This API may change across minor Ray releases.",
  89. )
  90. _mark_annotated(obj, type=AnnotationType.DEVELOPER_API)
  91. return obj
  92. return wrap
  93. class RayDeprecationWarning(DeprecationWarning):
  94. """Specialized Deprecation Warning for fine grained filtering control"""
  95. pass
  96. # By default, print the first occurrence of matching warnings for
  97. # each module where the warning is issued (regardless of line number)
  98. if not sys.warnoptions:
  99. warnings.filterwarnings("module", category=RayDeprecationWarning)
  100. @overload
  101. def Deprecated(obj: F) -> F:
  102. ...
  103. @overload
  104. def Deprecated(*, message: str = ..., warning: bool = False) -> Callable[[F], F]:
  105. ...
  106. def Deprecated(*args, **kwargs):
  107. """Annotation for documenting a deprecated API.
  108. Deprecated APIs may be removed in future releases of Ray.
  109. Args:
  110. message: a message to help users understand the reason for the
  111. deprecation, and provide a migration path.
  112. Examples:
  113. >>> from ray.util.annotations import Deprecated
  114. >>> @Deprecated
  115. ... def func(x):
  116. ... return x
  117. >>> @Deprecated(message="g() is deprecated because the API is error "
  118. ... "prone. Please call h() instead.")
  119. ... def g(y):
  120. ... return y
  121. """
  122. if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
  123. return Deprecated()(args[0])
  124. doc_message = (
  125. "**DEPRECATED**: This API is deprecated and may be removed "
  126. "in future Ray releases."
  127. )
  128. warning_message = (
  129. "This API is deprecated and may be removed in future Ray releases. "
  130. "You could suppress this warning by setting env variable "
  131. 'PYTHONWARNINGS="ignore::DeprecationWarning"'
  132. )
  133. warning = kwargs.pop("warning", False)
  134. if "message" in kwargs:
  135. doc_message = doc_message + "\n" + kwargs["message"]
  136. warning_message = warning_message + "\n" + kwargs["message"]
  137. del kwargs["message"]
  138. if kwargs:
  139. raise ValueError("Unknown kwargs: {}".format(kwargs.keys()))
  140. def inner(obj: F) -> F:
  141. _append_doc(obj, message=doc_message, directive="warning")
  142. _mark_annotated(obj, type=AnnotationType.DEPRECATED)
  143. if not warning:
  144. return obj
  145. if inspect.isclass(obj):
  146. obj_init = obj.__init__
  147. def patched_init(*args, **kwargs):
  148. warnings.warn(warning_message, RayDeprecationWarning, stacklevel=2)
  149. return obj_init(*args, **kwargs)
  150. obj.__init__ = patched_init
  151. return obj
  152. else:
  153. # class method or function.
  154. def wrapper(*args, **kwargs):
  155. warnings.warn(warning_message, RayDeprecationWarning, stacklevel=2)
  156. return obj(*args, **kwargs)
  157. # Only apply @wraps for actual callables, not properties/descriptors.
  158. # Setting __wrapped__ on a property causes inspect.unwrap() to return
  159. # the property, which breaks inspect.signature() in the tracing helper.
  160. if callable(obj):
  161. wrapper = wraps(obj)(wrapper)
  162. return cast(F, wrapper)
  163. return inner
  164. def _append_doc(obj, *, message: str, directive: Optional[str] = None) -> None:
  165. if not obj.__doc__:
  166. obj.__doc__ = ""
  167. obj.__doc__ = obj.__doc__.rstrip()
  168. indent = _get_indent(obj.__doc__)
  169. obj.__doc__ += "\n\n"
  170. if directive is not None:
  171. obj.__doc__ += f"{' ' * indent}.. {directive}::\n\n"
  172. message = message.replace("\n", "\n" + " " * (indent + 4))
  173. obj.__doc__ += f"{' ' * (indent + 4)}{message}"
  174. else:
  175. message = message.replace("\n", "\n" + " " * (indent + 4))
  176. obj.__doc__ += f"{' ' * indent}{message}"
  177. obj.__doc__ += f"\n{' ' * indent}"
  178. def _get_indent(docstring: str) -> int:
  179. """
  180. Example:
  181. >>> def f():
  182. ... '''Docstring summary.'''
  183. >>> f.__doc__
  184. 'Docstring summary.'
  185. >>> _get_indent(f.__doc__)
  186. 0
  187. >>> def g(foo):
  188. ... '''Docstring summary.
  189. ...
  190. ... Args:
  191. ... foo: Does bar.
  192. ... '''
  193. >>> g.__doc__
  194. 'Docstring summary.\\n\\n Args:\\n foo: Does bar.\\n '
  195. >>> _get_indent(g.__doc__)
  196. 4
  197. >>> class A:
  198. ... def h():
  199. ... '''Docstring summary.
  200. ...
  201. ... Returns:
  202. ... None.
  203. ... '''
  204. >>> A.h.__doc__
  205. 'Docstring summary.\\n\\n Returns:\\n None.\\n '
  206. >>> _get_indent(A.h.__doc__)
  207. 8
  208. """
  209. if not docstring:
  210. return 0
  211. non_empty_lines = [line for line in docstring.splitlines() if line]
  212. if len(non_empty_lines) == 1:
  213. # Docstring contains summary only.
  214. return 0
  215. # The docstring summary isn't indented, so check the indentation of the second
  216. # non-empty line.
  217. return len(non_empty_lines[1]) - len(non_empty_lines[1].lstrip())
  218. def _mark_annotated(
  219. obj, type: AnnotationType = AnnotationType.UNKNOWN, api_group="Others"
  220. ) -> None:
  221. # Set magic token for check_api_annotations linter.
  222. if hasattr(obj, "__name__"):
  223. obj._annotated = obj.__name__
  224. obj._annotated_type = type
  225. obj._annotated_api_group = api_group
  226. def _is_annotated(obj) -> bool:
  227. # Check the magic token exists and applies to this class (not a subclass).
  228. return hasattr(obj, "_annotated") and obj._annotated == obj.__name__
  229. def _get_annotation_type(obj) -> Optional[str]:
  230. if not _is_annotated(obj):
  231. return None
  232. return obj._annotated_type.value