Skip to content

Scanner

The top-level scan() entry point runs the full scan → generate → validate pipeline in a single call.

lcp.scan

scan(package_name: str, *, include_private: bool = False, recursive: bool = True, validate: bool = True) -> LCPDocument

Scan a Python package and generate an LCP document.

This is the main entry point for the SDK.

Parameters:

Name Type Description Default
package_name str

The name of an installed Python package to scan.

required
include_private bool

Include private symbols (starting with _).

False
recursive bool

Scan submodules recursively.

True
validate bool

Validate the output against the LCP schema.

True

Returns:

Type Description
LCPDocument

An LCPDocument containing the scanned library information.

Raises:

Type Description
ImportError

If the package cannot be imported.

LCPValidationError

If validation is enabled and the output is invalid.

Example

from lcp import scan doc = scan("json") doc.to_file("json.lcp.json")

Source code in src/lcp/__init__.py
def scan(
    package_name: str,
    *,
    include_private: bool = False,
    recursive: bool = True,
    validate: bool = True,
) -> LCPDocument:
    """Scan a Python package and generate an LCP document.

    This is the main entry point for the SDK.

    Args:
        package_name: The name of an installed Python package to scan.
        include_private: Include private symbols (starting with _).
        recursive: Scan submodules recursively.
        validate: Validate the output against the LCP schema.

    Returns:
        An LCPDocument containing the scanned library information.

    Raises:
        ImportError: If the package cannot be imported.
        LCPValidationError: If validation is enabled and the output is invalid.

    Example:
        >>> from lcp import scan
        >>> doc = scan("json")
        >>> doc.to_file("json.lcp.json")
    """
    scanned = scan_package(
        package_name,
        include_private=include_private,
        recursive=recursive,
    )

    lcp_doc = generate_lcp(scanned)

    if validate:
        validate_or_raise(lcp_doc)

    return lcp_doc

lcp.scanner

Scanner module for introspecting Python packages.

ScannedParam dataclass

Scanned parameter information.

Source code in src/lcp/scanner.py
@dataclass
class ScannedParam:
    """Scanned parameter information."""

    name: str
    type_hint: str | None = None
    default: Any = inspect.Parameter.empty
    kind: str = "positional"
    description: str | None = None

    @property
    def has_default(self) -> bool:
        return self.default is not inspect.Parameter.empty

    @property
    def is_variadic(self) -> bool:
        return self.kind in ("rest", "keyword_rest")

ScannedSignature dataclass

Scanned function/method signature.

Source code in src/lcp/scanner.py
@dataclass
class ScannedSignature:
    """Scanned function/method signature."""

    params: list[ScannedParam] = field(default_factory=list)
    return_type: str | None = None
    is_async: bool = False
    raises: list[str] = field(default_factory=list)

ScannedSymbol dataclass

Scanned symbol information.

Source code in src/lcp/scanner.py
@dataclass
class ScannedSymbol:
    """Scanned symbol information."""

    name: str
    qualified_name: str
    module_path: str
    kind: str  # function, class, method, attribute, constant, module
    summary: str | None = None
    description: str | None = None
    signature: ScannedSignature | None = None
    members: list[ScannedSymbol] = field(default_factory=list)
    source_file: str | None = None
    source_lines: tuple[int, int] | None = None

ScannedModule dataclass

Scanned module information.

Source code in src/lcp/scanner.py
@dataclass
class ScannedModule:
    """Scanned module information."""

    name: str
    version: str
    symbols: list[ScannedSymbol] = field(default_factory=list)
    submodules: list[ScannedModule] = field(default_factory=list)

scan_module

scan_module(module: ModuleType, include_private: bool = False, _visited: set | None = None, _package_root: str | None = None) -> list[ScannedSymbol]

Scan a module for symbols.

Source code in src/lcp/scanner.py
def scan_module(
    module: ModuleType,
    include_private: bool = False,
    _visited: set | None = None,
    _package_root: str | None = None,
) -> list[ScannedSymbol]:
    """Scan a module for symbols."""
    if _visited is None:
        _visited = set()

    if _package_root is None:
        _package_root = module.__name__.split(".")[0]

    module_id = id(module)
    if module_id in _visited:
        return []
    _visited.add(module_id)

    module_path = module.__name__
    symbols: list[ScannedSymbol] = []

    # Add module as a symbol
    mod_summary, mod_desc = _parse_docstring(module.__doc__)
    symbols.append(
        ScannedSymbol(
            name=module_path,
            qualified_name="",  # Empty entity path for modules
            module_path=module_path,
            kind="module",
            summary=mod_summary or f"Module {module_path}",
            description=mod_desc,
        )
    )

    # Get all public names
    if hasattr(module, "__all__"):
        public_names = set(module.__all__)
    else:
        public_names = None

    for name, obj in inspect.getmembers(module):
        # Skip private symbols
        if not _is_public(name, include_private):
            continue

        # If __all__ is defined, respect it
        if public_names is not None and name not in public_names:
            continue

        # Skip imported modules (they belong to their own package)
        if inspect.ismodule(obj):
            continue

        # Check if this symbol is defined in this module
        obj_module = getattr(obj, "__module__", None)
        if obj_module and obj_module != module_path:
            # Skip re-exported symbols (they're documented in their origin module)
            continue

        if inspect.isclass(obj):
            symbols.append(
                _scan_class(obj, module_path, include_private, package_root=_package_root)
            )
        elif inspect.isfunction(obj):
            symbols.append(_scan_function(obj, module_path, name))
        elif _is_constant(name, obj):
            symbols.append(
                ScannedSymbol(
                    name=name,
                    qualified_name=name,
                    module_path=module_path,
                    kind="constant",
                    summary=f"Constant {name}",
                )
            )

    return symbols

scan_package

scan_package(package_name: str, include_private: bool = False, recursive: bool = True) -> ScannedModule

Scan an installed package and return scanned information.

Source code in src/lcp/scanner.py
def scan_package(
    package_name: str, include_private: bool = False, recursive: bool = True
) -> ScannedModule:
    """Scan an installed package and return scanned information."""
    try:
        module = importlib.import_module(package_name)
    except ImportError as e:
        raise ImportError(f"Cannot import package '{package_name}': {e}") from e

    version = _get_package_version(package_name)
    visited: set = set()
    package_root = package_name.split(".")[0]

    # Scan main module
    symbols = scan_module(module, include_private, visited, _package_root=package_root)

    # Scan submodules if it's a package
    if recursive and hasattr(module, "__path__"):
        for submod in _iter_submodules(module):
            symbols.extend(
                scan_module(submod, include_private, visited, _package_root=package_root)
            )

    return ScannedModule(name=package_name, version=version, symbols=symbols)