Skip to content

API Reference

This section is generated directly from the Spytial source code using mkdocstrings.

Public package

spytial

RelationalizerBase

Bases: ABC

Abstract base class for relationalizers.

Relationalizers convert Python objects into atoms and relations for spatial visualization. They define how objects are represented as atoms and how relationships between objects are expressed as spatial relations.

can_handle(obj) abstractmethod

Return True if this relationalizer can handle the given object.

Parameters:

Name Type Description Default
obj Any

The Python object to test

required

Returns:

Type Description
bool

True if this relationalizer can process the object, False otherwise

relationalize(obj, walker_func) abstractmethod

Convert object to atoms and relations using structured types.

Parameters:

Name Type Description Default
obj Any

The object to convert

required
walker_func Any

Function to recursively walk nested objects

required

Returns:

Name Type Description
List[Atom]

Tuple of (atoms_list, relations_list)

atoms_list List[Relation]

List of Atom instances representing the object

relations_list Tuple[List[Atom], List[Relation]]

List of Relation instances connecting this object to others

declared_relations(obj)

Return the relation names obj's type declares, populated or not.

relationalize is extensional: it emits a tuple only where the instance holds a value. A field that no walked instance populates therefore leaves no trace at all, and a selector naming it resolves to an arity-0 atom literal — indistinguishable from a misspelling. Naming the field here lets the builder emit it as an empty relation instead, so the data instance carries the type's shape and not only its contents.

Parameters:

Name Type Description Default
obj Any

The object whose type is being described

required

Returns:

Type Description
List[str]

Relation names declared by the type; the builder emits any that no

List[str]

instance populated as a relation with no tuples.

Defaults to none. Containers and primitives have no declared schema, and a relationalizer that renames fields (TupleRelationalizer emits t0, t1, …) would otherwise declare names it never populates.

RelationalizerRegistry

Registry for relationalizers.

register(relationalizer_cls, priority=0) classmethod

Register a relationalizer class with given priority.

find_relationalizer(obj) classmethod

Find the first relationalizer that can handle the given object.

list_relationalizers() classmethod

List all registered relationalizers with their priorities.

Returns:

Type Description
List[Tuple[int, str]]

List of (priority, relationalizer_class_name) tuples, sorted by priority (highest first)

clear() classmethod

Clear all registered relationalizers (for testing).

SequenceRecorder

Context manager for recording a sequence of object snapshots for visualization.

Uses a single shared builder across all :meth:record calls so that atom IDs remain stable across frames. This handles two common patterns:

  • In-place mutation — the same Python objects are modified between frames. Because id(obj) never changes, the persistent ID table keeps IDs stable automatically with no extra configuration.

  • Snapshot / deepcopy — a fresh copy of the data structure is created each step (e.g. functional algorithms, undo-history). Pass an identity callable that returns a stable string key for each object; atoms will then receive an identity:<key> ID that stays constant across frames.

Example — in-place mutation::

with spytial.sequence() as seq:
    seq.record(root)
    insert(root, 42)
    seq.record(root)
    insert(root, 7)
    seq.record(root)
seq.diagram()

Example — deepcopy snapshots with identity::

with spytial.sequence(identity=lambda n: str(n.id)) as seq:
    for snap in snapshots:
        seq.record(snap)
seq.diagram(method="file")

Atom labels are kept stable across frames by label_strategy. By default ("persist") each atom's label is locked at the first frame it appears. Pass label_strategy="back_construct" to defer label resolution until :meth:diagram, which then picks the most informative label per atom across the whole sequence — useful when record() runs inside the constructor that produces the object (e.g. a make_set() that snaps before its caller binding has completed; see issue #94).

record(obj, label=None, note=None)

Capture a snapshot of obj as the next frame in the sequence.

Parameters:

Name Type Description Default
obj Any

The object to snapshot.

required
label Optional[str]

Optional short label describing this step (e.g. "rotate left"). Shown in the status bar and as a scrubber tooltip. Whitespace is stripped; empty results are treated as no label. Truncated to 200 characters because the header is one line.

None
note Optional[str]

Optional longer description, shown in a collapsible panel. Multi-line text is fine; whitespace is stripped.

None

diagram(method=None, auto_open=None, width=None, height=None, title=None, sequence_policy=None)

Render all recorded frames as an interactive sequence visualization.

Parameters override the values supplied to :func:sequence / the constructor; omit them to use the constructor defaults.

EditCancelled

Bases: Exception

Raised by :func:edit when the editor is cancelled and on_cancel="raise".

AnnotatedType

A callable type alias that preserves spytial annotations.

This provides a clean syntax for reusable annotated types:

# Define once
Graph = AnnotatedType(Dict[int, List[int]],
    InferredEdge(selector=EDGE_SEL, name=" "))

# Use naturally - Graph() wraps the value with type info
g = Graph({0: [1], 1: [2], 2: []})
diagram(g)  # Annotations applied automatically!

The AnnotatedType acts as both: - A type hint (for documentation/type checkers) - A constructor (wraps values with the type info)

__init__(base_type, *annotations)

Create an annotated type alias.

Parameters:

Name Type Description Default
base_type Any

The underlying type (e.g., Dict[int, List[int]])

required
*annotations Any

SpytialAnnotation instances (Orientation, InferredEdge, etc.)

()

__call__(value)

Wrap a value with this type's annotations.

Parameters:

Name Type Description Default
value Any

The value to wrap

required

Returns:

Name Type Description
Typed Typed

A wrapper that diagram() will recognize

LineStyle dataclass

Bases: _StyleBlock

Styling for a drawn edge line (edgeStyle, inferredEdge, GroupEdge connector).

All fields optional: color (CSS color), pattern ('solid'|'dashed'|'dotted'), weight (number > 0), highlight (CSS color).

TextStyle dataclass

Bases: _StyleBlock

Styling for a label (edge labels, atom labels, group labels).

All fields optional: size ('small'|'normal'|'large'), color (CSS color).

BorderStyle dataclass

Bases: _StyleBlock

Styling for an atom's border: color (CSS color), width (number > 0).

FillStyle dataclass

Bases: _StyleBlock

Styling for an atom's interior fill: color (CSS color).

IconStyle dataclass

Bases: _StyleBlock

An icon drawn on an atom (spytial-core 4.3).

All fields optional, including path, so a rule on a supertype can supply the icon and a rule on a subtype tune only its opacity. An IconStyle with no path draws nothing.

  • path -- a bundled icon name ('person'), an icon-pack reference ('bi:person-fill'), a URL, or a path.
  • placement -- 'full' (the icon occupies the box) or 'badge' (a small marker in the corner, secondary to the label). Defaults to 'full'.
  • opacity -- alpha in [0, 1]. Fade a 'full' icon to use it as a watermark behind the label.

GroupEdge dataclass

Bases: _StyleBlock

Rich form of a selector-group's addEdge: direction plus connector styling.

points ('none'|'togroup'|'fromgroup') says which way the connector between the group's key and the group points — the same key the YAML block uses; lineStyle / textStyle style the connector edge and its label. Serializes to addEdge: {points: ..., lineStyle: ..., textStyle: ...}.

SpytialAnnotation

Base class for spytial type annotations.

Subclasses represent specific constraints or directives that can be attached to type aliases using typing.Annotated.

__new__(*args, **kwargs)

Keep the arguments as passed, before any subclass touches them.

A subclass init coerces style blocks to plain dicts and rewrites a deprecated spelling into its replacement before calling up, so by the time self.kwargs exists it describes what the rule became. The source block has to say what was written: Group(addEdge=GroupEdge( points='togroup')), not Group(addEdge={'points': 'togroup'}), and AtomColor(value='red'), not AtomColor(borderStyle={'color': 'red'}). An Annotated[...] rule is a call and not a decorator, so it never has AST text to read back and this is the only record of it.

Every subclass takes keyword arguments only, so kwargs is the whole call.

to_entry()

Convert to the internal registry format.

A scalar form serializes as a bare value rather than a mapping (- flag: hideDisconnected); SCALAR_ITEMS says which keyword carries it, so this matches the decorator path without either restating which forms are scalar.

Orientation

Bases: SpytialAnnotation

Orientation constraint for spatial layout.

