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
|
|
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:
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
|