# 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"]