Source code for AFQ.utils.docstring_parser
import inspect
import re
__all__ = ["parse_numpy_docstring"]
def _white_space(text):
if text is None:
return None
else:
return re.sub(r"[\n\t\s]+", " ", text).strip()
def _split_parameter_blocks(docstring):
# First extract just the Parameters section
params_section = re.search(
r"Parameters\n-+\n(.*?)(?=\n\s*\n|\n-+\n|\Z)", docstring, re.DOTALL
)
if not params_section:
return []
params_text = params_section.group(1)
params_text = re.sub(r"\n-+$", "", params_text)
lines = params_text.split("\n")
# Find base indentation (minimum indent of parameter lines)
base_indent = float("inf")
for line in lines:
if re.match(r"\S", line):
indent = len(re.match(r"^\s*", line).group())
base_indent = min(base_indent, indent)
if base_indent == float("inf"):
return []
# Split into blocks
param_blocks = []
current_block = []
for line in lines:
line_indent = len(re.match(r"^\s*", line).group())
if line_indent == base_indent and re.match(r"\S", line):
if current_block:
param_blocks.append("\n".join(current_block))
current_block = []
current_block.append(line)
if current_block:
param_blocks.append("\n".join(current_block))
return param_blocks
[docs]
def parse_numpy_docstring(docstring):
"""
Parse a NumPy-style docstring into parameter information.
Returns:
dict: {
"description": str (first section of docstring),
"arguments": {
param_name: {
"help": str (description),
"metavar": str (type),
"default": any (default value)
}
}
}
"""
if callable(docstring):
docstring = inspect.getdoc(docstring)
if not docstring:
return {"description": "", "arguments": {}}
# Find the description section
sections = re.split(r"\n\s*\n", docstring.strip())
description = sections[0].strip()
# Find the parameters section
param_blocks = _split_parameter_blocks(docstring)
params = {}
for block in param_blocks:
# Split into (param: value) and description
parts = block.split("\n", maxsplit=1)
if len(parts) == 0:
continue
# Parse the (param: value)
header_match = re.match(
r"(\w+)\s*:\s*([^,\n]+)(?:,\s*optional)?", parts[0].strip()
)
if not header_match:
continue
name, type_info = header_match.groups()
desc = parts[1].strip() if len(parts) > 1 else ""
# Parse the description
default = None
default_match = re.search(r"[Dd]efault:\s*([^\n]+)", desc)
if default_match:
default = default_match.group(1).strip(" .")
try:
default = eval(default)
except Exception:
default = _white_space(default)
params[name] = {
"help": _white_space(desc),
"metavar": _white_space(type_info),
"default": default,
}
return {"description": _white_space(description), "arguments": params}