tidy up old code

This commit is contained in:
2026-04-02 00:40:09 -07:00
parent 6bfa295d25
commit 7de9bddec7
39 changed files with 219 additions and 533 deletions

View File

@@ -8,6 +8,7 @@ tono is a node-based SPM image processing and analysis tool. The main focus is o
It is heavily inspired by [Gwyddion](https://gwyddion.net/), one of my favorite scientific FOSS programs on the web.
<img src="frontend/public/default-workflow.png" width="600">
## Quick start
Install a local binary from the Releases section, or run locally:

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
from contextlib import contextmanager
from contextvars import ContextVar
import inspect
from typing import Any, Callable
Callback = Callable[[str, Any], None]
@@ -13,16 +12,6 @@ _callbacks_var: ContextVar[dict[str, Callback | None]] = ContextVar(
)
_node_id_var: ContextVar[str | None] = ContextVar("tono_execution_node_id", default=None)
_LEGACY_CALLBACK_ATTRS = {
"preview": "_broadcast_fn",
"table": "_broadcast_table_fn",
"mesh": "_broadcast_mesh_fn",
"overlay": "_broadcast_overlay_fn",
"value": "_broadcast_value_fn",
"warning": "_broadcast_warning_fn",
"file_download": "_broadcast_file_download_fn",
}
@contextmanager
def execution_callbacks(
@@ -63,42 +52,12 @@ def current_node_id() -> str | None:
return _node_id_var.get()
def _legacy_emit(kind: str, payload: Any) -> bool:
callback_attr = _LEGACY_CALLBACK_ATTRS.get(kind)
if not callback_attr:
return False
frame = inspect.currentframe()
try:
frame = frame.f_back
while frame is not None:
for owner_name in ("self", "cls"):
owner = frame.f_locals.get(owner_name)
if owner is None:
continue
candidate = owner if isinstance(owner, type) else owner.__class__
callback = getattr(candidate, callback_attr, None)
node_id = getattr(candidate, "_current_node_id", None)
if callback is not None and node_id:
callback(str(node_id), payload)
return True
frame = frame.f_back
finally:
del frame
return False
def _emit(kind: str, payload: Any) -> None:
callbacks = _callbacks_var.get()
callback = callbacks.get(kind)
node_id = current_node_id()
if callback is not None and node_id:
callback(node_id, payload)
return
_legacy_emit(kind, payload)
def emit_preview(payload: Any) -> None:

View File

@@ -16,9 +16,6 @@ from backend.data_types import (
@register_node(display_name="Annotations")
class Annotations:
_broadcast_warning_fn = None
_current_node_id: str = ""
@classmethod
def INPUT_TYPES(cls):
return {

View File

@@ -37,9 +37,6 @@ class CropResizeField:
"resizing preserves the cropped physical size."
)
_broadcast_overlay_fn = None
_current_node_id: str = ""
def process(
self,
field: DataField,

View File

@@ -39,9 +39,6 @@ class CrossSection:
"Equivalent to gwy_data_field_get_profile."
)
_broadcast_overlay_fn = None
_current_node_id: str = ""
def process(
self, field: DataField,
x1: float, y1: float, x2: float, y2: float,

View File

@@ -39,9 +39,6 @@ class Cursors:
"On fields it reports x/y/z at both markers plus dx/dy/dz."
)
_broadcast_overlay_fn = None
_current_node_id: str = ""
def process(
self, line, x1: float, y1: float, x2: float, y2: float,
coord_pair=None,

View File

@@ -30,9 +30,6 @@ class FFT1D:
"Returns the FFT spectrum of the line, and identifies peaks."
)
_broadcast_overlay_fn = None
_current_node_id: str = ""
def process(
self, profile,
) -> tuple:

View File

@@ -122,84 +122,6 @@ def _measurement_value(table: list, selection: str) -> float:
raise ValueError(f"Measurement '{row.get('quantity', selection)}' does not have a numeric value.")
# ---------------------------------------------------------------------------
# SI formatting helpers (from display.py — used by Annotations)
# ---------------------------------------------------------------------------
_SI_PREFIXES = [
(1e24, "Y"), (1e21, "Z"), (1e18, "E"), (1e15, "P"), (1e12, "T"),
(1e9, "G"), (1e6, "M"), (1e3, "k"), (1.0, ""), (1e-3, "m"),
(1e-6, "u"), (1e-9, "n"), (1e-12, "p"), (1e-15, "f"),
(1e-18, "a"), (1e-21, "z"), (1e-24, "y"),
]
_PREFIXABLE_UNITS = {"m", "s", "A", "V", "W", "Hz", "F", "C", "J", "N", "Pa", "T", "H", "S", "g", "K", "Ohm", "ohm", "\u03a9"}
def _format_numeric(value: float) -> str:
if not np.isfinite(value):
return str(value)
abs_value = abs(value)
if abs_value == 0:
return "0"
if abs_value >= 1e4 or abs_value < 1e-3:
return f"{value:.3e}"
return f"{value:.4g}"
def _format_with_unit(value: float, unit: str) -> str:
unit = (unit or "").strip()
if not unit:
return _format_numeric(value)
if unit in _PREFIXABLE_UNITS and np.isfinite(value) and value != 0:
abs_value = abs(value)
for scale, prefix in _SI_PREFIXES:
scaled = abs_value / scale
if 1 <= scaled < 1000:
signed = value / scale
return f"{_format_numeric(signed)} {prefix}{unit}"
return f"{_format_numeric(value)} {unit}"
def _nice_length(target: float) -> float:
if not np.isfinite(target) or target <= 0:
return 0.0
exponent = np.floor(np.log10(target))
base = 10.0 ** exponent
for step in (5.0, 2.0, 1.0):
candidate = step * base
if candidate <= target:
return candidate
return base
def _display_value_range(field) -> tuple[float, float, float]:
data = np.asarray(field.data, dtype=np.float64)
dmin = float(data.min())
dmax = float(data.max())
if not np.isfinite(dmin) or not np.isfinite(dmax) or dmax <= dmin:
return dmin, dmin, dmin
offset = float(field.display_offset)
scale = float(field.display_scale)
if not np.isfinite(offset):
offset = 0.0
if not np.isfinite(scale) or scale <= 0.0:
scale = 1.0
low_norm = float(np.clip(offset, 0.0, 1.0))
high_norm = float(np.clip(offset + scale, 0.0, 1.0))
if high_norm < low_norm:
low_norm, high_norm = high_norm, low_norm
mid_norm = 0.5 * (low_norm + high_norm)
span = dmax - dmin
return (
dmin + low_norm * span,
dmin + mid_norm * span,
dmin + high_norm * span,
)
def _render_annotation_text(text: str, size_px: int, color: tuple[int, int, int]):
from PIL import Image, ImageDraw, ImageFont

View File

@@ -34,9 +34,6 @@ class Histogram:
"Equivalent to gwy_data_field_dh."
)
_broadcast_overlay_fn = None
_current_node_id: str = ""
def process(
self,
field: DataField,

View File

@@ -36,9 +36,6 @@ class Image:
"Images (.png, .tiff, .jpg) and arrays (.npy, .npz) are loaded as uncalibrated fields."
)
_broadcast_warning_fn = None
_current_node_id = None
def load(self, filename: str = "", colormap: str = "viridis", colormap_map=None, path: str | None = None):
selected_path = str(path).strip() if path is not None else str(filename).strip()
if not selected_path:

View File

@@ -26,9 +26,6 @@ class ImageDemo:
DESCRIPTION = "Load a bundled demo file so you can try the app without providing your own data."
_broadcast_warning_fn = None
_current_node_id = None
def load(self, name: str = "", colormap: str = "viridis", colormap_map=None):
from backend.nodes.image import Image
loader = Image()

View File

@@ -43,9 +43,6 @@ class Markup:
"or rasterize markup directly onto an IMAGE."
)
_broadcast_overlay_fn = None
_current_node_id: str = ""
def process(
self,
input,

View File

@@ -33,9 +33,6 @@ class DrawMask:
"and invert flips the final binary output."
)
_broadcast_overlay_fn = None
_current_node_id: str = ""
def process(self, field: DataField, pen_size: int, invert: bool, mask_paths: str) -> tuple:
strokes = _parse_mask_strokes(mask_paths)
mask = _rasterize_mask(field.xres, field.yres, strokes, pen_size)

View File

@@ -28,9 +28,6 @@ class MaskInvert:
DESCRIPTION = "Invert a binary mask — swap masked and unmasked regions."
_broadcast_fn = None
_current_node_id: str = ""
def process(self, mask: np.ndarray, field: DataField | None = None) -> tuple:
out = np.where(mask > 127, np.uint8(0), np.uint8(255))

View File

@@ -41,9 +41,6 @@ class MaskMorphology:
"Equivalent to Gwyddion mask_morph."
)
_broadcast_fn = None
_current_node_id: str = ""
def process(self, mask: np.ndarray, operation: str, radius: int, shape: str,
field: DataField | None = None) -> tuple:
from scipy.ndimage import binary_closing, binary_dilation, binary_erosion, binary_opening