directions are the placement words -- above/below/left/right and the adjacency variants directlyAbove/directlyBelow/directlyLeft/directlyRight. (horizontal/vertical are Align's vocabulary, not this one; core matches no case for them here and the constraint would be dropped.)

Usage

from typing import Annotated IntList = Annotated[list[int], Orientation(selector='items', directions=['left'])]

Cyclic

Bases: SpytialAnnotation

Cyclic constraint for circular layouts.

Usage

NodeRing = Annotated[list[Node], Cyclic(selector='items', direction='clockwise')]

Align

Bases: SpytialAnnotation

Alignment constraint.

direction is the shared axis: 'horizontal' or 'vertical'. Core rejects anything else as internally inconsistent.

Usage

AlignedList = Annotated[list[int], Align(selector='items', direction='horizontal')]

Group

Bases: SpytialAnnotation

Grouping constraint.

Usage

Grouped = Annotated[MyType, Group(selector='items', name='mygroup')]

Set showLabel=False to hide the caption and its background pill while keeping the group boundary. Keyed captions use the key's readable label; IDs are added only to distinguish duplicate captions. Unary groups show only name.

For the by-field form spytial-core 5.0 removed (field/groupOn/addToGroup), see the group decorator's docstring, which names the selector that replaces it.

addEdge controls the edge drawn between the group's key and the group itself. For a binary selector with tuples (a, b), (a, c), (a, d) the group is keyed by a and contains {b, c, d}:

- ``'none'``      -> draw nothing (default)
- ``'togroup'``   -> edge from the key into the group (a -> group)
- ``'fromgroup'`` -> edge from the group back to the key (group -> a)

Legacy addEdge=True is still accepted and maps to 'togroup'.

Grouped = Annotated[MyType, Group(selector='...', name='g', addEdge='togroup')]

To also style the connector, pass a GroupEdge (spytial-core 3.0), and style the group's own label with a top-level textStyle:

Grouped = Annotated[MyType, Group(selector='...', name='g',
    addEdge=GroupEdge(points='togroup', lineStyle=LineStyle(pattern='dashed')),
    textStyle=TextStyle(color='navy'))]

AtomColor

Bases: AtomStyle

Deprecated legacy atom color directive; use AtomStyle instead.

Rewrites to an atomStyle entry with value as the border color (the legacy directive colored the border, not the fill).

AtomStyle

Bases: SpytialAnnotation

Atom style directive (spytial-core 3.0).

Styles an atom's border, interior fill, icon, and label independently.

showLabel is independent of the icon: iconStyle.placement controls the icon's geometry, showLabel controls whether the label is drawn at all. That split is what makes a faded watermark, or an icon with no label, expressible (spytial-core 4.3).

Usage

Styled = Annotated[list[int], AtomStyle(selector='self', borderStyle=BorderStyle(color='blue'))] Filled = Annotated[Tree, AtomStyle(selector='Node', fillStyle=FillStyle(color='#eef6ff'), textStyle=TextStyle(size='large'))] Marked = Annotated[Tree, AtomStyle(selector='Dir', iconStyle=IconStyle(path='bi:folder-fill', placement='badge'))]

Size

Bases: SpytialAnnotation

Size constraint.

Size fixes a node's geometry, which is what the layout solves over rather than presentation layered on a solved layout, so it belongs under constraints (spytial-core 4.3; the directives section still accepts it behind a deprecation warning).

selector is optional -- omit it to resize every node.

Usage

SizedList = Annotated[list[int], Size(selector='items', height=50, width=50)] AllSquare = Annotated[list[int], Size(height=50, width=50)]

Icon

Bases: SpytialAnnotation

Icon directive.

Usage

IconList = Annotated[list[int], Icon(selector='items', path='icon.svg', showLabels=True)]

EdgeColor

Bases: EdgeStyle

Deprecated legacy edge color directive; use EdgeStyle instead.

Rewrites to an edgeStyle entry: value -> lineStyle.color, style -> lineStyle.pattern, weight -> lineStyle.weight.

EdgeStyle

Bases: SpytialAnnotation

Edge style directive (spytial-core 3.0).

Styles the edges of a field/relation: the drawn line via lineStyle, the edge's label via textStyle, plus the showLabel/hidden flags.

Usage

StyledEdges = Annotated[Tree, EdgeStyle(field='children', lineStyle=LineStyle(color='red', pattern='dashed'))] LabeledEdges = Annotated[Tree, EdgeStyle(field='Uses', lineStyle=LineStyle(color='#d10000', weight=3), textStyle=TextStyle(size='small'), showLabel=True)] HiddenEdges = Annotated[Tree, EdgeStyle(field='internal', hidden=True)]

HideField

Bases: SpytialAnnotation

Hide field directive.

Usage

CleanView = Annotated[MyClass, HideField(field='_private')] Filtered = Annotated[MyClass, HideField(field='debug', filter='debug & Production')]

HideAtom

Bases: SpytialAnnotation

Hide atom constraint.

Hiding an atom changes what the layout has to place, and can make a spec unsatisfiable against the other constraints, so it belongs under constraints (spytial-core 4.3; the directives section still accepts it behind a deprecation warning).

Usage

Filtered = Annotated[list[int], HideAtom(selector='hidden')]

Projection

Bases: SpytialAnnotation

Deprecated projection directive; it has no effect.

spytial-core's layout-spec parser does not read projection — projecting over a sig is a pre-layout transform on the data instance, driven by the viewer's projection controls, rather than something a layout spec declares. Authoring one emits YAML that core discards, so remove it.

Usage

Projected = Annotated[MyType, Projection(sig='MySig')] # no-op

Attribute

Bases: SpytialAnnotation

Attribute directive.

Usage

WithAttr = Annotated[MyType, Attribute(field='value')] Filtered = Annotated[MyType, Attribute(field='status', filter='status & Active')]

InferredEdge

Bases: SpytialAnnotation

Inferred edge directive.

Styling uses the shared lineStyle/textStyle blocks (spytial-core 3.0); the inline color/style/weight keys are deprecated and rewritten into lineStyle.

draw (spytial-core 3.2) is '<end> -> <end>', each end '_' (the atom itself) or the name of a group constraint, in which case that end attaches to that group's hull -- giving group-to-group and node-to-group edges. A keyed group (binary selector) is picked by the end's atom; a unary group is attached to directly. '_ -> _' means the same as omitting it.

Usage

WithEdges = Annotated[Graph, InferredEdge(name='connection', selector='nodes')] StyledEdges = Annotated[Graph, InferredEdge(name='ancestor', selector='^parent', lineStyle=LineStyle(color='gray', pattern='dotted'))] HullEdges = Annotated[Graph, InferredEdge(name='reports to', selector='^parent', draw='regions -> regions')]

Tag

Bases: SpytialAnnotation

Tag directive for adding computed attributes to nodes.

Unlike 'attribute', this does NOT remove edges - it adds computed attribute values to nodes based on selector evaluation.

Usage

Simple binary tag - displays as "age: 25"

Tagged = Annotated[MyType, Tag(toTag='Person', name='age', value='age')]

Ternary selector - displays as "score[Math]: 95"

Graded = Annotated[MyType, Tag(toTag='Student', name='score', value='grades')]

Four-column result (student, course, exam, grade) displays as

"score[Math,Midterm]: 95".

ExamGraded = Annotated[MyType, Tag(toTag='Student', name='score', value='examGrades')]

name is literal. The value selector must preserve the tagged atom as its first column; its last column is displayed as the value. A selector such as Person.age returns ages without their owning Person atoms and therefore cannot attach those values as tags. Use age instead.

Flag

Bases: SpytialAnnotation

Flag directive.

name is one of the two whole-diagram switches core acts on: 'hideDisconnected' (drop atoms with no edges) or 'hideDisconnectedBuiltIns' (drop only disconnected built-in atoms).

Usage

Flagged = Annotated[MyType, Flag(name='hideDisconnected')]

relationalizer(cls=None, *, priority=0)

Decorator to register a class as a relationalizer.

Parameters:

Name Type Description Default
priority int

Higher priority relationalizers are tried first. Priorities 0-99 are reserved for built-in relationalizers.

0

reify(data_instance, root_id=None)

Reconstruct a Python object from a Spytial data instance.

Rebuild the object named by the instance's root (or root_id). Supports built-in values, resolvable references, and class instances with reconstructible state. Unsupported types can return attribute proxies or incomplete instances. Shared references and many mutable cycles survive; cycles through immutable containers can lose identity. This is not a lossless inverse for every Python value. For dataclass instances whose fields an editor may have stripped, :func:spytial.edit registers reifiers that apply declared field defaults before restoring recorded values.

replit(data_instance, root_id=None)

Return repr() of the object :func:reify would reconstruct.

Reproduces the host REPL's echo for a data instance.

diagram(obj, method=None, auto_open=True, width=None, height=None, title=None, perf_path=None, perf_iterations=None, headless=False, timeout=None, as_type=None)

Display a Python object in the sPyTial visualizer.

Parameters:

Name Type Description Default
obj Any

Any Python object to visualize.

required
method Optional[str]

Display method - "inline" (Jupyter), "browser" (new tab), "file" (save to file), or "headless" (headless browser for testing) If None (default), automatically selects "inline" in notebooks or "browser" otherwise.

None
auto_open bool

Whether to automatically open browser (for "browser" method)

True
width Optional[int]

Width of the visualization container in pixels (default: auto-detected)

None
height Optional[int]

Height of the visualization container in pixels (default: auto-detected)

None
title Optional[str]

Title for the browser tab/page (default: "sPyTial Visualization")

None
perf_path Optional[Union[str, Path]]

Optional path to save performance metrics JSON file. If None, metrics are not saved (unless perf_iterations is set).

None
perf_iterations Optional[int]

Optional number of times to render for performance benchmarking. If provided, the visualization will be rendered N times and metrics will be aggregated. Works with method="browser", "file", or "headless".

None
headless bool

Run in headless Chrome for testing/benchmarking (requires selenium and chromedriver). If True, method is automatically set to "headless".

False
timeout Optional[float]

Timeout in seconds for headless mode. If None, automatically calculated based on perf_iterations (default: max(120, perf_iterations * 5)). For large/complex visualizations, set this higher (e.g., timeout=600 for 10 minutes).

None
as_type Optional[Any]

Optional annotated type to treat the object as. Annotations from this type will be applied in addition to any introspected from the object itself. Use AnnotatedType() or typing.Annotated to define types with spytial annotations.

None

Examples:

Define an annotated type

Graph = AnnotatedType(Dict[int, List[int]], InferredEdge(...), Orientation(...))

Display a dict as a Graph

g = {0: [1], 1: [2]} diagram(g, as_type=Graph)

Returns:

Name Type Description
str Optional[Union[str, Dict[str, Any]]]

Path to the generated HTML file (if method="file", "browser", or "headless")

dict Optional[Union[str, Dict[str, Any]]]

Performance metrics (if method="headless" and perf_iterations > 0)

sequence(identity=None, sequence_policy='stability', method=None, auto_open=True, width=None, height=None, title=None, as_type=None, label_strategy='persist')

Create a :class:SequenceRecorder for capturing algorithm steps or temporal snapshots.

The recorder uses a shared builder so atom IDs are stable across every :meth:~SequenceRecorder.record call within the same session.

Parameters:

Name Type Description Default
identity Optional[Callable[[Any], Optional[str]]]

Optional callable (obj) -> str | None. When provided, any object for which it returns a non-None string will receive an atom ID of identity:<returned-string>. Use this for deepcopy / snapshot workflows where Python's id() changes each frame but each node has a stable semantic key (e.g. a node's .id field).

