code-node-graph 0.2.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 +42 -8
- code_node/report/walk.py +42 -7
- code_node/serve.py +2 -2
- {code_node_graph-0.2.0.dist-info → code_node_graph-0.4.0.dist-info}/METADATA +1 -1
- {code_node_graph-0.2.0.dist-info → code_node_graph-0.4.0.dist-info}/RECORD +13 -13
- {code_node_graph-0.2.0.dist-info → code_node_graph-0.4.0.dist-info}/WHEEL +0 -0
- {code_node_graph-0.2.0.dist-info → code_node_graph-0.4.0.dist-info}/entry_points.txt +0 -0
- {code_node_graph-0.2.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}
|
|
@@ -298,13 +312,27 @@ _EXTRA_CSS = """\
|
|
|
298
312
|
.cnd .walk .ws{border-left:1px solid rgba(127,127,127,.2)}
|
|
299
313
|
.cnd .walk .wch{position:sticky;top:0;z-index:2;padding:12px 16px 9px;font-size:12.5px;font-weight:650;background:var(--bg);border-bottom:1px solid rgba(127,127,127,.14)}
|
|
300
314
|
.cnd .walk .wr{display:block;width:0;height:0;margin:0;border:0;padding:0;opacity:0;appearance:none;-webkit-appearance:none;pointer-events:none}
|
|
301
|
-
.cnd .walk .wil{display:flex;align-items:center;gap:10px;min-height:30px;padding:0 14px 0 4px;cursor:pointer;white-space:nowrap;user-select:none;-webkit-user-select:none;transition:background-color .
|
|
315
|
+
.cnd .walk .wil{display:flex;align-items:center;gap:10px;min-height:30px;padding:0 14px 0 4px;cursor:pointer;white-space:nowrap;user-select:none;-webkit-user-select:none;transition:background-color .2s,box-shadow .2s;animation:cnd-row .22s ease-out}
|
|
316
|
+
@keyframes cnd-row{from{opacity:0;transform:translateY(-3px)}}
|
|
317
|
+
.cnd .walk .wcx{position:absolute;width:0;height:0;margin:0;opacity:0;pointer-events:none}
|
|
318
|
+
.cnd .walk .wtg{position:relative;z-index:1;display:block;height:0}
|
|
319
|
+
.cnd .walk .wtg i{position:absolute;top:0;left:calc(40px + var(--d)*16px);width:16px;height:30px;border-radius:4px;cursor:pointer;opacity:.45;transition:opacity .15s,background-color .15s}
|
|
320
|
+
.cnd .walk .wtg i::before{content:"";position:absolute;left:5px;top:12px;width:5px;height:5px;border-right:1.6px solid currentColor;border-bottom:1.6px solid currentColor;transform:rotate(45deg);transition:transform .2s cubic-bezier(.3,.7,.3,1)}
|
|
321
|
+
.cnd .walk .wtg:hover i{opacity:1;background:rgba(127,127,127,.14)}
|
|
322
|
+
.cnd .walk .wcx:checked+.wtg i{opacity:.8}
|
|
323
|
+
.cnd .walk .wcx:checked+.wtg i::before{transform:translate(-1px,1px) rotate(-45deg)}
|
|
324
|
+
.cnd .walk .wcnt{display:none;flex:none;font-size:11px;padding:0 7px;border-radius:9px;line-height:17px;background:rgba(127,127,127,.14);opacity:.75;animation:cnd-in .2s ease}
|
|
325
|
+
.cnd .walk .wcx:checked+.wtg+.wr+.wil .wcnt{display:inline-block}
|
|
302
326
|
.cnd .walk .wil:hover{background-color:rgba(127,127,127,.08)}
|
|
303
327
|
.cnd .walk .wr:checked+.wil{background-color:rgba(245,158,11,.16);box-shadow:inset 3px 0 #f59e0b}
|
|
304
328
|
.cnd .walk .wr:focus-visible+.wil{outline:2px solid var(--blue);outline-offset:-2px}
|
|
305
329
|
.cnd .walk .wno{flex:0 0 26px;text-align:right;font:11px var(--mono);opacity:.4;font-variant-numeric:tabular-nums}
|
|
306
|
-
.cnd .walk .wr:checked+.wil .wno{opacity:.
|
|
307
|
-
.cnd .walk .
|
|
330
|
+
.cnd .walk .wr:checked+.wil .wno{opacity:.9;color:#d97706;font-weight:600}
|
|
331
|
+
.cnd .walk .wtx{transition:transform .2s ease}
|
|
332
|
+
.cnd .walk .wr:checked+.wil .wtx{transform:translateX(2px)}
|
|
333
|
+
.cnd .walk .wr:checked+.wil .wdot{animation:cnd-ping 1s ease-out 2}
|
|
334
|
+
@keyframes cnd-ping{from{box-shadow:0 0 0 0 var(--c,rgba(127,127,127,.6))}to{box-shadow:0 0 0 6px transparent}}
|
|
335
|
+
.cnd .walk .wx{display:flex;align-items:center;align-self:stretch;gap:8px;min-width:0;flex:1;padding-left:calc(var(--d)*16px + 18px);background:repeating-linear-gradient(90deg,rgba(127,127,127,.18) 0 1px,transparent 1px 16px) 8px 0/calc(var(--d)*16px) 100% no-repeat}
|
|
308
336
|
.cnd .walk .wtx{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
309
337
|
.cnd .walk .wtx code{font:12px var(--mono);background:none;padding:0}
|
|
310
338
|
.cnd .walk :is(.k-def,.k-module) .wtx b{font:650 12.5px var(--mono)}
|
|
@@ -312,7 +340,10 @@ _EXTRA_CSS = """\
|
|
|
312
340
|
.cnd .walk .wdot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--c,rgba(127,127,127,.6))}
|
|
313
341
|
.cnd .walk :is(.d-aws-reads,.d-lib-reads){--c:#3b82f6}.cnd .walk :is(.d-aws-writes,.d-lib-writes){--c:#ea580c}
|
|
314
342
|
.cnd .walk :is(.d-aws-sends,.d-lib-sends,.d-http){--c:#db2777}.cnd .walk :is(.d-aws-runs,.d-lib-runs){--c:#9333ea}.cnd .walk .d-aws-signs{--c:#64748b}
|
|
315
|
-
.cnd .walk .wc{display:none;position:absolute;left:var(--lw);right:0;top:0;height:var(--ch);box-sizing:border-box;overflow:auto;padding:14px 22px 12px;white-space:normal;cursor:auto;border-left:1px solid rgba(127,127,127,.2);border-bottom:1px solid rgba(127,127,127,.18);background:var(--bg)
|
|
343
|
+
.cnd .walk .wc{display:none;position:absolute;left:var(--lw);right:0;top:0;height:var(--ch);box-sizing:border-box;overflow:auto;padding:14px 22px 12px;white-space:normal;cursor:auto;border-left:1px solid rgba(127,127,127,.2);border-bottom:1px solid rgba(127,127,127,.18);background:var(--bg)}
|
|
344
|
+
.cnd .walk .wc>*{animation:cnd-in .24s ease both}
|
|
345
|
+
.cnd .walk .wc>.whead{animation-delay:.03s}.cnd .walk .wc>.wstk{animation-delay:.06s}.cnd .walk .wc>.wsay{animation-delay:.09s}.cnd .walk .wc>.wthen{animation-delay:.12s}
|
|
346
|
+
.cnd .walk .wfacts li{animation:cnd-in .24s ease both;animation-delay:.12s}.cnd .walk .wfacts li:nth-child(2){animation-delay:.15s}.cnd .walk .wfacts li:nth-child(n+3){animation-delay:.18s}
|
|
316
347
|
.cnd .walk .wr:checked+.wil+.wc{display:block}
|
|
317
348
|
.cnd .walk .wnav{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
|
318
349
|
.cnd .walk .wbt{display:inline-flex;align-items:center;gap:5px;padding:4px 14px;border:1px solid rgba(127,127,127,.32);border-radius:8px;font-size:13px;font-weight:600;line-height:20px;cursor:pointer;user-select:none;-webkit-user-select:none;transition:background-color .12s,transform .06s}
|
|
@@ -348,7 +379,9 @@ _EXTRA_CSS = """\
|
|
|
348
379
|
.cnd .walk :is(.wfm,.wfv){opacity:.6}
|
|
349
380
|
.cnd .walk .wthen{margin-top:10px;font-size:12px;opacity:.6}
|
|
350
381
|
.cnd .walk .wf{display:none;position:absolute;left:var(--lw);right:0;top:var(--ch);bottom:0;flex-direction:column;border-left:1px solid rgba(127,127,127,.2);background:var(--bg)}
|
|
351
|
-
.cnd .walk .wfn>.wr:checked~.wf{display:flex}
|
|
382
|
+
.cnd .walk .wfn>.wr:checked~.wf{display:flex;animation:cnd-fade .25s ease}
|
|
383
|
+
.cnd .walk .wfh{animation:cnd-in .25s ease}
|
|
384
|
+
@keyframes cnd-fade{from{opacity:0}}
|
|
352
385
|
.cnd .walk .wfh{display:flex;flex-wrap:wrap;align-items:baseline;gap:4px 10px;padding:8px 22px;font-size:12px;border-bottom:1px solid rgba(127,127,127,.12)}
|
|
353
386
|
.cnd .walk .wfh b{font:650 12.5px var(--mono)}
|
|
354
387
|
.cnd .walk .wfh span{opacity:.62}
|
|
@@ -356,15 +389,16 @@ _EXTRA_CSS = """\
|
|
|
356
389
|
.cnd .walk .wfw{position:relative;flex:1;min-height:0;overflow-x:auto;overflow-y:hidden;container-type:size;background:rgba(127,127,127,.03)}
|
|
357
390
|
.cnd .walk .wfp{position:relative;box-sizing:border-box;width:max-content;min-width:100%;padding:var(--pad) 0;font:12.5px/var(--lh) var(--mono);
|
|
358
391
|
translate:0 min(0px,max(calc(100cqh - var(--n)*var(--lh) - 2*var(--pad)),calc(36cqh - var(--k,0)*var(--lh))));transition:translate .32s cubic-bezier(.25,.8,.25,1)}
|
|
359
|
-
.cnd .walk .wbar{position:absolute;left:0;right:0;top:calc(var(--pad) + var(--k,-9)*var(--lh));height:var(--lh);background:rgba(245,158,11,.2);box-shadow:inset 3px 0 #f59e0b;transition:top .32s cubic-bezier(.25,.8,.25,1)}
|
|
392
|
+
.cnd .walk .wbar{position:absolute;left:0;right:0;top:calc(var(--pad) + var(--k,-9)*var(--lh));height:var(--lh);background:rgba(245,158,11,.2);box-shadow:inset 3px 0 #f59e0b;transition:top .32s cubic-bezier(.25,.8,.25,1);animation:cnd-glow 1.2s ease-in-out 2}
|
|
393
|
+
@keyframes cnd-glow{50%{background:rgba(245,158,11,.3)}}
|
|
360
394
|
.cnd .walk .sl{position:relative;display:block;padding-right:16px;white-space:pre}
|
|
361
395
|
.cnd .walk span.sl{opacity:.6}
|
|
362
|
-
.cnd .walk label.sl{cursor:pointer}
|
|
396
|
+
.cnd .walk label.sl{cursor:pointer;transition:background-color .15s}
|
|
363
397
|
.cnd .walk label.sl:hover{background:rgba(127,127,127,.1)}
|
|
364
398
|
.cnd .walk .sn{display:inline-block;width:3.2em;padding-right:14px;text-align:right;opacity:.42;user-select:none;-webkit-user-select:none}
|
|
365
399
|
.cnd .walk label.sl .sn{opacity:.8}
|
|
366
400
|
@media (max-width:860px){.cnd .walk{--lt:40vh;grid-template-columns:minmax(0,1fr);grid-template-rows:var(--lt) 1fr;height:calc(var(--lt) + var(--ch) + 380px)}.cnd .walk .wl{max-height:none}.cnd .walk :is(.wc,.wf){left:0;border-left:0}.cnd .walk .wc{top:var(--lt);border-top:1px solid rgba(127,127,127,.2);padding:12px 16px}.cnd .walk .wf{top:calc(var(--lt) + var(--ch))}.cnd .walk .ws{border-left:0}}
|
|
367
|
-
@media (prefers-reduced-motion:reduce){.cnd .walk .wfp,.
|
|
401
|
+
@media (prefers-reduced-motion:reduce){.cnd .walk :is(.wfp,.wbar,.wc,.wc>*,.wfacts li,.wil,.wtx,.wdot,.wcnt,.wf,.wfh,.wtg i,.wtg i::before),.cnd .tabs>.tp{transition:none;animation:none}}"""
|
|
368
402
|
|
|
369
403
|
_CSS = _base_css(_ROOT, _EXTRA_CSS)
|
|
370
404
|
|
code_node/report/walk.py
CHANGED
|
@@ -7,6 +7,11 @@ No JavaScript: each step is a radio button (one group, so once one has focus the
|
|
|
7
7
|
Each function's steps sit in a box of their own, with the function's code drawn once at the end of it; the CSS shows
|
|
8
8
|
the card after the checked button and the code of its box with sibling selectors, so the light slides from line to
|
|
9
9
|
line as you step, and a step costs the same however many there are.
|
|
10
|
+
|
|
11
|
+
The list folds like a tree: a step with steps under it has a checkbox before it, and its arrow folds them away. Each
|
|
12
|
+
step under it carries the class `wu<step>`, so one rule per fold hides them, whether they sit beside it or inside the
|
|
13
|
+
box of a function it calls (`#cx5:checked ~ .wu5, #cx5:checked ~ * .wu5`). Only the steps' rows are hidden: the
|
|
14
|
+
current one's card and code still show when it's folded away.
|
|
10
15
|
"""
|
|
11
16
|
|
|
12
17
|
from __future__ import annotations
|
|
@@ -523,11 +528,29 @@ def _frame_html(index: int, frame: _Frame, rows: list[tuple[int, str]]) -> str:
|
|
|
523
528
|
)
|
|
524
529
|
|
|
525
530
|
|
|
526
|
-
def
|
|
531
|
+
def _folds(stops: list[_Stop]) -> tuple[list[int], list[list[int]]]:
|
|
532
|
+
"""
|
|
533
|
+
How many steps sit under each stop in the list, and the stops each one sits under, outermost first.
|
|
534
|
+
"""
|
|
535
|
+
under = [0] * len(stops)
|
|
536
|
+
above: list[list[int]] = []
|
|
537
|
+
opened: list[int] = []
|
|
538
|
+
for i, stop in enumerate(stops):
|
|
539
|
+
while opened and (stops[opened[-1]].depth >= stop.depth or stops[opened[-1]].chapter != stop.chapter):
|
|
540
|
+
opened.pop()
|
|
541
|
+
for j in opened:
|
|
542
|
+
under[j] += 1
|
|
543
|
+
above.append(list(opened))
|
|
544
|
+
opened.append(i)
|
|
545
|
+
return under, above
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _item_html(stops: list[_Stop], i: int, card: str, under: int = 0, above: tuple[int, ...] = ()) -> str:
|
|
527
549
|
"""
|
|
528
550
|
A step in the list: its radio button, then its number and what it does in a few words (or, for a function it goes
|
|
529
551
|
into, the function's name and file, as a heading over its steps) as the button's label, then its card, which shows
|
|
530
|
-
(beside the list) while the button is checked.
|
|
552
|
+
(beside the list) while the button is checked. under: how many steps sit under it, folded away by the checkbox and
|
|
553
|
+
arrow before it; above: the steps it sits under, whose folds hide it.
|
|
531
554
|
"""
|
|
532
555
|
stop = stops[i]
|
|
533
556
|
step = stop.step
|
|
@@ -543,10 +566,18 @@ def _item_html(stops: list[_Stop], i: int, card: str) -> str:
|
|
|
543
566
|
dot = f'<span class="wdot d-{said}" title="{_esc(_NOTE_TIPS.get(said, ""))}"></span>' if said in _DOTS else ""
|
|
544
567
|
text = f'<span class="wtx">{headline}</span>{dot}'
|
|
545
568
|
checked = " checked" if i == 0 else ""
|
|
569
|
+
hidden_by = "".join(f" wu{j}" for j in above)
|
|
570
|
+
fold = ""
|
|
571
|
+
if under:
|
|
572
|
+
fold = (
|
|
573
|
+
f'<input type="checkbox" class="wcx" id="cx{i}" tabindex="-1"><label class="wtg{hidden_by}" for="cx{i}" '
|
|
574
|
+
f'style="--d:{stop.depth}" title="Fold or unfold the {_plural(under, "step")} under it"><i></i></label>'
|
|
575
|
+
)
|
|
576
|
+
text += f'<span class="wcnt">{_plural(under, "step")}</span>'
|
|
546
577
|
return (
|
|
547
|
-
f'<input type="radio" class="wr" name="{_GROUP}" id="cw{i}"{checked}><label class="wil
|
|
548
|
-
f'for="cw{i}" style="--d:{stop.depth}"><span class="wno">{i + 1}</span
|
|
549
|
-
f"</label>{card}
|
|
578
|
+
f'{fold}<input type="radio" class="wr" name="{_GROUP}" id="cw{i}"{checked}><label class="wil '
|
|
579
|
+
f'k-{_esc(step.kind)}{hidden_by}" for="cw{i}" style="--d:{stop.depth}"><span class="wno">{i + 1}</span>'
|
|
580
|
+
f'<span class="wx">{text}</span></label>{card}'
|
|
550
581
|
)
|
|
551
582
|
|
|
552
583
|
|
|
@@ -576,6 +607,7 @@ def _walk_html(block: _Walk, report: _Report) -> str:
|
|
|
576
607
|
rows = _frame_rows(frame, highlighted.get(frame.file, []), stops)
|
|
577
608
|
rows_of.append({number: n for n, (number, _) in enumerate(rows) if number})
|
|
578
609
|
drawn.append(_frame_html(k, frame, rows))
|
|
610
|
+
under, above = _folds(stops)
|
|
579
611
|
out: list[str] = []
|
|
580
612
|
opened: list[int] = [] # the frames whose boxes are open, innermost last
|
|
581
613
|
chapter = -1
|
|
@@ -593,7 +625,9 @@ def _walk_html(block: _Walk, report: _Report) -> str:
|
|
|
593
625
|
if not opened or opened[-1] != stop.frame: # into a call
|
|
594
626
|
out.append('<div class="wfn">')
|
|
595
627
|
opened.append(stop.frame)
|
|
596
|
-
out.append(_item_html(stops, i, _card_html(stops, i, first, docs, block.editor)))
|
|
628
|
+
out.append(_item_html(stops, i, _card_html(stops, i, first, docs, block.editor), under[i], tuple(above[i])))
|
|
629
|
+
if under[i]: # its fold hides the steps under it, beside it or in the boxes of what it calls
|
|
630
|
+
report.rules.add(f".{root} #cx{i}:checked~.wu{i},.{root} #cx{i}:checked~* .wu{i}{{display:none}}")
|
|
597
631
|
at = rows_of[stop.frame].get(stop.lit)
|
|
598
632
|
if at is not None: # the line its code lights
|
|
599
633
|
report.rules.add(f".{root} #cw{i}:checked~.wf{stop.frame}{{--k:{at}}}")
|
|
@@ -601,7 +635,8 @@ def _walk_html(block: _Walk, report: _Report) -> str:
|
|
|
601
635
|
out.append(drawn[opened.pop()] + "</div>")
|
|
602
636
|
sub = (
|
|
603
637
|
f'<div class="wsub">{_plural(len(stops), "step")}, in the order the code runs. Press Next or click a step to '
|
|
604
|
-
"light its line in the code; after a click, <kbd>↑</kbd> <kbd>↓</kbd> move through them
|
|
638
|
+
"light its line in the code; after a click, <kbd>↑</kbd> <kbd>↓</kbd> move through them. The arrow beside a "
|
|
639
|
+
"step folds the steps under it away.</div>"
|
|
605
640
|
)
|
|
606
641
|
return (
|
|
607
642
|
f'{title}{sub}<div class="walk"><div class="wl" role="radiogroup" aria-label="The steps">{"".join(out)}'
|
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,13 +32,13 @@ 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
|
-
code_node/report/walk.py,sha256=
|
|
41
|
+
code_node/report/walk.py,sha256=LWJDGJ3efF0fPWlMO4G4HBQqnNVn2RHXgLeuLrC-WnA,29466
|
|
42
42
|
code_node/report/logos/README.md,sha256=8Evw8eApl_56QJrwY7LAtGgJMZE0mp0-HIpRz9bu1ME,613
|
|
43
43
|
code_node/report/logos/anthropic-icon.svg,sha256=-_25zx86lfDTm-ksVXkLE4uQ_ldzLbTd3U0z-wjT-JA,302
|
|
44
44
|
code_node/report/logos/aws-amplify.svg,sha256=62WXM0XCn4WrQYWcVZFz4ln62xidiqF8eO4ELlgO8hg,531
|
|
@@ -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
|