Skip to content

Index

axm_ast

axm-ast — AST introspection CLI for AI agents, powered by tree-sitter.

This package provides deterministic, fast parsing of Python libraries to extract structured information (functions, classes, imports, docstrings, call graphs) at multiple granularity levels.

Example

from axm_ast import analyze_package from pathlib import Path

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

ClassInfo

Bases: BaseModel

Metadata for a single class.

Example

cls = ClassInfo(name="Parser", line_start=1, line_end=50) cls.is_public True

Source code in packages/axm-ast/src/axm_ast/models/nodes.py
Python
class ClassInfo(BaseModel):
    """Metadata for a single class.

    Example:
        >>> cls = ClassInfo(name="Parser", line_start=1, line_end=50)
        >>> cls.is_public
        True
    """

    model_config = ConfigDict(extra="forbid")

    name: str = Field(description="Class name")
    bases: list[str] = Field(default_factory=list, description="Base class names")
    methods: list[FunctionInfo] = Field(default_factory=list, description="Methods")
    docstring: str | None = Field(default=None, description="Docstring content")
    decorators: list[str] = Field(default_factory=list, description="Decorator names")
    line_start: int = Field(description="Start line (1-indexed)")
    line_end: int = Field(description="End line (1-indexed)")

    @property
    def is_public(self) -> bool:
        """Whether this class is part of the public API."""
        return not self.name.startswith("_")
is_public property

Whether this class is part of the public API.

FunctionInfo

Bases: BaseModel

Metadata for a single function or method.

Example

fn = FunctionInfo(name="parse", line_start=10, line_end=25) fn.is_public True

Source code in packages/axm-ast/src/axm_ast/models/nodes.py
Python
class FunctionInfo(BaseModel):
    """Metadata for a single function or method.

    Example:
        >>> fn = FunctionInfo(name="parse", line_start=10, line_end=25)
        >>> fn.is_public
        True
    """

    model_config = ConfigDict(extra="forbid")

    name: str = Field(description="Function/method name")
    params: list[ParameterInfo] = Field(default_factory=list, description="Parameters")
    return_type: str | None = Field(default=None, description="Return type annotation")
    docstring: str | None = Field(default=None, description="Docstring content")
    decorators: list[str] = Field(default_factory=list, description="Decorator names")
    kind: FunctionKind = Field(
        default=FunctionKind.FUNCTION, description="Callable classification"
    )
    line_start: int = Field(description="Start line (1-indexed)")
    line_end: int = Field(description="End line (1-indexed)")
    is_async: bool = Field(default=False, description="Whether function is async")
    signature: str | None = Field(default=None, description="Human-readable signature")

    @property
    def is_public(self) -> bool:
        """Whether this function is part of the public API."""
        return not self.name.startswith("_")

    def model_post_init(self, __context: object) -> None:
        """Compute signature if not explicitly provided.

        Strips ``Annotated[T, ...]`` wrappers from parameter and return-type
        annotations so that generated signatures show only the base type.
        """
        if self.signature is None:
            params_str = ", ".join(
                p.name + (f": {_strip_annotated(p.annotation)}" if p.annotation else "")
                for p in self.params
            )
            ret_type = _strip_annotated(self.return_type) if self.return_type else None
            ret = f" -> {ret_type}" if ret_type else ""
            prefix = "async " if self.is_async else ""
            self.signature = f"{prefix}def {self.name}({params_str}){ret}"
is_public property

Whether this function is part of the public API.

model_post_init(__context)

Compute signature if not explicitly provided.

Strips Annotated[T, ...] wrappers from parameter and return-type annotations so that generated signatures show only the base type.

