178 lines
5.4 KiB
Python
178 lines
5.4 KiB
Python
import importlib
|
|
import inspect
|
|
import re
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import Dict, List, Optional, Set, Tuple
|
|
|
|
_SPHINX_AUTOSUMMARY_HEADER = ".. autosummary::"
|
|
_SPHINX_AUTOCLASS_HEADER = ".. autoclass::"
|
|
# This is a special character used in autosummary to render only the api shortname, for
|
|
# example ~module.api_name will render only api_name
|
|
_SPHINX_AUTODOC_SHORTNAME = "~"
|
|
|
|
|
|
class AnnotationType(Enum):
|
|
PUBLIC_API = "PublicAPI"
|
|
DEVELOPER_API = "DeveloperAPI"
|
|
DEPRECATED = "Deprecated"
|
|
UNKNOWN = "Unknown"
|
|
|
|
|
|
class CodeType(Enum):
|
|
CLASS = "Class"
|
|
FUNCTION = "Function"
|
|
|
|
|
|
@dataclass
|
|
class API:
|
|
name: str
|
|
annotation_type: AnnotationType
|
|
code_type: CodeType
|
|
|
|
@staticmethod
|
|
def from_autosummary(doc: str, current_module: Optional[str] = None) -> List["API"]:
|
|
"""
|
|
Parse API from the following autosummary sphinx block.
|
|
|
|
.. autosummary::
|
|
:option_01
|
|
:option_02
|
|
|
|
api_01
|
|
api_02
|
|
"""
|
|
apis = []
|
|
lines = doc.splitlines()
|
|
if not lines:
|
|
return apis
|
|
|
|
if lines[0].strip() != _SPHINX_AUTOSUMMARY_HEADER:
|
|
return apis
|
|
|
|
for line in lines:
|
|
if line == _SPHINX_AUTOSUMMARY_HEADER:
|
|
continue
|
|
if line.strip().startswith(":"):
|
|
# option lines
|
|
continue
|
|
if line.strip().startswith(".."):
|
|
# comment lines
|
|
continue
|
|
if not line.strip():
|
|
# empty lines
|
|
continue
|
|
if not re.match(r"\s", line):
|
|
# end of autosummary, \s means empty space, this line is checking if
|
|
# the line is not empty and not starting with empty space
|
|
break
|
|
attribute = line.strip().removeprefix(_SPHINX_AUTODOC_SHORTNAME)
|
|
api_name = f"{current_module}.{attribute}" if current_module else attribute
|
|
apis.append(
|
|
API(
|
|
name=api_name,
|
|
annotation_type=AnnotationType.PUBLIC_API,
|
|
code_type=CodeType.FUNCTION,
|
|
)
|
|
)
|
|
|
|
return apis
|
|
|
|
@staticmethod
|
|
def from_autoclass(
|
|
doc: str, current_module: Optional[str] = None
|
|
) -> Optional["API"]:
|
|
"""
|
|
Parse API from the following autoclass sphinx block.
|
|
|
|
.. autoclass:: api_01
|
|
"""
|
|
doc = doc.strip()
|
|
if not doc.startswith(_SPHINX_AUTOCLASS_HEADER):
|
|
return None
|
|
cls = (
|
|
doc[len(_SPHINX_AUTOCLASS_HEADER) :]
|
|
.strip()
|
|
.removeprefix(_SPHINX_AUTODOC_SHORTNAME)
|
|
)
|
|
api_name = f"{current_module}.{cls}" if current_module else cls
|
|
|
|
return API(
|
|
name=api_name,
|
|
annotation_type=AnnotationType.PUBLIC_API,
|
|
code_type=CodeType.CLASS,
|
|
)
|
|
|
|
def get_canonical_name(self) -> str:
|
|
"""
|
|
Some APIs have aliases declared in __init__.py file (see ray/data/__init__.py
|
|
for example). This method converts the alias to full name. This is to make sure
|
|
out analysis can be performed on the same set of canonial names.
|
|
"""
|
|
tokens = self.name.split(".")
|
|
|
|
# convert the name into a python object, by converting the module token by token
|
|
attribute = importlib.import_module(tokens[0])
|
|
for token in tokens[1:]:
|
|
if not hasattr(attribute, token):
|
|
# return as it is if the name seems malformed
|
|
return self.name
|
|
attribute = getattr(attribute, token)
|
|
|
|
if inspect.isclass(attribute) or inspect.isfunction(attribute):
|
|
return f"{attribute.__module__}.{attribute.__qualname__}"
|
|
return self.name
|
|
|
|
def _is_private_name(self) -> bool:
|
|
"""
|
|
Check if this API has a private name. Private names are those that start with
|
|
underscores.
|
|
"""
|
|
name_has_underscore = self.name.split(".")[-1].startswith("_")
|
|
is_internal = "._internal." in self.name
|
|
|
|
return name_has_underscore or is_internal
|
|
|
|
def is_public(self) -> bool:
|
|
"""
|
|
Check if this API is public. Public APIs are those that are annotated as public
|
|
and not have private names.
|
|
"""
|
|
return (
|
|
self.annotation_type == AnnotationType.PUBLIC_API
|
|
and not self._is_private_name()
|
|
)
|
|
|
|
def is_deprecated(self) -> bool:
|
|
"""
|
|
Check if this API is deprecated. Deprecated APIs are those that are annotated as
|
|
deprecated.
|
|
"""
|
|
return self.annotation_type == AnnotationType.DEPRECATED
|
|
|
|
@staticmethod
|
|
def split_good_and_bad_apis(
|
|
api_in_codes: Dict[str, "API"], api_in_docs: Set[str], white_list_apis: Set[str]
|
|
) -> Tuple[List[str]]:
|
|
"""
|
|
Given the APIs in the codebase and the documentation, split the APIs into good
|
|
and bad APIs. Good APIs are those that are public and documented, bad APIs are
|
|
those that are public but NOT documented.
|
|
"""
|
|
good_apis = []
|
|
bad_apis = []
|
|
|
|
for name, api in api_in_codes.items():
|
|
if not api.is_public():
|
|
continue
|
|
|
|
if name in white_list_apis:
|
|
continue
|
|
|
|
if name in api_in_docs:
|
|
good_apis.append(name)
|
|
else:
|
|
bad_apis.append(name)
|
|
|
|
return good_apis, bad_apis
|