Skip to content
Merged
Show file tree
Hide file tree
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
15 changes: 13 additions & 2 deletions src/mdxify/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,12 @@ def main():
default=False,
help="Show verbose output including parsing warnings (default: False)",
)
parser.add_argument(
"--docstring-style",
choices=["google", "numpy", "sphinx"],
default="google",
help="Docstring style to parse: 'google' (default), 'numpy', or 'sphinx'",
)

args = parser.parse_args()

Expand Down Expand Up @@ -373,12 +379,13 @@ def main():
output_file = args.output_dir / f"{module_name.replace('.', '-')}.{ext}"

generate_mdx(
module_info,
module_info,
output_file,
repo_url=repo_url,
branch=args.branch,
root_module=args.root_module,
renderer=renderer,
docstring_style=args.docstring_style,
)

generated_modules.append(module_name)
Expand All @@ -398,6 +405,9 @@ def main():
args.include_inheritance = False

else:
# Capture docstring_style for use in process_module closure
docstring_style = args.docstring_style

def process_module(module_data):
"""Process a single module."""
i, module_name, include_internal, verbose = module_data
Expand Down Expand Up @@ -434,12 +444,13 @@ def process_module(module_data):

file_existed = output_file.exists()
generate_mdx(
module_info,
module_info,
output_file,
repo_url=repo_url,
branch=args.branch,
root_module=args.root_module,
renderer=renderer,
docstring_style=docstring_style,
)

module_time = time.time() - module_start
Expand Down
22 changes: 19 additions & 3 deletions src/mdxify/formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,20 @@
_ANGLE_BRACKET_PATTERN = re.compile(r"<([^>]+)>")


def format_docstring_with_griffe(docstring: str) -> str:
"""Format a docstring using Griffe for better structure."""
def format_docstring_with_griffe(docstring: str, style: str = "google") -> str:
"""Format a docstring using Griffe for better structure.

Args:
docstring: The raw docstring text to format.
style: The docstring style to parse. One of "google", "numpy", or "sphinx".
"""
if not docstring:
return ""

try:
dedented_docstring = textwrap.dedent(docstring).strip()
doc = Docstring(dedented_docstring, lineno=1)
sections = doc.parse("google")
sections = doc.parse(style)

lines = []

Expand All @@ -42,6 +47,17 @@ def format_docstring_with_griffe(docstring: str) -> str:
lines.append(f"- `{name}`: {desc}")
lines.append("")

elif section.kind.value == "attributes" and section.value:
lines.append("**Attributes:**")
for attr in section.value:
name = attr.name
desc = attr.description if hasattr(attr, "description") else ""
# Escape colons in the description to prevent Markdown definition list interpretation
desc = desc.replace(":", "\\:")
# Format as a list item
lines.append(f"- `{name}`: {desc}")
lines.append("")

elif section.kind.value == "returns" and section.value:
lines.append("**Returns:**")
# Returns can be a list of return values
Expand Down
13 changes: 8 additions & 5 deletions src/mdxify/generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,24 @@ def is_module_empty(module_path: Path) -> bool:


def generate_mdx(
module_info: dict[str, Any],
module_info: dict[str, Any],
output_file: Path,
repo_url: Optional[str] = None,
branch: str = "main",
root_module: Optional[str] = None,
renderer: Optional[Renderer] = None,
docstring_style: str = "google",
) -> None:
"""Generate MDX documentation from module info.

Args:
module_info: Parsed module information
output_file: Path to write the MDX file
repo_url: GitHub repository URL for source links
branch: Git branch name for source links
root_module: Root module name for finding relative paths
renderer: The renderer to use for output
docstring_style: The docstring style to parse ("google", "numpy", or "sphinx")
"""
if renderer is None:
renderer = MDXRenderer()
Expand Down Expand Up @@ -112,7 +115,7 @@ def generate_mdx(
if func["docstring"]:
lines.append("")
# Format docstring with Griffe
formatted_docstring = format_docstring_with_griffe(func["docstring"])
formatted_docstring = format_docstring_with_griffe(func["docstring"], docstring_style)
lines.append(renderer.escape(formatted_docstring))
lines.append("")

Expand Down Expand Up @@ -141,7 +144,7 @@ def generate_mdx(
if cls["docstring"]:
lines.append("")
# Format docstring with Griffe
formatted_docstring = format_docstring_with_griffe(cls["docstring"])
formatted_docstring = format_docstring_with_griffe(cls["docstring"], docstring_style)
lines.append(renderer.escape(formatted_docstring))
lines.append("")

Expand Down Expand Up @@ -179,7 +182,7 @@ def generate_mdx(

if method["docstring"]:
formatted_docstring = format_docstring_with_griffe(
method["docstring"]
method["docstring"], docstring_style
)
lines.append(renderer.escape(formatted_docstring))
lines.append("")
Expand Down
118 changes: 117 additions & 1 deletion tests/test_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,120 @@ def my_flow(name):
# Check that Examples is rendered as a bold header
assert "**Examples:**" in result
# Check that other sections are also bold
assert "**Args:**" in result
assert "**Args:**" in result


def test_format_docstring_with_attributes_section():
"""Test that Attributes section in class docstrings is rendered correctly.

Regression test for issue #32.
"""
docstring = '''Schema for a column in a table.

Attributes:
name (str): The name of the column.
type (str): The data type of the column, represented as a string.
'''

result = format_docstring_with_griffe(docstring)

# Check that Attributes is rendered as a bold header
assert "**Attributes:**" in result
# Check that attribute names are properly formatted
assert "- `name`:" in result
assert "- `type`:" in result
# Check that descriptions are present
assert "The name of the column" in result
assert "The data type of the column" in result


def test_format_docstring_with_attributes_and_methods():
"""Test class docstring with both Attributes and other sections."""
docstring = '''Configuration for database connections.

Attributes:
host (str): The database host address.
port (int): The port number for the connection.

Examples:
>>> config = DatabaseConfig("localhost", 5432)
>>> print(config.host)
localhost
'''

result = format_docstring_with_griffe(docstring)

# Check that both sections are rendered
assert "**Attributes:**" in result
assert "**Examples:**" in result
# Check ordering - Attributes should come before Examples
assert result.index("**Attributes:**") < result.index("**Examples:**")


def test_format_docstring_with_numpy_style():
"""Test formatting NumPy-style docstrings.

Addresses issue #31 - docstring style should be configurable.
"""
docstring = '''Calculate the mean of values.

Parameters
----------
values : list
A list of numeric values.

Returns
-------
float
The arithmetic mean.

Attributes
----------
result : float
The computed result.
'''

result = format_docstring_with_griffe(docstring, style="numpy")

# Check that Args is rendered (Parameters -> Args)
assert "**Args:**" in result
assert "- `values`:" in result
# Check Returns
assert "**Returns:**" in result
# Check Attributes
assert "**Attributes:**" in result
assert "- `result`:" in result


def test_format_docstring_with_sphinx_style():
"""Test formatting Sphinx-style docstrings."""
docstring = '''Process the given data.

:param data: The input data.
:type data: list
:returns: Processed data.
:rtype: dict
'''

result = format_docstring_with_griffe(docstring, style="sphinx")

# Check that Args is rendered
assert "**Args:**" in result
assert "- `data`:" in result
# Check Returns
assert "**Returns:**" in result


def test_format_docstring_style_default_is_google():
"""Test that Google style is the default."""
docstring = '''A simple function.

Args:
x: First parameter.
'''

# Call without style parameter - should use google
result = format_docstring_with_griffe(docstring)

assert "**Args:**" in result
assert "- `x`:" in result
Loading