"""This module defines function to serialize objects. These functions simply take objects as parameters and return dictionaries that can be dumped by `json.dumps`. """ import inspect import re from re import Match, Pattern from typing import Any, Optional from pytkdocs.objects import Object, Source from pytkdocs.parsers.docstrings.base import AnnotatedObject, Attribute, Parameter, Section try: from typing import GenericMeta # type: ignore[attr-defined] except ImportError: # in 3.7, GenericMeta doesn't exist but we don't need it class GenericMeta(type): # type: ignore[no-redef] """GenericMeta type.""" RE_OPTIONAL: Pattern = re.compile(r"Union\[(.+), NoneType\]") """Regular expression to match optional annotations of the form `Union[T, NoneType]`.""" RE_FORWARD_REF: Pattern = re.compile(r"_?ForwardRef\('([^']+)'\)") """Regular expression to match forward-reference annotations of the form `_ForwardRef('T')`.""" def rebuild_optional(match: Match) -> str: """Rebuild `Union[T, None]` as `Optional[T]`. Arguments: match: The match object when matching against a regular expression (by the parent caller). Returns: The rebuilt type string. """ group = match.group(1) brackets_level = 0 for char in group: if char == "," and brackets_level == 0: return f"Union[{group}]" if char == "[": brackets_level += 1 elif char == "]": brackets_level -= 1 return f"Optional[{group}]" def annotation_to_string(annotation: Any) -> str: """Return an annotation as a string. Arguments: annotation: The annotation to return as a string. Returns: The annotation as a string. """ if annotation is inspect.Signature.empty: return "" if inspect.isclass(annotation) and not isinstance(annotation, GenericMeta): string = annotation.__name__ else: string = str(annotation).replace("typing.", "") string = RE_FORWARD_REF.sub(lambda match: match.group(1), string) return RE_OPTIONAL.sub(rebuild_optional, string) def serialize_annotated_object(obj: AnnotatedObject) -> dict: """Serialize an instance of [`AnnotatedObject`][pytkdocs.parsers.docstrings.base.AnnotatedObject]. Arguments: obj: The object to serialize. Returns: A JSON-serializable dictionary. """ return {"description": obj.description, "annotation": annotation_to_string(obj.annotation)} def serialize_attribute(attribute: Attribute) -> dict: """Serialize an instance of [`Attribute`][pytkdocs.parsers.docstrings.base.Attribute]. Arguments: attribute: The attribute to serialize. Returns: A JSON-serializable dictionary. """ return { "name": attribute.name, "description": attribute.description, "annotation": annotation_to_string(attribute.annotation), } def serialize_parameter(parameter: Parameter) -> dict: """Serialize an instance of [`Parameter`][pytkdocs.parsers.docstrings.base.Parameter]. Arguments: parameter: The parameter to serialize. Returns: A JSON-serializable dictionary. """ serialized = serialize_annotated_object(parameter) serialized.update( { "name": parameter.name, "kind": str(parameter.kind), "default": parameter.default_string, "is_optional": parameter.is_optional, "is_required": parameter.is_required, "is_args": parameter.is_args, "is_kwargs": parameter.is_kwargs, }, ) return serialized def serialize_signature_parameter(parameter: inspect.Parameter) -> dict: """Serialize an instance of `inspect.Parameter`. Arguments: parameter: The parameter to serialize. Returns: A JSON-serializable dictionary. """ serialized = {"kind": str(parameter.kind), "name": parameter.name} if parameter.annotation is not parameter.empty: serialized["annotation"] = annotation_to_string(parameter.annotation) if parameter.default is not parameter.empty: serialized["default"] = repr(parameter.default) return serialized def serialize_signature(signature: inspect.Signature) -> dict: """Serialize an instance of `inspect.Signature`. Arguments: signature: The signature to serialize. Returns: A JSON-serializable dictionary. """ if signature is None: return {} serialized: dict = { "parameters": [serialize_signature_parameter(value) for name, value in signature.parameters.items()], } if signature.return_annotation is not inspect.Signature.empty: serialized["return_annotation"] = annotation_to_string(signature.return_annotation) return serialized def serialize_docstring_section(section: Section) -> dict: """Serialize an instance of `inspect.Signature`. Arguments: section: The section to serialize. Returns: A JSON-serializable dictionary. """ serialized = {"type": section.type} if section.type == section.Type.MARKDOWN: serialized.update({"value": section.value}) elif section.type == section.Type.RETURN or section.type == section.Type.YIELD: # noqa: PLR1714 serialized.update({"value": serialize_annotated_object(section.value)}) # type: ignore[dict-item] elif section.type == section.Type.EXCEPTIONS: serialized.update({"value": [serialize_annotated_object(exc) for exc in section.value]}) # type: ignore[dict-item] elif section.type == section.Type.PARAMETERS or section.type == section.Type.KEYWORD_ARGS: # noqa: PLR1714 serialized.update({"value": [serialize_parameter(param) for param in section.value]}) # type: ignore[dict-item] elif section.type == section.Type.ATTRIBUTES: serialized.update({"value": [serialize_attribute(attr) for attr in section.value]}) # type: ignore[dict-item] elif section.type == section.Type.EXAMPLES: serialized.update({"value": section.value}) return serialized def serialize_source(source: Optional[Source]) -> dict: """Serialize an instance of [`Source`][pytkdocs.objects.Source]. Arguments: source: The source to serialize. Returns: A JSON-serializable dictionary. """ if source: return {"code": source.code, "line_start": source.line_start} return {} def serialize_object(obj: Object) -> dict: """Serialize an instance of a subclass of [`Object`][pytkdocs.objects.Object]. Arguments: obj: The object to serialize. Returns: A JSON-serializable dictionary. """ serialized = { "name": obj.name, "path": obj.path, "category": obj.category, "file_path": obj.file_path, "relative_file_path": obj.relative_file_path, "properties": sorted(set(obj.properties + obj.name_properties)), "parent_path": obj.parent_path, "has_contents": obj.has_contents(), "docstring": obj.docstring, "docstring_sections": [serialize_docstring_section(sec) for sec in obj.docstring_sections], "source": serialize_source(obj.source), "children": {child.path: serialize_object(child) for child in obj.children}, "attributes": [attr.path for attr in obj.attributes], "methods": [meth.path for meth in obj.methods], "functions": [func.path for func in obj.functions], "modules": [mod.path for mod in obj.modules], "classes": [clas.path for clas in obj.classes], } if hasattr(obj, "type"): serialized["type"] = annotation_to_string(obj.type) if hasattr(obj, "signature"): serialized["signature"] = serialize_signature(obj.signature) if hasattr(obj, "bases"): serialized["bases"] = obj.bases return serialized