Skip to content

Analyzer

analyzer

High-level package analysis engine.

This module builds on the tree-sitter parser to provide package-wide analysis: module discovery, dependency graphs, public API extraction, and semantic search.

Example

from pathlib import Path from axm_ast.core.analyzer import analyze_package pkg = analyze_package(Path("src/mylib")) [m.path.name for m in pkg.modules]['__init__.py', 'core.py', 'utils.py']

analyze_package(path)

Analyze a Python package directory.

Discovers all .py files, parses them with tree-sitter, and builds a complete PackageInfo with dependency edges.

Parameters:

Name Type Description Default
path Path

Path to the package root directory.

required

Returns:

Type Description
PackageInfo

PackageInfo with all modules and dependency edges.

Raises:

Type Description
ValueError

If path is not a directory.

Example

pkg = analyze_package(Path("src/mylib")) pkg.name 'mylib'

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
def analyze_package(path: Path) -> PackageInfo:
    """Analyze a Python package directory.

    Discovers all ``.py`` files, parses them with tree-sitter, and
    builds a complete ``PackageInfo`` with dependency edges.

    Args:
        path: Path to the package root directory.

    Returns:
        PackageInfo with all modules and dependency edges.

    Raises:
        ValueError: If path is not a directory.

    Example:
        >>> pkg = analyze_package(Path("src/mylib"))
        >>> pkg.name
        'mylib'
    """
    path = Path(path).resolve()
    if not path.is_dir():
        msg = f"{path} is not a directory"
        raise ValueError(msg)

    t0 = time.perf_counter()

    # Discover all .py files, skipping virtual envs and caches
    py_files = sorted(_discover_py_files(path))
    modules: list[ModuleInfo] = []
    for py_file in py_files:
        modules.append(extract_module_info(py_file))

    # Build dependency edges from internal imports
    dep_edges = _build_edges(modules, path)

    pkg = PackageInfo(
        name=path.name,
        root=path,
        modules=modules,
        dependency_edges=dep_edges,
    )

    elapsed = time.perf_counter() - t0
    logger.debug("Analyzed %s in %.2fs (%d modules)", path.name, elapsed, len(modules))

    return pkg

build_import_graph(pkg)

Build an adjacency-list import graph from package info.

Parameters:

Name Type Description Default
pkg PackageInfo

Analyzed package info.

required

Returns:

Type Description
dict[str, list[str]]

Dict mapping module name to list of modules it imports.

Example

graph = build_import_graph(pkg) graph["cli"]['core', 'models']

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
def build_import_graph(pkg: PackageInfo) -> dict[str, list[str]]:
    """Build an adjacency-list import graph from package info.

    Args:
        pkg: Analyzed package info.

    Returns:
        Dict mapping module name to list of modules it imports.

    Example:
        >>> graph = build_import_graph(pkg)
        >>> graph["cli"]
        ['core', 'models']
    """
    graph: dict[str, list[str]] = {}
    for src, target in pkg.dependency_edges:
        graph.setdefault(src, []).append(target)
    return graph

find_module_for_symbol(pkg, symbol)

Find the module containing a symbol.

Supports two lookup modes:

  • Object (FunctionInfo / ClassInfo): identity-first match, then name fallback.
  • String: name-based search across functions, methods, and classes.

Parameters:

Name Type Description Default
pkg PackageInfo

Analyzed package info.

required
symbol str | FunctionInfo | ClassInfo | VariableInfo

Symbol name or object to locate.

required

Returns:

Type Description
ModuleInfo | None

The ModuleInfo containing the symbol, or None.

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
def find_module_for_symbol(
    pkg: PackageInfo,
    symbol: str | FunctionInfo | ClassInfo | VariableInfo,
) -> ModuleInfo | None:
    """Find the module containing a symbol.

    Supports two lookup modes:

    - **Object** (``FunctionInfo`` / ``ClassInfo``): identity-first match,
      then name fallback.
    - **String**: name-based search across functions, methods, and classes.

    Args:
        pkg: Analyzed package info.
        symbol: Symbol name or object to locate.

    Returns:
        The ``ModuleInfo`` containing the symbol, or ``None``.
    """
    if not isinstance(symbol, str):
        # Identity match (when passed an object)
        result = _find_module_by_identity(pkg, symbol)
        if result is not None:
            return result
        # Fallback to name-based search
        symbol = symbol.name

    return _find_module_by_name(pkg, symbol)