Source code in packages/axm-ast/src/axm_ast/models/nodes.py
Python
def model_post_init(self, __context: object) -> None:
    """Compute signature if not explicitly provided.

    Strips ``Annotated[T, ...]`` wrappers from parameter and return-type
    annotations so that generated signatures show only the base type.
    """
    if self.signature is None:
        params_str = ", ".join(
            p.name + (f": {_strip_annotated(p.annotation)}" if p.annotation else "")
            for p in self.params
        )
        ret_type = _strip_annotated(self.return_type) if self.return_type else None
        ret = f" -> {ret_type}" if ret_type else ""
        prefix = "async " if self.is_async else ""
        self.signature = f"{prefix}def {self.name}({params_str}){ret}"

FunctionKind

Bases: StrEnum

Classification of a callable based on its decorators.

Source code in packages/axm-ast/src/axm_ast/models/nodes.py
Python
class FunctionKind(enum.StrEnum):
    """Classification of a callable based on its decorators."""

    FUNCTION = "function"
    METHOD = "method"
    PROPERTY = "property"
    CLASSMETHOD = "classmethod"
    STATICMETHOD = "staticmethod"
    ABSTRACT = "abstract"

ImportInfo

Bases: BaseModel

A single import statement.

Example

imp = ImportInfo(module="pathlib", names=["Path"]) imp.is_relative False

Source code in packages/axm-ast/src/axm_ast/models/nodes.py
Python
class ImportInfo(BaseModel):
    """A single import statement.

    Example:
        >>> imp = ImportInfo(module="pathlib", names=["Path"])
        >>> imp.is_relative
        False
    """

    model_config = ConfigDict(extra="forbid")

    module: str | None = Field(
        default=None, description="Module path (None for 'import x')"
    )
    names: list[str] = Field(default_factory=list, description="Imported names")
    alias: str | None = Field(default=None, description="Alias (as ...)")
    is_relative: bool = Field(default=False, description="Relative import")
    level: int = Field(default=0, description="Number of leading dots")

ModuleInfo

Bases: BaseModel

Full introspection result for a single Python module.

Example

mod = ModuleInfo(path=Path("foo.py")) len(mod.functions) 0

Source code in packages/axm-ast/src/axm_ast/models/nodes.py
Python
class ModuleInfo(BaseModel):
    """Full introspection result for a single Python module.

    Example:
        >>> mod = ModuleInfo(path=Path("foo.py"))
        >>> len(mod.functions)
        0
    """

    model_config = ConfigDict(extra="forbid", arbitrary_types_allowed=True)

    path: Path = Field(description="File path")
    name: str | None = Field(default=None, description="Module name")
    docstring: str | None = Field(default=None, description="Module-level docstring")
    functions: list[FunctionInfo] = Field(
        default_factory=list, description="Top-level functions"
    )
    classes: list[ClassInfo] = Field(
        default_factory=list, description="Top-level classes"
    )
    imports: list[ImportInfo] = Field(
        default_factory=list, description="Import statements"
    )
    variables: list[VariableInfo] = Field(
        default_factory=list, description="Module-level variables"
    )
    all_exports: list[str] | None = Field(
        default=None,
        description="Contents of __all__, None if not defined",
    )

    @property
    def public_functions(self) -> list[FunctionInfo]:
        """Functions that are part of the public API."""
        if self.all_exports is not None:
            return [f for f in self.functions if f.name in self.all_exports]
        return [f for f in self.functions if f.is_public]

    @property
    def public_classes(self) -> list[ClassInfo]:
        """Classes that are part of the public API."""
        if self.all_exports is not None:
            return [c for c in self.classes if c.name in self.all_exports]
        return [c for c in self.classes if c.is_public]
public_classes property

Classes that are part of the public API.

public_functions property

Functions that are part of the public API.

PackageInfo

Bases: BaseModel

Full introspection result for a Python package.

Example

pkg = PackageInfo(name="mylib", root=Path("src/mylib")) len(pkg.modules) 0

