Spaces:
Paused
Paused
| """Contains methods that generate documentation for Gradio functions and classes.""" | |
| from __future__ import annotations | |
| import dataclasses | |
| import inspect | |
| import warnings | |
| from collections import defaultdict | |
| from collections.abc import Callable | |
| from functools import lru_cache | |
| classes_to_document = defaultdict(list) | |
| classes_inherit_documentation = {} | |
| def set_documentation_group(m): # noqa: ARG001 | |
| """A no-op for backwards compatibility of custom components published prior to 4.16.0""" | |
| pass | |
| def extract_instance_attr_doc(cls, attr): | |
| code = inspect.getsource(cls.__init__) | |
| lines = [line.strip() for line in code.split("\n")] | |
| i = None | |
| for i, line in enumerate(lines): # noqa: B007 | |
| if line.startswith("self." + attr + ":") or line.startswith( | |
| "self." + attr + " =" | |
| ): | |
| break | |
| if i is None: | |
| raise NameError(f"Could not find {attr} in {cls.__name__}") | |
| start_line = lines.index('"""', i) | |
| end_line = lines.index('"""', start_line + 1) | |
| for j in range(i + 1, start_line): | |
| if lines[j].startswith("self."): | |
| raise ValueError( | |
| f"Found another attribute before docstring for {attr} in {cls.__name__}: " | |
| + lines[j] | |
| + "\n start:" | |
| + lines[i] | |
| ) | |
| doc_string = " ".join(lines[start_line + 1 : end_line]) | |
| return doc_string | |
| _module_prefixes = [ | |
| ("gradio._simple_templates", "component"), | |
| ("gradio.block", "block"), | |
| ("gradio.chat", "chatinterface"), | |
| ("gradio.component", "component"), | |
| ("gradio.events", "helpers"), | |
| ("gradio.data_classes", "helpers"), | |
| ("gradio.exceptions", "helpers"), | |
| ("gradio.external", "helpers"), | |
| ("gradio.flag", "flagging"), | |
| ("gradio.helpers", "helpers"), | |
| ("gradio.interface", "interface"), | |
| ("gradio.layout", "layout"), | |
| ("gradio.route", "routes"), | |
| ("gradio.theme", "themes"), | |
| ("gradio_client.", "py-client"), | |
| ("gradio.utils", "helpers"), | |
| ("gradio.renderable", "renderable"), | |
| ] | |
| def _get_module_documentation_group(modname) -> str: | |
| for prefix, group in _module_prefixes: | |
| if modname.startswith(prefix): | |
| return group | |
| raise ValueError(f"No known documentation group for module {modname!r}") | |
| def document(*fns, inherit=False, documentation_group=None): | |
| """ | |
| Defines the @document decorator which adds classes or functions to the Gradio | |
| documentation at www.gradio.app/docs. | |
| Usage examples: | |
| - Put @document() above a class to document the class and its constructor. | |
| - Put @document("fn1", "fn2") above a class to also document methods fn1 and fn2. | |
| - Put @document("*fn3") with an asterisk above a class to document the instance attribute methods f3. | |
| """ | |
| _documentation_group = documentation_group | |
| def inner_doc(cls): | |
| functions = list(fns) | |
| if hasattr(cls, "EVENTS"): | |
| functions += cls.EVENTS | |
| if inherit: | |
| classes_inherit_documentation[cls] = None | |
| documentation_group = _documentation_group # avoid `nonlocal` reassignment | |
| if _documentation_group is None: | |
| try: | |
| modname = inspect.getmodule(cls).__name__ # type: ignore | |
| if modname.startswith("gradio.") or modname.startswith( | |
| "gradio_client." | |
| ): | |
| documentation_group = _get_module_documentation_group(modname) | |
| else: | |
| # Then this is likely a custom Gradio component that we do not include in the documentation | |
| pass | |
| except Exception as exc: | |
| warnings.warn(f"Could not get documentation group for {cls}: {exc}") | |
| classes_to_document[documentation_group].append((cls, functions)) | |
| return cls | |
| return inner_doc | |
| def document_fn(fn: Callable, cls) -> tuple[str, list[dict], dict, str | None]: | |
| """ | |
| Generates documentation for any function. | |
| Parameters: | |
| fn: Function to document | |
| Returns: | |
| description: General description of fn | |
| parameters: A list of dicts for each parameter, storing data for the parameter name, annotation and doc | |
| return: A dict storing data for the returned annotation and doc | |
| example: Code for an example use of the fn | |
| """ | |
| doc_str = inspect.getdoc(fn) or "" | |
| doc_lines = doc_str.split("\n") | |
| signature = inspect.signature(fn) | |
| description, parameters, returns, examples = [], {}, [], [] | |
| mode = "description" | |
| for line in doc_lines: | |
| line = line.rstrip() | |
| if line == "Parameters:": | |
| mode = "parameter" | |
| elif line.startswith("Example:"): | |
| mode = "example" | |
| if "(" in line and ")" in line: | |
| c = line.split("(")[1].split(")")[0] | |
| if c != cls.__name__: | |
| mode = "ignore" | |
| elif line == "Returns:": | |
| mode = "return" | |
| else: | |
| if mode == "description": | |
| description.append(line if line.strip() else "<br>") | |
| continue | |
| if not (line.startswith(" ") or line.strip() == ""): | |
| print(line) | |
| if not (line.startswith(" ") or line.strip() == ""): | |
| raise SyntaxError( | |
| f"Documentation format for {fn.__name__} has format error in line: {line}" | |
| ) | |
| line = line[4:] | |
| if mode == "parameter": | |
| colon_index = line.index(": ") | |
| if colon_index < -1: | |
| raise SyntaxError( | |
| f"Documentation format for {fn.__name__} has format error in line: {line}" | |
| ) | |
| parameter = line[:colon_index] | |
| parameter_doc = line[colon_index + 2 :] | |
| parameters[parameter] = parameter_doc | |
| elif mode == "return": | |
| returns.append(line) | |
| elif mode == "example": | |
| examples.append(line) | |
| description_doc = " ".join(description) | |
| parameter_docs = [] | |
| for param_name, param in signature.parameters.items(): | |
| if param_name.startswith("_"): | |
| continue | |
| if param_name == "self": | |
| continue | |
| if param_name in ["kwargs", "args"] and param_name not in parameters: | |
| continue | |
| parameter_doc = { | |
| "name": param_name, | |
| "annotation": param.annotation, | |
| "doc": parameters.get(param_name), | |
| } | |
| if param_name in parameters: | |
| del parameters[param_name] | |
| if param.default != inspect.Parameter.empty: | |
| default = param.default | |
| if isinstance(default, str): | |
| default = '"' + default + '"' | |
| if default.__class__.__module__ != "builtins": | |
| default = f"{default.__class__.__name__}()" | |
| parameter_doc["default"] = default | |
| elif parameter_doc["doc"] is not None: | |
| if "kwargs" in parameter_doc["doc"]: | |
| parameter_doc["kwargs"] = True | |
| if "args" in parameter_doc["doc"]: | |
| parameter_doc["args"] = True | |
| parameter_docs.append(parameter_doc) | |
| if parameters: | |
| raise ValueError( | |
| f"Documentation format for {fn.__name__} documents " | |
| f"nonexistent parameters: {', '.join(parameters.keys())}. " | |
| f"Valid parameters: {', '.join(signature.parameters.keys())}" | |
| ) | |
| if len(returns) == 0: | |
| return_docs = {} | |
| elif len(returns) == 1: | |
| return_docs = {"annotation": signature.return_annotation, "doc": returns[0]} | |
| else: | |
| return_docs = {} | |
| # raise ValueError("Does not support multiple returns yet.") | |
| examples_doc = "\n".join(examples) if len(examples) > 0 else None | |
| return description_doc, parameter_docs, return_docs, examples_doc | |
| def document_cls(cls): | |
| doc_str = inspect.getdoc(cls) | |
| if doc_str is None: | |
| return "", {}, "" | |
| tags = {} | |
| description_lines = [] | |
| mode = "description" | |
| for line in doc_str.split("\n"): | |
| line = line.rstrip() | |
| if line.endswith(":") and " " not in line: | |
| mode = line[:-1].lower() | |
| tags[mode] = [] | |
| elif line.split(" ")[0].endswith(":") and not line.startswith(" "): | |
| tag = line[: line.index(":")].lower() | |
| value = line[line.index(":") + 2 :] | |
| tags[tag] = value | |
| elif mode == "description": | |
| description_lines.append(line if line.strip() else "<br>") | |
| else: | |
| if not (line.startswith(" ") or not line.strip()): | |
| raise SyntaxError( | |
| f"Documentation format for {cls.__name__} has format error in line: {line}" | |
| ) | |
| tags[mode].append(line[4:]) | |
| if "example" in tags: | |
| example = "\n".join(tags["example"]) | |
| del tags["example"] | |
| else: | |
| example = None | |
| for key, val in tags.items(): | |
| if isinstance(val, list): | |
| tags[key] = "<br>".join(val) | |
| description = " ".join(description_lines).replace("\n", "<br>") | |
| return description, tags, example | |
| def generate_documentation(): | |
| documentation = {} | |
| for mode, class_list in classes_to_document.items(): | |
| documentation[mode] = [] | |
| for cls, fns in class_list: | |
| fn_to_document = ( | |
| cls | |
| if inspect.isfunction(cls) or dataclasses.is_dataclass(cls) | |
| else cls.__init__ | |
| ) | |
| _, parameter_doc, return_doc, _ = document_fn(fn_to_document, cls) | |
| if ( | |
| hasattr(cls, "preprocess") | |
| and callable(cls.preprocess) # type: ignore | |
| and hasattr(cls, "postprocess") | |
| and callable(cls.postprocess) # type: ignore | |
| ): | |
| preprocess_doc = document_fn(cls.preprocess, cls) # type: ignore | |
| postprocess_doc = document_fn(cls.postprocess, cls) # type: ignore | |
| preprocess_doc, postprocess_doc = ( | |
| { | |
| "parameter_doc": preprocess_doc[1], | |
| "return_doc": preprocess_doc[2], | |
| }, | |
| { | |
| "parameter_doc": postprocess_doc[1], | |
| "return_doc": postprocess_doc[2], | |
| }, | |
| ) | |
| cls_description, cls_tags, cls_example = document_cls(cls) | |
| cls_documentation = { | |
| "class": cls, | |
| "name": cls.__name__, | |
| "description": cls_description, | |
| "tags": cls_tags, | |
| "parameters": parameter_doc, | |
| "returns": return_doc, | |
| "example": cls_example, | |
| "fns": [], | |
| } | |
| if ( | |
| hasattr(cls, "preprocess") | |
| and callable(cls.preprocess) # type: ignore | |
| and hasattr(cls, "postprocess") | |
| and callable(cls.postprocess) # type: ignore | |
| ): | |
| cls_documentation["preprocess"] = preprocess_doc # type: ignore | |
| cls_documentation["postprocess"] = postprocess_doc # type: ignore | |
| for fn_name in fns: | |
| instance_attribute_fn = fn_name.startswith("*") | |
| if instance_attribute_fn: | |
| fn_name = fn_name[1:] | |
| # Instance attribute fns are classes | |
| # whose __call__ method determines their behavior | |
| fn = getattr(cls(), fn_name).__call__ | |
| else: | |
| fn = getattr(cls, fn_name) | |
| if not callable(fn): | |
| description_doc = str(fn) | |
| parameter_docs = {} | |
| return_docs = {} | |
| examples_doc = "" | |
| override_signature = f"gr.{cls.__name__}.{fn_name}" | |
| else: | |
| ( | |
| description_doc, | |
| parameter_docs, | |
| return_docs, | |
| examples_doc, | |
| ) = document_fn(fn, cls) | |
| if fn_name in getattr(cls, "EVENTS", []): | |
| parameter_docs = parameter_docs[1:] | |
| override_signature = None | |
| if instance_attribute_fn: | |
| description_doc = extract_instance_attr_doc(cls, fn_name) | |
| cls_documentation["fns"].append( | |
| { | |
| "fn": fn, | |
| "name": fn_name, | |
| "description": description_doc, | |
| "tags": {}, | |
| "parameters": parameter_docs, | |
| "returns": return_docs, | |
| "example": examples_doc, | |
| "override_signature": override_signature, | |
| } | |
| ) | |
| documentation[mode].append(cls_documentation) | |
| if cls in classes_inherit_documentation: | |
| classes_inherit_documentation[cls] = cls_documentation["fns"] | |
| for mode, class_list in classes_to_document.items(): | |
| for i, (cls, _) in enumerate(class_list): | |
| for super_class in classes_inherit_documentation: | |
| if ( | |
| inspect.isclass(cls) | |
| and issubclass(cls, super_class) | |
| and cls != super_class | |
| ): | |
| for inherited_fn in classes_inherit_documentation[super_class]: | |
| inherited_fn = dict(inherited_fn) | |
| try: | |
| inherited_fn["description"] = extract_instance_attr_doc( | |
| cls, inherited_fn["name"] | |
| ) | |
| except ValueError: | |
| pass | |
| documentation[mode][i]["fns"].append(inherited_fn) | |
| return documentation | |