Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,13 @@ the wheel and publishes it to PyPI on a `v<version>` tag (version in
also reads it into `NAJA_SCHEMATIC_VERSION_STRING` for the C++ app's About
dialog, so a release is one edit plus a tag). For local work set
`NAJA_SCHEMATIC_BUNDLE` to a locally built bundle (see `_bundle.py`). Tests:
`pytest python/tests`. `python/notebooks/colab_test.ipynb` is the
`pytest python/tests`. `python/notebooks/getting_started.ipynb` is the
Open-in-Colab notebook (badge in the top `README.md`): a walkthrough of the
widget against the released PyPI package. `tests/test_notebook.py` runs it
in a real kernel (nbclient) and splices in a cell that plays the viewer
widget against the released PyPI package. `python/notebooks/tinyrocket.ipynb` (a
gate-level RISC-V core with Liberty) is its sequel, kept apart because
najaeda holds one design at a time: in one notebook, *Run all* would leave
every view showing the last design. `tests/test_notebook.py` runs both
in a real kernel (nbclient) and, in the walkthrough, splices in a cell that plays the viewer
against its `view`, so a `Schematic`/protocol change that breaks it fails
CI; its install cell skips `pip install` when `naja_schematic` is already
importable, so CI tests the fresh wheel, not PyPI's. The workflow's
Expand Down Expand Up @@ -551,7 +554,18 @@ the same `id_path`/`path` pair as diagnosis items:
observes the ids, so moving between anonymous siblings fires it). The other hosts only log it (`protocol.py`) or ignore it
(`LocalSNLProvider`).

`design_changed` is a third server push, `{"response":"design_changed",
`export_svg` is a server push too, `{"response":"export_svg"}`, from
`Schematic.export_svg()` (`ViewerSession.export_svg()`): the viewer sets
`AppState::svgExportRequested` and, at the first frame its schematic shows
something, answers with a notification,
`{"request":"svg_exported","svg":"<svg ...>"}` -- what File > Export
Schematic as SVG... saves. The session intercepts it (like
`instance_selected`), keeps it as `svg` and calls the request's one-shot
callbacks; the widget also writes it to the `path` given. A notebook reads
it in a later cell or through the callback (e.g. updating a
`display_id` output), since comm messages reach the kernel between cells.

`design_changed` is another server push, `{"response":"design_changed",
"generation":N}`: the host replaced or edited the design, so every tree
node, instance id and schematic box the viewer holds may be stale -- or,
worse, name something else, since a new design can reuse ids. The viewer
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Native macOS Build](https://github.com/keplertech/naja-schematic/actions/workflows/native-macos.yml/badge.svg?branch=main)](https://github.com/keplertech/naja-schematic/actions/workflows/native-macos.yml)
[![Native Linux Build](https://github.com/keplertech/naja-schematic/actions/workflows/native-linux.yml/badge.svg?branch=main)](https://github.com/keplertech/naja-schematic/actions/workflows/native-linux.yml)
[![Emscripten Build](https://github.com/keplertech/naja-schematic/actions/workflows/emscripten.yml/badge.svg?branch=main)](https://github.com/keplertech/naja-schematic/actions/workflows/emscripten.yml)
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/keplertech/naja-schematic/blob/main/python/notebooks/colab_test.ipynb)
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/keplertech/naja-schematic/blob/main/python/notebooks/getting_started.ipynb)


> **Note:** This is a proof of concept in beta. Expect rough edges, breaking
Expand Down
3 changes: 2 additions & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ path = [
"python/naja_schematic/static/index.html",
"python/naja_schematic/static/widget.js",
"python/naja_schematic/widget.py",
"python/notebooks/colab_test.ipynb",
"python/notebooks/getting_started.ipynb",
"python/notebooks/tinyrocket.ipynb",
"python/pyproject.toml",
"python/tests/conftest.py",
"python/tests/test_anonymous_instances.py",
Expand Down
4 changes: 2 additions & 2 deletions python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ pip install naja-schematic

## In a notebook (Jupyter, Google Colab, VSCode)

[Try it in Colab](https://colab.research.google.com/github/keplertech/naja-schematic/blob/main/python/notebooks/colab_test.ipynb):
a walkthrough of every widget feature below, runnable as is.
[Try it in Colab](https://colab.research.google.com/github/keplertech/naja-schematic/blob/main/python/notebooks/getting_started.ipynb):
a walkthrough of every widget feature below, runnable as is. Then a real design: [a RISC-V core](https://colab.research.google.com/github/keplertech/naja-schematic/blob/main/python/notebooks/tinyrocket.ipynb).

```python
from najaeda import netlist
Expand Down
2 changes: 1 addition & 1 deletion python/naja_schematic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"""
# Single version for the whole project: CMakeLists.txt reads this line into the
# C++ app too (NAJA_SCHEMATIC_VERSION_STRING), so keep it a plain literal.
__version__ = "0.1.7"
__version__ = "0.1.8"

from .protocol import diagnosis_response, handle_request
from .server import ViewerServer
Expand Down
8 changes: 8 additions & 0 deletions python/naja_schematic/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,3 +904,11 @@ def design_changed():
ignores messages for any other generation, and stamps its own requests
with it."""
return {"response": "design_changed"}


