Skip to main content

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