get_public_api(pkg)

Extract the public API surface of a package.

Uses __all__ when available, otherwise filters by name convention.

Parameters:

Name Type Description Default
pkg PackageInfo

Analyzed package info.

required

Returns:

Type Description
list[FunctionInfo | ClassInfo]

List of public functions and classes.

Example

api = get_public_api(pkg) [a.name for a in api]['main', 'Config']

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
def get_public_api(pkg: PackageInfo) -> list[FunctionInfo | ClassInfo]:
    """Extract the public API surface of a package.

    Uses ``__all__`` when available, otherwise filters by name convention.

    Args:
        pkg: Analyzed package info.

    Returns:
        List of public functions and classes.

    Example:
        >>> api = get_public_api(pkg)
        >>> [a.name for a in api]
        ['main', 'Config']
    """
    return pkg.public_api

module_dotted_name(mod_path, root)

Convert a module file path to a dotted name relative to root.

Parameters:

Name Type Description Default
mod_path Path

Absolute path to a .py file.

required
root Path

Package root directory.

required

Returns:

Type Description
str

Dotted module name (e.g. "core.parser").

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
def module_dotted_name(mod_path: Path, root: Path) -> str:
    """Convert a module file path to a dotted name relative to root.

    Args:
        mod_path: Absolute path to a ``.py`` file.
        root: Package root directory.

    Returns:
        Dotted module name (e.g. ``"core.parser"``).
    """
    try:
        rel = mod_path.relative_to(root)
    except ValueError:
        return mod_path.stem
    parts = list(rel.with_suffix("").parts)
    if parts and parts[-1] == "__init__":
        parts = parts[:-1]
    return ".".join(parts) if parts else root.name

search_symbols(pkg, *, name=None, returns=None, kind=None, inherits=None)

Search for symbols across a package with filters.

All filters are AND-combined. A symbol must match all provided filters to be included in results.

Parameters:

Name Type Description Default
pkg PackageInfo

Analyzed package info.

required
name str | None

Filter by symbol name (substring match).

None
returns str | None

Filter functions by return type (substring match).

None
kind SymbolKind | None

Filter by SymbolKind (function, method, property, classmethod, staticmethod, abstract, class, variable).

None
inherits str | None

Filter classes by base class name.

None

Returns:

Type Description
list[FunctionInfo | ClassInfo | VariableInfo]

List of matching symbols.

Example

results = search_symbols(pkg, returns="str") [r.name for r in results]['greet', 'version']

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
def search_symbols(
    pkg: PackageInfo,
    *,
    name: str | None = None,
    returns: str | None = None,
    kind: SymbolKind | None = None,
    inherits: str | None = None,
) -> list[FunctionInfo | ClassInfo | VariableInfo]:
    """Search for symbols across a package with filters.

    All filters are AND-combined. A symbol must match all provided
    filters to be included in results.

    Args:
        pkg: Analyzed package info.
        name: Filter by symbol name (substring match).
        returns: Filter functions by return type (substring match).
        kind: Filter by SymbolKind (function, method, property,
            classmethod, staticmethod, abstract, class, variable).
        inherits: Filter classes by base class name.

    Returns:
        List of matching symbols.

    Example:
        >>> results = search_symbols(pkg, returns="str")
        >>> [r.name for r in results]
        ['greet', 'version']
    """
    results: list[FunctionInfo | ClassInfo | VariableInfo] = []

    for mod in pkg.modules:
        results.extend(
            _search_module(
                mod,
                name=name,
                returns=returns,
                kind=kind,
                inherits=inherits,
            )
        )

    return results