Source code for Karana.KUtils.MultibodyWebUI._python_reference

# Copyright (c) 2024-2026 Karana Dynamics Pty Ltd. All rights reserved.
#
# NOTICE TO USER:
#
# This source code and/or documentation (the "Licensed Materials") is
# the confidential and proprietary information of Karana Dynamics Inc.
# Use of these Licensed Materials is governed by the terms and conditions
# of a separate software license agreement between Karana Dynamics and the
# Licensee ("License Agreement"). Unless expressly permitted under that
# agreement, any reproduction, modification, distribution, or disclosure
# of the Licensed Materials, in whole or in part, to any third party
# without the prior written consent of Karana Dynamics is strictly prohibited.
#
# THE LICENSED MATERIALS ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
# KARANA DYNAMICS DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND
# FITNESS FOR A PARTICULAR PURPOSE.
#
# IN NO EVENT SHALL KARANA DYNAMICS BE LIABLE FOR ANY DAMAGES WHATSOEVER,
# INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA, OR USE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, WHETHER IN CONTRACT, TORT,
# OR OTHERWISE ARISING OUT OF OR IN CONNECTION WITH THE LICENSED MATERIALS.
#
# U.S. Government End Users: The Licensed Materials are a "commercial item"
# as defined at 48 C.F.R. 2.101, and are provided to the U.S. Government
# only as a commercial end item under the terms of this license.
#
# Any use of the Licensed Materials in individual or commercial software must
# include, in the user documentation and internal source code comments,
# this Notice, Disclaimer, and U.S. Government Use Provision.

"""Tools for viewing Python object referrers with vis-network.js."""

from typing import SupportsIndex, SupportsInt
import webbrowser

import Karana.Core as kc
import Karana.WebUI as kw
from Karana.KUtils.visjs import PythonReferenceGraphServer, NetworkOptions


# This wrapper owns two servers and the widgets connecting their callbacks.
[docs] class PythonReferenceGraphGui: """GUI with live traversal controls and an embedded reference graph.""" def __init__( self, target: object, *, port: int = 0, graph_port: int = 0, title: str = "Python reference owners", target_label: str | None = None, max_depth: int = 2, max_nodes: int = 100, network_options: NetworkOptions | None = None, ): # pylint: disable=too-many-arguments """Create a controlled Python reference graph GUI. Parameters ---------- target : object Weak-referenceable object whose Python referrers should be shown. port : int Port for the outer GUI. Port 0 selects an arbitrary free port. graph_port : int Port for the embedded graph server. title : str GUI and graph title. target_label : str | None Optional display label for the target node. max_depth : int Initial number of referrer links to traverse. max_nodes : int Initial maximum number of graph nodes. network_options : NetworkOptions | None visjs network options for the embedded graph. """ self.graph_server = PythonReferenceGraphServer( target, port=graph_port, title=title, target_label=target_label, max_depth=max_depth, max_nodes=max_nodes, network_options=network_options, ) self.server = kw.HttpWsServer(port=port, display_name=title) frontend = kc.findResource("WebUI/frontend") self.server.serveFile("/", frontend / "index.html") self.server.serveFile("/main.js", frontend / "router-main-bundle.js") self.server.serveFile("/style.css", frontend / "style.css") self.router = kw.Router(self.server) self.root = kw.Layout( self.router, style={ "display": "flex", "flexDirection": "column", "height": "100vh", }, ) self.controls = kw.InputGroup( self.router, title="Reference graph controls", style={"display": "flex", "alignItems": "end", "gap": "0.5em"}, ) self.depth_input = kw.IntInput( self.router, "Maximum depth", "Number of owner links to traverse from the target", on_change=self.setMaxDepth, ) self.depth_input.setMin(0) self.depth_input.setStep(1) self.depth_input.setValue(max_depth) self.depth_input.setSizeClass(kw.SizeClass.NARROW) self.max_nodes_input = kw.IntInput( self.router, "Maximum nodes", "Maximum number of objects to show", on_change=self.setMaxNodes, ) self.max_nodes_input.setMin(1) self.max_nodes_input.setStep(1) self.max_nodes_input.setValue(max_nodes) self.max_nodes_input.setSizeClass(kw.SizeClass.NARROW) self.refresh_button = kw.Button( self.router, text="Refresh", on_press=self.refresh, tooltip="Recompute the graph with the current limits", ) self.controls.addChild(self.depth_input) self.controls.addChild(self.max_nodes_input) self.controls.addChild(self.refresh_button) self.graph_frame = kw.IFrame(self.router, self.graph_server.getUrl()) self.graph_dock = kw.Dock(self.router, style={"flexGrow": "1"}) self.graph_dock.addChild(title=title, widget=self.graph_frame) self.root.addChild(self.controls) self.root.addChild(self.graph_dock) self.root.addToDomRoot() self._closed = False
[docs] def getUrl(self) -> str: """Return the outer GUI URL.""" return self.server.getUrl()
[docs] def launchLocalClient(self): """Open the GUI in the default local web browser.""" webbrowser.open(self.getUrl())
[docs] def maxDepth(self) -> int: """Return the current maximum traversal depth.""" return self.graph_server.maxDepth()
[docs] def maxNodes(self) -> int: """Return the current maximum node count.""" return self.graph_server.maxNodes()
[docs] def refresh(self): """Recompute the embedded reference graph.""" self.graph_server.refresh()
[docs] def setMaxDepth(self, max_depth: SupportsIndex | SupportsInt): """Change the depth and immediately recompute the graph.""" self.graph_server.setMaxDepth(int(max_depth))
[docs] def setMaxNodes(self, max_nodes: SupportsIndex | SupportsInt): """Change the node limit and immediately recompute the graph.""" self.graph_server.setMaxNodes(int(max_nodes))
[docs] def close(self): """Idempotently close the GUI and embedded graph servers.""" if self._closed: return self._closed = True self.server.close() self.graph_server.close()
__all__ = ["PythonReferenceGraphGui"]