Source code in packages/axm-ast/src/axm_ast/models/nodes.py
Python
class PackageInfo(BaseModel):
    """Full introspection result for a Python package.

    Example:
        >>> pkg = PackageInfo(name="mylib", root=Path("src/mylib"))
        >>> len(pkg.modules)
        0
    """

    model_config = ConfigDict(extra="forbid")

    name: str = Field(description="Package name")
    root: Path = Field(description="Package root directory")
    modules: list[ModuleInfo] = Field(default_factory=list, description="All modules")
    dependency_edges: list[tuple[str, str]] = Field(
        default_factory=list,
        description="Internal import edges (from_module, to_module)",
    )

    @property
    def public_api(self) -> list[FunctionInfo | ClassInfo]:
        """All public functions and classes across the package."""
        result: list[FunctionInfo | ClassInfo] = []
        for mod in self.modules:
            result.extend(mod.public_functions)
            result.extend(mod.public_classes)
        return result

    @property
    def module_names(self) -> list[str]:
        """List of dotted module names."""
        names: list[str] = []
        for mod in self.modules:
            try:
                rel = mod.path.relative_to(self.root)
            except ValueError:
                names.append(mod.path.stem)
                continue
            parts = list(rel.with_suffix("").parts)
            if parts and parts[-1] == "__init__":
                parts = parts[:-1]
            if parts:
                names.append(".".join(parts))
            else:
                names.append(self.name)
        return names
module_names property

List of dotted module names.

public_api property

All public functions and classes across the package.

ParameterInfo

Bases: BaseModel

A single function/method parameter.

Example

p = ParameterInfo(name="path", annotation="Path", default="None") p.name 'path'

Source code in packages/axm-ast/src/axm_ast/models/nodes.py
Python
class ParameterInfo(BaseModel):
    """A single function/method parameter.

    Example:
        >>> p = ParameterInfo(name="path", annotation="Path", default="None")
        >>> p.name
        'path'
    """

    model_config = ConfigDict(extra="forbid")

    name: str = Field(description="Parameter name")
    annotation: str | None = Field(
        default=None, description="Type annotation as string"
    )
    default: str | None = Field(default=None, description="Default value as string")

VariableInfo

Bases: BaseModel

A module-level variable or constant.

Example

v = VariableInfo(name="all", line=5) v.name 'all'

Source code in packages/axm-ast/src/axm_ast/models/nodes.py
Python
class VariableInfo(BaseModel):
    """A module-level variable or constant.

    Example:
        >>> v = VariableInfo(name="__all__", line=5)
        >>> v.name
        '__all__'
    """

    model_config = ConfigDict(extra="forbid")

    name: str = Field(description="Variable name")
    annotation: str | None = Field(default=None, description="Type annotation")
    value_repr: str | None = Field(
        default=None, description="Short repr of assigned value"
    )
    line: int = Field(description="Line number (1-indexed)")

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
Python
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)

    # Detect src-layout: src/<pkg>/__init__.py
    src_dir = path / "src"
    if src_dir.is_dir():
        pkg_dirs = [
            child
            for child in src_dir.iterdir()
            if child.is_dir() and (child / "__init__.py").exists()
        ]
        if pkg_dirs:
            path = pkg_dirs[0]

    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

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[tuple[str, FunctionInfo | ClassInfo | VariableInfo]]

List of (module_name, symbol) tuples for matching symbols.

Example

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

Source code in packages/axm-ast/src/axm_ast/core/analyzer.py
Python
def search_symbols(
    pkg: PackageInfo,
    *,
    name: str | None = None,
    returns: str | None = None,
    kind: SymbolKind | None = None,
    inherits: str | None = None,
) -> list[tuple[str, 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 (module_name, symbol) tuples for matching symbols.

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

    for mod in pkg.modules:
        mod_dotted = mod.name or module_dotted_name(mod.path, pkg.root)
        for sym in _search_module(
            mod,
            name=name,
            returns=returns,
            kind=kind,
            inherits=inherits,
        ):
            results.append((mod_dotted, sym))

    return results