None
sequence_policy str

Continuity policy for the frontend renderer. One of "stability" (default), "ignore_history", "change_emphasis", or "random_positioning".

'stability'
method Optional[str]

Display method — "inline" (Jupyter), "browser", or "file". Defaults to auto-detection.

None
auto_open bool

Whether to open the browser automatically ("browser" method).

True
width Optional[int]

Width of the visualization in pixels.

None
height Optional[int]

Height of the visualization in pixels.

None
title Optional[str]

Browser tab / page title.

None
as_type Optional[Any]

Optional annotated type applied to every recorded object.

None
label_strategy str

How atom labels are kept consistent across frames. "persist" (default) locks each atom's label at the first frame it appears. "back_construct" defers label resolution to :meth:~SequenceRecorder.diagram, then picks the most informative label for each atom (preferring real names over <type><digits> placeholders) and applies it uniformly across the sequence — this recovers the real name for atoms snapshotted before their caller binding completed (the _snap()-during-construction pattern).

'persist'

Returns:

Name Type Description
A SequenceRecorder

class:SequenceRecorder instance, usable as a context manager.

Example::

with spytial.sequence() as seq:
    seq.record(data)
    step(data)
    seq.record(data)
seq.diagram()

evaluate(obj, method=None, auto_open=True, width=None, height=None, as_type=None)

Evaluate a Python object using the sPyTial evaluator.

Parameters:

Name Type Description Default
obj Any

Any Python object to evaluate.

required
method Optional[str]

Display method - "inline" (Jupyter), "browser" (new tab), or "file" (save to file). If None (default), automatically selects "inline" in notebooks or "browser" otherwise.

None
auto_open bool

Whether to automatically open the browser (for "browser" method).

True
width Optional[int]

Width of the evaluation container in pixels (default: auto-detected).

None
height Optional[int]

Height of the evaluation container in pixels (default: auto-detected).

None
as_type Optional[Any]

Optional annotated type to treat the object as. Annotations from this type will be applied in addition to any introspected from the object itself.

None

Returns:

Name Type Description
str Optional[str]

Path to the generated HTML file (if method="file" or "browser").

edit(instance, *, on_cancel='seed', timeout=None, height=560)

Open a structured editor for any value and return the reified result on Done.

The interactive counterpart to :func:spytial.diagram. Serves the editor as a page from an ephemeral 127.0.0.1 server (inline in a local notebook, or a browser tab from a script), blocks until you click Done or Cancel, then reconstructs a fresh Python object with :func:reify and returns it. instance itself is never mutated.

Accepts any value (dataclasses, dicts, lists, arbitrary objects), mirroring :func:spytial.diagram. No Jupyter comm and no extra dependencies — only the standard library and a browser. The supported transport is a local script or a local Jupyter kernel; in pyodide and hosted notebooks (Colab, JupyterHub, Binder) the browser can't reach the local server, so edit() prints a note, shows :func:edit_html (standalone, Export button) instead, and returns None.

The editor can never hang the kernel: if the page never connects, stops responding, or you close it, edit() unblocks and returns per on_cancel.

Parameters:

Name Type Description Default
instance Any

Any value to seed the editor with.

required
on_cancel str

What to return if the editor is cancelled / closed / disconnected — "seed" (return the original instance unchanged, default), "none" (return None), or "raise" (raise :class:EditCancelled).

'seed'
timeout Optional[float]

Optional overall seconds to wait; None (default) waits as long as the page stays alive (a closed/unreachable page still unblocks promptly via liveness detection).

None
height int

Pixel height of the inline iframe in a notebook.

560

Returns:

Type Description
Any

The reified Python object on Done; otherwise per on_cancel.

Example::

result = spytial.edit(TreeNode(value=1))   # edit visually, click Done

edit_html(instance, *, method=None, auto_open=True, height=500)

Render a visual structured-value editor as standalone HTML.

This is the pyodide-compatible path. It generates a full HTML page with the <structured-input-graph> editor and displays it via an inline iframe (in notebooks) or a browser tab. Use the built-in Export button to copy the resulting Python constructor code.

Accepts any value (dataclasses, dicts, lists, arbitrary objects). To get the edited object back in Python directly (local script / local Jupyter), use :func:edit instead.

Parameters:

Name Type Description Default
instance Any

Any value to start editing from.

required
method Optional[str]

"inline" (default in notebooks), "browser", or None (auto-detect).

None
auto_open bool

Open the browser tab automatically (only for method="browser").

True
height int

Pixel height of the inline iframe.

500

Returns:

Type Description
Optional[str]

None when displayed inline, or the path to the temp HTML file

Optional[str]

when displayed in a browser.

Example::

spytial.edit_html(TreeNode(value=1))   # or {"a": 1}, [1, 2], …

get_spytial_core_version()

Return the version of spytial-core used by this package.

annotate(obj, annotation_type, **kwargs)

Apply any annotation type to a specific object. :param obj: The object to annotate. :param annotation_type: The type of annotation (e.g., 'orientation', 'cyclic'). :param kwargs: The annotation parameters. :return: The annotated object (for chaining).

annotate_orientation(obj, **kwargs)

Apply orientation annotation to a specific object.

annotate_cyclic(obj, **kwargs)

Apply cyclic annotation to a specific object.

annotate_align(obj, **kwargs)

Apply align annotation to a specific object.

annotate_group(obj, **kwargs)

Apply group annotation to a specific object.

annotate_atomColor(obj, **kwargs)

Apply atomColor annotation to a specific object (deprecated; use annotate_atomStyle).

annotate_atomStyle(obj, **kwargs)

Apply atomStyle annotation to a specific object.

annotate_size(obj, **kwargs)

Apply size annotation to a specific object.

annotate_icon(obj, **kwargs)

Apply icon annotation to a specific object.

annotate_edgeColor(obj, **kwargs)

Apply edgeColor annotation to a specific object (deprecated; use annotate_edgeStyle).

annotate_edgeStyle(obj, **kwargs)

Apply edgeStyle annotation to a specific object.

annotate_projection(obj, **kwargs)

Apply projection annotation to a specific object.

annotate_attribute(obj, **kwargs)

Apply attribute annotation to a specific object.

annotate_hideField(obj, **kwargs)

Apply hideField annotation to a specific object.

annotate_hideAtom(obj, **kwargs)

Apply hideAtom annotation to a specific object.

annotate_inferredEdge(obj, **kwargs)

Apply inferredEdge annotation to a specific object.

annotate_tag(obj, **kwargs)

Apply tag annotation to a specific object.

annotate_flag(obj, **kwargs)

Apply flag annotation to a specific object.

extract_spytial_annotations(type_hint)

Extract spytial annotations from a typing.Annotated type hint.

:param type_hint: A type hint, possibly Annotated with spytial markers. :return: A dict with 'constraints' and 'directives' lists, or None if no annotations.

get_base_type(type_hint)

Get the base type from an Annotated type hint.

:param type_hint: A type hint, possibly Annotated. :return: The base type (unwrapped from Annotated if applicable).

annotate_type_alias(type_alias, annotation_type, **kwargs)

Attach a spytial annotation to a type alias.

This function allows you to annotate type aliases (e.g., list[int], MyTypeAlias) with spatial constraints and directives. When objects are visualized, if their type annotation matches a registered type alias, these annotations are applied.

Usage

Define a type alias

IntList = list[int]

Annotate it

annotate_type_alias(IntList, 'orientation', selector='items', directions=['left']) annotate_type_alias(IntList, 'atomColor', selector='self', value='blue')

Or use the convenience functions:

annotate_type_alias_orientation(IntList, selector='items', directions=['left'])

:param type_alias: The type alias to annotate (e.g., list[int], MyClass, etc.) :param annotation_type: The type of annotation ('orientation', 'group', etc.) :param kwargs: The annotation parameters. :return: The type alias (for chaining).

get_type_alias_annotations(type_alias)

