Using graphlens in code
This guide covers the everyday Python workflow: load an adapter, analyze a
project, and work with the resulting GraphLens. For an exhaustive method list
see the GraphLens API reference.
Loading an adapter
The core never imports adapters directly. Resolve them through the registry, which discovers installed adapters via entry points:
from graphlens import adapter_registry
adapter_registry.available() # ['python', 'typescript', ...]
adapter = adapter_registry.load("python")()
load() returns the adapter class; call it to get an instance. If no
adapter is registered for the name, it raises AdapterNotFoundError.
Analyzing a project
from pathlib import Path
graph = adapter.analyze(Path("./my-project"))
analyze collects every source file the adapter owns, parses each one, runs the
type-aware resolver, and returns a populated GraphLens. To analyze a specific
subset of files (for incremental updates or custom filtering), pass them
explicitly:
files = [Path("src/app/main.py"), Path("src/app/db.py")]
graph = adapter.analyze(Path("./my-project"), files=files)
Always check the resolver status
from graphlens import RESOLVER_STATUS_KEY
status = graph.metadata[RESOLVER_STATUS_KEY] # 'ok' | 'degraded' | 'unavailable'
if status != "ok":
raise SystemExit(f"resolver did not complete: {status}")
Or let the adapter raise for you with strict=True, which raises
AdapterError when the resolver status is not ok:
graph = adapter.analyze(Path("./my-project"), strict=True)
Inspecting nodes
A GraphLens exposes nodes (a dict[str, Node] keyed by id) and relations
(a list[Relation]), plus indexed lookups so you rarely touch them directly:
from graphlens import NodeKind
graph.nodes_by_kind(NodeKind.CLASS) # all classes
graph.nodes_by_name("UserService") # short or qualified name match
graph.nodes_in_file("src/app/main.py") # everything declared in a file
Each Node is a frozen dataclass:
node = graph.nodes_by_name("UserService")[0]
node.id # 'a1b2c3d4e5f6a7b8'
node.kind # NodeKind.CLASS
node.qualified_name # 'app.services.UserService'
node.name # 'UserService'
node.file_path # 'src/app/services.py'
node.span # Span(start_line=12, start_col=1, end_line=48, end_col=2)
node.metadata # {...}
Walking the graph
The query methods take a node id and return lists of Node:
fn = graph.nodes_by_name("process_order")[0]
graph.callers(fn.id) # functions/methods that call fn
graph.callees(fn.id) # functions/methods fn calls
graph.references_to(fn.id) # nodes that REFERENCE fn (variable/attribute use)
graph.neighbors(fn.id, depth=2) # distinct nodes within 2 hops, any direction
For lower-level access to the raw edges, use outgoing/incoming, optionally
filtered by relation kind:
from graphlens import RelationKind
graph.outgoing(fn.id, RelationKind.CALLS) # list[Relation] leaving fn
graph.incoming(fn.id, RelationKind.CALLS) # list[Relation] arriving at fn
See the Querying guide for recipes built on these primitives.
Extracting a subgraph
Carve out a focused slice — useful for visualizing or exporting one file or one feature:
sub = graph.subgraph_for_file("src/app/services.py")
ids = {n.id for n in graph.nodes_by_kind(NodeKind.CLASS)}
classes_only = graph.subgraph(ids)
Both return a new GraphLens containing the requested nodes and every relation
incident to them.
Serializing and reloading
The graph round-trips through JSON losslessly, so you can compute it once and reuse it everywhere (CI artifact, agent input, cache):
text = graph.to_json(indent=2)
graph.to_dict() # JSON-compatible dict instead of a string
restored = type(graph).from_json(text)
# or: from graphlens import GraphLens; GraphLens.from_json(text)
The serialized payload carries a schema version; loading a payload from an
incompatible schema raises SerializationError.
Diffing two graphs
diff = old_graph.diff(new_graph)
diff.added_nodes # list[Node] present only in new_graph
diff.removed_nodes # list[Node] present only in old_graph
diff.changed_nodes # list[tuple[Node, Node]] of (old, new) with same id
diff.added_relations # list[Relation]
diff.removed_relations # list[Relation]
diff.is_empty # True when the two graphs are structurally identical
Because node IDs are deterministic, the diff lines up nodes by identity across scans rather than by position.
Merging graphs
Combine graphs from several languages or several sub-projects into one:
combined = python_graph
combined.merge(typescript_graph, allow_shared=True)
Pass allow_shared=True when the graphs may contain identical nodes that
should coincide — most importantly cross-language BOUNDARY nodes, which is
the basis for cross-language linking.
Putting it together
from pathlib import Path
from graphlens import adapter_registry, NodeKind, RESOLVER_STATUS_KEY
adapter = adapter_registry.load("python")()
graph = adapter.analyze(Path("./my-project"))
assert graph.metadata[RESOLVER_STATUS_KEY] == "ok"
# Report the 10 most-called functions
funcs = graph.nodes_by_kind(NodeKind.FUNCTION) + graph.nodes_by_kind(NodeKind.METHOD)
ranked = sorted(funcs, key=lambda n: len(graph.callers(n.id)), reverse=True)
for n in ranked[:10]:
print(f"{len(graph.callers(n.id)):4d} {n.qualified_name}")
# Persist for later
Path("graph.json").write_text(graph.to_json(indent=2))
Next steps
- Querying the graph — practical recipes.
- Cross-language linking — connect services across languages.
GraphLensAPI reference — every method and signature.