Skip to content

Coverage

lcp.coverage

Coverage module for analyzing documentation completeness.

UndocumentedSymbol dataclass

A symbol missing documentation.

Source code in src/lcp/coverage.py
@dataclass
class UndocumentedSymbol:
    """A symbol missing documentation."""

    kind: str
    module: str
    entity: str
    source_file: str | None = None

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        result = {
            "kind": self.kind,
            "module": self.module,
            "entity": self.entity,
        }
        if self.source_file:
            result["source_file"] = self.source_file
        return result

to_dict

to_dict() -> dict

Convert to dictionary.

Source code in src/lcp/coverage.py
def to_dict(self) -> dict:
    """Convert to dictionary."""
    result = {
        "kind": self.kind,
        "module": self.module,
        "entity": self.entity,
    }
    if self.source_file:
        result["source_file"] = self.source_file
    return result

KindStats dataclass

Statistics for a specific symbol kind.

Source code in src/lcp/coverage.py
@dataclass
class KindStats:
    """Statistics for a specific symbol kind."""

    total: int = 0
    documented: int = 0
    undocumented: int = 0

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "total": self.total,
            "documented": self.documented,
            "undocumented": self.undocumented,
        }

to_dict

to_dict() -> dict

Convert to dictionary.

Source code in src/lcp/coverage.py
def to_dict(self) -> dict:
    """Convert to dictionary."""
    return {
        "total": self.total,
        "documented": self.documented,
        "undocumented": self.undocumented,
    }

CoverageSummary dataclass

Summary of documentation coverage.

Source code in src/lcp/coverage.py
@dataclass
class CoverageSummary:
    """Summary of documentation coverage."""

    total_symbols: int = 0
    documented: int = 0
    undocumented: int = 0
    coverage_percent: float = 0.0
    by_kind: dict[str, KindStats] = field(default_factory=dict)

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "total_symbols": self.total_symbols,
            "documented": self.documented,
            "undocumented": self.undocumented,
            "coverage_percent": round(self.coverage_percent, 1),
            "by_kind": {k: v.to_dict() for k, v in self.by_kind.items()},
        }

to_dict

to_dict() -> dict

Convert to dictionary.

Source code in src/lcp/coverage.py
def to_dict(self) -> dict:
    """Convert to dictionary."""
    return {
        "total_symbols": self.total_symbols,
        "documented": self.documented,
        "undocumented": self.undocumented,
        "coverage_percent": round(self.coverage_percent, 1),
        "by_kind": {k: v.to_dict() for k, v in self.by_kind.items()},
    }

CoverageReport dataclass

Documentation coverage report for a package.

Source code in src/lcp/coverage.py
@dataclass
class CoverageReport:
    """Documentation coverage report for a package."""

    package: str
    version: str
    generated_at: datetime
    summary: CoverageSummary
    undocumented: list[UndocumentedSymbol] = field(default_factory=list)

    def to_dict(self) -> dict:
        """Convert to dictionary suitable for JSON serialization."""
        return {
            "package": self.package,
            "version": self.version,
            "generated_at": self.generated_at.isoformat(),
            "summary": self.summary.to_dict(),
            "undocumented": [s.to_dict() for s in self.undocumented],
        }

    def to_json(self, indent: int = 2) -> str:
        """Convert to JSON string."""
        return json.dumps(self.to_dict(), indent=indent)

    def to_markdown(self) -> str:
        """Convert to Markdown format."""
        lines = [
            "# Documentation Coverage Report",
            "",
            f"**Package:** {self.package}  ",
            f"**Version:** {self.version}  ",
            f"**Generated:** {self.generated_at.strftime('%Y-%m-%d %H:%M:%S')}",
            "",
            "## Summary",
            "",
            "| Kind | Total | Documented | Undocumented | Coverage |",
            "|------|-------|------------|--------------|----------|",
        ]

        # Add rows for each kind
        for kind, stats in sorted(self.summary.by_kind.items()):
            if stats.total > 0:
                coverage = (stats.documented / stats.total) * 100
                lines.append(
                    f"| {kind} | {stats.total} | {stats.documented} | "
                    f"{stats.undocumented} | {coverage:.1f}% |"
                )

        # Add total row
        lines.append(
            f"| **Total** | **{self.summary.total_symbols}** | "
            f"**{self.summary.documented}** | **{self.summary.undocumented}** | "
            f"**{self.summary.coverage_percent:.1f}%** |"
        )

        # Add undocumented symbols section
        if self.undocumented:
            lines.extend(["", "## Undocumented Symbols", ""])

            # Group by kind
            by_kind: dict[str, list[UndocumentedSymbol]] = {}
            for symbol in self.undocumented:
                by_kind.setdefault(symbol.kind, []).append(symbol)

            for kind in sorted(by_kind.keys()):
                symbols = by_kind[kind]
                kind_title = kind.capitalize() + "s" if not kind.endswith("s") else kind.capitalize()
                lines.extend([f"### {kind_title} ({len(symbols)})", ""])

                for symbol in symbols:
                    source_info = f" - `{symbol.source_file}`" if symbol.source_file else ""
                    lines.append(f"- `{symbol.module}:{symbol.entity}`{source_info}")

                lines.append("")

        return "\n".join(lines)

    def to_file(self, path: str, format: str | None = None) -> None:
        """Write report to a file.

        Args:
            path: Output file path.
            format: Output format ('json' or 'markdown'). If None, inferred from extension.
        """
        if format is None:
            format = "markdown" if path.endswith(".md") else "json"

        content = self.to_markdown() if format == "markdown" else self.to_json()

        with open(path, "w", encoding="utf-8") as f:
            f.write(content)

to_dict

to_dict() -> dict