Retrieve annotations registered for a type alias.

:param type_alias: The type alias to look up. :return: A dictionary with 'constraints' and 'directives' lists, or None if not found.

clear_type_alias_annotations(type_alias=None)

Clear annotations for a specific type alias or all type aliases.

Note: This only clears the legacy registry-based annotations. Annotations using typing.Annotated are immutable and don't need clearing.

:param type_alias: If provided, clear only this type alias's annotations. If None, clear all type alias annotations.

list_type_alias_annotations()

List all registered type alias annotations (legacy registry).

:return: A dictionary mapping type alias keys to their annotation registries.

dont_inherit_constraints(cls)

Mark a class to not inherit constraints from parent classes.

This is useful when you want to override a parent class's constraints. Constraints applied directly to this class are still included.

Example

@orientation(selector='children', directions=['below']) class Parent: pass

@dont_inherit_constraints class Child(Parent): # Will NOT have Parent's orientation constraint pass

:param cls: The class to mark as not inheriting constraints. :return: The class (for chaining).

dont_inherit_directives(cls)

Mark a class to not inherit directives from parent classes.

This is useful when you want to override a parent class's directives. Directives applied directly to this class are still included.

Example

@atomColor(selector='root', value='red') class Parent: pass

@dont_inherit_directives class Child(Parent): # Will NOT have Parent's atomColor directive pass

:param cls: The class to mark as not inheriting directives. :return: The class (for chaining).

dont_inherit_annotations(cls)

Mark a class to not inherit any annotations from parent classes.

Shorthand for applying both dont_inherit_constraints and dont_inherit_directives.

Example

@orientation(selector='children', directions=['below']) @atomColor(selector='root', value='red') class Parent: pass

@dont_inherit_annotations class Child(Parent): # Will NOT have any annotations from Parent pass

:param cls: The class to mark as not inheriting annotations. :return: The class (for chaining).

collect_decorators(obj, type_hint=None)

Collect all decorators applied to the class of the given object, as well as any annotations applied directly to the object instance, and any annotations registered for matching type aliases. Respects inheritance control flags (dont_inherit_constraints, dont_inherit_directives). :param obj: The object whose class decorators and object annotations should be collected. :param type_hint: Optional type hint to look up type alias annotations. :return: A combined dictionary of constraints and directives (deduplicated).

serialize_to_yaml_string(decorators)

Serialize the collected constraints and directives to a YAML string. :param decorators: The collected decorators (constraints and directives). :return: YAML string representation of the decorators.

reset_object_ids()

Reset the global object-level state. Useful for testing or when you need deterministic object ID generation across multiple runs.

Clears both global registries — object IDs and object annotations — for the built-in values that can't store that state on themselves.

Warning: This will clear all existing object ID mappings and any annotations recorded for un-attributable objects, so existing selectors that depend on previous object IDs may no longer work.

apply_if(condition, *decorators)

Conditional decorator macro for sPyTial. Usage: @apply_if(CONDITION, orientation(...), hideField(...), attribute(...), ... ) class MyClass: ... If CONDITION is True, applies all decorators in order to the class. If CONDITION is False, returns the class unchanged.

Diagramming

spytial.visualizer

sPyTial Visualization Display Module

This module provides functions to display Python objects using the sPyTial visualizer.

SequenceRecorder

Context manager for recording a sequence of object snapshots for visualization.

Uses a single shared builder across all :meth:record calls so that atom IDs remain stable across frames. This handles two common patterns:

  • In-place mutation — the same Python objects are modified between frames. Because id(obj) never changes, the persistent ID table keeps IDs stable automatically with no extra configuration.

  • Snapshot / deepcopy — a fresh copy of the data structure is created each step (e.g. functional algorithms, undo-history). Pass an identity callable that returns a stable string key for each object; atoms will then receive an identity:<key> ID that stays constant across frames.

Example — in-place mutation::

with spytial.sequence() as seq:
    seq.record(root)
    insert(root, 42)
    seq.record(root)
    insert(root, 7)
    seq.record(root)
seq.diagram()

Example — deepcopy snapshots with identity::

with spytial.sequence(identity=lambda n: str(n.id)) as seq:
    for snap in snapshots:
        seq.record(snap)
seq.diagram(method="file")

Atom labels are kept stable across frames by label_strategy. By default ("persist") each atom's label is locked at the first frame it appears. Pass label_strategy="back_construct" to defer label resolution until :meth:diagram, which then picks the most informative label per atom across the whole sequence — useful when record() runs inside the constructor that produces the object (e.g. a make_set() that snaps before its caller binding has completed; see issue #94).

record(obj, label=None, note=None)

Capture a snapshot of obj as the next frame in the sequence.

Parameters:

Name Type Description Default
obj Any

The object to snapshot.

required
label Optional[str]

Optional short label describing this step (e.g. "rotate left"). Shown in the status bar and as a scrubber tooltip. Whitespace is stripped; empty results are treated as no label. Truncated to 200 characters because the header is one line.

None
note Optional[str]

Optional longer description, shown in a collapsible panel. Multi-line text is fine; whitespace is stripped.

None

diagram(method=None, auto_open=None, width=None, height=None, title=None, sequence_policy=None)

Render all recorded frames as an interactive sequence visualization.

Parameters override the values supplied to :func:sequence / the constructor; omit them to use the constructor defaults.

diagram(obj, method=None, auto_open=True, width=None, height=None, title=None, perf_path=None, perf_iterations=None, headless=False, timeout=None, as_type=None)

Display a Python object in the sPyTial visualizer.

Parameters:

Name Type Description Default
obj Any

Any Python object to visualize.

required
method Optional[str]

Display method - "inline" (Jupyter), "browser" (new tab), "file" (save to file), or "headless" (headless browser for testing) If None (default), automatically selects "inline" in notebooks or "browser" otherwise.

None
auto_open bool

Whether to automatically open browser (for "browser" method)

True
width Optional[int]

Width of the visualization container in pixels (default: auto-detected)

None
height Optional[int]

Height of the visualization container in pixels (default: auto-detected)

None
title Optional[str]

Title for the browser tab/page (default: "sPyTial Visualization")

None
perf_path Optional[Union[str, Path]]

Optional path to save performance metrics JSON file. If None, metrics are not saved (unless perf_iterations is set).

None
perf_iterations Optional[int]

Optional number of times to render for performance benchmarking. If provided, the visualization will be rendered N times and metrics will be aggregated. Works with method="browser", "file", or "headless".

None
headless bool

Run in headless Chrome for testing/benchmarking (requires selenium and chromedriver). If True, method is automatically set to "headless".

False
timeout Optional[float]

Timeout in seconds for headless mode. If None, automatically calculated based on perf_iterations (default: max(120, perf_iterations * 5)). For large/complex visualizations, set this higher (e.g., timeout=600 for 10 minutes).

None
as_type Optional[Any]

Optional annotated type to treat the object as. Annotations from this type will be applied in addition to any introspected from the object itself. Use AnnotatedType() or typing.Annotated to define types with spytial annotations.

None

Examples:

Define an annotated type

Graph = AnnotatedType(Dict[int, List[int]], InferredEdge(...), Orientation(...))

Display a dict as a Graph

g = {0: [1], 1: [2]} diagram(g, as_type=Graph)

Returns:

Name Type Description
str Optional[Union[str, Dict[str, Any]]]

Path to the generated HTML file (if method="file", "browser", or "headless")

dict Optional[Union[str, Dict[str, Any]]]

Performance metrics (if method="headless" and perf_iterations > 0)

sequence(identity=None, sequence_policy='stability', method=None, auto_open=True, width=None, height=None, title=None, as_type=None, label_strategy='persist')

Create a :class:SequenceRecorder for capturing algorithm steps or temporal snapshots.

The recorder uses a shared builder so atom IDs are stable across every :meth:~SequenceRecorder.record call within the same session.

Parameters:

Name Type Description Default
identity Optional[Callable[[Any], Optional[str]]]

Optional callable (obj) -> str | None. When provided, any object for which it returns a non-None string will receive an atom ID of identity:<returned-string>. Use this for deepcopy / snapshot workflows where Python's id() changes each frame but each node has a stable semantic key (e.g. a node's .id field).

None
sequence_policy str

Continuity policy for the frontend renderer. One of "stability" (default), "ignore_history", "change_emphasis", or "random_positioning".

'stability'
method Optional[str]

Display method — "inline" (Jupyter), "browser", or "file". Defaults to auto-detection.

None
auto_open bool

Whether to open the browser automatically ("browser" method).

True
width Optional[int]

Width of the visualization in pixels.

None
height Optional[int]

Height of the visualization in pixels.

None
title Optional[str]

Browser tab / page title.

None
as_type Optional[Any]

Optional annotated type applied to every recorded object.

None
label_strategy str

How atom labels are kept consistent across frames. "persist" (default) locks each atom's label at the first frame it appears. "back_construct" defers label resolution to :meth:~SequenceRecorder.diagram, then picks the most informative label for each atom (preferring real names over <type><digits> placeholders) and applies it uniformly across the sequence — this recovers the real name for atoms snapshotted before their caller binding completed (the _snap()-during-construction pattern).

'persist'

Returns:

Name Type Description
A SequenceRecorder

class:SequenceRecorder instance, usable as a context manager.

