Files
2026-07-13 13:17:40 +08:00

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