code-node-graph 0.3.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_node/__init__.py +1 -1
- code_node/analysis/graph.py +148 -7
- code_node/analysis/reader.py +20 -3
- code_node/report/build.py +5 -3
- code_node/report/graph.py +232 -52
- code_node/report/style.py +14 -0
- code_node/serve.py +2 -2
- {code_node_graph-0.3.0.dist-info → code_node_graph-0.4.0.dist-info}/METADATA +1 -1
- {code_node_graph-0.3.0.dist-info → code_node_graph-0.4.0.dist-info}/RECORD +12 -12
- {code_node_graph-0.3.0.dist-info → code_node_graph-0.4.0.dist-info}/WHEEL +0 -0
- {code_node_graph-0.3.0.dist-info → code_node_graph-0.4.0.dist-info}/entry_points.txt +0 -0
- {code_node_graph-0.3.0.dist-info → code_node_graph-0.4.0.dist-info}/licenses/LICENSE +0 -0
code_node/__init__.py
CHANGED
|
@@ -13,7 +13,7 @@ from .libraries import Library, register_library
|
|
|
13
13
|
from .platforms import Platform, get_platform, register_platform
|
|
14
14
|
from .project import EntryPoint, Project, entry_points, read_project
|
|
15
15
|
|
|
16
|
-
__version__ = "0.
|
|
16
|
+
__version__ = "0.4.0"
|
|
17
17
|
|
|
18
18
|
__all__ = [
|
|
19
19
|
"CodeGraph",
|
code_node/analysis/graph.py
CHANGED
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
import heapq
|
|
9
9
|
import re
|
|
10
10
|
from collections import defaultdict
|
|
11
|
-
from dataclasses import dataclass, field
|
|
11
|
+
from dataclasses import dataclass, field, replace
|
|
12
12
|
|
|
13
13
|
from ..kit.fmt import _plural
|
|
14
14
|
from ..kit.text import _clip
|
|
@@ -55,6 +55,7 @@ class GraphNode:
|
|
|
55
55
|
when: str = "" # a function called only under an if, everywhere it's called: 'check(order)'
|
|
56
56
|
icon: str = "" # a service's icon, when its plugin names one: 'database', 'queue', 'spark'; or the entry
|
|
57
57
|
# point's logo, when its platform names one: 'fastapi-icon'
|
|
58
|
+
name: str = "" # a function's box: the function, as module.name ('orders.repo.save')
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
@dataclass
|
|
@@ -85,6 +86,7 @@ class CodeGraph:
|
|
|
85
86
|
nodes: list[GraphNode] = field(default_factory=list)
|
|
86
87
|
edges: list[GraphEdge] = field(default_factory=list)
|
|
87
88
|
hidden: int = 0 # functions left out to keep the graph readable (too deep, or too many)
|
|
89
|
+
too_deep: int = 0 # ...of them, the ones deeper than it draws
|
|
88
90
|
|
|
89
91
|
def node(self, node_id: str) -> GraphNode | None:
|
|
90
92
|
return next((n for n in self.nodes if n.id == node_id), None)
|
|
@@ -256,11 +258,13 @@ def _pick(
|
|
|
256
258
|
order: dict[str, int],
|
|
257
259
|
room: int,
|
|
258
260
|
max_depth: int,
|
|
261
|
+
first: bool = False,
|
|
259
262
|
) -> list[str]:
|
|
260
263
|
"""
|
|
261
264
|
The functions the graph draws besides the entry point, at most `room` of them and max_depth calls deep: grown out
|
|
262
265
|
from the entry point (and what runs at import), each time taking the one the drawn boxes call that leads to the
|
|
263
|
-
most code, so the boxes are the ones that do the work and each is joined to one above it.
|
|
266
|
+
most code, so the boxes are the ones that do the work and each is joined to one above it. first takes every
|
|
267
|
+
function the entry point calls before the rest.
|
|
264
268
|
"""
|
|
265
269
|
|
|
266
270
|
def depth(name: str) -> int:
|
|
@@ -283,6 +287,14 @@ def _pick(
|
|
|
283
287
|
if name not in level:
|
|
284
288
|
offer(name)
|
|
285
289
|
picked: list[str] = []
|
|
290
|
+
if first:
|
|
291
|
+
for child, _ in children.get(entry, []):
|
|
292
|
+
if child in by_name and child != entry and child not in picked and depth(child) <= max_depth:
|
|
293
|
+
picked.append(child)
|
|
294
|
+
for name in list(picked):
|
|
295
|
+
for child, _ in children.get(name, []):
|
|
296
|
+
if child not in picked:
|
|
297
|
+
offer(child)
|
|
286
298
|
while queue and len(picked) < room:
|
|
287
299
|
name = heapq.heappop(queue)[3]
|
|
288
300
|
if name in picked:
|
|
@@ -294,7 +306,46 @@ def _pick(
|
|
|
294
306
|
return picked
|
|
295
307
|
|
|
296
308
|
|
|
297
|
-
def
|
|
309
|
+
def _focused(cmap: CodeMap, focus: str) -> CodeMap:
|
|
310
|
+
"""
|
|
311
|
+
The map cut down to one of its functions and what it calls, however deep, as if the code started there: its
|
|
312
|
+
calls in from elsewhere, what runs at import and the entry point's input left out.
|
|
313
|
+
"""
|
|
314
|
+
children: dict[str, list[str]] = defaultdict(list)
|
|
315
|
+
for f in cmap.functions:
|
|
316
|
+
for where in f.called_from:
|
|
317
|
+
children[where.rpartition(" line ")[0]].append(f.name)
|
|
318
|
+
scope, todo = {focus}, [focus]
|
|
319
|
+
while todo:
|
|
320
|
+
for child in children.get(todo.pop(), []):
|
|
321
|
+
if child not in scope:
|
|
322
|
+
scope.add(child)
|
|
323
|
+
todo.append(child)
|
|
324
|
+
root = next(f for f in cmap.functions if f.name == focus)
|
|
325
|
+
functions = []
|
|
326
|
+
for f in cmap.functions:
|
|
327
|
+
if f.name in scope:
|
|
328
|
+
kept = [] if f.name == focus else [w for w in f.called_from if w.rpartition(" line ")[0] in scope]
|
|
329
|
+
functions.append(replace(f, called_from=kept, when={w: t for w, t in f.when.items() if w in kept}))
|
|
330
|
+
return replace(
|
|
331
|
+
cmap,
|
|
332
|
+
entry=focus,
|
|
333
|
+
handler_file=root.file,
|
|
334
|
+
entry_line=root.line,
|
|
335
|
+
startup=[],
|
|
336
|
+
event=[],
|
|
337
|
+
functions=functions,
|
|
338
|
+
aws=[c for c in cmap.aws if c.function in scope],
|
|
339
|
+
library_calls=[c for c in cmap.library_calls if c.function in scope],
|
|
340
|
+
env=[e for e in cmap.env if e.function in scope],
|
|
341
|
+
spots=[s for s in cmap.spots if s.function in scope and not s.startup],
|
|
342
|
+
routes=[r for r in cmap.routes if r[2] in scope],
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def code_graph(
|
|
347
|
+
cmap: CodeMap, *, max_functions: int = _GRAPH_FUNCTIONS, max_depth: int = _GRAPH_DEPTH, focus: str = ""
|
|
348
|
+
) -> CodeGraph:
|
|
298
349
|
"""
|
|
299
350
|
How the code fits together, as boxes and arrows, from a CodeMap: on the left what comes in (the event, with the
|
|
300
351
|
fields it reads, and the environment variables), then the handler, what runs at start-up, and its own functions
|
|
@@ -304,10 +355,16 @@ def code_graph(cmap: CodeMap, *, max_functions: int = _GRAPH_FUNCTIONS, max_dept
|
|
|
304
355
|
they do is drawn from the nearest box above them that is.
|
|
305
356
|
The functions drawn are grown out from the entry point, each time the one that leads to the most code (and to AWS,
|
|
306
357
|
the web and the environment), so the boxes are the ones that do the work, not the nearest helpers.
|
|
358
|
+
focus draws the graph from one of its functions instead (module.name): it and what it calls, every call it makes
|
|
359
|
+
drawn even past max_functions, and nothing from what comes in or what runs at import.
|
|
307
360
|
"""
|
|
308
361
|
graph = CodeGraph()
|
|
309
362
|
if cmap.note or cmap.entry is None:
|
|
310
363
|
return graph
|
|
364
|
+
if focus:
|
|
365
|
+
if not any(f.name == focus for f in cmap.functions):
|
|
366
|
+
return graph
|
|
367
|
+
cmap = _focused(cmap, focus)
|
|
311
368
|
platform = get_platform(cmap.platform)
|
|
312
369
|
entry = cmap.entry
|
|
313
370
|
by_name = {f.name: f for f in cmap.functions}
|
|
@@ -342,11 +399,15 @@ def code_graph(cmap: CodeMap, *, max_functions: int = _GRAPH_FUNCTIONS, max_dept
|
|
|
342
399
|
todo.append(child)
|
|
343
400
|
order = {f.name: i for i, f in enumerate(cmap.functions)}
|
|
344
401
|
weight = _weights(cmap, children)
|
|
345
|
-
|
|
402
|
+
room = max_functions - entry_is_function
|
|
403
|
+
if focus: # a function opened on its own shows every call it makes
|
|
404
|
+
room = max(room, len({child for child, _ in children.get(entry, [])}))
|
|
405
|
+
picked = _pick(cmap, entry, start_key, children, level, weight, order, room, max_depth, first=bool(focus))
|
|
346
406
|
shown = ([by_name[entry]] if entry_is_function else []) + sorted(
|
|
347
407
|
(by_name[name] for name in picked), key=lambda f: (level.get(f.name, f.depth + 1), order[f.name])
|
|
348
408
|
)
|
|
349
409
|
graph.hidden = len(cmap.functions) - len(shown)
|
|
410
|
+
graph.too_deep = sum(1 for f in cmap.functions if level.get(f.name, f.depth + 1) > max_depth)
|
|
350
411
|
ids = {f.name: f"f{i}" for i, f in enumerate(shown)}
|
|
351
412
|
drawn_level = {entry: 0, start_key: 0} # calls from the entry point through the boxes drawn: each box's column
|
|
352
413
|
todo = [entry, start_key]
|
|
@@ -408,7 +469,9 @@ def code_graph(cmap: CodeMap, *, max_functions: int = _GRAPH_FUNCTIONS, max_dept
|
|
|
408
469
|
reasons[call.function].append(f"{call.label} isn't allowed: {denied[call.permission]} (line {call.line})")
|
|
409
470
|
for function, why in platform.marks(cmap).items():
|
|
410
471
|
reasons[function] += why
|
|
411
|
-
handler_reasons =
|
|
472
|
+
handler_reasons = (
|
|
473
|
+
[] if focus else [m.split(":")[0] for level_, m in platform.entry_findings(cmap) if level_ == "warn"]
|
|
474
|
+
)
|
|
412
475
|
unset = platform.unset_env(cmap)
|
|
413
476
|
|
|
414
477
|
# the lines in each function worth pointing at: where it calls, reads or sends what the graph draws
|
|
@@ -455,7 +518,7 @@ def code_graph(cmap: CodeMap, *, max_functions: int = _GRAPH_FUNCTIONS, max_dept
|
|
|
455
518
|
when=_when(f, [where for where in f.called_from if where.rpartition(" line ")[0] == caller]),
|
|
456
519
|
)
|
|
457
520
|
)
|
|
458
|
-
has_input = bool(cmap.event) or platform.draws_input
|
|
521
|
+
has_input = not focus and (bool(cmap.event) or platform.draws_input)
|
|
459
522
|
if has_input:
|
|
460
523
|
edges.insert(0, GraphEdge("event", entry_id, "event", "its input"))
|
|
461
524
|
|
|
@@ -769,7 +832,8 @@ def code_graph(cmap: CodeMap, *, max_functions: int = _GRAPH_FUNCTIONS, max_dept
|
|
|
769
832
|
],
|
|
770
833
|
more=more,
|
|
771
834
|
when=_when(f, f.called_from),
|
|
772
|
-
icon=platform.entry_icon(cmap) if is_entry else "",
|
|
835
|
+
icon=platform.entry_icon(cmap) if is_entry and not focus else "",
|
|
836
|
+
name=f.name,
|
|
773
837
|
)
|
|
774
838
|
)
|
|
775
839
|
out_column = 1 + max([n.column for n in graph.nodes if n.kind in ("handler", "function", "start")] + [1])
|
|
@@ -778,3 +842,80 @@ def code_graph(cmap: CodeMap, *, max_functions: int = _GRAPH_FUNCTIONS, max_dept
|
|
|
778
842
|
graph.nodes += reach
|
|
779
843
|
graph.edges = edges
|
|
780
844
|
return graph
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
_GRAPH_VIEWS = 120 # functions opened on their own, at most: past that a box's calls left out stay in the steps
|
|
848
|
+
|
|
849
|
+
_GRAPH_EVERYTHING = 150 # functions, at most, for a graph of all of them besides the one drawn
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
@dataclass
|
|
853
|
+
class GraphView:
|
|
854
|
+
"""
|
|
855
|
+
The graph from one of the functions a graph leaves calls out of: the function and what it calls (`graph`), and the
|
|
856
|
+
calls from the entry point down to it (`path`, module.name each, the entry point first and the function last).
|
|
857
|
+
"""
|
|
858
|
+
|
|
859
|
+
name: str
|
|
860
|
+
graph: CodeGraph
|
|
861
|
+
path: list[str] = field(default_factory=list)
|
|
862
|
+
|
|
863
|
+
|
|
864
|
+
def _call_path(cmap: CodeMap, name: str) -> list[str]:
|
|
865
|
+
"""
|
|
866
|
+
The fewest calls from the entry point to a function: [entry, ..., name]. [name] when the entry point isn't above it.
|
|
867
|
+
"""
|
|
868
|
+
known = {f.name for f in cmap.functions} | {cmap.entry or ""}
|
|
869
|
+
parents: dict[str, list[str]] = defaultdict(list)
|
|
870
|
+
for f in cmap.functions:
|
|
871
|
+
for where in f.called_from:
|
|
872
|
+
caller = where.rpartition(" line ")[0]
|
|
873
|
+
if caller in known:
|
|
874
|
+
parents[f.name].append(caller)
|
|
875
|
+
came: dict[str, str] = {name: ""}
|
|
876
|
+
todo = [name]
|
|
877
|
+
while todo:
|
|
878
|
+
key = todo.pop(0)
|
|
879
|
+
if key == cmap.entry:
|
|
880
|
+
path = [key]
|
|
881
|
+
while came[path[-1]]:
|
|
882
|
+
path.append(came[path[-1]])
|
|
883
|
+
return path
|
|
884
|
+
for parent in parents.get(key, []):
|
|
885
|
+
if parent not in came:
|
|
886
|
+
came[parent] = key
|
|
887
|
+
todo.append(parent)
|
|
888
|
+
return [name]
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
def graph_views(
|
|
892
|
+
cmap: CodeMap,
|
|
893
|
+
graph: CodeGraph,
|
|
894
|
+
*,
|
|
895
|
+
max_functions: int = _GRAPH_FUNCTIONS,
|
|
896
|
+
max_depth: int = _GRAPH_DEPTH,
|
|
897
|
+
most: int = _GRAPH_VIEWS,
|
|
898
|
+
) -> list[GraphView]:
|
|
899
|
+
"""
|
|
900
|
+
A graph from each function whose calls `graph` leaves out, and from each such function in those, so that from any
|
|
901
|
+
box with calls not drawn, its own graph has them: at most `most`, nearest the entry point first.
|
|
902
|
+
"""
|
|
903
|
+
views: dict[str, GraphView] = {}
|
|
904
|
+
todo = [graph]
|
|
905
|
+
while todo and len(views) < most:
|
|
906
|
+
for n in todo.pop(0).nodes:
|
|
907
|
+
if not (n.more and n.name) or n.name in views or len(views) >= most:
|
|
908
|
+
continue
|
|
909
|
+
opened = code_graph(cmap, max_functions=max_functions, max_depth=max_depth, focus=n.name)
|
|
910
|
+
views[n.name] = GraphView(n.name, opened, _call_path(cmap, n.name))
|
|
911
|
+
todo.append(opened)
|
|
912
|
+
return list(views.values())
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
def everything_graph(cmap: CodeMap, graph: CodeGraph, most: int = _GRAPH_EVERYTHING) -> CodeGraph | None:
|
|
916
|
+
"""
|
|
917
|
+
Every function in one graph, when `graph` leaves some out and there are at most `most` of them; else None.
|
|
918
|
+
"""
|
|
919
|
+
if not graph.hidden or len(cmap.functions) > most:
|
|
920
|
+
return None
|
|
921
|
+
return code_graph(cmap, max_functions=len(cmap.functions) + 1, max_depth=len(cmap.functions) + 1)
|
code_node/analysis/reader.py
CHANGED
|
@@ -203,6 +203,22 @@ class _CodeReader:
|
|
|
203
203
|
module = module[: -len(".__init__")] if module.endswith(".__init__") else module
|
|
204
204
|
if module and module != "__init__":
|
|
205
205
|
self.sources[module] = path
|
|
206
|
+
here = (cmap.handler_file or "").rpartition("/")[0]
|
|
207
|
+
here = here + "/" if here else ""
|
|
208
|
+
if here.startswith(root) and here != root:
|
|
209
|
+
# the entry point's own folder is on sys.path too, as a script's is and a Lambda function's code is:
|
|
210
|
+
# `from helpers import x` beside it is its own code, though the folder is also a package from the root
|
|
211
|
+
beside: set[str] = set()
|
|
212
|
+
for path in texts:
|
|
213
|
+
if not path.startswith(here) or not path.endswith(".py") or "site-packages" in path:
|
|
214
|
+
continue
|
|
215
|
+
module = path[len(here) : -3].replace("/", ".")
|
|
216
|
+
module = module[: -len(".__init__")] if module.endswith(".__init__") else module
|
|
217
|
+
if module and module != "__init__" and "-" not in module and module not in self.sources:
|
|
218
|
+
self.sources[module] = path
|
|
219
|
+
beside.add(module.split(".")[0])
|
|
220
|
+
self.tops |= beside
|
|
221
|
+
self.libraries = libraries - beside
|
|
206
222
|
self.modules: dict[str, _Module] = {}
|
|
207
223
|
self.defs: dict[str, tuple[str, Any, str | None]] = {} # key -> (module, node, class key)
|
|
208
224
|
self.classes: dict[str, dict[str, Any]] = {}
|
|
@@ -311,8 +327,9 @@ class _CodeReader:
|
|
|
311
327
|
cmap.event = list(self.event_reads.values())
|
|
312
328
|
cmap.routes = [(method, path, key) for method, path, key, _ in self.routes]
|
|
313
329
|
loaded = [m for m in self.modules.values() if m.tree is not None]
|
|
314
|
-
|
|
315
|
-
|
|
330
|
+
read = {path for name, path in self.sources.items() if name in self.modules} # a file has two names, at most
|
|
331
|
+
cmap.other_files = len(
|
|
332
|
+
{path for path in self.sources.values() if path not in read and not _JUNK_RE.search(path)}
|
|
316
333
|
)
|
|
317
334
|
for m in loaded:
|
|
318
335
|
for node in m.tree.body:
|
|
@@ -324,7 +341,7 @@ class _CodeReader:
|
|
|
324
341
|
if isinstance(f, (ast.FunctionDef, ast.AsyncFunctionDef)) and prefix + f.name not in self.reached
|
|
325
342
|
)
|
|
326
343
|
self._secrets(m)
|
|
327
|
-
for name, path in self.sources.items():
|
|
344
|
+
for path, name in {path: name for name, path in reversed(self.sources.items())}.items():
|
|
328
345
|
for match in _AWS_KEY_RE.finditer(self.texts[path]):
|
|
329
346
|
line = self.texts[path].count("\n", 0, match.start()) + 1
|
|
330
347
|
self._spot("secret", path, line, name, "", "an AWS access key", ok=False)
|
code_node/report/build.py
CHANGED
|
@@ -10,7 +10,7 @@ from ..kit.blocks import _Note, _Text
|
|
|
10
10
|
from ..kit.fmt import _plural
|
|
11
11
|
|
|
12
12
|
from ..analysis.findings import _listed, _short_name
|
|
13
|
-
from ..analysis.graph import CodeGraph, code_graph
|
|
13
|
+
from ..analysis.graph import CodeGraph, code_graph, everything_graph, graph_views
|
|
14
14
|
from ..analysis.model import CodeMap
|
|
15
15
|
from .graph import _Graph
|
|
16
16
|
from .tabs import _Tabs
|
|
@@ -34,8 +34,8 @@ def _closing_note(cmap: CodeMap, graph: CodeGraph) -> str:
|
|
|
34
34
|
said.append(f"Calls to {_listed(services, 4)} are read by code-node's library plugins.")
|
|
35
35
|
if graph.hidden:
|
|
36
36
|
said.append(
|
|
37
|
-
f"The graph leaves out {_plural(graph.hidden, 'function')} to stay readable:
|
|
38
|
-
"
|
|
37
|
+
f"The graph leaves out {_plural(graph.hidden, 'function')} to stay readable: a box's line of calls not "
|
|
38
|
+
"drawn opens them, and --all draws every one."
|
|
39
39
|
)
|
|
40
40
|
if cmap.other_files:
|
|
41
41
|
said.append(
|
|
@@ -99,6 +99,8 @@ def page_blocks(
|
|
|
99
99
|
"How it fits together: what comes in, who calls whom, and what it reaches",
|
|
100
100
|
sources=sources or {},
|
|
101
101
|
editor=editor,
|
|
102
|
+
views=graph_views(cmap, graph, max_functions=max_functions, max_depth=max_depth),
|
|
103
|
+
everything=everything_graph(cmap, graph),
|
|
102
104
|
),
|
|
103
105
|
_Text(
|
|
104
106
|
graph.mermaid(),
|
code_node/report/graph.py
CHANGED
|
@@ -17,7 +17,8 @@ from ..kit.highlight import _python_html, _python_lines
|
|
|
17
17
|
from ..kit.render import _CALL_RE, _prose
|
|
18
18
|
from ..kit.text import _esc
|
|
19
19
|
|
|
20
|
-
from ..analysis.
|
|
20
|
+
from ..analysis.findings import _short_name
|
|
21
|
+
from ..analysis.graph import CodeGraph, GraphEdge, GraphNode, GraphView
|
|
21
22
|
from .brands import AWS_LOGOS, SERVICE_LOGOS, fills, logo, small_logo
|
|
22
23
|
from .editor import editor_name, open_at
|
|
23
24
|
from .style import _ROOT
|
|
@@ -36,6 +37,9 @@ class _Graph:
|
|
|
36
37
|
empty: str = "(nothing to draw)"
|
|
37
38
|
sources: dict[str, str] = field(default_factory=dict)
|
|
38
39
|
editor: str = "" # a link to open a line in an editor, from editor_link(): each place in a panel links to it
|
|
40
|
+
views: list[GraphView] = field(default_factory=list) # with JavaScript, a box's line of calls not drawn opens the
|
|
41
|
+
# graph from that function, from these (graph_views())
|
|
42
|
+
everything: CodeGraph | None = None # ...and a button in the bar draws every function, this graph
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
_EDGE_WORDS = {
|
|
@@ -999,12 +1003,32 @@ def _g_calls(count: int) -> str:
|
|
|
999
1003
|
return f"{count} call" if count == 1 else f"{count} calls"
|
|
1000
1004
|
|
|
1001
1005
|
|
|
1006
|
+
def _g_prose_top(card: _GCard) -> float:
|
|
1007
|
+
"""
|
|
1008
|
+
Where a box's prose starts: under its header, its route's pill and its when pill.
|
|
1009
|
+
"""
|
|
1010
|
+
return card.head + (_GRAPH_ROUTE if card.route else 0) + (_GRAPH_ROUTE if card.node.when else 0)
|
|
1011
|
+
|
|
1012
|
+
|
|
1013
|
+
def _g_more_y(card: _GCard) -> float:
|
|
1014
|
+
"""
|
|
1015
|
+
The baseline of a box's line of calls not drawn.
|
|
1016
|
+
"""
|
|
1017
|
+
return _g_prose_top(card) + 13 + _GRAPH_PROSE * len(card.prose)
|
|
1018
|
+
|
|
1019
|
+
|
|
1002
1020
|
def _node_svg(
|
|
1003
|
-
card: _GCard,
|
|
1021
|
+
card: _GCard,
|
|
1022
|
+
box: tuple[float, float, float, float],
|
|
1023
|
+
neighbours: list[str],
|
|
1024
|
+
access: dict[str, str],
|
|
1025
|
+
panel: str = "",
|
|
1026
|
+
opens: bool = False,
|
|
1004
1027
|
) -> str:
|
|
1005
1028
|
"""
|
|
1006
1029
|
A box: its badge and name, what's under it, a route's pill, its listed lines (an operation's dot in its arrows'
|
|
1007
|
-
colour, a line a warning is about with its flag) or its docstring, and a warning badge.
|
|
1030
|
+
colour, a line a warning is about with its flag) or its docstring, and a warning badge. panel is the id of its
|
|
1031
|
+
code's panel, and opens says its line of calls not drawn opens a graph of its own (a link drawn over it).
|
|
1008
1032
|
"""
|
|
1009
1033
|
n = card.node
|
|
1010
1034
|
x, y, w, h = box
|
|
@@ -1071,15 +1095,18 @@ def _node_svg(
|
|
|
1071
1095
|
if _g_sans(f"{said}: {names}", 12.5) <= w - 28:
|
|
1072
1096
|
said = f"{said}: {names}"
|
|
1073
1097
|
break
|
|
1074
|
-
parts.append(
|
|
1075
|
-
f'<text class="pr mo" x="14" y="{top + 13 + _GRAPH_PROSE * len(card.prose):.1f}">{_esc(said)}</text>'
|
|
1076
|
-
)
|
|
1098
|
+
parts.append(f'<text class="pr mo md" x="14" y="{_g_more_y(card):.1f}">{_esc(said)}</text>')
|
|
1077
1099
|
if warn:
|
|
1078
1100
|
parts.append(
|
|
1079
1101
|
f'<g class="wb" transform="translate({w - 21:.0f},{badge_y + _G_BADGE / 2:.0f})"><circle r="8.5"/>'
|
|
1080
1102
|
'<path d="M0,-4.2V.6M0,3.9V4"/></g>'
|
|
1081
1103
|
)
|
|
1082
|
-
classes = " ".join(
|
|
1104
|
+
classes = " ".join(
|
|
1105
|
+
["cn", f"k-{n.kind}", f"n-{n.id}", *(f"a-{m}" for m in neighbours)]
|
|
1106
|
+
+ ([f"p-{panel}"] if panel else [])
|
|
1107
|
+
+ (["hd"] if opens else [])
|
|
1108
|
+
+ (["warn"] if warn else [])
|
|
1109
|
+
)
|
|
1083
1110
|
return (
|
|
1084
1111
|
f'<g class="{_esc(classes)}" transform="translate({x:.1f},{y:.1f})">'
|
|
1085
1112
|
f'<rect class="bg" width="{w:.0f}" height="{h:.0f}" rx="11"/>'
|
|
@@ -1114,11 +1141,11 @@ def _g_filled(line: str) -> bool:
|
|
|
1114
1141
|
return bool(re.sub(r"<[^>]*>", "", line).strip())
|
|
1115
1142
|
|
|
1116
1143
|
|
|
1117
|
-
def _g_panel(n: GraphNode, lines_of: Callable[[str], list[str]], editor: str = "") -> str:
|
|
1144
|
+
def _g_panel(n: GraphNode, lines_of: Callable[[str], list[str]], editor: str = "", panel: str = "") -> str:
|
|
1118
1145
|
"""
|
|
1119
|
-
A box's code, in a panel that slides in from the right when the box is clicked (its id
|
|
1120
|
-
it's CSS's :target that opens it): what the box is, then each place it's about with its
|
|
1121
|
-
worth pointing at lit. '' when none of its code is in the files read.
|
|
1146
|
+
A box's code, in a panel that slides in from the right when the box is clicked (its id, panel or the box's own, is
|
|
1147
|
+
the link's target, so it's CSS's :target that opens it): what the box is, then each place it's about with its
|
|
1148
|
+
lines numbered, the lines worth pointing at lit. '' when none of its code is in the files read.
|
|
1122
1149
|
"""
|
|
1123
1150
|
shown, lit_any = [], False
|
|
1124
1151
|
for span in n.code[:_G_PANEL_SPANS]:
|
|
@@ -1157,7 +1184,11 @@ def _g_panel(n: GraphNode, lines_of: Callable[[str], list[str]], editor: str = "
|
|
|
1157
1184
|
rest = len(n.code) - _G_PANEL_SPANS
|
|
1158
1185
|
if rest > 0:
|
|
1159
1186
|
shown.append(f'<div class="csm">+{_plural(rest, "more place")}: the tables below list them all</div>')
|
|
1160
|
-
said = "".join(
|
|
1187
|
+
said = "".join( # not the calls this graph leaves out: a function's panel is the same in every graph it's in
|
|
1188
|
+
f'<p class="csd">{_prose(line)}</p>'
|
|
1189
|
+
for line in (n.tip or "").split("\n")
|
|
1190
|
+
if line and not (n.more and line.startswith("It calls ") and "the graph leaves out" in line)
|
|
1191
|
+
)
|
|
1161
1192
|
if lit_any:
|
|
1162
1193
|
said += (
|
|
1163
1194
|
'<p class="csd csl">The lit lines are where it calls, reads or sends what its arrows show.</p>'
|
|
@@ -1166,7 +1197,7 @@ def _g_panel(n: GraphNode, lines_of: Callable[[str], list[str]], editor: str = "
|
|
|
1166
1197
|
)
|
|
1167
1198
|
mono = " mono" if n.kind in ("handler", "function") else ""
|
|
1168
1199
|
return (
|
|
1169
|
-
f'<aside class="cside" id="{_esc(_g_panel_id(n))}" aria-label="{_esc("The code of " + n.label)}">'
|
|
1200
|
+
f'<aside class="cside" id="{_esc(panel or _g_panel_id(n))}" aria-label="{_esc("The code of " + n.label)}">'
|
|
1170
1201
|
f'<div class="csh"><div class="cst"><span class="csk">{_esc(_G_PANEL_KINDS.get(n.kind, n.kind))}</span>'
|
|
1171
1202
|
f'<b class="csn{mono}">{_esc(n.label)}</b>'
|
|
1172
1203
|
+ (f'<span class="css">{_esc(n.sub)}</span>' if n.sub else "")
|
|
@@ -1368,12 +1399,87 @@ def _g_swatch(kind: str) -> str:
|
|
|
1368
1399
|
)
|
|
1369
1400
|
|
|
1370
1401
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1402
|
+
_G_SCRIPT = """<script>
|
|
1403
|
+
(() => {
|
|
1404
|
+
// A graph's views: a box's line of calls not drawn, the path over a view and the bar's All button link to
|
|
1405
|
+
// #<group>-v<k> (or -all, or -main), and each is a <template> the graph's box, path and key are swapped from.
|
|
1406
|
+
// Any other address (a box's code open, or closed) leaves the view as it is.
|
|
1407
|
+
const parts = ["cgc", "cg", "cgk"];
|
|
1408
|
+
const show = (wrap) => {
|
|
1409
|
+
const group = wrap.dataset.group, id = decodeURIComponent(location.hash.slice(1));
|
|
1410
|
+
const named = id === group + "-main" || id === group + "-all" || id.startsWith(group + "-v");
|
|
1411
|
+
if (id && !named) return;
|
|
1412
|
+
const view = named && id !== group + "-main" ? id : "";
|
|
1413
|
+
if ((wrap.dataset.view || "") === view) return;
|
|
1414
|
+
const from = view ? document.getElementById(view)?.content : wrap.main;
|
|
1415
|
+
if (!from) return;
|
|
1416
|
+
for (const part of parts) {
|
|
1417
|
+
wrap.querySelector(".cgb > ." + part).innerHTML = from.querySelector(".x-" + part).innerHTML;
|
|
1418
|
+
}
|
|
1419
|
+
wrap.dataset.view = view;
|
|
1420
|
+
wrap.querySelector(".cgb").scrollIntoView({block: "nearest"});
|
|
1421
|
+
};
|
|
1422
|
+
for (const wrap of document.querySelectorAll(".cgw[data-group]")) {
|
|
1423
|
+
const main = document.createElement("template");
|
|
1424
|
+
for (const part of parts) {
|
|
1425
|
+
const kept = document.createElement("div");
|
|
1426
|
+
kept.className = "x-" + part;
|
|
1427
|
+
kept.innerHTML = wrap.querySelector(".cgb > ." + part).innerHTML;
|
|
1428
|
+
main.content.append(kept);
|
|
1429
|
+
}
|
|
1430
|
+
wrap.main = main.content;
|
|
1431
|
+
wrap.classList.add("js");
|
|
1432
|
+
show(wrap);
|
|
1433
|
+
}
|
|
1434
|
+
addEventListener("hashchange", () => document.querySelectorAll(".cgw.js").forEach(show));
|
|
1435
|
+
// A box's code: a click anywhere outside its panel closes it, as its × does, and so does Escape. A link (another
|
|
1436
|
+
// box, a view) and the graph's bar do what they do with the panel left to them.
|
|
1437
|
+
const close = () => { if (document.querySelector(".cside:target")) location.hash = "cn-closed"; };
|
|
1438
|
+
document.addEventListener("click", (e) => {
|
|
1439
|
+
const open = document.querySelector(".cside:target");
|
|
1440
|
+
if (open && !open.contains(e.target) && !e.target.closest("a[href], label, input, .cgt")) close();
|
|
1441
|
+
});
|
|
1442
|
+
document.addEventListener("keydown", (e) => { if (e.key === "Escape") close(); });
|
|
1443
|
+
})();
|
|
1444
|
+
</script>"""
|
|
1445
|
+
|
|
1446
|
+
|
|
1447
|
+
def _g_hidden(graph: CodeGraph, opens: bool) -> str:
|
|
1448
|
+
"""
|
|
1449
|
+
The legend's pill for the functions a graph leaves out, and why: without JavaScript where to find them, with it
|
|
1450
|
+
(when opens) that a box's line of them opens them.
|
|
1451
|
+
"""
|
|
1452
|
+
if not graph.hidden:
|
|
1453
|
+
return ""
|
|
1454
|
+
why = (
|
|
1455
|
+
"too deep"
|
|
1456
|
+
if graph.too_deep == graph.hidden
|
|
1457
|
+
else "too many to draw at once"
|
|
1458
|
+
if not graph.too_deep
|
|
1459
|
+
else "too deep or too many"
|
|
1460
|
+
)
|
|
1461
|
+
head = f"<b>+{graph.hidden}</b>{'more function' if graph.hidden == 1 else 'more functions'} not drawn ({why})"
|
|
1462
|
+
pill = f'<span class="kp kh{" nj" if opens else ""}">{head}: the Step by step tab has them all</span>'
|
|
1463
|
+
if opens:
|
|
1464
|
+
pill += f'<span class="kp kh oj">{head}: click a box\'s blue line of calls to open them</span>'
|
|
1465
|
+
return pill
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
def _g_canvas(
|
|
1469
|
+
graph: CodeGraph,
|
|
1470
|
+
block: _Graph,
|
|
1471
|
+
report: _Report,
|
|
1472
|
+
lines_of: Callable[[str], list[str]],
|
|
1473
|
+
panels: dict[str, tuple[str, str]],
|
|
1474
|
+
opens: dict[str, str],
|
|
1475
|
+
prefix: str,
|
|
1476
|
+
) -> tuple[str, str]:
|
|
1477
|
+
"""
|
|
1478
|
+
One graph drawn: its SVG and its legend. Its boxes' panels go into panels ({a function, or a panel without its
|
|
1479
|
+
id: (its id, the panel)}, one for a function or the same code wherever it's drawn, the first graph's; a new one's
|
|
1480
|
+
id starts with prefix), and a box whose function is in opens ({module.name: a
|
|
1481
|
+
link}) has its line of calls not drawn made that link.
|
|
1482
|
+
"""
|
|
1377
1483
|
cards = _g_cards(graph)
|
|
1378
1484
|
boxes, routes, width, tall, xs, widths, column_of = _graph_layout(graph, cards)
|
|
1379
1485
|
ends = _g_anchors(graph, cards, boxes)
|
|
@@ -1386,20 +1492,24 @@ def _graph_html(block: _Graph, report: _Report) -> str:
|
|
|
1386
1492
|
for name in _g_names(e):
|
|
1387
1493
|
access[e.target].setdefault(name, e.kind)
|
|
1388
1494
|
root = report.root or _ROOT
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1495
|
+
panel_of: dict[str, str] = {} # a box -> its panel's id
|
|
1496
|
+
if block.sources:
|
|
1497
|
+
for n in graph.nodes:
|
|
1498
|
+
same = "\0" + n.name if n.name else ""
|
|
1499
|
+
if same not in panels:
|
|
1500
|
+
html = _g_panel(n, lines_of, block.editor, "\0")
|
|
1501
|
+
if not html:
|
|
1502
|
+
continue
|
|
1503
|
+
same = same or html
|
|
1504
|
+
if same not in panels:
|
|
1505
|
+
key = prefix + _g_panel_id(n)
|
|
1506
|
+
panels[same] = (key, html.replace('id="\0"', f'id="{key}"', 1))
|
|
1507
|
+
panel_of[n.id] = panels[same][0]
|
|
1508
|
+
for key in set(panel_of.values()): # the box whose code is open, outlined
|
|
1509
|
+
report.rules.add(
|
|
1510
|
+
f".{root} .cgw:has(#{key}:target) .cg .p-{key} .fg"
|
|
1511
|
+
"{stroke:light-dark(#2563eb,#60a5fa);stroke-opacity:1;stroke-width:2}"
|
|
1512
|
+
)
|
|
1403
1513
|
for n in graph.nodes: # pointing at a box leaves only it, its arrows and the boxes they join in sight
|
|
1404
1514
|
report.rules.add(f".{root} .cg:has(.n-{n.id}:hover) .ce:not(.s-{n.id}):not(.t-{n.id}){{opacity:.06}}")
|
|
1405
1515
|
report.rules.add(f".{root} .cg:has(.n-{n.id}:hover) .cn:not(.n-{n.id}):not(.a-{n.id}){{opacity:.28}}")
|
|
@@ -1446,16 +1556,33 @@ def _graph_html(block: _Graph, report: _Report) -> str:
|
|
|
1446
1556
|
marked.add((e.source, e.target))
|
|
1447
1557
|
edges = "".join(body for body, _ in drawn)
|
|
1448
1558
|
ports = "".join(dots for _, dots in drawn)
|
|
1559
|
+
opened = {n.id: opens[n.name] for n in graph.nodes if n.more and n.name in opens}
|
|
1449
1560
|
nodes = "".join(
|
|
1450
1561
|
(
|
|
1451
|
-
f'<a class="cl" href="#{_esc(
|
|
1452
|
-
if
|
|
1562
|
+
f'<a class="cl" href="#{_esc(panel_of[n.id])}" aria-label="{_esc("Show the code of " + n.label)}">{box}</a>'
|
|
1563
|
+
if n.id in panel_of
|
|
1453
1564
|
else box
|
|
1454
1565
|
)
|
|
1455
1566
|
for n in graph.nodes
|
|
1456
|
-
for box in [
|
|
1567
|
+
for box in [
|
|
1568
|
+
_node_svg(
|
|
1569
|
+
cards[n.id],
|
|
1570
|
+
boxes[n.id],
|
|
1571
|
+
list(dict.fromkeys(near[n.id])),
|
|
1572
|
+
access[n.id],
|
|
1573
|
+
panel_of.get(n.id, ""),
|
|
1574
|
+
n.id in opened,
|
|
1575
|
+
)
|
|
1576
|
+
]
|
|
1577
|
+
)
|
|
1578
|
+
links = "".join( # over each line of calls not drawn that opens a graph of its own: drawn with JavaScript only
|
|
1579
|
+
f'<a class="dr" href="#{_esc(link)}" aria-label="{_esc(f"Open the calls {n.label} makes")}">'
|
|
1580
|
+
f'<rect x="{boxes[n.id][0] + 8:.1f}" y="{boxes[n.id][1] + _g_more_y(cards[n.id]) - 14:.1f}" '
|
|
1581
|
+
f'width="{boxes[n.id][2] - 16:.0f}" height="19" rx="5"/></a>'
|
|
1582
|
+
for n in graph.nodes
|
|
1583
|
+
if (link := opened.get(n.id))
|
|
1457
1584
|
)
|
|
1458
|
-
tips = "".join(_g_tip_svg(n, boxes[n.id], width, tall, lines_of,
|
|
1585
|
+
tips = "".join(_g_tip_svg(n, boxes[n.id], width, tall, lines_of, n.id in panel_of) for n in graph.nodes)
|
|
1459
1586
|
kinds = {e.kind for e in graph.edges}
|
|
1460
1587
|
pills = [f'<span class="kp">{_g_swatch(k)}{_esc(word)}</span>' for k, word in _G_LEGEND if k in kinds]
|
|
1461
1588
|
if any(e.guessed for e in graph.edges):
|
|
@@ -1474,22 +1601,71 @@ def _graph_html(block: _Graph, report: _Report) -> str:
|
|
|
1474
1601
|
f'aria-hidden="true"><circle r="7"/><path transform="scale(.6) translate(-8,-8)" d="{_G_LOOP}"/>'
|
|
1475
1602
|
"</svg>in a loop: once per item</span>"
|
|
1476
1603
|
)
|
|
1477
|
-
hidden = (
|
|
1478
|
-
(
|
|
1479
|
-
f'<span class="kp kh"><b>+{graph.hidden}</b>{"more function" if graph.hidden == 1 else "more functions"} '
|
|
1480
|
-
"not drawn (too deep or too many): the Step by step tab has them all</span>"
|
|
1481
|
-
)
|
|
1482
|
-
if graph.hidden
|
|
1483
|
-
else ""
|
|
1484
|
-
)
|
|
1485
1604
|
hint = (
|
|
1486
1605
|
'<span class="kt">Point at a box to see only what it\'s joined to'
|
|
1487
|
-
+ ("; click it to read its code" if
|
|
1606
|
+
+ ("; click it to read its code" if panel_of else "")
|
|
1488
1607
|
+ "</span>"
|
|
1489
1608
|
)
|
|
1609
|
+
svg = (
|
|
1610
|
+
f'<svg viewBox="0 0 {width:.0f} {tall:.0f}" width="{width:.0f}" height="{tall:.0f}" '
|
|
1611
|
+
f'style="--w:{width:.0f}px" role="img" aria-label="{_esc(block.title or "How the code fits together")}">'
|
|
1612
|
+
f"{_g_captions(graph, xs, widths, column_of)}{edges}{nodes}{links}{ports}{tips}</svg>"
|
|
1613
|
+
)
|
|
1614
|
+
return svg, "".join(pills) + _g_hidden(graph, bool(opened)) + hint
|
|
1615
|
+
|
|
1616
|
+
|
|
1617
|
+
def _g_path(path: list[str], links: dict[str, str], main: str, last: str = "") -> str:
|
|
1618
|
+
"""
|
|
1619
|
+
The calls from the entry point down to a view's function, over its graph: each one that has a view of its own a
|
|
1620
|
+
link to it, and a link back to the whole graph first. last, when given, is said after them in place of the last.
|
|
1621
|
+
"""
|
|
1622
|
+
names = [_esc(_short_name(name)) + "()" for name in path]
|
|
1623
|
+
steps = [
|
|
1624
|
+
f'<a href="#{_esc(links[name])}"><code>{said}</code></a>' if name in links else f"<code>{said}</code>"
|
|
1625
|
+
for name, said in zip(path[:-1], names[:-1])
|
|
1626
|
+
]
|
|
1627
|
+
steps.append(_esc(last) if last else f"<b><code>{names[-1]}</code></b>" if names else "")
|
|
1628
|
+
return f'<a class="cgcb" href="#{_esc(main)}">← The whole graph</a>' + "".join(
|
|
1629
|
+
f'<span class="cgcs">›</span>{step}' for step in steps
|
|
1630
|
+
)
|
|
1631
|
+
|
|
1632
|
+
|
|
1633
|
+
@_draws(_Graph, "html")
|
|
1634
|
+
def _graph_html(block: _Graph, report: _Report) -> str:
|
|
1635
|
+
title = f"<h4>{_prose(block.title)}</h4>" if block.title else ""
|
|
1636
|
+
graph = block.graph
|
|
1637
|
+
if not graph.nodes:
|
|
1638
|
+
return title + f'<div class="more">{_esc(block.empty)}</div>'
|
|
1639
|
+
highlighted: dict[str, list[str]] = {}
|
|
1640
|
+
|
|
1641
|
+
def lines_of(file: str) -> list[str]: # a file's lines in colour, each file coloured once
|
|
1642
|
+
if file not in highlighted:
|
|
1643
|
+
highlighted[file] = _python_lines(block.sources[file]) if file in block.sources else []
|
|
1644
|
+
return highlighted[file]
|
|
1645
|
+
|
|
1490
1646
|
made = report.shared("graphs", lambda: [0]) # graphs drawn so far, so each one's buttons have ids of their own
|
|
1491
1647
|
group = f"cn-graph{made[0]}"
|
|
1492
1648
|
made[0] += 1
|
|
1649
|
+
links = {view.name: f"{group}-v{k}" for k, view in enumerate(block.views, 1)}
|
|
1650
|
+
panels: dict[str, tuple[str, str]] = {}
|
|
1651
|
+
svg, legend = _g_canvas(graph, block, report, lines_of, panels, links, "")
|
|
1652
|
+
views = []
|
|
1653
|
+
for k, view in enumerate(block.views, 1):
|
|
1654
|
+
drawn, key = _g_canvas(view.graph, block, report, lines_of, panels, links, f"v{k}-")
|
|
1655
|
+
views.append(
|
|
1656
|
+
f'<template id="{group}-v{k}"><div class="x-cgc">{_g_path(view.path, links, group + "-main")}</div>'
|
|
1657
|
+
f'<div class="x-cg">{drawn}</div><div class="x-cgk">{key}</div></template>'
|
|
1658
|
+
)
|
|
1659
|
+
everything = ""
|
|
1660
|
+
if block.everything is not None and block.everything.nodes:
|
|
1661
|
+
count = sum(1 for n in block.everything.nodes if n.kind in ("handler", "function"))
|
|
1662
|
+
drawn, key = _g_canvas(block.everything, block, report, lines_of, panels, {}, "all-")
|
|
1663
|
+
views.append(
|
|
1664
|
+
f'<template id="{group}-all"><div class="x-cgc">'
|
|
1665
|
+
f"{_g_path([], links, group + '-main', f'Every function, all {count} of them')}</div>"
|
|
1666
|
+
f'<div class="x-cg">{drawn}</div><div class="x-cgk">{key}</div></template>'
|
|
1667
|
+
)
|
|
1668
|
+
everything = f'<a class="cgta oj" href="#{group}-all" title="Every function in one graph">All {count}</a>'
|
|
1493
1669
|
# its zoom and full screen: inputs before the box they change, so CSS reaches it with ~ (no :has(), no script)
|
|
1494
1670
|
switches = f'<input type="checkbox" class="cgs cgfs" id="{group}-full">' + "".join(
|
|
1495
1671
|
f'<input type="radio" class="cgs cgz-{key}" name="{group}-zoom" id="{group}-{key}"'
|
|
@@ -1497,7 +1673,7 @@ def _graph_html(block: _Graph, report: _Report) -> str:
|
|
|
1497
1673
|
for key, _, _ in _GRAPH_ZOOMS
|
|
1498
1674
|
)
|
|
1499
1675
|
bar = (
|
|
1500
|
-
'<div class="cgt"
|
|
1676
|
+
f'<div class="cgt">{everything}<span class="cgtz" role="group" aria-label="Zoom">'
|
|
1501
1677
|
+ "".join(
|
|
1502
1678
|
f'<label class="zl-{key}" for="{group}-{key}" title="{_esc(tip)}">{_esc(word)}</label>'
|
|
1503
1679
|
for key, word, tip in _GRAPH_ZOOMS
|
|
@@ -1505,14 +1681,18 @@ def _graph_html(block: _Graph, report: _Report) -> str:
|
|
|
1505
1681
|
+ f'</span><label class="cgtf" for="{group}-full"><span class="on">Full screen</span>'
|
|
1506
1682
|
'<span class="off">Close full screen</span></label></div>'
|
|
1507
1683
|
)
|
|
1684
|
+
script = ""
|
|
1685
|
+
if (views or panels) and not report.shared("graph-script", lambda: [False])[0]:
|
|
1686
|
+
report.shared("graph-script", lambda: [False])[0] = True
|
|
1687
|
+
script = _G_SCRIPT
|
|
1508
1688
|
# in a box of its own with its panels: the :has() rules that outline a box whose code is open look only in here,
|
|
1509
1689
|
# so a radio button elsewhere (the walk's steps) doesn't make the browser restyle the whole page
|
|
1690
|
+
data = f' data-group="{group}"' if views else ""
|
|
1510
1691
|
return (
|
|
1511
|
-
f'{title}<div class="cgw">{switches}<div class="cgb">{bar}
|
|
1512
|
-
f'<
|
|
1513
|
-
f'
|
|
1514
|
-
f"{
|
|
1515
|
-
f'<div class="cgk">{"".join(pills)}{hidden}{hint}</div></div>{"".join(panels.values())}</div>'
|
|
1692
|
+
f'{title}<div class="cgw"{data}>{switches}<div class="cgb">{bar}'
|
|
1693
|
+
f'<nav class="cgc" aria-label="Where this graph starts"></nav><div class="cg">{svg}</div>'
|
|
1694
|
+
f'<div class="cgk">{legend}</div></div>{"".join(html for _, html in panels.values())}{"".join(views)}</div>'
|
|
1695
|
+
f"{script}"
|
|
1516
1696
|
)
|
|
1517
1697
|
|
|
1518
1698
|
|
code_node/report/style.py
CHANGED
|
@@ -125,6 +125,20 @@ _EXTRA_CSS = """\
|
|
|
125
125
|
.cnd .cgfs:checked~.cgz-fit:checked~.cgb .cg svg{width:100%;height:100%;max-width:none}
|
|
126
126
|
.cnd .cgfs:checked~.cgz-auto:checked~.cgb .cg svg{min-width:0}
|
|
127
127
|
@media (min-width:1100px){.cnd .cgw:has(.cside:target) .cgfs:checked~.cgb{right:min(660px,94vw)}}
|
|
128
|
+
.cnd .cgw:not(.js) :is(.oj,.dr),.cnd .cgw.js .nj{display:none}
|
|
129
|
+
.cnd .cgta{padding:3px 10px;border:1px solid rgba(127,127,127,.28);border-radius:8px;color:inherit;text-decoration:none;opacity:.7}
|
|
130
|
+
.cnd .cgta:hover{opacity:.95;background:rgba(127,127,127,.08)}
|
|
131
|
+
.cnd .cgw[data-view$="-all"] .cgta{opacity:1;font-weight:650;background:rgba(127,127,127,.14)}
|
|
132
|
+
.cnd .cgc{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 7px;margin:0 0 6px;font-size:13px}
|
|
133
|
+
.cnd .cgc:empty{display:none}
|
|
134
|
+
.cnd .cgc a{color:light-dark(#2563eb,#60a5fa);text-decoration:none}
|
|
135
|
+
.cnd .cgc a:hover{text-decoration:underline}
|
|
136
|
+
.cnd .cgc code{font-family:var(--mono);font-size:12.5px}
|
|
137
|
+
.cnd .cgc .cgcs{opacity:.4}
|
|
138
|
+
.cnd .cg .dr rect{fill:transparent;cursor:pointer}
|
|
139
|
+
.cnd .cg .dr:hover rect{fill:rgba(37,99,235,.09)}
|
|
140
|
+
.cnd .cg .dr:focus-visible{outline:2px solid light-dark(#2563eb,#60a5fa)}
|
|
141
|
+
.cnd .cgw.js .cg .hd .md{fill:light-dark(#2563eb,#60a5fa);fill-opacity:1;font-style:normal}
|
|
128
142
|
.cnd .cg .cap{font-size:12px;font-weight:650;letter-spacing:1.2px;fill:currentColor;fill-opacity:.6}
|
|
129
143
|
.cnd .cg .capl{stroke:currentColor;stroke-opacity:.14;stroke-width:1;fill:none}
|
|
130
144
|
.cnd .cg .cn,.cnd .cg .ce{transition:opacity .15s}
|
code_node/serve.py
CHANGED
|
@@ -3,8 +3,8 @@ code-node --serve: the page on a local server, made again whenever a .py file (o
|
|
|
3
3
|
under the folder changes, and the browser told to reload (keeping its scroll position, and the tab and step that were
|
|
4
4
|
open). Standard library only: a polling watcher thread, a threaded HTTP server, and Server-Sent Events for the reload.
|
|
5
5
|
|
|
6
|
-
The
|
|
7
|
-
|
|
6
|
+
The served page gets a few lines more than a saved one (_LIVE_SCRIPT), to reload and to say when the server has
|
|
7
|
+
stopped.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: code-node-graph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: See how Python code works without running it: a graph of who calls whom from an entry point, and what each path reaches.
|
|
5
5
|
Project-URL: Homepage, https://github.com/utkarsh5026/code-node
|
|
6
6
|
Project-URL: Documentation, https://utkarsh5026.github.io/code-node/
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
code_node/__init__.py,sha256=
|
|
1
|
+
code_node/__init__.py,sha256=hzITZuWbNbw6NyeHwo4MC9MApmpFcoJwdY43MsPrPQw,1106
|
|
2
2
|
code_node/__main__.py,sha256=0W2bmerF1S96rjlTNJZKVzO4FvVhN6ENzAl451A08sQ,101
|
|
3
3
|
code_node/cli.py,sha256=GI0vZY1t4Pf7qtZI2coTpWZi-ulQEWNREDJPkTtmxG0,12302
|
|
4
4
|
code_node/pages.py,sha256=2UOrHUz3G_vAjPO89phJxhF4652FysVLgrLtJ-Iw6p4,3234
|
|
5
5
|
code_node/project.py,sha256=1s6pBwQ-hiEf9v0YzE8b6TETT9E0XBnzQPWGjYDJsnw,16361
|
|
6
|
-
code_node/serve.py,sha256=
|
|
6
|
+
code_node/serve.py,sha256=4yvzDaaL1XpsVinLmvlm1W2bM6WJRGe5GI8wG3XuHic,11456
|
|
7
7
|
code_node/analysis/__init__.py,sha256=-KM77rYv9pPutUG29DsU8ZkAUtr2AU9RsqrDPFAjLoQ,103
|
|
8
8
|
code_node/analysis/aws_model.py,sha256=R2sHppdu9PmlsOGDXf6_6JHumbA5PqbiJ0JgnyB9Xcs,6648
|
|
9
9
|
code_node/analysis/findings.py,sha256=S1-VQ1iNi1PkC-2eigNzbJrULa_2Y46avmtwAlV9iqU,19010
|
|
10
|
-
code_node/analysis/graph.py,sha256=
|
|
10
|
+
code_node/analysis/graph.py,sha256=4FCzBo3G0XOkXV-zkshFxaoCalqWqbJfGDGDe-pg8S8,39373
|
|
11
11
|
code_node/analysis/mapping.py,sha256=6wYkUqaQdM-Vio8XGf_Lw8N9dP_90YtbUxauH4IQ8Tg,5102
|
|
12
12
|
code_node/analysis/model.py,sha256=ORGT6BEQFonQjdRGPEbuGVGoyyK54nTEA4Th-7rEGvU,13614
|
|
13
|
-
code_node/analysis/reader.py,sha256=
|
|
13
|
+
code_node/analysis/reader.py,sha256=QIqVN2RV477ON69_5YVLXhfnfL3seS8kvP7krpmZPNI,89304
|
|
14
14
|
code_node/analysis/syntax.py,sha256=wyeUr68IDjGp3i9HYYYzcjw8-CvL6a6mmZdWz5LY8j8,7952
|
|
15
15
|
code_node/analysis/terms.py,sha256=ec0f51DkfDRixJSRX1hnyq9Hr5aLRhXuBRHlNJBlZD8,13782
|
|
16
16
|
code_node/deploys/__init__.py,sha256=QEH2oQNPACPekg6QSBpfSbqkuhrdNDpelhn_VaY0RNs,11572
|
|
@@ -32,11 +32,11 @@ code_node/platforms/aws_lambda.py,sha256=IoYxJ2IPNkuxXMWSRK5mxaaDtmKQsyq7Q1EkXCp
|
|
|
32
32
|
code_node/platforms/web.py,sha256=lNjwkz3D0zJkFcJhqD_ShcfTj4uF8miLNqKMtXu-t1U,11762
|
|
33
33
|
code_node/report/__init__.py,sha256=l3dfZFbLEo-7Fl7O_Rk2lvyAElQnyugSZPI_PzHrG_Q,76
|
|
34
34
|
code_node/report/brands.py,sha256=n32BfFz2d02ZVUHvy8L4wPHBtiD_1FCTKzdR4NyvU88,8372
|
|
35
|
-
code_node/report/build.py,sha256=
|
|
35
|
+
code_node/report/build.py,sha256=xnPK_6FMTSV2nyS-N6KcApdq3ZNBA_0Hy8AUZDVIea0,4884
|
|
36
36
|
code_node/report/editor.py,sha256=TRUX6BTB-oe-m5Vf3xM9PppnC99sCLL4PObXkYAGKoU,1466
|
|
37
|
-
code_node/report/graph.py,sha256=
|
|
37
|
+
code_node/report/graph.py,sha256=1SEMabLWWL9E0kwhKLYZhRFj0pRzLk4eDpHfLsxuuiQ,75162
|
|
38
38
|
code_node/report/outline.py,sha256=3RRxU4FMUZKBVjFDsscKZkvDlj_W4ZeW4DwUztg4wd0,8539
|
|
39
|
-
code_node/report/style.py,sha256=
|
|
39
|
+
code_node/report/style.py,sha256=YTQXlWjsnVfAZBhTr_Ot0PjdyLRSW83WDR3F02YBm4E,36833
|
|
40
40
|
code_node/report/tabs.py,sha256=K2XR16owfgEL1kfux61PI9nBcRsgjQurTx1ptMM5Mac,3016
|
|
41
41
|
code_node/report/walk.py,sha256=LWJDGJ3efF0fPWlMO4G4HBQqnNVn2RHXgLeuLrC-WnA,29466
|
|
42
42
|
code_node/report/logos/README.md,sha256=8Evw8eApl_56QJrwY7LAtGgJMZE0mp0-HIpRz9bu1ME,613
|
|
@@ -171,8 +171,8 @@ code_node/report/logos/small/aws-systems-manager.svg,sha256=9VzzFdTjaRZO9ulR8Ulp
|
|
|
171
171
|
code_node/report/logos/small/aws-timestream.svg,sha256=yKdKIj7pbooFqDKClEQxW9h7dYV5mnDn0RCHnLUDf8Q,639
|
|
172
172
|
code_node/report/logos/small/aws-waf.svg,sha256=l9ZzBZAl2g8bK1-m6A50ga37l5g1t-9Mj70bxWDykWE,591
|
|
173
173
|
code_node/report/logos/small/aws-xray.svg,sha256=VKg_QtKGZKDCbZvKBRDn7eReuNVbJpduBUVV8MQrZZU,591
|
|
174
|
-
code_node_graph-0.
|
|
175
|
-
code_node_graph-0.
|
|
176
|
-
code_node_graph-0.
|
|
177
|
-
code_node_graph-0.
|
|
178
|
-
code_node_graph-0.
|
|
174
|
+
code_node_graph-0.4.0.dist-info/METADATA,sha256=7tOOzTXKx0lvUJUZRXK4KfuWwvi_lz3RDPhnpnTn3aE,7619
|
|
175
|
+
code_node_graph-0.4.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
176
|
+
code_node_graph-0.4.0.dist-info/entry_points.txt,sha256=eEArp-uk7ZzODIWr6l62rI0-mIA0-mLDaqJEyKikPJw,49
|
|
177
|
+
code_node_graph-0.4.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
178
|
+
code_node_graph-0.4.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|