Example::

with spytial.sequence() as seq:
    seq.record(data)
    step(data)
    seq.record(data)
seq.diagram()

Evaluator

spytial.evaluator

sPyTial Evaluator Module

This module provides functions to evaluate expressions using the sPyTial evaluator.

evaluate(obj, method=None, auto_open=True, width=None, height=None, as_type=None)

Evaluate a Python object using the sPyTial evaluator.

Parameters:

Name Type Description Default
obj Any

Any Python object to evaluate.

required
method Optional[str]

Display method - "inline" (Jupyter), "browser" (new tab), or "file" (save to file). If None (default), automatically selects "inline" in notebooks or "browser" otherwise.

None
auto_open bool

Whether to automatically open the browser (for "browser" method).

True
width Optional[int]

Width of the evaluation container in pixels (default: auto-detected).

None
height Optional[int]

Height of the evaluation container in pixels (default: auto-detected).

None
as_type Optional[Any]

Optional annotated type to treat the object as. Annotations from this type will be applied in addition to any introspected from the object itself.

None

Returns:

Name Type Description
str Optional[str]

Path to the generated HTML file (if method="file" or "browser").

Structured input

spytial.structured_input

sPyTial structured input — visual editing of any value.

Two ways to edit a structured value and get it back as a Python object:

  • :func:edit — the interactive verb. Serves the editor from an ephemeral 127.0.0.1 server (see :mod:spytial._edit_server), blocks until the user clicks Done, then reconstructs the object with :func:reify and returns it. Needs a local kernel the browser can reach (local script / local Jupyter); hosted notebooks and pyodide fall back to :func:edit_html.

  • :func:edit_html — renders standalone HTML via input_template.html with an Export button (copy-paste constructor code). No server; works in pyodide and other lightweight / hosted environments, and is the fallback :func:edit uses where the local server isn't reachable.

EditCancelled

Bases: Exception

Raised by :func:edit when the editor is cancelled and on_cancel="raise".

edit(instance, *, on_cancel='seed', timeout=None, height=560)

Open a structured editor for any value and return the reified result on Done.

The interactive counterpart to :func:spytial.diagram. Serves the editor as a page from an ephemeral 127.0.0.1 server (inline in a local notebook, or a browser tab from a script), blocks until you click Done or Cancel, then reconstructs a fresh Python object with :func:reify and returns it. instance itself is never mutated.

Accepts any value (dataclasses, dicts, lists, arbitrary objects), mirroring :func:spytial.diagram. No Jupyter comm and no extra dependencies — only the standard library and a browser. The supported transport is a local script or a local Jupyter kernel; in pyodide and hosted notebooks (Colab, JupyterHub, Binder) the browser can't reach the local server, so edit() prints a note, shows :func:edit_html (standalone, Export button) instead, and returns None.

The editor can never hang the kernel: if the page never connects, stops responding, or you close it, edit() unblocks and returns per on_cancel.

Parameters:

Name Type Description Default
instance Any

Any value to seed the editor with.

required
on_cancel str

What to return if the editor is cancelled / closed / disconnected — "seed" (return the original instance unchanged, default), "none" (return None), or "raise" (raise :class:EditCancelled).

'seed'
timeout Optional[float]

Optional overall seconds to wait; None (default) waits as long as the page stays alive (a closed/unreachable page still unblocks promptly via liveness detection).

None
height int

Pixel height of the inline iframe in a notebook.

560

Returns:

Type Description
Any

The reified Python object on Done; otherwise per on_cancel.

Example::

result = spytial.edit(TreeNode(value=1))   # edit visually, click Done

edit_html(instance, *, method=None, auto_open=True, height=500)

Render a visual structured-value editor as standalone HTML.

This is the pyodide-compatible path. It generates a full HTML page with the <structured-input-graph> editor and displays it via an inline iframe (in notebooks) or a browser tab. Use the built-in Export button to copy the resulting Python constructor code.

Accepts any value (dataclasses, dicts, lists, arbitrary objects). To get the edited object back in Python directly (local script / local Jupyter), use :func:edit instead.

Parameters:

Name Type Description Default
instance Any

Any value to start editing from.

required
method Optional[str]

"inline" (default in notebooks), "browser", or None (auto-detect).

None
auto_open bool

Open the browser tab automatically (only for method="browser").

True
height int

Pixel height of the inline iframe.

500

Returns:

Type Description
Optional[str]

None when displayed inline, or the path to the temp HTML file

Optional[str]

when displayed in a browser.

Example::

spytial.edit_html(TreeNode(value=1))   # or {"a": 1}, [1, 2], …

Suggestion

spytial.suggest

spytial.suggest — scaffold a SpyTial spec from class analysis.

A lightweight, deterministic starting point you then edit::

from spytial.suggest import suggest

draft = suggest(TreeNode)                 # static analysis only
print(draft.to_source())                  # paste-able @spytial.* stack
draft.apply()                             # or decorate the class live
draft                                     # rich panel in Jupyter

Pass instance= when a plain class can't be read statically (e.g. children default to None), and registry= to run an isolated set of heuristics. Extend the rules with the :func:heuristic decorator.

This subpackage is intentionally not imported by spytial itself: the ordinary static path adds no dependencies and stays dormant until you reach for it. Class-only ask= calls lazily use Hypothesis to derive a concrete witness; pass examples= or strategy= to control that witness set.

AskError

Bases: RuntimeError

ask= could not be honored — and the user explicitly asked, so we say so.

Raised (never swallowed) for: no/unresolvable provider, no buildable example datum, no headless evaluator, a failed provider call, a model "cannot express that" reply, or a full round of candidates that all failed validation. The message carries the reasons — the model's own, or the per-candidate diagnostics from evaluation.

ClassInfo dataclass

A class and its discovered fields; passed to class-scope heuristics.

self_ref_fields property

Names of fields that point back to a node-like type.

FieldInfo dataclass

Everything the analyzer learned about a single field of a class.

SpecDraft

The result of :func:spytial.suggest.suggest.

Holds the proposed directives plus the conflict "losers" (alternatives) and any informational notes, and knows how to render itself four ways.

to_source(enabled_only=True, with_comments=True)

A paste-able stack of @spytial.* decorators.

to_registry(enabled_only=True)

The {"constraints": [...], "directives": [...]} registry dict.

apply(enabled_only=True)

Decorate the class live with the enabled suggestions; returns the class.

Suggestion dataclass

One proposed directive, with the reasoning that produced it.

dedup_key()

Identity for de-duplication: same directive + same kwargs.

ClaudeCode

Provider backed by the Claude Code CLI (claude -p) — your subscription.

Runs on Claude Code's auth (OAuth/subscription), so it needs no separate API key. claude has no native JSON-schema flag, so the schema is injected into the prompt and the reply is parsed with :func:extract_json.

Individual, local use only: Claude Code's subscription token isn't licensed for multi-user or hosted services — use an API key (an :class:LlmModel) for those.

Codex

Provider backed by the OpenAI Codex CLI (codex exec) — your ChatGPT plan.

Uses --output-schema for native JSON-schema-enforced output, so the reply conforms to schema by construction. Runs on your ChatGPT (Plus/Pro/…) sign-in.

Individual, local use only — as with :class:ClaudeCode.

EnrichError

Bases: RuntimeError

A provider couldn't be built or a provider call failed.

suggest catches this (and, at call time, any exception a provider raises) and records a note instead of propagating — enrichment is best-effort.

EnrichProvider

Bases: Protocol

The provider slot: a callable (prompt, *, schema) -> dict.

Implement it as a function or as a class with __call__. The return value is a JSON object (mapping) conforming to schema — the model's structured reply.

LlmModel

Provider backed by a named model in Simon Willison's llm library.

enrich="llama3.2" / enrich="gpt-4o" resolve here. The id is explicit — no llm default model is ever consulted, so the same call enriches the same way everywhere. Raises :class:EnrichError if llm isn't installed or the id is unknown, so a typo fails loudly rather than silently doing nothing.

HeuristicRegistry

An ordered collection of heuristics.

Use the default registry for global rules, or construct one for an isolated rule set passed to :func:suggest.

heuristic(fn=None, *, scope='field', priority=0)

Decorator form of :meth:register.

run(class_info)

Invoke every heuristic; return (priority, suggestion) pairs.

copy()

A new registry seeded with this one's heuristics (e.g. the built-ins).

build_class_info(cls, instance=None)

Return a :class:ClassInfo for cls, optionally sampling instance.

heuristic(fn=None, *, scope='field', priority=0)

Register a heuristic into the default registry.

Works bare (@heuristic) or parameterized (@heuristic(scope='class')).

suggest(target, *, instance=None, examples=None, strategy=None, registry=None, enrich=None, ask=None)

Analyze a class and return a :class:SpecDraft of proposed directives.

Parameters:

Name Type Description Default
target Any

the class to analyze, or an instance (its class is used and the instance is sampled).

required
instance Any

an optional example object to sample for types static reading can't recover. Also the default example for selector validation (see examples).

None
examples Optional[List[Any]]

a list of example instances to validate model-authored selectors against (the enrich selector tier). A selector is admitted only if it resolves on every example, so several instances narrow the gap toward "correct on all data". Defaults to [instance] (or the instance form of target); pass more for the instance-set form. Ignored unless enrich.

