Update api references (#8646)

Update API reference documentation. This PR will pick up a number of missing classes, it also applies selective formatting based on the class / object type.
pull/8773/head
Eugene Yurtsev 1 year ago committed by GitHub
parent 8374367de2
commit 003e1ca9a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -100,6 +100,9 @@ extensions = [
] ]
source_suffix = [".rst"] source_suffix = [".rst"]
# some autodoc pydantic options are repeated in the actual template.
# potentially user error, but there may be bugs in the sphinx extension
# with options not being passed through correctly (from either the location in the code)
autodoc_pydantic_model_show_json = False autodoc_pydantic_model_show_json = False
autodoc_pydantic_field_list_validators = False autodoc_pydantic_field_list_validators = False
autodoc_pydantic_config_members = False autodoc_pydantic_config_members = False
@ -112,13 +115,6 @@ autodoc_member_order = "groupwise"
autoclass_content = "both" autoclass_content = "both"
autodoc_typehints_format = "short" autodoc_typehints_format = "short"
autodoc_default_options = {
"members": True,
"show-inheritance": True,
"inherited-members": "BaseModel",
"undoc-members": True,
"special-members": "__call__",
}
# autodoc_typehints = "description" # autodoc_typehints = "description"
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ["templates"] templates_path = ["templates"]

