diff --git a/src/scenex/__init__.py b/src/scenex/__init__.py index af878a2c..c14d9960 100644 --- a/src/scenex/__init__.py +++ b/src/scenex/__init__.py @@ -65,7 +65,7 @@ from .model._nodes.volume import Volume from .model._transform import Transform from .model._view import Letterbox, ResizePolicy, View -from .util import native, run, set_cursor, show +from .utils._app import native, run, set_cursor, show __all__ = [ "Camera", diff --git a/src/scenex/model/_nodes/node.py b/src/scenex/model/_nodes/node.py index d6062495..0655111b 100644 --- a/src/scenex/model/_nodes/node.py +++ b/src/scenex/model/_nodes/node.py @@ -382,6 +382,6 @@ def iter_parents(self) -> Iterator[Node]: def tree_repr(self) -> str: """Return an ASCII/Unicode tree representation of self and its descendants.""" - from scenex.util import tree_repr + from scenex.utils._tree import tree_repr return tree_repr(self, node_repr=object.__repr__) diff --git a/src/scenex/utils/__init__.py b/src/scenex/utils/__init__.py index f6029205..c985d3e2 100644 --- a/src/scenex/utils/__init__.py +++ b/src/scenex/utils/__init__.py @@ -1,3 +1,5 @@ """Utilities for working with scenex structures.""" -__all__ = [] +from ._app import native, run, set_cursor, show + +__all__ = ["native", "run", "set_cursor", "show"] diff --git a/src/scenex/util.py b/src/scenex/utils/_app.py similarity index 58% rename from src/scenex/util.py rename to src/scenex/utils/_app.py index 75ecab21..8dcf6146 100644 --- a/src/scenex/util.py +++ b/src/scenex/utils/_app.py @@ -1,8 +1,4 @@ -"""Utility functions for displaying and debugging scenex visualizations. - -This module provides helper functions for common visualization tasks including -displaying models, formatting scene graph trees, and utility functions used -internally by scenex. +"""Utility functions pertaining to the underlying application. The `show()` function is the primary entry point for creating visualizations, handling the details of canvas creation, backend selection, and camera fitting @@ -12,98 +8,20 @@ from __future__ import annotations import logging -from collections.abc import Callable, Iterable -from contextlib import suppress -from typing import TYPE_CHECKING, Any, Protocol, cast +from typing import TYPE_CHECKING, Any, cast from scenex import model from scenex.app import app -from scenex.utils import projections -if TYPE_CHECKING: - from typing import TypeAlias +from . import projections +if TYPE_CHECKING: from scenex.adaptors._base import CanvasAdaptor from scenex.app._auto import CursorType - Tree: TypeAlias = str | dict[str, list["Tree"]] - - class SupportsChildren(Protocol): - """Protocol for node-like objects that have children.""" - - @property - def children(self) -> Iterable[SupportsChildren]: - """Return the children of the node.""" - ... - - -__all__ = ["native", "show", "tree_dict", "tree_repr"] - logger = logging.getLogger("scenex") -def tree_repr( - node: SupportsChildren, - *, - node_repr: Callable[[Any], str] = object.__repr__, - _prefix: str = "", - _is_last: bool = True, -) -> str: - """ - Return an ASCII/Unicode tree representation of `node` and its descendants. - - This assumes that `node` is a tree-like object with a `children` attribute that is - either a property or a callable that returns an iterable of child nodes. - - Parameters - ---------- - node : SupportsChildren - Any object that has a `children` attribute or method that returns an iterable - of child nodes. - node_repr : Callable[[Any], str], optional - Function to convert the node to a string. Defaults to `object.__repr__` (which - avoids complex repr functions on objects, but use `repr` if you want to see - the full representation). - _prefix : str, optional - Prefix to use for each line of the tree. Defaults to an empty string. - _is_last : bool, optional - Whether this node is the last child of its parent. Defaults to `True`. - This is used to determine the branch character to use in the tree - representation. - """ - if _prefix: - branch = "└── " if _is_last else "├── " - else: - branch = "" - - lines: list[str] = [f"{_prefix}{branch}{node_repr(node)}"] - if children := list(_get_children(node)): - prefix_child = _prefix + (" " if _is_last else "│ ") - for idx, child in enumerate(children): - lines.append( - tree_repr( - child, - node_repr=node_repr, - _prefix=prefix_child, - _is_last=idx == len(children) - 1, - ) - ) - return "\n".join(lines) - - -def _ensure_iterable(obj: object) -> Iterable[Any]: - """Ensure the object is iterable.""" - if isinstance(obj, Iterable): - return obj - if callable(obj): - with suppress(TypeError): - return _ensure_iterable(obj()) - raise TypeError( - f"Expected an iterable or callable that returns an iterable, " - f"got {type(obj).__name__}" - ) - - def show( obj: model.Node | model.View | model.Canvas, *, backend: str | None = None ) -> model.Canvas: @@ -159,7 +77,7 @@ def show( - Call `run()` after `show()` to enter the event loop in desktop applications - In Jupyter notebooks, visualizations appear automatically without `run()` """ - from .adaptors import get_adaptor_registry + from scenex.adaptors import get_adaptor_registry view = None if isinstance(obj, model.Canvas): @@ -183,10 +101,6 @@ def show( app().create_app() for view in canvas.views: projections.zoom_to_fit(view, zoom_factor=0.9, letterbox=True) - - # logger.debug("SHOW MODEL %s", tree_repr(view.scene)) - # native_scene = view.scene._get_native() - # logger.debug("SHOW NATIVE %s", tree_repr(native_scene)) return canvas @@ -258,51 +172,6 @@ def run() -> None: app().run() -def _cls_name_with_id(obj: Any) -> str: - return f"{obj.__class__.__name__}:{id(obj)}" - - -def tree_dict( - node: SupportsChildren, - *, - obj_name: Callable[[Any], str] = _cls_name_with_id, -) -> Tree: - """Build an intermediate representation of the tree rooted at `node`. - - Leaves are represented as strings, and non-leaf nodes are represented as - dictionaries with the node name as the key and a list of child nodes as the value. - This is useful for debugging and visualization purposes. - - Parameters - ---------- - node : SupportsChildren - The root node of the tree to be represented. - obj_name : Callable[[Any], str], optional - A function to convert the node to a string. Defaults to a lambda function that - returns the class name and ID - - Returns - ------- - str | dict[str, list[dict | str]] - A string, if the node is a leaf, or a dictionary representing the tree, - if the node has children, like `{"node_name": ["child1", "child2", ...]}`. - """ - node_name = obj_name(node) - if not (children := _get_children(node)): - return node_name - - result: list[dict | str] = [] - for child in children: - result.append(tree_dict(child, obj_name=obj_name)) - return {obj_name(node): result} - - -def _get_children(obj: Any) -> Iterable[Any]: - if (children := getattr(obj, "children", None)) is None: - return () - return _ensure_iterable(children) - - def set_cursor(canvas: model.Canvas, cursor: CursorType) -> None: """Set the cursor for the given canvas. diff --git a/src/scenex/utils/_tree.py b/src/scenex/utils/_tree.py new file mode 100644 index 00000000..40861b17 --- /dev/null +++ b/src/scenex/utils/_tree.py @@ -0,0 +1,126 @@ +"""Utility functions for displaying and debugging scenex scenegraphs.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from contextlib import suppress +from typing import TYPE_CHECKING, Any, Protocol + +if TYPE_CHECKING: + from typing import TypeAlias + + Tree: TypeAlias = str | dict[str, list["Tree"]] + + class SupportsChildren(Protocol): + """Protocol for node-like objects that have children.""" + + @property + def children(self) -> Iterable[SupportsChildren]: + """Return the children of the node.""" + ... + + +def _cls_name_with_id(obj: Any) -> str: + return f"{obj.__class__.__name__}:{id(obj)}" + + +def tree_repr( + node: SupportsChildren, + *, + node_repr: Callable[[Any], str] = object.__repr__, + _prefix: str = "", + _is_last: bool = True, +) -> str: + """ + Return an ASCII/Unicode tree representation of `node` and its descendants. + + This assumes that `node` is a tree-like object with a `children` attribute that is + either a property or a callable that returns an iterable of child nodes. + + Parameters + ---------- + node : SupportsChildren + Any object that has a `children` attribute or method that returns an iterable + of child nodes. + node_repr : Callable[[Any], str], optional + Function to convert the node to a string. Defaults to `object.__repr__` (which + avoids complex repr functions on objects, but use `repr` if you want to see + the full representation). + _prefix : str, optional + Prefix to use for each line of the tree. Defaults to an empty string. + _is_last : bool, optional + Whether this node is the last child of its parent. Defaults to `True`. + This is used to determine the branch character to use in the tree + representation. + """ + if _prefix: + branch = "└── " if _is_last else "├── " + else: + branch = "" + + lines: list[str] = [f"{_prefix}{branch}{node_repr(node)}"] + if children := list(_get_children(node)): + prefix_child = _prefix + (" " if _is_last else "│ ") + for idx, child in enumerate(children): + lines.append( + tree_repr( + child, + node_repr=node_repr, + _prefix=prefix_child, + _is_last=idx == len(children) - 1, + ) + ) + return "\n".join(lines) + + +def tree_dict( + node: SupportsChildren, + *, + obj_name: Callable[[Any], str] = _cls_name_with_id, +) -> Tree: + """Build an intermediate representation of the tree rooted at `node`. + + Leaves are represented as strings, and non-leaf nodes are represented as + dictionaries with the node name as the key and a list of child nodes as the value. + This is useful for debugging and visualization purposes. + + Parameters + ---------- + node : SupportsChildren + The root node of the tree to be represented. + obj_name : Callable[[Any], str], optional + A function to convert the node to a string. Defaults to a lambda function that + returns the class name and ID + + Returns + ------- + str | dict[str, list[dict | str]] + A string, if the node is a leaf, or a dictionary representing the tree, + if the node has children, like `{"node_name": ["child1", "child2", ...]}`. + """ + node_name = obj_name(node) + if not (children := _get_children(node)): + return node_name + + result: list[dict | str] = [] + for child in children: + result.append(tree_dict(child, obj_name=obj_name)) + return {obj_name(node): result} + + +def _get_children(obj: Any) -> Iterable[Any]: + if (children := getattr(obj, "children", None)) is None: + return () + return _ensure_iterable(children) + + +def _ensure_iterable(obj: object) -> Iterable[Any]: + if isinstance(obj, Iterable): + return obj + if callable(obj): + with suppress(TypeError): + return _ensure_iterable(obj()) + raise TypeError( + f"Expected an iterable or callable that returns an iterable, " + f"got {type(obj).__name__}" + ) diff --git a/tests/test_basic_scene.py b/tests/test_basic_scene.py index 5bd0439c..2320c850 100644 --- a/tests/test_basic_scene.py +++ b/tests/test_basic_scene.py @@ -6,6 +6,7 @@ import scenex as snx from scenex.adaptors._auto import determine_backend +from scenex.utils._tree import tree_dict, tree_repr if TYPE_CHECKING: from scenex.adaptors._pygfx._scene import Scene as PygfxScene @@ -70,7 +71,7 @@ def test_basic_view(basic_view: snx.View) -> None: assert isinstance(repr(basic_view), str) assert isinstance(basic_view.model_dump(), dict) assert isinstance(basic_view.model_dump_json(), str) - assert snx.util.tree_repr(basic_view.scene, node_repr=_obj_name) == EXPECT_REPR + assert tree_repr(basic_view.scene, node_repr=_obj_name) == EXPECT_REPR ary = basic_view.render() assert isinstance(ary, np.ndarray) @@ -88,9 +89,9 @@ def test_view_tree_matches_native(basic_view: snx.View) -> None: structure of the tree generated by the native backend.""" basic_view._get_adaptors(create=True) - model_tree = snx.util.tree_dict(basic_view.scene, obj_name=_obj_name) + model_tree = tree_dict(basic_view.scene, obj_name=_obj_name) native_scene = _native_scene(basic_view.scene) - view_tree = snx.util.tree_dict(native_scene, obj_name=_obj_name) + view_tree = tree_dict(native_scene, obj_name=_obj_name) assert isinstance(view_tree, dict) assert model_tree == _filter_backend_nodes(view_tree)