None
strategy Any

an optional Hypothesis strategy that supplies one generated witness in addition to any fixed examples. Pass "auto" to derive it with hypothesis.strategies.from_type(target). If ask= is used with a class and no instance/examples/strategy, "auto" is implied, so suggest(TreeNode, ask=..., enrich=...) works directly for a type Hypothesis can construct. Generation is bounded and provides a concrete vocabulary for selector validation; it is not a proof over the whole strategy. Requires the optional [suggest-search] extra. A failed strategy degrades to a static-draft note unless ask= makes the witness mandatory, in which case it raises :class:AskError.

None
registry Optional[HeuristicRegistry]

an alternate :class:HeuristicRegistry; defaults to the global one populated with the built-in rules.

None
enrich Any

turn on the optional LLM enrichment layer by saying what to enrich with — there is no ambient default. Either a model-id string resolved through the llm library (e.g. enrich="llama3.2", "gpt-4o"; the [suggest-llm] extra), or any callable provider with the signature (prompt, *, schema) -> dict — a function, or a built-in like :class:~spytial.suggest.ClaudeCode / :class:~spytial.suggest.Codex to enrich from a subscription instead of a metered key. None (default) or False leaves the draft static. Enrichment suggests the spatial shape of the structure (orientation directions per field, cyclic for ring-like links, group for collections). In this mode the model is the primary shape author — even for fields the built-in rules already name: its choice wins and is enabled, and the rule it beats is demoted to a backup alternative (kept, off, one toggle away). The deterministic shape returns as the active default only if the whole provider call fails. The model only chooses the shape; spytial supplies the render-verified selector. Enriched rows are tagged source="llm". When examples are available it additionally authors selectors for relational cases the shape tier can't express, validating each by evaluating it over every example. A spec that can't be resolved (llm missing, unknown id) degrades to the static draft with a note — never raises.

None
ask Optional[str]

a natural-language layout request to translate into directive(s) — e.g. ask="all binary tree children should be below their parents". Uses the same provider as enrich (which is therefore required), and admits a translation only after it passes the full gauntlet: known directive kind with in-vocabulary kwargs, selector evaluated at the right arity on every example, and the same authoring-time validation the @spytial.* decorators run. Admitted rows are enabled by default and tagged source="llm"; a translation that matches an existing row enables that row instead of duplicating it. The ask is authoritative on the ground it covers: an admitted orientation/cyclic/align row demotes every enabled geometric row whose selector overlaps its own (checked by evaluating the intersection over the examples) to draft.alternatives. Unlike enrichment, ask fails loudly: a missing provider, no buildable fixed or generated witness, no headless evaluator, or a translation that can't be validated raises :class:AskError with the reasons.

None

Annotations

spytial.annotations

LineStyle dataclass

Bases: _StyleBlock

Styling for a drawn edge line (edgeStyle, inferredEdge, GroupEdge connector).

All fields optional: color (CSS color), pattern ('solid'|'dashed'|'dotted'), weight (number > 0), highlight (CSS color).

TextStyle dataclass

Bases: _StyleBlock

Styling for a label (edge labels, atom labels, group labels).

All fields optional: size ('small'|'normal'|'large'), color (CSS color).

BorderStyle dataclass

Bases: _StyleBlock

Styling for an atom's border: color (CSS color), width (number > 0).

FillStyle dataclass

Bases: _StyleBlock

Styling for an atom's interior fill: color (CSS color).

IconStyle dataclass

Bases: _StyleBlock

An icon drawn on an atom (spytial-core 4.3).

All fields optional, including path, so a rule on a supertype can supply the icon and a rule on a subtype tune only its opacity. An IconStyle with no path draws nothing.

  • path -- a bundled icon name ('person'), an icon-pack reference ('bi:person-fill'), a URL, or a path.
  • placement -- 'full' (the icon occupies the box) or 'badge' (a small marker in the corner, secondary to the label). Defaults to 'full'.
  • opacity -- alpha in [0, 1]. Fade a 'full' icon to use it as a watermark behind the label.

GroupEdge dataclass

Bases: _StyleBlock

Rich form of a selector-group's addEdge: direction plus connector styling.

points ('none'|'togroup'|'fromgroup') says which way the connector between the group's key and the group points — the same key the YAML block uses; lineStyle / textStyle style the connector edge and its label. Serializes to addEdge: {points: ..., lineStyle: ..., textStyle: ...}.

SpytialAnnotation

Base class for spytial type annotations.

Subclasses represent specific constraints or directives that can be attached to type aliases using typing.Annotated.

__new__(*args, **kwargs)

Keep the arguments as passed, before any subclass touches them.

A subclass init coerces style blocks to plain dicts and rewrites a deprecated spelling into its replacement before calling up, so by the time self.kwargs exists it describes what the rule became. The source block has to say what was written: Group(addEdge=GroupEdge( points='togroup')), not Group(addEdge={'points': 'togroup'}), and AtomColor(value='red'), not AtomColor(borderStyle={'color': 'red'}). An Annotated[...] rule is a call and not a decorator, so it never has AST text to read back and this is the only record of it.

Every subclass takes keyword arguments only, so kwargs is the whole call.

to_entry()

Convert to the internal registry format.

A scalar form serializes as a bare value rather than a mapping (- flag: hideDisconnected); SCALAR_ITEMS says which keyword carries it, so this matches the decorator path without either restating which forms are scalar.

Orientation

Bases: SpytialAnnotation

Orientation constraint for spatial layout.