@ -1,49 +1,209 @@
"""Script for auto-generating api_reference.rst""" """Script for auto-generating api_reference.rst."""
import glob import importlib
import re import inspect
import typing
from pathlib import Path from pathlib import Path
from typing import TypedDict, Sequence, List, Dict, Literal, Union
from enum import Enum
from pydantic import BaseModel
ROOT_DIR = Path(__file__).parents[2].absolute() ROOT_DIR = Path(__file__).parents[2].absolute()
HERE = Path(__file__).parent
PKG_DIR = ROOT_DIR / "libs" / "langchain" / "langchain" PKG_DIR = ROOT_DIR / "libs" / "langchain" / "langchain"
EXP_DIR = ROOT_DIR / "libs" / "experimental" / "langchain_experimental" EXP_DIR = ROOT_DIR / "libs" / "experimental" / "langchain_experimental"
WRITE_FILE = Path(__file__).parent / "api_reference.rst" WRITE_FILE = HERE / "api_reference.rst"
EXP_WRITE_FILE = Path(__file__).parent / "experimental_api_reference.rst" EXP_WRITE_FILE = HERE / "experimental_api_reference.rst"
def load_members(dir: Path) -> dict: ClassKind = Literal["TypedDict", "Regular", "Pydantic", "enum"]
members: dict = {}
for py in glob.glob(str(dir) + "/**/*.py", recursive=True):
module = py[len(str(dir)) + 1 :].replace(".py", "").replace("/", ".") class ClassInfo(TypedDict):
top_level = module.split(".")[0] """Information about a class."""
if top_level not in members:
members[top_level] = {"classes": [], "functions": []} name: str
with open(py, "r") as f: """The name of the class."""
for line in f.readlines(): qualified_name: str
cls = re.findall(r"^class ([^_].*)\(", line) """The fully qualified name of the class."""
members[top_level]["classes"].extend([module + "." + c for c in cls]) kind: ClassKind
func = re.findall(r"^def ([^_].*)\(", line) """The kind of the class."""
afunc = re.findall(r"^async def ([^_].*)\(", line) is_public: bool
func_strings = [module + "." + f for f in func + afunc] """Whether the class is public or not."""
members[top_level]["functions"].extend(func_strings)
return members
class FunctionInfo(TypedDict):
"""Information about a function."""
def construct_doc(pkg: str, members: dict) -> str:
name: str
"""The name of the function."""
qualified_name: str
"""The fully qualified name of the function."""
is_public: bool
"""Whether the function is public or not."""
class ModuleMembers(TypedDict):
"""A dictionary of module members."""
classes_: Sequence[ClassInfo]
functions: Sequence[FunctionInfo]
def _load_module_members(module_path: str, namespace: str) -> ModuleMembers:
"""Load all members of a module.
Args:
module_path: Path to the module.
namespace: the namespace of the module.
Returns:
list: A list of loaded module objects.
"""
classes_: List[ClassInfo] = []
functions: List[FunctionInfo] = []
module = importlib.import_module(module_path)
for name, type_ in inspect.getmembers(module):
if not hasattr(type_, "__module__"):
continue
if type_.__module__ != module_path:
continue
if inspect.isclass(type_):
if type(type_) == typing._TypedDictMeta: # type: ignore
kind: ClassKind = "TypedDict"
elif issubclass(type_, Enum):
kind = "enum"
elif issubclass(type_, BaseModel):
kind = "Pydantic"
else:
kind = "Regular"
classes_.append(
ClassInfo(
name=name,
qualified_name=f"{namespace}.{name}",
kind=kind,
is_public=not name.startswith("_"),
)
)
elif inspect.isfunction(type_):
functions.append(
FunctionInfo(
name=name,
qualified_name=f"{namespace}.{name}",
is_public=not name.startswith("_"),
)
)
else:
continue
return ModuleMembers(
classes_=classes_,
functions=functions,
)
def _merge_module_members(
module_members: Sequence[ModuleMembers],
) -> ModuleMembers:
"""Merge module members."""
classes_: List[ClassInfo] = []
functions: List[FunctionInfo] = []
for module in module_members:
classes_.extend(module["classes_"])
functions.extend(module["functions"])
return ModuleMembers(
classes_=classes_,
functions=functions,
)
def _load_package_modules(
package_directory: Union[str, Path]
) -> Dict[str, ModuleMembers]:
"""Recursively load modules of a package based on the file system.
Traversal based on the file system makes it easy to determine which
of the modules/packages are part of the package vs. 3rd party or built-in.
Parameters:
package_directory: Path to the package directory.
Returns:
list: A list of loaded module objects.
"""
package_path = (
Path(package_directory)
if isinstance(package_directory, str)
else package_directory
)
modules_by_namespace = {}
package_name = package_path.name
for file_path in package_path.rglob("*.py"):
if not file_path.name.startswith("__"):
relative_module_name = file_path.relative_to(package_path)
# Get the full namespace of the module
namespace = str(relative_module_name).replace(".py", "").replace("/", ".")
# Keep only the top level namespace
top_namespace = namespace.split(".")[0]
try:
module_members = _load_module_members(
f"{package_name}.{namespace}", namespace
)
# Merge module members if the namespace already exists
if top_namespace in modules_by_namespace:
existing_module_members = modules_by_namespace[top_namespace]
_module_members = _merge_module_members(
[existing_module_members, module_members]
)
else:
_module_members = module_members
modules_by_namespace[top_namespace] = _module_members
except ImportError as e:
print(f"Error: Unable to import module '{namespace}' with error: {e}")
return modules_by_namespace
def _construct_doc(pkg: str, members_by_namespace: Dict[str, ModuleMembers]) -> str:
"""Construct the contents of the reference.rst file for the given package.
Args:
pkg: The package name
members_by_namespace: The members of the package, dict organized by top level
module contains a list of classes and functions
inside of the top level namespace.
Returns:
The contents of the reference.rst file.
"""
full_doc = f"""\ full_doc = f"""\
============= =======================
``{pkg}`` API Reference ``{pkg}`` API Reference
============= =======================
""" """
for module, _members in sorted(members.items(), key=lambda kv: kv[0]): namespaces = sorted(members_by_namespace)
classes = _members["classes"]
for module in namespaces:
_members = members_by_namespace[module]
classes = _members["classes_"]
functions = _members["functions"] functions = _members["functions"]
if not (classes or functions): if not (classes or functions):
continue continue
section = f":mod:`{pkg}.{module}`" section = f":mod:`{pkg}.{module}`"
underline = "=" * (len(section) + 1)
full_doc += f"""\ full_doc += f"""\
{section} {section}
{'=' * (len(section) + 1)} {underline}
.. automodule:: {pkg}.{module} .. automodule:: {pkg}.{module}
:no-members: :no-members:
@ -52,7 +212,6 @@ def construct_doc(pkg: str, members: dict) -> str:
""" """
if classes: if classes:
cstring = "\n ".join(sorted(classes))
full_doc += f"""\ full_doc += f"""\
Classes Classes
-------------- --------------
@ -60,13 +219,31 @@ Classes
.. autosummary:: .. autosummary::
:toctree: {module} :toctree: {module}
:template: class.rst """
for class_ in classes:
if not class_['is_public']:
continue
if class_["kind"] == "TypedDict":
template = "typeddict.rst"
elif class_["kind"] == "enum":
template = "enum.rst"
elif class_["kind"] == "Pydantic":
template = "pydantic.rst"
else:
template = "class.rst"
{cstring} full_doc += f"""\
:template: {template}
{class_["qualified_name"]}
""" """
if functions: if functions:
fstring = "\n ".join(sorted(functions)) _functions = [f["qualified_name"] for f in functions if f["is_public"]]
fstring = "\n ".join(sorted(_functions))
full_doc += f"""\ full_doc += f"""\
Functions Functions
-------------- --------------
@ -83,12 +260,15 @@ Functions
def main() -> None: def main() -> None:
lc_members = load_members(PKG_DIR) """Generate the reference.rst file for each package."""
lc_doc = ".. _api_reference:\n\n" + construct_doc("langchain", lc_members) lc_members = _load_package_modules(PKG_DIR)
lc_doc = ".. _api_reference:\n\n" + _construct_doc("langchain", lc_members)
with open(WRITE_FILE, "w") as f: with open(WRITE_FILE, "w") as f:
f.write(lc_doc) f.write(lc_doc)
exp_members = load_members(EXP_DIR) exp_members = _load_package_modules(EXP_DIR)
exp_doc = ".. _experimental_api_reference:\n\n" + construct_doc("langchain_experimental", exp_members) exp_doc = ".. _experimental_api_reference:\n\n" + _construct_doc(
"langchain_experimental", exp_members
)
with open(EXP_WRITE_FILE, "w") as f: with open(EXP_WRITE_FILE, "w") as f:
f.write(exp_doc) f.write(exp_doc)