Convert to dictionary suitable for JSON serialization.

Source code in src/lcp/coverage.py
def to_dict(self) -> dict:
    """Convert to dictionary suitable for JSON serialization."""
    return {
        "package": self.package,
        "version": self.version,
        "generated_at": self.generated_at.isoformat(),
        "summary": self.summary.to_dict(),
        "undocumented": [s.to_dict() for s in self.undocumented],
    }

to_json

to_json(indent: int = 2) -> str

Convert to JSON string.

Source code in src/lcp/coverage.py
def to_json(self, indent: int = 2) -> str:
    """Convert to JSON string."""
    return json.dumps(self.to_dict(), indent=indent)

to_markdown

to_markdown() -> str

Convert to Markdown format.

Source code in src/lcp/coverage.py
def to_markdown(self) -> str:
    """Convert to Markdown format."""
    lines = [
        "# Documentation Coverage Report",
        "",
        f"**Package:** {self.package}  ",
        f"**Version:** {self.version}  ",
        f"**Generated:** {self.generated_at.strftime('%Y-%m-%d %H:%M:%S')}",
        "",
        "## Summary",
        "",
        "| Kind | Total | Documented | Undocumented | Coverage |",
        "|------|-------|------------|--------------|----------|",
    ]

    # Add rows for each kind
    for kind, stats in sorted(self.summary.by_kind.items()):
        if stats.total > 0:
            coverage = (stats.documented / stats.total) * 100
            lines.append(
                f"| {kind} | {stats.total} | {stats.documented} | "
                f"{stats.undocumented} | {coverage:.1f}% |"
            )

    # Add total row
    lines.append(
        f"| **Total** | **{self.summary.total_symbols}** | "
        f"**{self.summary.documented}** | **{self.summary.undocumented}** | "
        f"**{self.summary.coverage_percent:.1f}%** |"
    )

    # Add undocumented symbols section
    if self.undocumented:
        lines.extend(["", "## Undocumented Symbols", ""])

        # Group by kind
        by_kind: dict[str, list[UndocumentedSymbol]] = {}
        for symbol in self.undocumented:
            by_kind.setdefault(symbol.kind, []).append(symbol)

        for kind in sorted(by_kind.keys()):
            symbols = by_kind[kind]
            kind_title = kind.capitalize() + "s" if not kind.endswith("s") else kind.capitalize()
            lines.extend([f"### {kind_title} ({len(symbols)})", ""])

            for symbol in symbols:
                source_info = f" - `{symbol.source_file}`" if symbol.source_file else ""
                lines.append(f"- `{symbol.module}:{symbol.entity}`{source_info}")

            lines.append("")

    return "\n".join(lines)

to_file

to_file(path: str, format: str | None = None) -> None

Write report to a file.

Parameters:

Name Type Description Default
path str

Output file path.

required
format str | None

Output format ('json' or 'markdown'). If None, inferred from extension.

None
Source code in src/lcp/coverage.py
def to_file(self, path: str, format: str | None = None) -> None:
    """Write report to a file.

    Args:
        path: Output file path.
        format: Output format ('json' or 'markdown'). If None, inferred from extension.
    """
    if format is None:
        format = "markdown" if path.endswith(".md") else "json"

    content = self.to_markdown() if format == "markdown" else self.to_json()

    with open(path, "w", encoding="utf-8") as f:
        f.write(content)

generate_coverage

generate_coverage(package_name: str, include_private: bool = False, recursive: bool = True) -> CoverageReport

Generate documentation coverage report for a package.

Parameters:

Name Type Description Default
package_name str

The name of an installed Python package to analyze.

required
include_private bool

Include private symbols (starting with _).

False
recursive bool

Scan submodules recursively.

True

Returns:

Type Description
CoverageReport

A CoverageReport containing coverage statistics and undocumented symbols.

Raises:

Type Description
ImportError

If the package cannot be imported.

Example

from lcp import generate_coverage report = generate_coverage("requests") print(f"Coverage: {report.summary.coverage_percent}%") report.to_file("coverage.json")

Source code in src/lcp/coverage.py
def generate_coverage(
    package_name: str,
    include_private: bool = False,
    recursive: bool = True,
) -> CoverageReport:
    """Generate documentation coverage report for a package.

    Args:
        package_name: The name of an installed Python package to analyze.
        include_private: Include private symbols (starting with _).
        recursive: Scan submodules recursively.

    Returns:
        A CoverageReport containing coverage statistics and undocumented symbols.

    Raises:
        ImportError: If the package cannot be imported.

    Example:
        >>> from lcp import generate_coverage
        >>> report = generate_coverage("requests")
        >>> print(f"Coverage: {report.summary.coverage_percent}%")
        >>> report.to_file("coverage.json")
    """
    scanned = scan_package(package_name, include_private, recursive)
    return _analyze_coverage(scanned)

generate_coverage_from_scanned

generate_coverage_from_scanned(scanned: ScannedModule) -> CoverageReport

Generate coverage report from already scanned module data.

This is useful when you want to generate both LCP and coverage from the same scan, avoiding duplicate work.

Parameters:

Name Type Description Default
scanned ScannedModule

Pre-scanned module data from scan_package().

required

Returns:

Type Description
CoverageReport

A CoverageReport containing coverage statistics.

Source code in src/lcp/coverage.py
def generate_coverage_from_scanned(scanned: ScannedModule) -> CoverageReport:
    """Generate coverage report from already scanned module data.

    This is useful when you want to generate both LCP and coverage
    from the same scan, avoiding duplicate work.

    Args:
        scanned: Pre-scanned module data from scan_package().

    Returns:
        A CoverageReport containing coverage statistics.
    """
    return _analyze_coverage(scanned)