directions are the placement words -- above/below/left/right and the adjacency variants directlyAbove/directlyBelow/directlyLeft/directlyRight. (horizontal/vertical are Align's vocabulary, not this one; core matches no case for them here and the constraint would be dropped.)

Usage

from typing import Annotated IntList = Annotated[list[int], Orientation(selector='items', directions=['left'])]

Cyclic

Bases: SpytialAnnotation

Cyclic constraint for circular layouts.

Usage

NodeRing = Annotated[list[Node], Cyclic(selector='items', direction='clockwise')]

Align

Bases: SpytialAnnotation

Alignment constraint.

direction is the shared axis: 'horizontal' or 'vertical'. Core rejects anything else as internally inconsistent.

Usage

AlignedList = Annotated[list[int], Align(selector='items', direction='horizontal')]

Group

Bases: SpytialAnnotation

Grouping constraint.

Usage

Grouped = Annotated[MyType, Group(selector='items', name='mygroup')]

Set showLabel=False to hide the caption and its background pill while keeping the group boundary. Keyed captions use the key's readable label; IDs are added only to distinguish duplicate captions. Unary groups show only name.

For the by-field form spytial-core 5.0 removed (field/groupOn/addToGroup), see the group decorator's docstring, which names the selector that replaces it.

addEdge controls the edge drawn between the group's key and the group itself. For a binary selector with tuples (a, b), (a, c), (a, d) the group is keyed by a and contains {b, c, d}:

- ``'none'``      -> draw nothing (default)
- ``'togroup'``   -> edge from the key into the group (a -> group)
- ``'fromgroup'`` -> edge from the group back to the key (group -> a)

Legacy addEdge=True is still accepted and maps to 'togroup'.

Grouped = Annotated[MyType, Group(selector='...', name='g', addEdge='togroup')]

To also style the connector, pass a GroupEdge (spytial-core 3.0), and style the group's own label with a top-level textStyle:

Grouped = Annotated[MyType, Group(selector='...', name='g',
    addEdge=GroupEdge(points='togroup', lineStyle=LineStyle(pattern='dashed')),
    textStyle=TextStyle(color='navy'))]

AtomStyle

Bases: SpytialAnnotation

Atom style directive (spytial-core 3.0).

Styles an atom's border, interior fill, icon, and label independently.

showLabel is independent of the icon: iconStyle.placement controls the icon's geometry, showLabel controls whether the label is drawn at all. That split is what makes a faded watermark, or an icon with no label, expressible (spytial-core 4.3).

Usage

Styled = Annotated[list[int], AtomStyle(selector='self', borderStyle=BorderStyle(color='blue'))] Filled = Annotated[Tree, AtomStyle(selector='Node', fillStyle=FillStyle(color='#eef6ff'), textStyle=TextStyle(size='large'))] Marked = Annotated[Tree, AtomStyle(selector='Dir', iconStyle=IconStyle(path='bi:folder-fill', placement='badge'))]

AtomColor

Bases: AtomStyle

Deprecated legacy atom color directive; use AtomStyle instead.

Rewrites to an atomStyle entry with value as the border color (the legacy directive colored the border, not the fill).

Size

Bases: SpytialAnnotation

Size constraint.

Size fixes a node's geometry, which is what the layout solves over rather than presentation layered on a solved layout, so it belongs under constraints (spytial-core 4.3; the directives section still accepts it behind a deprecation warning).

selector is optional -- omit it to resize every node.

Usage

SizedList = Annotated[list[int], Size(selector='items', height=50, width=50)] AllSquare = Annotated[list[int], Size(height=50, width=50)]

Icon

Bases: SpytialAnnotation

Icon directive.

Usage

IconList = Annotated[list[int], Icon(selector='items', path='icon.svg', showLabels=True)]

EdgeStyle

Bases: SpytialAnnotation

Edge style directive (spytial-core 3.0).

Styles the edges of a field/relation: the drawn line via lineStyle, the edge's label via textStyle, plus the showLabel/hidden flags.

Usage

StyledEdges = Annotated[Tree, EdgeStyle(field='children', lineStyle=LineStyle(color='red', pattern='dashed'))] LabeledEdges = Annotated[Tree, EdgeStyle(field='Uses', lineStyle=LineStyle(color='#d10000', weight=3), textStyle=TextStyle(size='small'), showLabel=True)] HiddenEdges = Annotated[Tree, EdgeStyle(field='internal', hidden=True)]

EdgeColor

Bases: EdgeStyle

Deprecated legacy edge color directive; use EdgeStyle instead.

Rewrites to an edgeStyle entry: value -> lineStyle.color, style -> lineStyle.pattern, weight -> lineStyle.weight.

HideField

Bases: SpytialAnnotation

Hide field directive.

Usage

CleanView = Annotated[MyClass, HideField(field='_private')] Filtered = Annotated[MyClass, HideField(field='debug', filter='debug & Production')]

HideAtom

Bases: SpytialAnnotation

Hide atom constraint.

Hiding an atom changes what the layout has to place, and can make a spec unsatisfiable against the other constraints, so it belongs under constraints (spytial-core 4.3; the directives section still accepts it behind a deprecation warning).

Usage

Filtered = Annotated[list[int], HideAtom(selector='hidden')]

Projection

Bases: SpytialAnnotation

Deprecated projection directive; it has no effect.

spytial-core's layout-spec parser does not read projection — projecting over a sig is a pre-layout transform on the data instance, driven by the viewer's projection controls, rather than something a layout spec declares. Authoring one emits YAML that core discards, so remove it.

Usage

Projected = Annotated[MyType, Projection(sig='MySig')] # no-op

Attribute

Bases: SpytialAnnotation

Attribute directive.

Usage

WithAttr = Annotated[MyType, Attribute(field='value')] Filtered = Annotated[MyType, Attribute(field='status', filter='status & Active')]

InferredEdge

Bases: SpytialAnnotation

Inferred edge directive.

Styling uses the shared lineStyle/textStyle blocks (spytial-core 3.0); the inline color/style/weight keys are deprecated and rewritten into lineStyle.

draw (spytial-core 3.2) is '<end> -> <end>', each end '_' (the atom itself) or the name of a group constraint, in which case that end attaches to that group's hull -- giving group-to-group and node-to-group edges. A keyed group (binary selector) is picked by the end's atom; a unary group is attached to directly. '_ -> _' means the same as omitting it.

Usage

WithEdges = Annotated[Graph, InferredEdge(name='connection', selector='nodes')] StyledEdges = Annotated[Graph, InferredEdge(name='ancestor', selector='^parent', lineStyle=LineStyle(color='gray', pattern='dotted'))] HullEdges = Annotated[Graph, InferredEdge(name='reports to', selector='^parent', draw='regions -> regions')]

Flag

Bases: SpytialAnnotation

Flag directive.

name is one of the two whole-diagram switches core acts on: 'hideDisconnected' (drop atoms with no edges) or 'hideDisconnectedBuiltIns' (drop only disconnected built-in atoms).

Usage

Flagged = Annotated[MyType, Flag(name='hideDisconnected')]

Tag

Bases: SpytialAnnotation

Tag directive for adding computed attributes to nodes.

Unlike 'attribute', this does NOT remove edges - it adds computed attribute values to nodes based on selector evaluation.

Usage

Simple binary tag - displays as "age: 25"

Tagged = Annotated[MyType, Tag(toTag='Person', name='age', value='age')]

Ternary selector - displays as "score[Math]: 95"

Graded = Annotated[MyType, Tag(toTag='Student', name='score', value='grades')]

Four-column result (student, course, exam, grade) displays as

"score[Math,Midterm]: 95".

ExamGraded = Annotated[MyType, Tag(toTag='Student', name='score', value='examGrades')]

name is literal. The value selector must preserve the tagged atom as its first column; its last column is displayed as the value. A selector such as Person.age returns ages without their owning Person atoms and therefore cannot attach those values as tags. Use age instead.

extract_spytial_annotations(type_hint)

Extract spytial annotations from a typing.Annotated type hint.

:param type_hint: A type hint, possibly Annotated with spytial markers. :return: A dict with 'constraints' and 'directives' lists, or None if no annotations.

get_base_type(type_hint)

Get the base type from an Annotated type hint.

:param type_hint: A type hint, possibly Annotated. :return: The base type (unwrapped from Annotated if applicable).

annotate_type_alias(type_alias, annotation_type, **kwargs)

Attach a spytial annotation to a type alias.

This function allows you to annotate type aliases (e.g., list[int], MyTypeAlias) with spatial constraints and directives. When objects are visualized, if their type annotation matches a registered type alias, these annotations are applied.

Usage

Define a type alias

IntList = list[int]

Annotate it

annotate_type_alias(IntList, 'orientation', selector='items', directions=['left']) annotate_type_alias(IntList, 'atomColor', selector='self', value='blue')

Or use the convenience functions:

annotate_type_alias_orientation(IntList, selector='items', directions=['left'])

:param type_alias: The type alias to annotate (e.g., list[int], MyClass, etc.) :param annotation_type: The type of annotation ('orientation', 'group', etc.) :param kwargs: The annotation parameters. :return: The type alias (for chaining).

get_type_alias_annotations(type_alias)

Retrieve annotations registered for a type alias.

:param type_alias: The type alias to look up. :return: A dictionary with 'constraints' and 'directives' lists, or None if not found.

clear_type_alias_annotations(type_alias=None)

Clear annotations for a specific type alias or all type aliases.

Note: This only clears the legacy registry-based annotations. Annotations using typing.Annotated are immutable and don't need clearing.

:param type_alias: If provided, clear only this type alias's annotations. If None, clear all type alias annotations.

list_type_alias_annotations()

List all registered type alias annotations (legacy registry).

:return: A dictionary mapping type alias keys to their annotation registries.

reset_object_ids()

Reset the global object-level state. Useful for testing or when you need deterministic object ID generation across multiple runs.

Clears both global registries — object IDs and object annotations — for the built-in values that can't store that state on themselves.

Warning: This will clear all existing object ID mappings and any annotations recorded for un-attributable objects, so existing selectors that depend on previous object IDs may no longer work.

validate_fields(type_, kwargs, valid_fields)

Validate that the required fields for a given type are present in kwargs. For constraints that support multiple parameter sets (like group), try each set. :param type_: The type of constraint or directive. :param kwargs: The provided fields for the decorator. :param valid_fields: The list of required fields for the type, or a list of alternative field sets. :raises ValueError: If no valid field set matches the provided kwargs.

annotate_orientation(obj, **kwargs)

Apply orientation annotation to a specific object.

annotate_cyclic(obj, **kwargs)

Apply cyclic annotation to a specific object.

annotate_align(obj, **kwargs)

Apply align annotation to a specific object.

annotate_group(obj, **kwargs)

Apply group annotation to a specific object.

annotate_atomColor(obj, **kwargs)

Apply atomColor annotation to a specific object (deprecated; use annotate_atomStyle).

annotate_atomStyle(obj, **kwargs)

Apply atomStyle annotation to a specific object.

annotate_size(obj, **kwargs)

Apply size annotation to a specific object.

annotate_icon(obj, **kwargs)

Apply icon annotation to a specific object.

annotate_edgeColor(obj, **kwargs)

Apply edgeColor annotation to a specific object (deprecated; use annotate_edgeStyle).

annotate_edgeStyle(obj, **kwargs)

Apply edgeStyle annotation to a specific object.

annotate_projection(obj, **kwargs)

Apply projection annotation to a specific object.

annotate_attribute(obj, **kwargs)

Apply attribute annotation to a specific object.

annotate_hideField(obj, **kwargs)

Apply hideField annotation to a specific object.

annotate_hideAtom(obj, **kwargs)

Apply hideAtom annotation to a specific object.

annotate_inferredEdge(obj, **kwargs)

Apply inferredEdge annotation to a specific object.

annotate_tag(obj, **kwargs)

Apply tag annotation to a specific object.

annotate_flag(obj, **kwargs)

Apply flag annotation to a specific object.

annotate(obj, annotation_type, **kwargs)

Apply any annotation type to a specific object. :param obj: The object to annotate. :param annotation_type: The type of annotation (e.g., 'orientation', 'cyclic'). :param kwargs: The annotation parameters. :return: The annotated object (for chaining).

apply_if(condition, *decorators)

Conditional decorator macro for sPyTial. Usage: @apply_if(CONDITION, orientation(...), hideField(...), attribute(...), ... ) class MyClass: ... If CONDITION is True, applies all decorators in order to the class. If CONDITION is False, returns the class unchanged.

collect_decorators(obj, type_hint=None)

Collect all decorators applied to the class of the given object, as well as any annotations applied directly to the object instance, and any annotations registered for matching type aliases. Respects inheritance control flags (dont_inherit_constraints, dont_inherit_directives). :param obj: The object whose class decorators and object annotations should be collected. :param type_hint: Optional type hint to look up type alias annotations. :return: A combined dictionary of constraints and directives (deduplicated).

serialize_to_yaml_string(decorators)

Serialize the collected constraints and directives to a YAML string. :param decorators: The collected decorators (constraints and directives). :return: YAML string representation of the decorators.

dont_inherit_constraints(cls)

Mark a class to not inherit constraints from parent classes.

This is useful when you want to override a parent class's constraints. Constraints applied directly to this class are still included.

Example

@orientation(selector='children', directions=['below']) class Parent: pass

@dont_inherit_constraints class Child(Parent): # Will NOT have Parent's orientation constraint pass

:param cls: The class to mark as not inheriting constraints. :return: The class (for chaining).

dont_inherit_directives(cls)

Mark a class to not inherit directives from parent classes.

This is useful when you want to override a parent class's directives. Directives applied directly to this class are still included.

Example

@atomColor(selector='root', value='red') class Parent: pass

@dont_inherit_directives class Child(Parent): # Will NOT have Parent's atomColor directive pass

:param cls: The class to mark as not inheriting directives. :return: The class (for chaining).

dont_inherit_annotations(cls)

Mark a class to not inherit any annotations from parent classes.

Shorthand for applying both dont_inherit_constraints and dont_inherit_directives.

Example

@orientation(selector='children', directions=['below']) @atomColor(selector='root', value='red') class Parent: pass

@dont_inherit_annotations class Child(Parent): # Will NOT have any annotations from Parent pass

:param cls: The class to mark as not inheriting annotations. :return: The class (for chaining).

Provider system

spytial.provider_system

Spytial-Core Data Instance Relationalizer Architecture

This module provides a pluggable system for converting Python objects into CnD-compatible atom/relation representations using relationalizers.

RelationalizerRegistry

Registry for relationalizers.

register(relationalizer_cls, priority=0) classmethod

Register a relationalizer class with given priority.

find_relationalizer(obj) classmethod

Find the first relationalizer that can handle the given object.

list_relationalizers() classmethod

List all registered relationalizers with their priorities.

Returns:

Type Description
List[Tuple[int, str]]

List of (priority, relationalizer_class_name) tuples, sorted by priority (highest first)

clear() classmethod

Clear all registered relationalizers (for testing).

CnDDataInstanceBuilder

Main builder that composes providers to build data instances.

build_instance(obj, as_type=None)

Build a complete data instance from an object.

Parameters:

Name Type Description Default
obj Any

The Python object to build an instance from.

required
as_type Optional[Any]

Optional annotated type to treat the object as. Annotations from this type will be applied in addition to introspected ones.

None

apply_label_persistence(instance)

Lock atom labels at first observation across builds.

Mutates instance in place so that any atom whose ID was already labelled in a prior build keeps that earlier label, and any new atom's label is recorded for future builds. Both the top-level atoms list and each types[*].atoms sub-list are rewritten so the data instance stays internally consistent.

Used by :class:SequenceRecorder when its label strategy is "persist" — the same atom ID across frames always renders with one label.

get_collected_decorators()

Get all decorators collected during the build process, deduplicated.

During _walk(), the same class-level decorators are collected once per object instance of that class. This deduplicates them so the final YAML spec contains each unique rule only once.

__call__(obj)

Allow the builder to be called as a function for recursive walking.

None means the walk refused the object — emit no relation for it.

build_types(atoms)

Build the types field for the data instance.

Parameters:

Name Type Description Default
atoms List[Dict]

List of atoms, each containing a "type" field with the most specific type.

required

Returns:

Type Description
List[Dict]

List of type definitions, each matching the desired format.

reify(data_instance, root_id=None)

Reconstruct Python objects from a Spytial-Core data instance.

This method reverses the build_instance operation, converting atoms and relations back into Python objects.

Parameters:

Name Type Description Default
data_instance Dict

Dictionary containing 'atoms', 'relations', and optionally 'rootId' as returned by build_instance().

required
root_id Optional[str]

Optional atom id to use as the reconstruction root. Overrides any 'rootId' present in data_instance. When neither is provided the root is inferred from the relation topology.

None

Returns:

Type Description
Any

The reconstructed Python object corresponding to the root atom

Raises:

Type Description
ValueError

If the data instance is malformed or cannot be reconstructed

replit(data_instance, root_id=None)

Reproduce the host REPL's output for a data instance.

Equivalent to repr(self.reify(data_instance, root_id)). Agreement with the original value's repr depends on reconstruction support and that repr's dependence on identity or iteration order. Proxies render as TypeName(field=value); a cyclic proxy can raise RecursionError.

can_reify(data_instance)

Check if a data instance can be reified.

Parameters:

Name Type Description Default
data_instance Dict

Dictionary to check

required

Returns:

Type Description
bool

True if the data instance appears to be valid for reification

register_reifier(type_name, reifier_func)

Register a custom reifier function for a specific type.

This provides an extensibility mechanism for complex reconstruction scenarios.

Parameters:

Name Type Description Default
type_name str

The atom type name to handle (e.g., "MyCustomClass")

required
reifier_func Callable[..., Any]

Function with signature (atom, relations, reify_atom) -> object where: - atom: Dict containing atom information (id, type, label, etc.) - relations: Dict mapping relation names to lists of target atom IDs - reify_atom: Function to recursively reify other atoms by ID

required
Example

def custom_reifier(atom, relations, reify_atom): # Custom reconstruction logic obj = MyCustomClass() for rel_name, target_ids in relations.items(): setattr(obj, rel_name, [reify_atom(tid) for tid in target_ids]) return obj

builder.register_reifier("MyCustomClass", custom_reifier)

unregister_reifier(type_name)

Remove a custom reifier for a specific type.

Parameters:

Name Type Description Default
type_name str

The atom type name to remove the reifier for

required

list_reifiers()

List all registered custom reifier type names.

Returns:

Type Description
List[str]

List of type names that have custom reifiers registered

default_max_depth()

Deepest nesting :meth:CnDDataInstanceBuilder._walk will follow.

Derived from sys.getrecursionlimit() at call time, so raising the interpreter's limit raises this too: 100 on a stock CPython (limit 1000), 375 at a limit of 3000. Tracked for removal in sidprasad/spytial#131, which makes the walk iterative and drops the ceiling entirely.

relationalizer(cls=None, *, priority=0)

Decorator to register a class as a relationalizer.

Parameters:

Name Type Description Default
priority int

Higher priority relationalizers are tried first. Priorities 0-99 are reserved for built-in relationalizers.

0

reify(data_instance, root_id=None)

Reconstruct a Python object from a Spytial data instance.

Rebuild the object named by the instance's root (or root_id). Supports built-in values, resolvable references, and class instances with reconstructible state. Unsupported types can return attribute proxies or incomplete instances. Shared references and many mutable cycles survive; cycles through immutable containers can lose identity. This is not a lossless inverse for every Python value. For dataclass instances whose fields an editor may have stripped, :func:spytial.edit registers reifiers that apply declared field defaults before restoring recorded values.

replit(data_instance, root_id=None)

Return repr() of the object :func:reify would reconstruct.

Reproduces the host REPL's echo for a data instance.

Utilities

spytial.utils

Shared utilities for sPyTial.

Typed

A wrapper that preserves type hint information for values.

Use this when you want to pass annotated type information to diagram() without explicitly passing type_hint=.

Example

Graph = Annotated[Dict[int, List[int]], Orientation(...)] g = {0: [1], 1: [2]} diagram(Typed(g, Graph)) # Annotations from Graph will be applied

AnnotatedType

A callable type alias that preserves spytial annotations.

This provides a clean syntax for reusable annotated types:

# Define once
Graph = AnnotatedType(Dict[int, List[int]],
    InferredEdge(selector=EDGE_SEL, name=" "))

# Use naturally - Graph() wraps the value with type info
g = Graph({0: [1], 1: [2], 2: []})
diagram(g)  # Annotations applied automatically!

The AnnotatedType acts as both: - A type hint (for documentation/type checkers) - A constructor (wraps values with the type info)

__init__(base_type, *annotations)

Create an annotated type alias.

Parameters:

Name Type Description Default
base_type Any

The underlying type (e.g., Dict[int, List[int]])

required
*annotations Any

SpytialAnnotation instances (Orientation, InferredEdge, etc.)

()

__call__(value)

Wrap a value with this type's annotations.

Parameters:

Name Type Description Default
value Any

The value to wrap

required

Returns:

Name Type Description
Typed Typed

A wrapper that diagram() will recognize

in_vscode()

True when running under the VS Code Python/Jupyter extension.

The local kernel is reachable there, but the webview's CSP frequently blanks an inline 127.0.0.1 iframe — so :func:spytial.edit opens an external browser instead.

edit_environment()

Classify the runtime for :func:spytial.edit's local-server transport.

Returns one of:

  • "pyodide" — Python runs in the browser; no sockets, so the local server can't be created at all.
  • "remote" — a hosted kernel whose 127.0.0.1 the user's browser can't reach (Google Colab, JupyterHub, Binder).
  • "local" — a local script or a local Jupyter kernel; the server transport works (VS Code is "local"; see :func:in_vscode).

is_notebook()

Detect if we're running in a Jupyter notebook environment. Returns True if in a notebook, False otherwise.

default_method()

Return the default display method based on environment. Returns 'inline' in notebooks, 'browser' otherwise.