import re
import os
from multidoc.template import render_python_docstring
from multidoc.template import render_cpp_docstring
from multidoc.template import get_docstring_template, get_property_template, get_enum_member_template
from multidoc.parsing.io import yaml2dict
from multidoc.parsing import logger
from multidoc.regex import p_package_file, p_module_file
import pathlib
def guess_project_type(project_src):
logger.warning(f"GUESSING project type, because `project_type` was not found for src {project_src}!")
files_cpp = pathlib.Path(project_src).glob("*.cpp|*.h|*.hpp")
files_py = pathlib.Path(project_src).glob("*.py")
# should check cpp for pybind headers.
# header_pybind =
if files_cpp and files_py:
return "py"
elif files_cpp:
return "cpp"
elif files_py:
return "py"
def parse_function(function, local):
"""
Parameters
----------
function
local
Returns
-------
"""
logger.info(f"Parsing function: {function.name} with locals: {local}")
if "cpp" in local.keys() and "py" in local.keys():
assert (local["cpp"] == local["cpp"] & local[
"cpp"] is True) is False
if "cpp" in local.keys():
if local["cpp"]:
return render_cpp_docstring(function)
if "py" in local.keys():
if local["py"]:
return render_python_docstring(function)
from collections import defaultdict
def parse_properties(structure, properties, **kwargs):
nl = '\n'
logger.info(
f"Parsing the following properties with {kwargs}: {nl}{nl.join([r' - ' + property.name for property in properties] + [''])} "
)
t = get_property_template(**kwargs)
for i, property in enumerate(properties):
structure[property.name] = t.render(**property.dict())
return structure
def parse_functions(structure, functions, **kwargs):
nl = '\n'
logger.info(
f"Parsing the following functions with {kwargs}: {nl}{nl.join([r' - ' + function.name for function in functions] + [''])} "
)
t = get_docstring_template(**kwargs)
result = defaultdict(list)
for i, f in enumerate(functions):
result[f.name].append(i)
for k, v in result.items():
if len(v) > 1: # overloaded
structure[k] = defaultdict(list)
structure[k]["overloaded"] = True
structure[k]["overloads"] = len(v)
for idx, overload in enumerate(v):
structure[k][idx] = t.render(**functions[overload].dict())
structure[k] = dict(structure[k])
else:
structure[k] = t.render(**functions[v[0]].dict())
return structure
# logger.info(f"Parsing class: {cls.name} with locals: {local}")
# _return = {}
# for method in cls.methods:
# _return[method.name] = parse_method(method, local)
# _return["__docstring__"] = parse_method(cls, local)
# return _return
def parse_enums(structure, enums, **kwargs):
nl = '\n'
logger.info(
f"Parsing the following enums with {kwargs}: {nl}{nl.join([r' - ' + enum.name for enum in enums] + [''])} "
)
t = get_enum_member_template(**kwargs)
t_general = get_docstring_template(**kwargs)
for enum in enums:
_result = {}
if enum.members:
for member in enum.members:
_result.update({member.name: t.render(**member.dict())})
_result.update({"__docstring__": t_general.render(**enum.dict())})
_result.update(enum.dict())
structure[enum.name] = _result
return structure
def parse_classes(structure, classes, **kwargs):
nl = '\n'
logger.info(
f"Parsing the following classes with {kwargs}: {nl}{nl.join([r' - ' + cls.name for cls in classes] + [''])} "
)
t = get_docstring_template(**kwargs)
for cls in classes:
_result = {}
if cls.methods:
_result.update(parse_functions(cls.dict(), cls.methods, **kwargs))
if cls.properties:
_result.update(parse_properties(cls.dict(), cls.properties, **kwargs))
_result.update({"__docstring__": t.render(**cls.dict())})
if cls.autoclass:
_result.update({"autoclass": cls.dict()["autoclass"]})
structure[cls.name] = _result
return structure
def parse_method(function, local):
"""
Parameters
----------
function
local
Returns
-------
"""
logger.info(f"Parsing method: {function.name} with locals: {local}")
if "cpp" in local.keys() and "py" in local.keys():
assert (local["cpp"] == local["cpp"] & local[
"cpp"] is True) is False
if "cpp" in local.keys():
if local["cpp"]:
return render_cpp_docstring(function)
if "py" in local.keys():
if local["py"]:
return render_python_docstring(function)
import json
def parse_class(cls, local):
"""
Parameters
----------
cls
local
Returns
-------
"""
logger.info(f"Parsing class: {cls.name} with locals: {local}")
_return = {}
for method in cls.methods:
_return[method.name] = parse_method(method, local)
_return["__docstring__"] = parse_method(cls, local)
return _return
def parse_constant(constant, local):
"""
Parameters
----------
constant
local
Returns
-------
"""
return constant
from multidoc.parsing.models import Module, Package
[docs]def parse_api_declaration(path: str, parent=None, **kwargs):
"""
Parameters
----------
path
local
parent
Returns
-------
"""
structure = dict()
local = kwargs if kwargs is not None else dict()
# if directory given, treats it as package declaration
if os.path.isdir(path):
# look for package declaration __package__(.yml|.yaml)
files = list(filter(p_package_file.match, os.listdir(path)))
if len(files) == 0:
raise ModuleNotFoundError("__package__.yaml/yml not found in "
"directory path.")
elif len(files) > 1:
raise ModuleNotFoundError("Multiple __package__.yaml/yml files "
"found in directory path.")
else:
module = Package.parse_yaml(os.path.join(path, files[0]), **kwargs)
# define type as package.
structure["type"] = "package"
structure["path"] = path
structure["file"] = files[0]
structure["_implicit_name"] = os.path.basename(path)
# if file given, treats it as module declaration
elif os.path.isfile(path):
_, extension = os.path.splitext(path)
#
if extension == ".yaml" or extension == ".yml":
if not p_package_file.match(path):
module = Module.parse_yaml(path)
# define type as package.
structure["type"] = "module"
structure["path"] = os.path.dirname(path)
structure["file"] = os.path.basename(path)
structure["_implicit_name"] = \
os.path.split(os.path.basename(path))[0]
else:
module = Package.parse_yaml(path)
structure["type"] = "package"
structure["path"] = os.path.dirname(path)
structure["file"] = os.path.basename(path)
structure["_implicit_name"] = \
os.path.split(os.path.dirname(path))[0]
else:
raise ModuleNotFoundError("Only .yml or .yaml files can be used "
"to declare modules and packages.")
# then path was given without yml/yaml extension.
# (i.e. directory/module, where directory/module.yml/yaml exists)
# TODO: Is this really necessary?
elif list(
filter(re.compile(fr"{os.path.basename(path)}(.yml|.yaml)").match,
os.listdir(os.path.dirname(path)))):
matches = list(
filter(re.compile(fr"{os.path.basename(path)}(.yml|.yaml)").match,
os.listdir(os.path.dirname(path))))
basename = os.path.basename(path)
if len(matches) > 1:
raise ModuleNotFoundError(f"Multiple {basename}.yaml/yml files "
"found for given path.")
else:
module = Module.parse_yaml(
os.path.join(os.path.dirname(path), matches[0]), **kwargs)
# define type as package.
structure["type"] = "module"
structure["path"] = os.path.dirname(path)
structure["file"] = matches[0]
structure["_implicit_name"] = os.path.split(
os.path.basename(path))[-1]
# user gives path that is neither file nor directory.
else:
raise ModuleNotFoundError("Path provided was not recognized as a "
"directory containing a __package__.yaml/yml"
"or as a module.yml/.yaml declaration.")
structure.update(module.dict())
if module.config:
# parse multidoc related configuration
if module.config.name: # package
structure["name"] = module.config.name
else:
structure["name"] = structure["_implicit_name"]
if module.config.version:
structure["version"] = module.config.version
else:
structure["version"] = None
else:
structure["name"] = structure["_implicit_name"]
structure["version"] = None
# module level functions
if module.functions:
structure = parse_functions(structure, module.functions, **local)
# module level functions
if module.classes:
structure = parse_classes(structure, module.classes, **local)
# module level enumerations
if module.enums:
structure = parse_enums(structure, module.enums, **local)
# module level functions
if module.constants:
for constant in module.constants:
structure[constant.name] = parse_constant(constant, local)
# iterate through api level modules
if type(module) == Package:
for submodule in module.modules:
module_path = os.path.join(structure["path"], submodule)
structure[submodule] = parse_api_declaration(module_path,
parent=structure,
**local)
# TODO: Figure out how to deal with submodule name clashes with
# reserved names. Hide all docstrings behind __docs__ key?
return structure
if __name__ == "__main__":
s = parse_api_declaration("../../tests/test-docstrings",
py=True)
if s["summary"]:
print(s["summary"])
# print(s)
import json
json.dumps(s, indent=4)