def export_svg():
"""Build an export_svg push message: the viewer answers, once its
schematic shows something, with an svg_exported notification carrying
the schematic as SVG (the whole sheet, as File > Export Schematic as
SVG... saves it). session.ViewerSession intercepts that answer."""
return {"response": "export_svg"}
40 changes: 40 additions & 0 deletions python/naja_schematic/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ def __init__(self, push=None, run=None, diagnosis=None, dispatch=None):
if diagnosis is not None else None)
self._focus_push = None
self._select_callbacks = []
self._svg_callbacks = [] # one-shot, for the next svg_exported
self.svg = None
self.selected_id_path = None
self.selected_path = None

Expand Down Expand Up @@ -207,10 +209,17 @@ def handle():
with self._state_lock:
generation = self._generation
pushes = [p for p in (self._diagnosis_push, self._focus_push) if p]
if self._svg_callbacks:
# An export asked before the view was up got lost with
# it: ask again once the view is (re)loaded.
pushes.append(protocol.export_svg())
if stamp is not None and stamp != generation:
return generation, None, pushes
if kind == "instance_selected":
return generation, self._resolve_selection(request), pushes
if kind == "svg_exported":
svg = request.get("svg")
return generation, svg if isinstance(svg, str) else "", pushes
return generation, protocol.handle_request(request), pushes

generation, result, pushes = self._run(handle)
Expand All @@ -220,6 +229,9 @@ def handle():
if kind == "instance_selected":
self._commit_selection(generation, *result)
return [] # a notification: no reply
if kind == "svg_exported":
self._commit_svg(generation, result)
return []
replies = []
for reply in result:
replies.append(self._stamp(reply, generation))
Expand Down Expand Up @@ -259,6 +271,22 @@ def _notify(self, generation, ids, path):
except Exception:
log.exception("Error in a selection callback")

def _commit_svg(self, generation, svg):
with self._state_lock:
if generation != self._generation:
return
self.svg = svg
callbacks, self._svg_callbacks = self._svg_callbacks, []
for callback in callbacks:
self._dispatch(lambda cb=callback: self._deliver_svg(cb, svg))

@staticmethod
def _deliver_svg(callback, svg):
try:
callback(svg)
except Exception:
log.exception("Error in an SVG export callback")

def on_select(self, callback):
"""Call `callback(id_path, path)` on each selection the viewer
reports for the current design, and with (None, None) when
Expand Down Expand Up @@ -298,6 +326,18 @@ def show_instance(self, target):
generation = self._generation
self._send(self._stamp(message, generation))

def export_svg(self, callback=None):
"""Ask the viewer(s) for the schematic as SVG (the whole sheet, as
File > Export Schematic as SVG... saves it). The answer comes
asynchronously, once a viewer's schematic shows something: it is
kept in `svg`, and `callback(svg)`, if given, is called once,
through `dispatch`."""
with self._state_lock:
if callback is not None:
self._svg_callbacks.append(callback)
generation = self._generation
self._send(self._stamp(protocol.export_svg(), generation))

def design_changed(self, diagnosis=None, instance=None):
"""Start the next design generation: the viewer(s) reload the design
from the root, and whatever they asked about or selected in the
Expand Down
23 changes: 23 additions & 0 deletions python/naja_schematic/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ class Schematic(anywidget.AnyWidget):
# a name path, so moving between them wouldn't change it.
selected_id_path = traitlets.List(default_value=None, allow_none=True)
selected_path = traitlets.List(default_value=None, allow_none=True)
# The schematic as SVG, from the last export_svg(); None before.
# Kernel-side only, like the selection.
svg = traitlets.Unicode(default_value=None, allow_none=True)

def __init__(self, **kwargs):
# Read before AnyWidget.__init__, which turns it into a synced trait.
Expand Down Expand Up @@ -97,6 +100,26 @@ def show_instance(self, target):
"""
self._session.show_instance(target)

def export_svg(self, path=None, callback=None):
"""Ask the view for its schematic as SVG: the whole sheet, as File >
Export Schematic as SVG... saves it. The view answers once it is
displayed and its schematic shows something -- after this cell, since
the kernel reads the view's messages between cells. The SVG then
lands in `svg`, is written to `path` if given, and is passed to
`callback(svg)`, e.g. to update a display:

out = display(Markdown("Waiting for the view..."), display_id=True)
view.export_svg("view.svg", lambda svg: out.update(SVG(svg)))
"""
def done(svg):
if path is not None:
with open(path, "w", encoding="utf-8") as f:
f.write(svg)
self.svg = svg
if callback is not None:
callback(svg)
self._session.export_svg(done)

def design_changed(self, diagnosis=None, instance=None):
"""Refresh this view after the design was replaced or edited in
later cells, instead of calling show() again. Kept diagnoses and
Expand Down
Loading
Loading