View File

@@ -33,9 +33,6 @@ class ThresholdMask:
"Equivalent to Gwyddion's threshold and otsu_threshold modules."
)
_broadcast_fn = None
_current_node_id: str = ""
def process(self, field: DataField, method: str, threshold: float, direction: str) -> tuple:
data = field.data

View File

@@ -36,9 +36,6 @@ class PreviewImage:
OUTPUT_NODE = True
DESCRIPTION = "Display an IMAGE or DATA_FIELD as a coloured thumbnail."
_broadcast_fn = None
_current_node_id: str = ""
def preview(
self,
colormap: str,

View File

@@ -21,9 +21,6 @@ class PrintTable:
OUTPUT_NODE = True
DESCRIPTION = "Send a measurement or record table to the browser as a WebSocket message for display."
_broadcast_table_fn = None
_current_node_id: str = ""
def print_table(self, table: list) -> tuple:
emit_table(table)
return ()

View File

@@ -28,9 +28,6 @@ class RotateField:
"Optionally expand the canvas to keep the full rotated field while preserving the field center."
)
_broadcast_warning_fn = None
_current_node_id: str = ""
def process(
self,
field: DataField,

View File

@@ -74,9 +74,6 @@ class Save:
"Save a single graph value to disk. Supports fields, images, lines, tables, scalars, and 3D meshes."
)
_broadcast_warning_fn = None
_current_node_id = None
def save(
self,
filename: str,

View File

@@ -63,9 +63,6 @@ class SaveImage:
"Click Save to write (does not auto-run)."
)
_broadcast_warning_fn = None
_current_node_id = None
def save(
self,
filename: str,
@@ -187,5 +184,3 @@ class SaveImage:
def _send_warning(self, message: str):
emit_warning(message)
return ()

View File

@@ -19,9 +19,6 @@ from backend.nodes.helpers import (
class Stats:
"""Polymorphic scalar stats node for LINE, DATA_TABLE, DATA_FIELD, or IMAGE inputs."""
_broadcast_value_fn = None
_current_node_id: str = ""
@classmethod
def INPUT_TYPES(cls):
return {

View File

@@ -41,9 +41,6 @@ class ValueIO:
DESCRIPTION = "Display a FLOAT, or a selected numeric row from a measurement table, and pass the value through unchanged."
_broadcast_value_fn = None
_current_node_id: str = ""
def display_value(self, number_input: str = "0", value=None, measurement: str = "") -> tuple:
unit = ""
if isinstance(value, RecordTable):

View File

@@ -148,9 +148,6 @@ class View3D:
"Drag to rotate, middle-drag to pan, and right-drag or scroll to zoom. z_scale exaggerates height."
)
_broadcast_mesh_fn = None
_current_node_id: str = ""
def render(
self, field: DataField,
colormap: str, z_scale: float, resolution: int, make_solid: bool = False,

View File

@@ -1,50 +0,0 @@
const EXCLUDED_CANVAS_TARGETS = '.context-menu, .react-flow__node, .react-flow__edge, .react-flow__controls, .react-flow__minimap, .surface-view-container';
const CANVAS_AREA_TARGETS = '.react-flow, .react-flow__renderer, .react-flow__viewport, .react-flow__pane, .react-flow__background, .react-flow__selectionpane';
function getTargetElement(target: EventTarget | null): Element | null {
if (!target) return null;
if (typeof (target as Element).closest === 'function') return target as Element;
const parent = (target as Node).parentElement;
if (parent && typeof parent.closest === 'function') {
return parent;
}
return null;
}
function supportsClosest(target: EventTarget | null): boolean {
return !!getTargetElement(target);
}
function matchesClosest(target: EventTarget | null, selector: string): boolean {
const element = getTargetElement(target);
return !!element && element.closest(selector) !== null;
}
export function isEditableInteractionTarget(target: EventTarget | null): boolean {
if (!supportsClosest(target)) return false;
if (matchesClosest(target, 'input, textarea, select')) return true;
return matchesClosest(target, '[contenteditable="true"]');
}
export function canStartCanvasRightDragZoomTarget(target: EventTarget | null): boolean {
if (!supportsClosest(target)) return false;
if (isEditableInteractionTarget(target)) return false;
if (matchesClosest(target, EXCLUDED_CANVAS_TARGETS)) {
return false;
}
return matchesClosest(target, CANVAS_AREA_TARGETS);
}
export function canOpenCanvasContextMenuTarget(target: EventTarget | null): boolean {
if (!supportsClosest(target)) return false;
if (isEditableInteractionTarget(target)) return false;
if (matchesClosest(target, EXCLUDED_CANVAS_TARGETS)) {
return false;
}
return matchesClosest(target, CANVAS_AREA_TARGETS);
}
export function isSecondaryCanvasContextEvent(event: MouseEvent | null): boolean {
if (!event || typeof event.button !== 'number') return false;
return event.button === 2 || (event.button === 0 && !!event.ctrlKey);
}

View File

@@ -1,49 +0,0 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
canOpenCanvasContextMenuTarget,
canStartCanvasRightDragZoomTarget,
isEditableInteractionTarget,
isSecondaryCanvasContextEvent,
} from '../src/canvasInteractionTargets.ts';
function makeTarget(activeSelectors = []) {
const selectorSet = new Set(activeSelectors);
return {
closest(selector) {
const parts = String(selector).split(',').map((part) => part.trim());
return parts.some((part) => selectorSet.has(part)) ? {} : null;
},
};
}
test('editable canvas targets stay editable', () => {
const inputTarget = makeTarget(['input']);
assert.equal(isEditableInteractionTarget(inputTarget), true);
assert.equal(canOpenCanvasContextMenuTarget(inputTarget), false);
assert.equal(canStartCanvasRightDragZoomTarget(inputTarget), false);
});
test('empty pane targets allow the custom canvas context menu', () => {
const paneTarget = makeTarget(['.react-flow__pane']);
assert.equal(canOpenCanvasContextMenuTarget(paneTarget), true);
assert.equal(canStartCanvasRightDragZoomTarget(paneTarget), true);
});
test('viewport-level targets also allow the custom canvas context menu', () => {
const viewportTarget = makeTarget(['.react-flow__viewport']);
assert.equal(canOpenCanvasContextMenuTarget(viewportTarget), true);
assert.equal(canStartCanvasRightDragZoomTarget(viewportTarget), true);
});
test('node and surface-view targets do not open the canvas context menu', () => {
assert.equal(canOpenCanvasContextMenuTarget(makeTarget(['.react-flow__node', '.react-flow__pane'])), false);
assert.equal(canOpenCanvasContextMenuTarget(makeTarget(['.surface-view-container', '.react-flow__pane'])), false);
});
test('secondary canvas context detection includes macOS ctrl-click', () => {
assert.equal(isSecondaryCanvasContextEvent({ button: 2, ctrlKey: false }), true);
assert.equal(isSecondaryCanvasContextEvent({ button: 0, ctrlKey: true }), true);
assert.equal(isSecondaryCanvasContextEvent({ button: 0, ctrlKey: false }), false);
});

View File

@@ -5,10 +5,6 @@ export default defineConfig({
plugins: [react()],
server: {
host: true,
allowedHosts: ["bronchial-lorita-gorgeously.ngrok-free.dev"],
hmr:{
clientPort: 80,
},
port: 5173,
proxy: {
'/nodes': 'http://127.0.0.1:8188',

View File

@@ -1,5 +1,6 @@
import numpy as np
from backend.data_types import DataField
from backend.execution_context import execution_callbacks, active_node
def test_crop_resize_field():
@@ -19,9 +20,7 @@ def test_crop_resize_field():
)
overlays = []
CropResizeField._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
CropResizeField._current_node_id = "test"
with execution_callbacks(overlay=lambda nid, data: overlays.append(data)), active_node("test"):
cropped, = node.process(field, x1=0.25, y1=0.25, x2=0.75, y2=1.0, target_width=0, target_height=0, interpolation="bilinear")
assert cropped.data.shape == (3, 4)
assert np.array_equal(cropped.data, data[1:4, 2:6])
@@ -56,5 +55,3 @@ def test_crop_resize_field():
raise AssertionError("Expected invalid crop bounds to raise ValueError")
except ValueError:
pass
CropResizeField._broadcast_overlay_fn = None

View File

@@ -42,11 +42,10 @@ def test_cross_section():
assert rows["dx"]["unit"] == field.si_unit_xy
assert rows["dy"]["unit"] == field.si_unit_z
from backend.execution_context import execution_callbacks, active_node
captured = []
Stats._broadcast_value_fn = lambda nid, payload: captured.append(payload)
Stats._current_node_id = "test"
with execution_callbacks(value=lambda nid, payload: captured.append(payload)), active_node("test"):
stats = Stats()
mean_value, = stats.process(profile, operation="mean", column="value")
assert mean_value > 0
assert captured[-1]["unit"] == field.si_unit_z
Stats._broadcast_value_fn = None

View File

@@ -12,10 +12,9 @@ def test_line_cursors():
line = np.linspace(0, 10, 100).astype(np.float64)
from backend.execution_context import execution_callbacks, active_node
overlays = []
Cursors._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
Cursors._current_node_id = "test"
with execution_callbacks(overlay=lambda nid, data: overlays.append(data)), active_node("test"):
table, coord_pair = node.process(line, x1=0.25, y1=0.5, x2=0.75, y2=0.5)
assert isinstance(coord_pair, tuple) and len(coord_pair) == 2
assert len(table) == 7
@@ -60,5 +59,3 @@ def test_line_cursors():
assert len(overlays) == 1
assert overlays[0]["kind"] == "cursor_points"
assert overlays[0]["image"].startswith("data:image/png;base64,")
Cursors._broadcast_overlay_fn = None

View File

@@ -109,7 +109,7 @@ def test_measurement_value_errors():
def test_format_with_unit():
from backend.nodes.helpers import _format_with_unit, _format_numeric
from backend.data_types import _format_with_unit, _format_numeric
assert _format_numeric(0.0) == "0"
assert not np.isfinite(float('inf')) or _format_numeric(float('inf')) is not None
@@ -182,7 +182,7 @@ def test_square_unit_and_apply():
def test_nice_length():
from backend.nodes.helpers import _nice_length
from backend.data_types import _nice_length
assert _nice_length(0.0) == 0.0
assert _nice_length(float('inf')) == 0.0

View File

@@ -9,10 +9,9 @@ def test_height_histogram():
data = np.linspace(0, 1, 1000).reshape(25, 40)
field = make_field(data=data)
from backend.execution_context import execution_callbacks, active_node
overlays = []
Histogram._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
Histogram._current_node_id = "test"
with execution_callbacks(overlay=lambda nid, data: overlays.append(data)), active_node("test"):
table, coord_pair = node.process(field, n_bins=10, y_scale="linear", x1=0.2, y1=0.5, x2=0.8, y2=0.5)
assert isinstance(coord_pair, tuple) and len(coord_pair) == 2
measurements = {row["quantity"]: row for row in table}
@@ -36,5 +35,3 @@ def test_height_histogram():
measurements["delta Y"]["value"],
measurements["B count"]["value"] - measurements["A count"]["value"],
)
Histogram._broadcast_overlay_fn = None

View File

@@ -126,12 +126,13 @@ def test_load_file_unsupported():
def test_load_file_warning():
from backend.nodes.image import Image as ImageNode
from backend.execution_context import execution_callbacks, active_node
node = ImageNode()
warnings = []
ImageNode._broadcast_warning_fn = lambda nid, msg: warnings.append(msg)
ImageNode._current_node_id = "test"
with tempfile.TemporaryDirectory() as tmpdir:
with tempfile.TemporaryDirectory() as tmpdir, \
execution_callbacks(warning=lambda nid, msg: warnings.append(msg)), \
active_node("test"):
arr = np.random.default_rng(10).integers(0, 256, (16, 16), dtype=np.uint8)
img = PILImage.fromarray(arr)
path = os.path.join(tmpdir, "test.png")
@@ -142,8 +143,6 @@ def test_load_file_warning():
assert len(warnings) == 1
assert "Uncalibrated" in warnings[0]
ImageNode._broadcast_warning_fn = None
def test_load_file_ibw():
from backend.nodes.image import Image

View File

@@ -8,10 +8,9 @@ def test_draw_mask():
node = DrawMask()
field = make_field(data=np.zeros((32, 32), dtype=np.float64))
from backend.execution_context import execution_callbacks, active_node
overlays = []
DrawMask._broadcast_overlay_fn = lambda nid, data: overlays.append(data)
DrawMask._current_node_id = "test"
with execution_callbacks(overlay=lambda nid, data: overlays.append(data)), active_node("test"):
mask_paths = [{"size": 5, "points": [{"x": 0.2, "y": 0.5}, {"x": 0.8, "y": 0.5}]}]
mask, = node.process(field, pen_size=2, invert=False, mask_paths=json.dumps(mask_paths))
@@ -36,5 +35,3 @@ def test_draw_mask():
cleared, = node.process(field, pen_size=12, invert=False, mask_paths="[]")
assert np.count_nonzero(cleared) == 0
DrawMask._broadcast_overlay_fn = None

View File

@@ -10,10 +10,9 @@ def test_threshold_mask():
data[:, 32:] = 1.0
field = make_field(data=data)
from backend.execution_context import execution_callbacks, active_node
previews = []
ThresholdMask._broadcast_fn = lambda nid, uri: previews.append(uri)
ThresholdMask._current_node_id = "test"
with execution_callbacks(preview=lambda nid, uri: previews.append(uri)), active_node("test"):
mask, table = node.process(field, method="absolute", threshold=0.5, direction="above")
assert mask.dtype == np.uint8
assert mask.shape == (64, 64)
@@ -33,8 +32,6 @@ def test_threshold_mask():
mask_otsu, _ = node.process(field, method="otsu", threshold=0.0, direction="above")
assert mask_otsu[:, 32:].sum() > mask_otsu[:, :32].sum()
ThresholdMask._broadcast_fn = None
def test_threshold_mask_unknown_method():
from backend.nodes.mask_threshold import ThresholdMask

View File

@@ -1,5 +1,6 @@
def test_print_table():
from backend.nodes.print_table import PrintTable
from backend.execution_context import execution_callbacks, active_node
node = PrintTable()
table_spec = PrintTable.INPUT_TYPES()["required"]["table"]
@@ -7,12 +8,8 @@ def test_print_table():
assert table_spec[1]["accepted_types"] == ["DATA_TABLE"]
captured = []
PrintTable._broadcast_table_fn = lambda node_id, rows: captured.append(rows)
PrintTable._current_node_id = "test"
with execution_callbacks(table=lambda nid, rows: captured.append(rows)), active_node("test"):
table = [{"quantity": "test", "value": 42.0, "unit": "m"}]
node.print_table(table=table)
assert len(captured) == 1
assert captured[0] == table
PrintTable._broadcast_table_fn = None

View File

@@ -42,23 +42,21 @@ def test_rotate_field():
def test_rotate_field_overlay_warning():
from backend.nodes.rotate import RotateField
from backend.execution_context import execution_callbacks, active_node
node = RotateField()
warnings = []
RotateField._broadcast_warning_fn = lambda nid, msg: warnings.append(msg)
RotateField._current_node_id = "test"
field = DataField(
data=np.arange(16, dtype=np.float64).reshape(4, 4),
overlays=[{"kind": "markup", "shapes": [{"kind": "line", "x1": 0.1, "y1": 0.1, "x2": 0.9, "y2": 0.9, "width": 2, "color": "#ffffff"}]}],
)
with execution_callbacks(warning=lambda nid, msg: warnings.append(msg)), active_node("test"):
rotated, = node.process(field, angle=30.0, interpolation="bilinear", expand_canvas=True)
assert rotated.overlays == []
assert len(warnings) == 1
assert "clears annotation/markup overlays" in warnings[0]
RotateField._broadcast_warning_fn = None
def test_rotate_unknown_interpolation():
from backend.nodes.rotate import RotateField

View File

@@ -11,10 +11,9 @@ def test_stats():
assert input_spec[0] == "DATA_FIELD"
assert input_spec[1]["accepted_types"] == ["IMAGE", "LINE", "DATA_TABLE"]
from backend.execution_context import execution_callbacks, active_node
captured = []
Stats._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
Stats._current_node_id = "test"
with execution_callbacks(value=lambda nid, payload: captured.append((nid, payload))), active_node("test"):
line = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64)
result, = node.process(line, operation="mean", column="value")
assert np.isclose(result, 2.5)
@@ -65,8 +64,6 @@ def test_stats():
except ValueError:
pass
Stats._broadcast_value_fn = None
def test_stats_empty_inputs():
from backend.nodes.stats import Stats

View File

@@ -11,10 +11,9 @@ def test_value_display():
assert value_spec[0] == "FLOAT"
assert value_spec[1]["accepted_types"] == ["RECORD_TABLE"]
from backend.execution_context import execution_callbacks, active_node
captured = []
ValueIO._broadcast_value_fn = lambda node_id, payload: captured.append((node_id, payload))
ValueIO._current_node_id = "test"
with execution_callbacks(value=lambda nid, payload: captured.append((nid, payload))), active_node("test"):
result = node.display_value(value=3.25)
assert result == (3.25,)
assert captured == [("test", {"value": 3.25})]
@@ -27,8 +26,6 @@ def test_value_display():
assert result == (1.7e-7,)
assert captured[-1] == ("test", {"value": 1.7e-7, "unit": "m"})
ValueIO._broadcast_value_fn = None
def test_value_display_string_input():
from backend.nodes.value_io import ValueIO