@ -1,4 +1,5 @@
-e libs/langchain -e libs/langchain
-e libs/experimental
autodoc_pydantic==1.8.0 autodoc_pydantic==1.8.0
myst_parser myst_parser
nbsphinx==0.8.9 nbsphinx==0.8.9

@ -5,26 +5,32 @@
.. autoclass:: {{ objname }} .. autoclass:: {{ objname }}
{% block methods %} {% block attributes %}
{% if methods %} {% if attributes %}
.. rubric:: {{ _('Methods') }} .. rubric:: {{ _('Attributes') }}
.. autosummary:: .. autosummary::
{% for item in methods %} {% for item in attributes %}
~{{ name }}.{{ item }} ~{{ name }}.{{ item }}
{%- endfor %} {%- endfor %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block attributes %} {% block methods %}
{% if attributes %} {% if methods %}
.. rubric:: {{ _('Attributes') }} .. rubric:: {{ _('Methods') }}
.. autosummary:: .. autosummary::
{% for item in attributes %} {% for item in methods %}
~{{ name }}.{{ item }} ~{{ name }}.{{ item }}
{%- endfor %} {%- endfor %}
{% for item in methods %}
.. automethod:: {{ name }}.{{ item }}
{%- endfor %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}
.. example_links:: {{ objname }} .. example_links:: {{ objname }}

@ -0,0 +1,14 @@
:mod:`{{module}}`.{{objname}}
{{ underline }}==============
.. currentmodule:: {{ module }}
.. autoclass:: {{ objname }}
{% block attributes %}
{% for item in attributes %}
.. autoattribute:: {{ item }}
{% endfor %}
{% endblock %}
.. example_links:: {{ objname }}

@ -0,0 +1,22 @@
:mod:`{{module}}`.{{objname}}
{{ underline }}==============
.. currentmodule:: {{ module }}
.. autopydantic_model:: {{ objname }}
:model-show-json: False
:model-show-config-summary: False
:model-show-validator-members: False
:model-show-field-summary: False
:field-signature-prefix: param
:members:
:undoc-members:
:inherited-members:
:member-order: groupwise
:show-inheritance: True
:special-members: __call__
{% block attributes %}
{% endblock %}
.. example_links:: {{ objname }}

@ -0,0 +1,14 @@
:mod:`{{module}}`.{{objname}}
{{ underline }}==============
.. currentmodule:: {{ module }}
.. autoclass:: {{ objname }}
{% block attributes %}
{% for item in attributes %}
.. autoattribute:: {{ item }}
{% endfor %}
{% endblock %}
.. example_links:: {{ objname }}
Loading…
Cancel
Save