"""Tests for API documentation extraction and generation."""
from pathlib import Path
from textwrap import dedent
import pytest
from local_deepwiki.core.parser import CodeParser
from local_deepwiki.generators.api_docs import (
APIDocExtractor,
ClassSignature,
FunctionSignature,
Parameter,
extract_class_signature,
extract_function_signature,
extract_python_decorators,
extract_python_docstring,
extract_python_parameters,
extract_python_return_type,
format_function_signature_line,
format_parameter,
generate_api_reference_markdown,
get_file_api_docs,
parse_docstring,
parse_google_docstring,
parse_numpy_docstring,
)
from local_deepwiki.models import Language
class TestParameter:
"""Test Parameter dataclass."""
def test_basic_parameter(self):
"""Test creating a basic parameter."""
param = Parameter(name="value")
assert param.name == "value"
assert param.type_hint is None
assert param.default_value is None
assert param.description is None
def test_full_parameter(self):
"""Test creating a parameter with all fields."""
param = Parameter(
name="count",
type_hint="int",
default_value="10",
description="The number of items.",
)
assert param.name == "count"
assert param.type_hint == "int"
assert param.default_value == "10"
assert param.description == "The number of items."
class TestExtractPythonParameters:
"""Test Python parameter extraction."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_simple_parameters(self, parser):
"""Test extracting simple parameters without types."""
source = dedent(
"""
def func(a, b, c):
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
params = extract_python_parameters(func_node, source.encode())
assert len(params) == 3
assert params[0].name == "a"
assert params[1].name == "b"
assert params[2].name == "c"
def test_typed_parameters(self, parser):
"""Test extracting parameters with type hints."""
source = dedent(
"""
def func(name: str, count: int):
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
params = extract_python_parameters(func_node, source.encode())
assert len(params) == 2
assert params[0].name == "name"
assert params[0].type_hint == "str"
assert params[1].name == "count"
assert params[1].type_hint == "int"
def test_default_parameters(self, parser):
"""Test extracting parameters with default values."""
source = dedent(
"""
def func(name="default", count=10):
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
params = extract_python_parameters(func_node, source.encode())
assert len(params) == 2
assert params[0].name == "name"
assert params[0].default_value == '"default"'
assert params[1].name == "count"
assert params[1].default_value == "10"
def test_typed_default_parameters(self, parser):
"""Test extracting parameters with types and defaults."""
source = dedent(
"""
def func(name: str = "test", count: int = 5):
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
params = extract_python_parameters(func_node, source.encode())
assert len(params) == 2
assert params[0].name == "name"
assert params[0].type_hint == "str"
assert params[0].default_value == '"test"'
assert params[1].name == "count"
assert params[1].type_hint == "int"
assert params[1].default_value == "5"
def test_excludes_self(self, parser):
"""Test that self is excluded from method parameters."""
source = dedent(
"""
def method(self, value: int):
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
params = extract_python_parameters(func_node, source.encode())
assert len(params) == 1
assert params[0].name == "value"
def test_excludes_cls(self, parser):
"""Test that cls is excluded from classmethod parameters."""
source = dedent(
"""
def classmethod_func(cls, value: int):
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
params = extract_python_parameters(func_node, source.encode())
assert len(params) == 1
assert params[0].name == "value"
class TestExtractPythonReturnType:
"""Test Python return type extraction."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_simple_return_type(self, parser):
"""Test extracting a simple return type."""
source = dedent(
"""
def func() -> str:
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
return_type = extract_python_return_type(func_node, source.encode())
assert return_type == "str"
def test_complex_return_type(self, parser):
"""Test extracting a complex return type."""
source = dedent(
"""
def func() -> list[str]:
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
return_type = extract_python_return_type(func_node, source.encode())
assert return_type == "list[str]"
def test_no_return_type(self, parser):
"""Test function with no return type."""
source = dedent(
"""
def func():
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
return_type = extract_python_return_type(func_node, source.encode())
assert return_type is None
class TestExtractPythonDocstring:
"""Test Python docstring extraction."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_triple_quote_docstring(self, parser):
"""Test extracting triple-quoted docstring."""
source = dedent(
'''
def func():
"""This is the docstring."""
pass
'''
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
docstring = extract_python_docstring(func_node, source.encode())
assert docstring == "This is the docstring."
def test_multiline_docstring(self, parser):
"""Test extracting multiline docstring."""
source = dedent(
'''
def func():
"""
This is a multiline
docstring.
"""
pass
'''
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
docstring = extract_python_docstring(func_node, source.encode())
assert "multiline" in docstring
assert "docstring" in docstring
def test_no_docstring(self, parser):
"""Test function with no docstring."""
source = dedent(
"""
def func():
x = 1
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
docstring = extract_python_docstring(func_node, source.encode())
assert docstring is None
class TestParseGoogleDocstring:
"""Test Google-style docstring parsing."""
def test_simple_description(self):
"""Test parsing simple description."""
docstring = "This is a simple description."
result = parse_google_docstring(docstring)
assert result["description"] == "This is a simple description."
def test_args_section(self):
"""Test parsing Args section."""
docstring = dedent(
"""
Do something.
Args:
name: The name to use.
count: How many items.
"""
).strip()
result = parse_google_docstring(docstring)
assert "name" in result["args"]
assert result["args"]["name"]["description"] == "The name to use."
assert "count" in result["args"]
assert result["args"]["count"]["description"] == "How many items."
def test_args_with_types(self):
"""Test parsing Args with type annotations."""
docstring = dedent(
"""
Do something.
Args:
name (str): The name to use.
count (int): How many items.
"""
).strip()
result = parse_google_docstring(docstring)
assert result["args"]["name"]["type"] == "str"
assert result["args"]["count"]["type"] == "int"
def test_returns_section(self):
"""Test parsing Returns section."""
docstring = dedent(
"""
Do something.
Returns:
The result string.
"""
).strip()
result = parse_google_docstring(docstring)
assert result["returns"] == "The result string."
class TestParseNumpyDocstring:
"""Test NumPy-style docstring parsing."""
def test_simple_description(self):
"""Test parsing simple description."""
docstring = "This is a simple description."
result = parse_numpy_docstring(docstring)
assert result["description"] == "This is a simple description."
def test_parameters_section(self):
"""Test parsing Parameters section."""
docstring = dedent(
"""
Do something.
Parameters
----------
name : str
The name to use.
count : int
How many items.
"""
).strip()
result = parse_numpy_docstring(docstring)
assert "name" in result["args"]
assert result["args"]["name"]["type"] == "str"
assert "count" in result["args"]
assert result["args"]["count"]["type"] == "int"
class TestExtractFunctionSignature:
"""Test function signature extraction."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_simple_function(self, parser):
"""Test extracting simple function signature."""
source = dedent(
'''
def greet(name: str) -> str:
"""Say hello."""
return f"Hello, {name}"
'''
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
sig = extract_function_signature(func_node, source.encode(), Language.PYTHON)
assert sig is not None
assert sig.name == "greet"
assert len(sig.parameters) == 1
assert sig.parameters[0].name == "name"
assert sig.parameters[0].type_hint == "str"
assert sig.return_type == "str"
assert sig.description == "Say hello."
def test_async_function(self, parser):
"""Test extracting async function signature."""
source = dedent(
'''
async def fetch_data(url: str) -> bytes:
"""Fetch data from URL."""
pass
'''
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
sig = extract_function_signature(func_node, source.encode(), Language.PYTHON)
assert sig is not None
assert sig.is_async is True
assert sig.name == "fetch_data"
class TestExtractClassSignature:
"""Test class signature extraction."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_simple_class(self, parser):
"""Test extracting simple class signature."""
source = dedent(
'''
class MyClass:
"""A simple class."""
def method(self, value: int) -> bool:
"""A method."""
pass
'''
).strip()
root = parser.parse_source(source, Language.PYTHON)
class_node = root.children[0]
sig = extract_class_signature(class_node, source.encode(), Language.PYTHON)
assert sig is not None
assert sig.name == "MyClass"
assert sig.description == "A simple class."
assert len(sig.methods) == 1
assert sig.methods[0].name == "method"
def test_class_with_inheritance(self, parser):
"""Test extracting class with base classes."""
source = dedent(
'''
class Child(Parent, Mixin):
"""A child class."""
pass
'''
).strip()
root = parser.parse_source(source, Language.PYTHON)
class_node = root.children[0]
sig = extract_class_signature(class_node, source.encode(), Language.PYTHON)
assert sig is not None
assert "Parent" in sig.bases
assert "Mixin" in sig.bases
class TestFormatParameter:
"""Test parameter formatting."""
def test_simple_param(self):
"""Test formatting simple parameter."""
param = Parameter(name="value")
assert format_parameter(param) == "value"
def test_typed_param(self):
"""Test formatting typed parameter."""
param = Parameter(name="value", type_hint="int")
assert format_parameter(param) == "value: int"
def test_default_param(self):
"""Test formatting parameter with default."""
param = Parameter(name="value", default_value="10")
assert format_parameter(param) == "value = 10"
def test_full_param(self):
"""Test formatting parameter with type and default."""
param = Parameter(name="value", type_hint="int", default_value="10")
assert format_parameter(param) == "value: int = 10"
class TestFormatFunctionSignatureLine:
"""Test function signature line formatting."""
def test_simple_function(self):
"""Test formatting simple function."""
sig = FunctionSignature(name="func")
result = format_function_signature_line(sig)
assert result == "def func()"
def test_function_with_params(self):
"""Test formatting function with parameters."""
sig = FunctionSignature(
name="func",
parameters=[
Parameter(name="a", type_hint="int"),
Parameter(name="b", type_hint="str", default_value='"x"'),
],
)
result = format_function_signature_line(sig)
assert result == 'def func(a: int, b: str = "x")'
def test_function_with_return_type(self):
"""Test formatting function with return type."""
sig = FunctionSignature(name="func", return_type="bool")
result = format_function_signature_line(sig)
assert result == "def func() -> bool"
def test_async_function(self):
"""Test formatting async function."""
sig = FunctionSignature(name="func", is_async=True)
result = format_function_signature_line(sig)
assert result == "async def func()"
class TestGenerateApiReferenceMarkdown:
"""Test API reference markdown generation."""
def test_empty_input(self):
"""Test with no functions or classes."""
result = generate_api_reference_markdown([], [])
assert result == ""
def test_function_documentation(self):
"""Test generating function documentation."""
functions = [
FunctionSignature(
name="process",
parameters=[
Parameter(name="data", type_hint="str", description="Input data."),
],
return_type="bool",
description="Process the input data.",
)
]
result = generate_api_reference_markdown(functions, [])
assert "### Functions" in result
assert "#### `process`" in result
assert "def process(data: str) -> bool" in result
assert "Process the input data." in result
assert "| `data` |" in result
assert "**Returns:** `bool`" in result
def test_class_documentation(self):
"""Test generating class documentation."""
classes = [
ClassSignature(
name="MyClass",
bases=["BaseClass"],
description="A test class.",
methods=[
FunctionSignature(
name="run",
parameters=[Parameter(name="value", type_hint="int")],
return_type="None",
description="Run the process.",
is_method=True,
),
],
)
]
result = generate_api_reference_markdown([], classes)
assert "### class `MyClass`" in result
assert "**Inherits from:** `BaseClass`" in result
assert "A test class." in result
assert "#### `run`" in result
def test_filters_private_items(self):
"""Test that private items are filtered by default."""
functions = [
FunctionSignature(name="_private_func"),
FunctionSignature(name="public_func"),
]
classes = [
ClassSignature(name="_PrivateClass"),
ClassSignature(name="PublicClass"),
]
result = generate_api_reference_markdown(functions, classes)
assert "_private_func" not in result
assert "public_func" in result
assert "_PrivateClass" not in result
assert "PublicClass" in result
def test_includes_private_when_requested(self):
"""Test including private items when specified."""
functions = [FunctionSignature(name="_private_func")]
result = generate_api_reference_markdown(functions, [], include_private=True)
assert "_private_func" in result
class TestAPIDocExtractor:
"""Test APIDocExtractor class."""
@pytest.fixture
def extractor(self):
return APIDocExtractor()
def test_extract_from_file(self, tmp_path, extractor):
"""Test extracting docs from a Python file."""
source = dedent(
'''
"""Module docstring."""
def helper(value: int) -> bool:
"""A helper function."""
return value > 0
class MyClass:
"""A sample class."""
def __init__(self, name: str):
"""Initialize with name."""
self.name = name
def process(self, data: list) -> dict:
"""Process the data."""
return {}
'''
).strip()
test_file = tmp_path / "test_module.py"
test_file.write_text(source)
functions, classes = extractor.extract_from_file(test_file)
assert len(functions) == 1
assert functions[0].name == "helper"
assert len(classes) == 1
assert classes[0].name == "MyClass"
assert len(classes[0].methods) == 2 # __init__ and process
def test_extract_unsupported_file(self, tmp_path, extractor):
"""Test extracting from unsupported file type."""
test_file = tmp_path / "readme.txt"
test_file.write_text("Not code")
functions, classes = extractor.extract_from_file(test_file)
assert functions == []
assert classes == []
class TestGetFileApiDocs:
"""Test the convenience function."""
def test_file_with_content(self, tmp_path):
"""Test getting API docs for a file with content."""
source = dedent(
'''
def process(value: int = 10) -> str:
"""Process a value.
Args:
value: The value to process.
Returns:
The processed string.
"""
return str(value)
'''
).strip()
test_file = tmp_path / "processor.py"
test_file.write_text(source)
result = get_file_api_docs(test_file)
assert result is not None
assert "process" in result
assert "value: int = 10" in result
assert "-> str" in result
def test_file_without_functions(self, tmp_path):
"""Test getting API docs for file without functions."""
source = dedent(
"""
X = 1
Y = 2
"""
).strip()
test_file = tmp_path / "constants.py"
test_file.write_text(source)
result = get_file_api_docs(test_file)
assert result is None
class TestArgsAndKwargsExtraction:
"""Test *args and **kwargs parameter extraction."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_args_extraction(self, parser):
"""Test extracting *args parameter."""
source = dedent(
"""
def func(*args):
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
params = extract_python_parameters(func_node, source.encode())
assert len(params) == 1
assert params[0].name == "*args"
def test_kwargs_extraction(self, parser):
"""Test extracting **kwargs parameter."""
source = dedent(
"""
def func(**kwargs):
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
params = extract_python_parameters(func_node, source.encode())
assert len(params) == 1
assert params[0].name == "**kwargs"
def test_args_and_kwargs_together(self, parser):
"""Test extracting both *args and **kwargs."""
source = dedent(
"""
def func(a, *args, **kwargs):
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
params = extract_python_parameters(func_node, source.encode())
assert len(params) == 3
assert params[0].name == "a"
assert params[1].name == "*args"
assert params[2].name == "**kwargs"
class TestDecoratorExtraction:
"""Test decorator extraction from functions."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_single_decorator(self, parser):
"""Test extracting a single decorator."""
source = dedent(
"""
@staticmethod
def func():
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
# Find the function_definition node (may be nested under decorated_definition)
func_node = None
for child in root.children:
if child.type == "decorated_definition":
for c in child.children:
if c.type == "function_definition":
func_node = c
break
elif child.type == "function_definition":
func_node = child
break
decorators = extract_python_decorators(func_node, source.encode())
assert len(decorators) == 1
assert "@staticmethod" in decorators[0]
def test_multiple_decorators(self, parser):
"""Test extracting multiple decorators."""
source = dedent(
"""
@classmethod
@cached_property
def func(cls):
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = None
for child in root.children:
if child.type == "decorated_definition":
for c in child.children:
if c.type == "function_definition":
func_node = c
break
decorators = extract_python_decorators(func_node, source.encode())
assert len(decorators) == 2
class TestDocstringEdgeCases:
"""Test docstring extraction edge cases."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_single_quote_docstring(self, parser):
"""Test extracting single-quoted docstring."""
source = dedent(
"""
def func():
'Single quote docstring.'
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
docstring = extract_python_docstring(func_node, source.encode())
assert docstring == "Single quote docstring."
def test_double_quote_single_line_docstring(self, parser):
"""Test extracting double-quoted single-line docstring."""
source = dedent(
"""
def func():
"Double quote docstring."
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
docstring = extract_python_docstring(func_node, source.encode())
assert docstring == "Double quote docstring."
def test_first_statement_not_docstring(self, parser):
"""Test function where first statement is not a docstring."""
source = dedent(
"""
def func():
x = 1
return x
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
docstring = extract_python_docstring(func_node, source.encode())
assert docstring is None
class TestGoogleDocstringEdgeCases:
"""Test Google docstring parsing edge cases."""
def test_empty_docstring(self):
"""Test parsing empty docstring."""
result = parse_google_docstring("")
assert result["description"] == ""
assert result["args"] == {}
assert result["returns"] is None
assert result["raises"] == []
def test_raises_section(self):
"""Test parsing Raises section."""
docstring = dedent(
"""
Do something.
Raises:
ValueError: If value is invalid.
"""
).strip()
result = parse_google_docstring(docstring)
assert result["description"] == "Do something."
def test_example_section(self):
"""Test that Example section is handled."""
docstring = dedent(
"""
Do something.
Example:
>>> func()
True
"""
).strip()
result = parse_google_docstring(docstring)
assert result["description"] == "Do something."
def test_notes_section(self):
"""Test that Notes section is handled."""
docstring = dedent(
"""
Do something.
Notes:
Some implementation notes.
"""
).strip()
result = parse_google_docstring(docstring)
assert result["description"] == "Do something."
def test_yields_section(self):
"""Test that Yields section is handled."""
docstring = dedent(
"""
Generate items.
Yields:
The next item.
"""
).strip()
result = parse_google_docstring(docstring)
assert result["description"] == "Generate items."
def test_param_continuation(self):
"""Test parameter description continuation across lines."""
docstring = dedent(
"""
Do something.
Args:
value: This is a very long description
that continues on the next line.
"""
).strip()
result = parse_google_docstring(docstring)
assert "value" in result["args"]
assert "very long description" in result["args"]["value"]["description"]
assert "continues" in result["args"]["value"]["description"]
def test_returns_continuation(self):
"""Test returns description continuation across lines."""
docstring = dedent(
"""
Do something.
Returns:
A result that has a very long
description spanning multiple lines.
"""
).strip()
result = parse_google_docstring(docstring)
assert result["returns"] is not None
assert "very long" in result["returns"]
assert "spanning" in result["returns"]
def test_description_with_paragraphs(self):
"""Test description truncation at paragraph break."""
docstring = dedent(
"""
First paragraph.
Second paragraph that should be ignored.
"""
).strip()
# Note: join with space creates "First paragraph. Second..."
# The split on \n\n won't work after joining
result = parse_google_docstring(docstring)
assert "First paragraph" in result["description"]
class TestNumpyDocstringEdgeCases:
"""Test NumPy docstring parsing edge cases."""
def test_empty_docstring(self):
"""Test parsing empty docstring."""
result = parse_numpy_docstring("")
assert result["description"] == ""
assert result["args"] == {}
assert result["returns"] is None
assert result["raises"] == []
def test_returns_section(self):
"""Test parsing Returns section."""
docstring = dedent(
"""
Do something.
Returns
-------
str
The result string.
"""
).strip()
result = parse_numpy_docstring(docstring)
assert result["returns"] is not None
assert "str" in result["returns"]
def test_raises_section(self):
"""Test parsing Raises section."""
docstring = dedent(
"""
Do something.
Raises
------
ValueError
If value is invalid.
"""
).strip()
result = parse_numpy_docstring(docstring)
# Raises is parsed but not populated (just changes section)
assert result["description"] == "Do something."
def test_other_section(self):
"""Test parsing other sections (like Examples)."""
docstring = dedent(
"""
Do something.
Examples
--------
>>> func()
True
"""
).strip()
result = parse_numpy_docstring(docstring)
assert result["description"] == "Do something."
def test_returns_continuation(self):
"""Test returns description continuation."""
docstring = dedent(
"""
Do something.
Returns
-------
str
First line of return description.
Second line of return description.
"""
).strip()
result = parse_numpy_docstring(docstring)
assert result["returns"] is not None
assert "First line" in result["returns"]
assert "Second line" in result["returns"]
def test_description_with_paragraphs(self):
"""Test description with paragraph break."""
docstring = "First paragraph.\n\nSecond paragraph."
result = parse_numpy_docstring(docstring)
# After join, it becomes "First paragraph. Second paragraph."
assert "First paragraph" in result["description"]
class TestParseDocstringAutoDetect:
"""Test automatic docstring format detection."""
def test_empty_docstring(self):
"""Test parsing empty docstring."""
result = parse_docstring("")
assert result["description"] == ""
assert result["args"] == {}
assert result["returns"] is None
assert result["raises"] == []
def test_detects_google_style(self):
"""Test detection of Google-style docstring."""
docstring = dedent(
"""
Do something.
Args:
value: The value.
"""
).strip()
result = parse_docstring(docstring)
assert "value" in result["args"]
def test_detects_numpy_style(self):
"""Test detection of NumPy-style docstring."""
docstring = dedent(
"""
Do something.
Parameters
----------
value : str
The value.
"""
).strip()
result = parse_docstring(docstring)
assert "value" in result["args"]
def test_simple_docstring_defaults_to_google(self):
"""Test simple docstring without sections defaults to Google."""
docstring = "Simple description without sections."
result = parse_docstring(docstring)
assert result["description"] == "Simple description without sections."
class TestFunctionSignatureEdgeCases:
"""Test function signature extraction edge cases."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_function_with_docstring_type_hints(self, parser):
"""Test that docstring type hints are used when code lacks them."""
source = dedent(
'''
def func(value):
"""Process value.
Args:
value (str): The value to process.
"""
pass
'''
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
sig = extract_function_signature(func_node, source.encode(), Language.PYTHON)
assert sig is not None
assert len(sig.parameters) == 1
assert sig.parameters[0].name == "value"
assert sig.parameters[0].type_hint == "str"
class TestClassSignatureEdgeCases:
"""Test class signature extraction edge cases."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_class_with_attribute_base(self, parser):
"""Test class with attribute-style base class (e.g., module.Class)."""
source = dedent(
'''
class MyClass(base.BaseClass):
"""A class with attribute base."""
pass
'''
).strip()
root = parser.parse_source(source, Language.PYTHON)
class_node = root.children[0]
sig = extract_class_signature(class_node, source.encode(), Language.PYTHON)
assert sig is not None
assert "base.BaseClass" in sig.bases
class TestMarkdownGenerationEdgeCases:
"""Test markdown generation edge cases."""
def test_function_with_decorators(self):
"""Test generating markdown for function with decorators."""
functions = [
FunctionSignature(
name="process",
decorators=["@staticmethod", "@cache"],
description="Process data.",
)
]
result = generate_api_reference_markdown(functions, [])
assert "### Functions" in result
assert "#### `process`" in result
assert "`@staticmethod`" in result
assert "`@cache`" in result
def test_class_with_separator(self):
"""Test that separator is added between classes and functions."""
classes = [ClassSignature(name="MyClass")]
functions = [FunctionSignature(name="my_func")]
result = generate_api_reference_markdown(functions, classes)
assert "### class `MyClass`" in result
assert "---" in result
assert "### Functions" in result
def test_special_methods_included(self):
"""Test that special methods like __init__ are included."""
classes = [
ClassSignature(
name="MyClass",
methods=[
FunctionSignature(name="__init__", is_method=True),
FunctionSignature(name="__call__", is_method=True),
FunctionSignature(name="__enter__", is_method=True),
FunctionSignature(name="__exit__", is_method=True),
FunctionSignature(name="_private_method", is_method=True),
],
)
]
result = generate_api_reference_markdown([], classes)
assert "__init__" in result
assert "__call__" in result
assert "__enter__" in result
assert "__exit__" in result
assert "_private_method" not in result
def test_class_without_methods(self):
"""Test class without methods."""
classes = [
ClassSignature(
name="EmptyClass",
description="An empty class.",
)
]
result = generate_api_reference_markdown([], classes)
assert "### class `EmptyClass`" in result
assert "An empty class." in result
assert "**Methods:**" not in result
def test_method_without_params(self):
"""Test method without parameters shows no table."""
classes = [
ClassSignature(
name="MyClass",
methods=[
FunctionSignature(
name="simple_method",
parameters=[],
is_method=True,
)
],
)
]
result = generate_api_reference_markdown([], classes)
assert "#### `simple_method`" in result
# Should not have parameter table header
lines = result.split("\n")
# Check that there's no parameter table after simple_method
method_idx = None
for i, line in enumerate(lines):
if "`simple_method`" in line:
method_idx = i
break
assert method_idx is not None
class TestNoParametersFunction:
"""Test function with no parameters at all."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_function_without_parentheses_content(self, parser):
"""Test function with empty parentheses returns empty params list."""
source = dedent(
"""
def func():
pass
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
func_node = root.children[0]
params = extract_python_parameters(func_node, source.encode())
assert params == []
class TestDescriptionParagraphSplit:
"""Test description splitting at paragraph breaks."""
def test_google_description_paragraph_split(self):
"""Test Google docstring description with actual newline paragraph."""
# Create a docstring where description has an actual double newline
docstring = "First paragraph of description.\n\nSecond paragraph."
result = parse_google_docstring(docstring)
# After processing, the description should contain the split
# Note: lines are joined with space, then split on \n\n
# The original has \n\n which becomes "First paragraph of description. Second paragraph."
# This won't trigger line 285 because the join removes \n\n
assert "First paragraph" in result["description"]
def test_numpy_description_paragraph_split(self):
"""Test NumPy docstring description with actual newline paragraph."""
docstring = "First paragraph of description.\n\nSecond paragraph."
result = parse_numpy_docstring(docstring)
assert "First paragraph" in result["description"]
class TestFunctionNameExtractionFailure:
"""Test function signature extraction when name extraction fails."""
@pytest.fixture
def parser(self):
return CodeParser()
def test_lambda_function_returns_none(self, parser):
"""Test that lambda expressions return None (no extractable name)."""
source = dedent(
"""
f = lambda x: x + 1
"""
).strip()
root = parser.parse_source(source, Language.PYTHON)
# Find the lambda node
lambda_node = None
for child in root.children:
if child.type == "expression_statement":
for c in child.children:
if c.type == "assignment":
for cc in c.children:
if cc.type == "lambda":
lambda_node = cc
break
if lambda_node:
sig = extract_function_signature(lambda_node, source.encode(), Language.PYTHON)
# Lambda doesn't have a name field like function_definition
assert sig is None
class TestMockedASTEdgeCases:
"""Test edge cases using mocked AST nodes."""
def test_extract_parameters_no_params_node(self):
"""Test extract_python_parameters when node has no parameters field."""
from unittest.mock import MagicMock
mock_node = MagicMock()
mock_node.child_by_field_name.return_value = None
result = extract_python_parameters(mock_node, b"")
assert result == []
def test_extract_docstring_no_body_node(self):
"""Test extract_python_docstring when node has no body field."""
from unittest.mock import MagicMock
mock_node = MagicMock()
mock_node.child_by_field_name.return_value = None
result = extract_python_docstring(mock_node, b"")
assert result is None
def test_extract_class_signature_no_name(self):
"""Test extract_class_signature when class has no name."""
from unittest.mock import MagicMock, patch
mock_node = MagicMock()
mock_node.children = []
with patch(
"local_deepwiki.generators.api_docs.get_node_name", return_value=None
):
result = extract_class_signature(mock_node, b"", Language.PYTHON)
assert result is None
def test_extract_function_signature_no_name(self):
"""Test extract_function_signature when function has no name."""
from unittest.mock import MagicMock, patch
mock_node = MagicMock()
mock_node.children = []
with patch(
"local_deepwiki.generators.api_docs.get_node_name", return_value=None
):
result = extract_function_signature(mock_node, b"", Language.PYTHON)
assert result is None
class TestDescriptionParagraphSplitEdgeCase:
"""Test edge case for description paragraph splitting."""
def test_google_docstring_with_embedded_newlines(self):
"""Test Google docstring with description containing literal newlines.
Lines 284-285 check for \\n\\n in description after joining with space.
This can only happen if a single line contains the literal sequence.
"""
# A line that contains the literal \n\n sequence
docstring = "First\n\nSecond" # This has actual newlines
result = parse_google_docstring(docstring)
# The lines will be ["First", "", "Second"]
# After join: "First Second" - no \n\n
# So these lines remain uncovered in normal usage
assert "First" in result["description"]
def test_numpy_docstring_with_embedded_newlines(self):
"""Test NumPy docstring with description containing literal newlines."""
docstring = "First\n\nSecond"
result = parse_numpy_docstring(docstring)
assert "First" in result["description"]