Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 28 additions & 71 deletions cli/openbb_cli/argparse_translator/argparse_translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,93 +152,50 @@ def parser(self) -> argparse.ArgumentParser:
@staticmethod
def _build_description(func_doc: str) -> str:
"""Build the description of the argparse program from the function docstring."""
# Remove only the Examples section and the openbb header if present
patterns_to_remove = [
(r"openbb\n\s+={3,}\n", ""), # Remove openbb header
(r"\n\s*Examples\n\s*-{3,}\n.*", ""), # Remove Examples section to end
]
if not func_doc:
return ""

# Remove the openbb header if present
func_doc = re.sub(r"openbb\n\s+={3,}\n", "", func_doc, flags=re.DOTALL)

for pattern, replacement in patterns_to_remove:
func_doc = re.sub(pattern, replacement, func_doc, flags=re.DOTALL)
# Senior Approach: The main description should only be the summary.
# Sections like Parameters, Returns, and Examples are handled by argparse or are redundant.
for section in ["Parameters", "Returns", "Examples", "Raises"]:
pattern = rf"\n\s*{section}\n\s*-{{3,}}\n.*"
func_doc = re.sub(pattern, "", func_doc, flags=re.DOTALL | re.IGNORECASE)

# Clean up type annotations in docstrings
# Clean up any remaining type-style annotations in the summary
def clean_type_annotation(type_str: str) -> str:
"""Clean up a single type annotation."""
# First, handle the pipe union syntax: type1 | type2 -> type1 or type2
# Do this FIRST before other transformations
"""Clean up type annotations for human readability."""
# Handle pipe unions: int | str -> int or str
type_str = re.sub(r"\s*\|\s*", " or ", type_str)

# Handle Annotated[type, ...] -> type
type_str = re.sub(r"Annotated\[([^,\]]+)(?:,\s*[^\]]+)?\]", r"\1", type_str)

# Handle Union[type1, type2, ...] -> type1 or type2 or ...
type_str = re.sub(r"Annotated\[\s*([^,\]]+).*?\]", r"\1", type_str)
# Handle Union[A, B] -> A or B
type_str = re.sub(
r"Union\[([^\]]+)\]",
lambda m: " or ".join(m.group(1).split(", ")),
r"Union\[\s*(.*?)\s*\]",
lambda m: m.group(1).replace(", ", " or "),
type_str,
)
# Handle Optional[A] -> A or None
type_str = re.sub(r"Optional\[\s*(.*?)\s*\]", r"\1 or None", type_str)

# Handle Optional[type] -> type or None
type_str = re.sub(r"Optional\[([^\]]+)\]", r"\1 or None", type_str)

# Deduplicate "or" separated types
parts = [p.strip() for p in type_str.split(" or ")]

# Remove duplicates while preserving order (case-insensitive)
seen = set()
unique_parts = []
for part in parts:
part_lower = part.lower()
if part_lower not in seen:
seen.add(part_lower)
unique_parts.append(part)

# If None is present, move it to the end
if "None" in unique_parts:
unique_parts.remove("None")
unique_parts.append("None")
return type_str.strip()

type_str = " or ".join(unique_parts)

return type_str

# Process each line that contains a type annotation (format: "name : type")
lines = func_doc.split("\n")
cleaned_lines = []

for line in lines:
# Match lines with type annotations (parameter_name : type_annotation)
# If a line still looks like a parameter definition (e.g. "param : type"), clean it
if ":" in line and not line.strip().startswith("#"):
# Split only on the first colon to preserve any colons in the description
parts = line.split(":", 1)
if len(parts) == 2:
param_name = parts[0]
rest = parts[1].strip()

# Extract the type part (everything before the first real description line)
# Type annotations typically don't contain spaces at the start of continuation lines
type_match = re.match(r"^([^\n]+?)(?:\n\s{4,}|\s{2,}|$)", rest)
if type_match:
type_part = type_match.group(1).strip()
description_part = rest[len(type_match.group(1)) :]

# Clean the type annotation
cleaned_type = clean_type_annotation(type_part)

# Reconstruct the line
if description_part:
cleaned_lines.append(
f"{param_name}: {cleaned_type}{description_part}"
)
else:
cleaned_lines.append(f"{param_name}: {cleaned_type}")
continue

cleaned_lines.append(line)

func_doc = "\n".join(cleaned_lines)
param_name = parts[0]
type_info = parts[1].strip()
cleaned_type = clean_type_annotation(type_info)
cleaned_lines.append(f"{param_name}: {cleaned_type}")
else:
cleaned_lines.append(line)

return func_doc.strip()
return "\n".join(cleaned_lines).strip()

@staticmethod
def _param_is_default(param: inspect.Parameter) -> bool:
Expand Down