code-node-graph 0.1.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 +40 -0
- code_node/__main__.py +7 -0
- code_node/analysis/__init__.py +3 -0
- code_node/analysis/aws_model.py +197 -0
- code_node/analysis/findings.py +462 -0
- code_node/analysis/graph.py +780 -0
- code_node/analysis/mapping.py +114 -0
- code_node/analysis/model.py +289 -0
- code_node/analysis/reader.py +1926 -0
- code_node/analysis/syntax.py +220 -0
- code_node/analysis/terms.py +505 -0
- code_node/cli.py +316 -0
- code_node/deploys/__init__.py +321 -0
- code_node/deploys/sam.py +371 -0
- code_node/deploys/serverless.py +117 -0
- code_node/kit/__init__.py +4 -0
- code_node/kit/blocks.py +184 -0
- code_node/kit/fmt.py +389 -0
- code_node/kit/highlight.py +131 -0
- code_node/kit/render.py +1066 -0
- code_node/kit/text.py +81 -0
- code_node/libraries/__init__.py +333 -0
- code_node/libraries/llm.py +124 -0
- code_node/libraries/mongo.py +169 -0
- code_node/libraries/redis.py +253 -0
- code_node/libraries/sql.py +232 -0
- code_node/pages.py +92 -0
- code_node/platforms/__init__.py +203 -0
- code_node/platforms/aws_lambda.py +290 -0
- code_node/platforms/web.py +301 -0
- code_node/project.py +402 -0
- code_node/report/__init__.py +3 -0
- code_node/report/brands.py +244 -0
- code_node/report/build.py +119 -0
- code_node/report/editor.py +40 -0
- code_node/report/graph.py +1411 -0
- code_node/report/logos/README.md +8 -0
- code_node/report/logos/anthropic-icon.svg +1 -0
- code_node/report/logos/aws-amplify.svg +1 -0
- code_node/report/logos/aws-api-gateway.svg +1 -0
- code_node/report/logos/aws-appflow.svg +1 -0
- code_node/report/logos/aws-appsync.svg +1 -0
- code_node/report/logos/aws-athena.svg +1 -0
- code_node/report/logos/aws-backup.svg +1 -0
- code_node/report/logos/aws-batch.svg +1 -0
- code_node/report/logos/aws-certificate-manager.svg +1 -0
- code_node/report/logos/aws-cloudformation.svg +1 -0
- code_node/report/logos/aws-cloudfront.svg +1 -0
- code_node/report/logos/aws-cloudtrail.svg +1 -0
- code_node/report/logos/aws-cloudwatch.svg +1 -0
- code_node/report/logos/aws-codebuild.svg +1 -0
- code_node/report/logos/aws-codecommit.svg +1 -0
- code_node/report/logos/aws-codedeploy.svg +1 -0
- code_node/report/logos/aws-codepipeline.svg +1 -0
- code_node/report/logos/aws-cognito.svg +1 -0
- code_node/report/logos/aws-config.svg +1 -0
- code_node/report/logos/aws-documentdb.svg +1 -0
- code_node/report/logos/aws-dynamodb.svg +1 -0
- code_node/report/logos/aws-ec2.svg +1 -0
- code_node/report/logos/aws-ecs.svg +1 -0
- code_node/report/logos/aws-eks.svg +1 -0
- code_node/report/logos/aws-elasticache.svg +1 -0
- code_node/report/logos/aws-elb.svg +1 -0
- code_node/report/logos/aws-eventbridge.svg +1 -0
- code_node/report/logos/aws-glacier.svg +1 -0
- code_node/report/logos/aws-glue.svg +1 -0
- code_node/report/logos/aws-iam.svg +1 -0
- code_node/report/logos/aws-keyspaces.svg +1 -0
- code_node/report/logos/aws-kinesis.svg +1 -0
- code_node/report/logos/aws-kms.svg +1 -0
- code_node/report/logos/aws-lake-formation.svg +1 -0
- code_node/report/logos/aws-lambda.svg +1 -0
- code_node/report/logos/aws-lightsail.svg +1 -0
- code_node/report/logos/aws-mq.svg +1 -0
- code_node/report/logos/aws-msk.svg +1 -0
- code_node/report/logos/aws-neptune.svg +1 -0
- code_node/report/logos/aws-open-search.svg +1 -0
- code_node/report/logos/aws-quicksight.svg +1 -0
- code_node/report/logos/aws-rds.svg +1 -0
- code_node/report/logos/aws-redshift.svg +1 -0
- code_node/report/logos/aws-route53.svg +1 -0
- code_node/report/logos/aws-s3.svg +1 -0
- code_node/report/logos/aws-secrets-manager.svg +1 -0
- code_node/report/logos/aws-ses.svg +1 -0
- code_node/report/logos/aws-shield.svg +1 -0
- code_node/report/logos/aws-sns.svg +1 -0
- code_node/report/logos/aws-sqs.svg +1 -0
- code_node/report/logos/aws-step-functions.svg +1 -0
- code_node/report/logos/aws-systems-manager.svg +1 -0
- code_node/report/logos/aws-timestream.svg +1 -0
- code_node/report/logos/aws-waf.svg +1 -0
- code_node/report/logos/aws-xray.svg +1 -0
- code_node/report/logos/aws.svg +1 -0
- code_node/report/logos/azure-icon.svg +1 -0
- code_node/report/logos/django-icon.svg +1 -0
- code_node/report/logos/elasticsearch.svg +1 -0
- code_node/report/logos/fastapi-icon.svg +1 -0
- code_node/report/logos/firebase-icon.svg +1 -0
- code_node/report/logos/flask.svg +1 -0
- code_node/report/logos/google-cloud.svg +1 -0
- code_node/report/logos/kafka-icon.svg +1 -0
- code_node/report/logos/mariadb-icon.svg +1 -0
- code_node/report/logos/mongodb-icon.svg +1 -0
- code_node/report/logos/mysql-icon.svg +1 -0
- code_node/report/logos/openai-icon.svg +1 -0
- code_node/report/logos/postgresql.svg +1 -0
- code_node/report/logos/python.svg +1 -0
- code_node/report/logos/rabbitmq-icon.svg +1 -0
- code_node/report/logos/redis.svg +1 -0
- code_node/report/logos/serverless.svg +1 -0
- code_node/report/logos/slack-icon.svg +1 -0
- code_node/report/logos/small/aws-api-gateway.svg +1 -0
- code_node/report/logos/small/aws-appflow.svg +1 -0
- code_node/report/logos/small/aws-appsync.svg +1 -0
- code_node/report/logos/small/aws-athena.svg +1 -0
- code_node/report/logos/small/aws-backup.svg +1 -0
- code_node/report/logos/small/aws-batch.svg +1 -0
- code_node/report/logos/small/aws-certificate-manager.svg +1 -0
- code_node/report/logos/small/aws-cloudformation.svg +1 -0
- code_node/report/logos/small/aws-cloudfront.svg +1 -0
- code_node/report/logos/small/aws-cloudtrail.svg +1 -0
- code_node/report/logos/small/aws-cloudwatch.svg +1 -0
- code_node/report/logos/small/aws-codebuild.svg +1 -0
- code_node/report/logos/small/aws-codecommit.svg +1 -0
- code_node/report/logos/small/aws-codedeploy.svg +1 -0
- code_node/report/logos/small/aws-codepipeline.svg +1 -0
- code_node/report/logos/small/aws-cognito.svg +1 -0
- code_node/report/logos/small/aws-config.svg +1 -0
- code_node/report/logos/small/aws-documentdb.svg +1 -0
- code_node/report/logos/small/aws-dynamodb.svg +1 -0
- code_node/report/logos/small/aws-ec2.svg +1 -0
- code_node/report/logos/small/aws-ecs.svg +1 -0
- code_node/report/logos/small/aws-eks.svg +1 -0
- code_node/report/logos/small/aws-elasticache.svg +1 -0
- code_node/report/logos/small/aws-elb.svg +1 -0
- code_node/report/logos/small/aws-eventbridge.svg +1 -0
- code_node/report/logos/small/aws-glacier.svg +1 -0
- code_node/report/logos/small/aws-glue.svg +1 -0
- code_node/report/logos/small/aws-iam.svg +1 -0
- code_node/report/logos/small/aws-keyspaces.svg +1 -0
- code_node/report/logos/small/aws-kinesis.svg +1 -0
- code_node/report/logos/small/aws-kms.svg +1 -0
- code_node/report/logos/small/aws-lake-formation.svg +1 -0
- code_node/report/logos/small/aws-lambda.svg +1 -0
- code_node/report/logos/small/aws-mq.svg +1 -0
- code_node/report/logos/small/aws-msk.svg +1 -0
- code_node/report/logos/small/aws-neptune.svg +1 -0
- code_node/report/logos/small/aws-open-search.svg +1 -0
- code_node/report/logos/small/aws-quicksight.svg +1 -0
- code_node/report/logos/small/aws-rds.svg +1 -0
- code_node/report/logos/small/aws-redshift.svg +1 -0
- code_node/report/logos/small/aws-route53.svg +1 -0
- code_node/report/logos/small/aws-s3.svg +1 -0
- code_node/report/logos/small/aws-secrets-manager.svg +1 -0
- code_node/report/logos/small/aws-ses.svg +1 -0
- code_node/report/logos/small/aws-shield.svg +1 -0
- code_node/report/logos/small/aws-sns.svg +1 -0
- code_node/report/logos/small/aws-sqs.svg +1 -0
- code_node/report/logos/small/aws-step-functions.svg +1 -0
- code_node/report/logos/small/aws-systems-manager.svg +1 -0
- code_node/report/logos/small/aws-timestream.svg +1 -0
- code_node/report/logos/small/aws-waf.svg +1 -0
- code_node/report/logos/small/aws-xray.svg +1 -0
- code_node/report/logos/snowflake-icon.svg +1 -0
- code_node/report/logos/sqlite.svg +1 -0
- code_node/report/logos/stripe.svg +1 -0
- code_node/report/logos/supabase-icon.svg +1 -0
- code_node/report/logos/twilio-icon.svg +1 -0
- code_node/report/outline.py +219 -0
- code_node/report/style.py +358 -0
- code_node/report/tabs.py +73 -0
- code_node/report/walk.py +624 -0
- code_node/serve.py +277 -0
- code_node_graph-0.1.0.dist-info/METADATA +165 -0
- code_node_graph-0.1.0.dist-info/RECORD +178 -0
- code_node_graph-0.1.0.dist-info/WHEEL +4 -0
- code_node_graph-0.1.0.dist-info/entry_points.txt +2 -0
- code_node_graph-0.1.0.dist-info/licenses/LICENSE +201 -0
code_node/report/walk.py
ADDED
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
"""
|
|
2
|
+
_Walk: the steps as a walk through the code, one line at a time, as a debugger steps: a list of every step on the left,
|
|
3
|
+
each said in a few plain words, and on the right what the current one does and the function it's in, its line lit,
|
|
4
|
+
with Back, Next, Skip call (step over) and Finish (step out).
|
|
5
|
+
|
|
6
|
+
No JavaScript: each step is a radio button (one group, so once one has focus the arrow keys step through them).
|
|
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
|
+
the card after the checked button and the code of its box with sibling selectors, so the light slides from line to
|
|
9
|
+
line as you step, and a step costs the same however many there are.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from ..kit.blocks import _Report, _draws
|
|
17
|
+
from ..kit.fmt import _plural
|
|
18
|
+
from ..kit.highlight import _python_html, _python_lines
|
|
19
|
+
from ..kit.render import _prose
|
|
20
|
+
from ..kit.text import _esc
|
|
21
|
+
|
|
22
|
+
from ..analysis.findings import _short_name
|
|
23
|
+
from ..analysis.model import CodeFunction, CodeStep
|
|
24
|
+
from ..analysis.syntax import _a
|
|
25
|
+
from .editor import editor_name, open_at
|
|
26
|
+
from .outline import _NOTE_TIPS, _Outline, _chips_html, _outline_lines
|
|
27
|
+
from .style import _ROOT
|
|
28
|
+
|
|
29
|
+
_GROUP = "cnd-walk" # the radio buttons' name: one group, so the arrow keys move through every step
|
|
30
|
+
|
|
31
|
+
_FRAME_LINES = 160 # a function or file longer than this shows only the lines around its steps
|
|
32
|
+
_FRAME_AROUND = 4 # lines kept above and below each step then
|
|
33
|
+
|
|
34
|
+
_KIND_SAYS = { # what a line of each kind means, said once on its card
|
|
35
|
+
"loop": "A loop: the lines under it run once for each item.",
|
|
36
|
+
"if": "A branch: the lines under it run only when this is true.",
|
|
37
|
+
"with": "The lines under it run inside this block, which tidies up after them.",
|
|
38
|
+
"try": "The lines under it run; if one of them raises, the except below it runs instead of the rest.",
|
|
39
|
+
"except": "Runs only when a line in the try above it raises.",
|
|
40
|
+
"raise": "Raises an error: the function stops here, and so does each caller that doesn't catch it.",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_AWS_DOES = { # an AWS call, by what it does, as a sentence: 'Reads from S3 (GetObject)'
|
|
44
|
+
"reads": "Reads from {}",
|
|
45
|
+
"writes": "Writes to {}",
|
|
46
|
+
"sends": "Sends a message with {}",
|
|
47
|
+
"runs": "Starts something in {}",
|
|
48
|
+
"signs": "Makes a signed link for {}",
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_LIB_DOES = {"reads": "Reads from {}", "writes": "Writes to {}", "sends": "Sends a message with {}", "runs": "Calls {}"}
|
|
52
|
+
|
|
53
|
+
_SAID_FIRST = (
|
|
54
|
+
"aws-reads",
|
|
55
|
+
"aws-writes",
|
|
56
|
+
"aws-sends",
|
|
57
|
+
"aws-runs",
|
|
58
|
+
"aws-signs",
|
|
59
|
+
"lib-reads",
|
|
60
|
+
"lib-writes",
|
|
61
|
+
"lib-sends",
|
|
62
|
+
"lib-runs",
|
|
63
|
+
"http",
|
|
64
|
+
"command",
|
|
65
|
+
"file",
|
|
66
|
+
"sleep",
|
|
67
|
+
"client",
|
|
68
|
+
) # the tags that say what a line is for, before the calls and names in it
|
|
69
|
+
|
|
70
|
+
_DOTS = (
|
|
71
|
+
"aws-reads",
|
|
72
|
+
"aws-writes",
|
|
73
|
+
"aws-sends",
|
|
74
|
+
"aws-runs",
|
|
75
|
+
"aws-signs",
|
|
76
|
+
"lib-reads",
|
|
77
|
+
"lib-writes",
|
|
78
|
+
"lib-sends",
|
|
79
|
+
"lib-runs",
|
|
80
|
+
"http",
|
|
81
|
+
) # what's marked in the list
|
|
82
|
+
|
|
83
|
+
_HEADERS = [ # a block's first line -> what it does: (pattern, sentence with {} for what it matched)
|
|
84
|
+
(r"(?:async )?for (.+?) in (.+):", "Goes through each {} in {}"),
|
|
85
|
+
(r"while (.+):", "Repeats while {}"),
|
|
86
|
+
(r"(?:el)?if (.+):", "If {}"),
|
|
87
|
+
(r"match (.+):", "Picks a case for {}"),
|
|
88
|
+
(r"case (.+):", "Case {}"),
|
|
89
|
+
(r"(?:async )?with (.+):", "With {}"),
|
|
90
|
+
(r"except (.+?)(?: as \w+)?:", "If it fails with {}"),
|
|
91
|
+
(r"return (.+)", "Returns {}"),
|
|
92
|
+
(r"raise (.+)", "Raises {}"),
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
_BARE = {
|
|
96
|
+
"try:": "Tries the lines under it",
|
|
97
|
+
"except:": "If anything under the try fails",
|
|
98
|
+
"else:": "Otherwise",
|
|
99
|
+
"finally:": "Finally, whatever happened above",
|
|
100
|
+
"return": "Returns",
|
|
101
|
+
"raise": "Raises the error again",
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class _Walk:
|
|
107
|
+
"""
|
|
108
|
+
What runs when the code is imported, then each call from the entry point, as stops to step through. functions
|
|
109
|
+
are its own functions (for where each one ends and its docstring) and sources the files read ({file: its text});
|
|
110
|
+
without a file's text, its function shows only the lines that are steps.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
startup: list[CodeStep]
|
|
114
|
+
steps: list[CodeStep]
|
|
115
|
+
title: str = ""
|
|
116
|
+
startup_title: str = "When Python imports it, once, before the first call"
|
|
117
|
+
calls_title: str = "Each call"
|
|
118
|
+
functions: list[CodeFunction] = field(default_factory=list)
|
|
119
|
+
sources: dict[str, str] = field(default_factory=dict)
|
|
120
|
+
empty: str = "(nothing it can follow)"
|
|
121
|
+
editor: str = "" # a link to open a line in an editor, from editor_link(): each step's card links to its line
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass
|
|
125
|
+
class _Frame:
|
|
126
|
+
"""
|
|
127
|
+
The code a run of stops is in: a function whole (its decorators, def and body) or a module's file.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
file: str
|
|
131
|
+
start: int
|
|
132
|
+
end: int
|
|
133
|
+
name: str # 'read_orders()', or the file for a module
|
|
134
|
+
stops: dict[int, int] = field(default_factory=dict) # line -> the first stop on it
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class _Stop:
|
|
139
|
+
"""
|
|
140
|
+
One step of the walk: a line (or a function being entered), where it sits in the list, and the calls it's inside.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
step: CodeStep
|
|
144
|
+
depth: int # how far it's indented in the list
|
|
145
|
+
frame: int
|
|
146
|
+
lit: int # the line of its frame it lights: its own, or for a call shown before, the line that calls it
|
|
147
|
+
stack: tuple[tuple[str, int], ...] # the calls it's inside, outermost first: (name, the stop that enters it)
|
|
148
|
+
chapter: int # 0: when it's imported, 1: each call
|
|
149
|
+
caller: int | None = None # a function being entered: the stop of the line that calls it
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
class _Walker:
|
|
153
|
+
"""
|
|
154
|
+
Lays the outline's tree out flat, in the order the code runs, with a frame for each function (or module) it goes
|
|
155
|
+
into: the stops in a frame are the ones its code shows.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(self, block: _Walk) -> None:
|
|
159
|
+
self.functions = {f.name: f for f in block.functions}
|
|
160
|
+
self.stops: list[_Stop] = []
|
|
161
|
+
self.frames: list[_Frame] = []
|
|
162
|
+
self.chapter = 0
|
|
163
|
+
self.lines(block.startup, 0, None, ())
|
|
164
|
+
self.chapter = 1
|
|
165
|
+
self.lines(block.steps, 0, None, ())
|
|
166
|
+
|
|
167
|
+
def frame(self, step: CodeStep) -> int:
|
|
168
|
+
"""
|
|
169
|
+
A new frame for the function or module step goes into (or the file a line run at import is in).
|
|
170
|
+
"""
|
|
171
|
+
known = self.functions.get(step.name) if step.kind == "def" else None
|
|
172
|
+
if known:
|
|
173
|
+
frame = _Frame(known.file, known.line, known.line + known.lines - 1, f"{_short_name(known.name)}()")
|
|
174
|
+
elif step.kind == "def":
|
|
175
|
+
last = max((s.line for s in step.walk() if s.line), default=step.line)
|
|
176
|
+
frame = _Frame(step.file, step.line, last, f"{_short_name(step.name)}()")
|
|
177
|
+
else:
|
|
178
|
+
frame = _Frame(step.file, 1, 0, step.file) # a module: its whole file, measured when it's drawn
|
|
179
|
+
self.frames.append(frame)
|
|
180
|
+
return len(self.frames) - 1
|
|
181
|
+
|
|
182
|
+
def add(
|
|
183
|
+
self,
|
|
184
|
+
step: CodeStep,
|
|
185
|
+
depth: int,
|
|
186
|
+
frame: int,
|
|
187
|
+
stack: tuple[tuple[str, int], ...],
|
|
188
|
+
caller: int | None = None,
|
|
189
|
+
lit: int | None = None,
|
|
190
|
+
) -> int:
|
|
191
|
+
index = len(self.stops)
|
|
192
|
+
lit = step.line if lit is None else lit
|
|
193
|
+
self.stops.append(_Stop(step, depth, frame, lit, stack, self.chapter, caller))
|
|
194
|
+
if lit:
|
|
195
|
+
self.frames[frame].stops.setdefault(lit, index)
|
|
196
|
+
return index
|
|
197
|
+
|
|
198
|
+
def enter(
|
|
199
|
+
self, step: CodeStep, depth: int, stack: tuple[tuple[str, int], ...], caller: int | None, frame: int | None
|
|
200
|
+
) -> None:
|
|
201
|
+
"""
|
|
202
|
+
A function (or module) being run: a stop on its def, then its own lines under it, in a frame of its own. One
|
|
203
|
+
whose steps were shown before stays in the caller's frame, with the line that calls it lit.
|
|
204
|
+
"""
|
|
205
|
+
name = f"{_short_name(step.name)}()" if step.kind == "def" else f"{step.file}, imported"
|
|
206
|
+
inside = (*stack, (name, len(self.stops)))
|
|
207
|
+
if step.seen and frame is not None and caller is not None:
|
|
208
|
+
self.add(step, depth, frame, inside, caller, lit=self.stops[caller].lit)
|
|
209
|
+
return
|
|
210
|
+
own = self.frame(step)
|
|
211
|
+
self.add(step, depth, own, inside, caller)
|
|
212
|
+
if not step.seen:
|
|
213
|
+
self.lines(step.steps, depth + 1, own, inside)
|
|
214
|
+
|
|
215
|
+
def lines(self, steps: list[CodeStep], depth: int, frame: int | None, stack: tuple[tuple[str, int], ...]) -> None:
|
|
216
|
+
for step in steps:
|
|
217
|
+
if step.kind in ("def", "module"):
|
|
218
|
+
self.enter(step, depth, stack, None, frame)
|
|
219
|
+
continue
|
|
220
|
+
if frame is None: # a line run when its file is imported, outside any function
|
|
221
|
+
frame = self.frame(step)
|
|
222
|
+
stack = ((f"{step.file}, imported", len(self.stops)),)
|
|
223
|
+
index = self.add(step, depth, frame, stack)
|
|
224
|
+
for call in step.calls:
|
|
225
|
+
self.enter(call, depth + 1, stack, index, frame)
|
|
226
|
+
self.lines(step.steps, depth + 1, frame, stack)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _after(stops: list[_Stop], i: int, deeper_than: int) -> int | None:
|
|
230
|
+
"""
|
|
231
|
+
The first stop after i in no more than deeper_than calls: where Skip call and Finish go.
|
|
232
|
+
"""
|
|
233
|
+
return next((j for j in range(i + 1, len(stops)) if len(stops[j].stack) <= deeper_than), None)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _name_of(step: CodeStep) -> str:
|
|
237
|
+
return f"{_short_name(step.name)}()" if step.kind == "def" else step.file
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _code(text: str) -> str:
|
|
241
|
+
return f"<code>{_esc(text)}</code>"
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _note_said(kind: str, text: str) -> str:
|
|
245
|
+
"""
|
|
246
|
+
One of a line's tags as the start of a sentence (HTML): 'Reads from S3 (GetObject)', 'Sends an HTTP POST request'.
|
|
247
|
+
"""
|
|
248
|
+
if kind.startswith("lib-"):
|
|
249
|
+
service, _, operation = text.partition(": ")
|
|
250
|
+
return _esc(_LIB_DOES.get(kind[4:], "Calls {}").format(service)) + (
|
|
251
|
+
f" ({_esc(operation)})" if operation else ""
|
|
252
|
+
)
|
|
253
|
+
if kind.startswith("aws-"):
|
|
254
|
+
service, _, operation = text.rpartition(" ")
|
|
255
|
+
if " waiter" in service:
|
|
256
|
+
return f"Waits for {_esc(text)}"
|
|
257
|
+
return _esc(_AWS_DOES.get(kind[4:], "Calls {}").format(service or text)) + (
|
|
258
|
+
f" ({_esc(operation)})" if service else ""
|
|
259
|
+
)
|
|
260
|
+
if kind == "http":
|
|
261
|
+
verb, _, where = text.removeprefix("HTTP ").partition(" ")
|
|
262
|
+
return f"Sends an HTTP {_esc(verb)} request" + (f" to {_code(where)}" if where else "")
|
|
263
|
+
if kind == "env":
|
|
264
|
+
return f"Reads the environment variable {_code(text)}"
|
|
265
|
+
if kind == "event": # 'request query.q': the platform's word for its input, then the field
|
|
266
|
+
word, _, part = text.partition(" ")
|
|
267
|
+
return f"Reads {_code(part)} from the {_esc(word)}"
|
|
268
|
+
return _esc(text[:1].upper() + text[1:])
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _headline(stop: _Stop) -> tuple[str, str]:
|
|
272
|
+
"""
|
|
273
|
+
What the stop does, in a few plain words (HTML), and the tag it's about, if any: 'Reads from S3 (GetObject)',
|
|
274
|
+
'Goes through each record in event['Records']', 'Takes bucket from the event'.
|
|
275
|
+
"""
|
|
276
|
+
step = stop.step
|
|
277
|
+
if step.kind == "module":
|
|
278
|
+
return f"Runs {_code(step.file)}, once", ""
|
|
279
|
+
if step.kind == "def":
|
|
280
|
+
name = _code(_name_of(step))
|
|
281
|
+
if stop.caller is None:
|
|
282
|
+
return (f"Each call starts in {name}" if stop.chapter else f"Runs {name}"), ""
|
|
283
|
+
return (f"Calls {name} again" if step.seen else f"Goes into {name}"), ""
|
|
284
|
+
if step.kind != "do":
|
|
285
|
+
bare = _BARE.get(step.code.strip())
|
|
286
|
+
if bare:
|
|
287
|
+
return bare, ""
|
|
288
|
+
for pattern, sentence in _HEADERS:
|
|
289
|
+
found = re.fullmatch(pattern, step.code.strip())
|
|
290
|
+
if found:
|
|
291
|
+
return sentence.format(*(_code(part) for part in found.groups())), ""
|
|
292
|
+
return f"Runs {_code(step.code)}", ""
|
|
293
|
+
for kind, text in step.notes:
|
|
294
|
+
if kind in _SAID_FIRST:
|
|
295
|
+
return _note_said(kind, text), kind
|
|
296
|
+
modules = [call for call in step.calls if call.kind == "module"]
|
|
297
|
+
if modules:
|
|
298
|
+
return "Imports " + ", ".join(_code(call.file) for call in modules), ""
|
|
299
|
+
if len(step.calls) == 1 and step.calls[0].name.endswith(".__init__"):
|
|
300
|
+
made = _short_name(step.calls[0].name)
|
|
301
|
+
return f"Makes {_a(made)} {_code(made)}", ""
|
|
302
|
+
if step.calls:
|
|
303
|
+
return "Calls " + ", ".join(_code(_name_of(call)) for call in step.calls), ""
|
|
304
|
+
for kind, text in step.notes:
|
|
305
|
+
if kind in ("log", "warn"):
|
|
306
|
+
return _note_said(kind, text), kind
|
|
307
|
+
for name, what in step.holds:
|
|
308
|
+
if what.startswith("event "):
|
|
309
|
+
return f"Takes {_code(name)} from the event", ""
|
|
310
|
+
if what.startswith("env "):
|
|
311
|
+
return f"Takes {_code(name)} from the environment", ""
|
|
312
|
+
return f"Sets {_code(name)} to {_esc(what)}", ""
|
|
313
|
+
if step.notes:
|
|
314
|
+
return _note_said(*step.notes[0]), step.notes[0][0]
|
|
315
|
+
return f"Runs {_code(step.code)}", ""
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _says(stops: list[_Stop], i: int, first: dict[str, int], docs: dict[str, str], over: bool) -> str:
|
|
319
|
+
"""
|
|
320
|
+
What the stop's line does, in a sentence or two (HTML), under its headline. over: it has a Skip call button.
|
|
321
|
+
"""
|
|
322
|
+
stop = stops[i]
|
|
323
|
+
step = stop.step
|
|
324
|
+
if step.kind == "module":
|
|
325
|
+
return "<p>Python runs this file from the top, once, the first time something imports it.</p>"
|
|
326
|
+
if step.kind == "def":
|
|
327
|
+
said = []
|
|
328
|
+
if stop.caller is not None:
|
|
329
|
+
when = f", only when {_code(' and '.join(step.when))}" if step.when else ""
|
|
330
|
+
said.append(f"Called from {_code(stop.stack[-2][0])}, line {stops[stop.caller].step.line}{when}.")
|
|
331
|
+
doc = docs.get(step.name, "")
|
|
332
|
+
out = "".join(f"<p>{line}</p>" for line in said) + (f'<p class="wdoc">{_esc(doc)}</p>' if doc else "")
|
|
333
|
+
if step.seen and step.name in first:
|
|
334
|
+
out += (
|
|
335
|
+
f'<p>Its steps are shown where it\'s first called: <label class="wjump" for="cw{first[step.name]}">'
|
|
336
|
+
f"step {first[step.name] + 1}</label>.</p>"
|
|
337
|
+
)
|
|
338
|
+
elif not step.steps and not step.notes:
|
|
339
|
+
out += "<p>There's nothing in it to follow.</p>"
|
|
340
|
+
return out
|
|
341
|
+
said = []
|
|
342
|
+
headline_tag = next((kind for kind, _ in step.notes if kind in _DOTS), "")
|
|
343
|
+
if headline_tag and step.kind == "do": # what it reaches outside the code: 'An AWS call that reads.'
|
|
344
|
+
tip = _NOTE_TIPS[headline_tag]
|
|
345
|
+
said.append(_esc(tip[:1].upper() + tip[1:]) + ".")
|
|
346
|
+
if step.kind == "return":
|
|
347
|
+
entered = stops[stop.stack[-1][1]] if stop.stack else None
|
|
348
|
+
if entered is not None and entered.caller is not None and len(stop.stack) > 1:
|
|
349
|
+
said.append(
|
|
350
|
+
f"{_code(stop.stack[-1][0])} ends here and hands this back to {_code(stop.stack[-2][0])}, "
|
|
351
|
+
f"line {stops[entered.caller].step.line}."
|
|
352
|
+
)
|
|
353
|
+
else:
|
|
354
|
+
said.append("The function ends here: this is what the call returns.")
|
|
355
|
+
elif step.kind == "else":
|
|
356
|
+
said.append(
|
|
357
|
+
"Runs last, whether or not a line above it raised."
|
|
358
|
+
if step.code.startswith("finally")
|
|
359
|
+
else "Runs when the branch or loop above it doesn't."
|
|
360
|
+
)
|
|
361
|
+
elif step.kind in _KIND_SAYS:
|
|
362
|
+
said.append(_KIND_SAYS[step.kind])
|
|
363
|
+
own = [call for call in step.calls if call.kind == "def"]
|
|
364
|
+
if own:
|
|
365
|
+
it = "it" if len(own) == 1 else "each"
|
|
366
|
+
said.append(
|
|
367
|
+
f"Next goes into {it} to show its steps"
|
|
368
|
+
+ (
|
|
369
|
+
"; Skip call runs it without going in."
|
|
370
|
+
if over and len(own) == 1
|
|
371
|
+
else "; Skip call runs them without going in."
|
|
372
|
+
if over
|
|
373
|
+
else "."
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
return "".join(f"<p>{line}</p>" for line in said)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _facts_html(step: CodeStep, said: str) -> str:
|
|
380
|
+
"""
|
|
381
|
+
What else the line touches, beyond its headline, and what it knows of values, a row each:
|
|
382
|
+
'env ORDERS_TABLE an environment variable it reads', 'bucket holds event Records[].s3.bucket.name',
|
|
383
|
+
'Bucket comes from event Records[].s3.bucket.name'.
|
|
384
|
+
"""
|
|
385
|
+
rows = [
|
|
386
|
+
f'<li>{_chips_html([note])}<span class="wfm">{_esc(_NOTE_TIPS.get(note[0], ""))}</span></li>'
|
|
387
|
+
for note in step.notes
|
|
388
|
+
if note[0] != said and note[0] != "more"
|
|
389
|
+
]
|
|
390
|
+
rows += [f'<li><span class="wfm">{_esc(text)}</span></li>' for kind, text in step.notes if kind == "more"]
|
|
391
|
+
rows += [f'<li>{_code(name)}<span class="wfv">holds</span>{_esc(what)}</li>' for name, what in step.holds]
|
|
392
|
+
one_call = len({arg.rsplit(" ", 1)[0] for arg, _ in step.given}) == 1 # its headline says which: just 'Bucket'
|
|
393
|
+
rows += [
|
|
394
|
+
f'<li>{_code(arg.rsplit(" ", 1)[-1] if one_call else arg)}<span class="wfv">comes from</span>'
|
|
395
|
+
f"{_esc(source)}</li>"
|
|
396
|
+
for arg, source in step.given
|
|
397
|
+
]
|
|
398
|
+
return f'<ul class="wfacts">{"".join(rows)}</ul>' if rows else ""
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _button(target: int | None, text: str, css: str = "", tip: str = "") -> str:
|
|
402
|
+
"""
|
|
403
|
+
A button that goes to stop target: a label for its radio button, or greyed out when there's nowhere to go.
|
|
404
|
+
"""
|
|
405
|
+
title = f' title="{_esc(tip)}"' if tip else ""
|
|
406
|
+
css = f"wbt {css}".strip()
|
|
407
|
+
if target is None:
|
|
408
|
+
return f'<span class="{css} off"{title}>{text}</span>'
|
|
409
|
+
return f'<label class="{css}" for="cw{target}"{title}>{text}</label>'
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _card_html(stops: list[_Stop], i: int, first: dict[str, int], docs: dict[str, str], editor: str = "") -> str:
|
|
413
|
+
"""
|
|
414
|
+
The current stop's card: Back and Next (and Skip call and Finish, past what a line calls), what the line does in
|
|
415
|
+
plain words, the calls it's inside (each a link back to its start), and what it touches.
|
|
416
|
+
"""
|
|
417
|
+
stop, total = stops[i], len(stops)
|
|
418
|
+
depth = len(stop.stack)
|
|
419
|
+
over = _after(stops, i, depth) if stop.step.calls else None
|
|
420
|
+
over = over if over != i + 1 else None
|
|
421
|
+
out = _after(stops, i, depth - 1) if depth > 1 else None
|
|
422
|
+
nav = [
|
|
423
|
+
_button(i - 1 if i else None, "← Back"),
|
|
424
|
+
_button(i + 1 if i + 1 < total else None, "Next →", "go"),
|
|
425
|
+
f'<span class="wpos">Step <b>{i + 1}</b> of {total}</span><span class="wgap"></span>',
|
|
426
|
+
]
|
|
427
|
+
if over is not None:
|
|
428
|
+
nav.append(
|
|
429
|
+
_button(over, "Skip call", tip="Run what this line calls without going in, and stop at the next line here")
|
|
430
|
+
)
|
|
431
|
+
if out is not None:
|
|
432
|
+
name = stop.stack[-1][0]
|
|
433
|
+
nav.append(
|
|
434
|
+
_button(
|
|
435
|
+
out,
|
|
436
|
+
f"Finish <code>{_esc(name)}</code>",
|
|
437
|
+
tip=f"Run the rest of {name} and go back to where it was called",
|
|
438
|
+
)
|
|
439
|
+
)
|
|
440
|
+
headline, said = _headline(stop)
|
|
441
|
+
trail = []
|
|
442
|
+
for k, (name, at) in enumerate(stop.stack):
|
|
443
|
+
css = ' class="here"' if k == depth - 1 else ""
|
|
444
|
+
trail.append(f'<label for="cw{at}"{css}>{_esc(name)}</label>')
|
|
445
|
+
where = '<span class="wsep">›</span>'.join(trail)
|
|
446
|
+
href = open_at(editor, stop.step.file, stop.step.line) if stop.step.file else ""
|
|
447
|
+
line = f"line {stop.step.line}" if stop.step.line else stop.step.file
|
|
448
|
+
if href:
|
|
449
|
+
line = (
|
|
450
|
+
f'<a class="wed" href="{_esc(href)}" title="Open {_esc(stop.step.file)} at this line in '
|
|
451
|
+
f'{_esc(editor_name(editor))}">{line} ↗</a>'
|
|
452
|
+
)
|
|
453
|
+
elif line:
|
|
454
|
+
line = f'<span class="wln">{_esc(line)}</span>'
|
|
455
|
+
then = ""
|
|
456
|
+
if i + 1 < total:
|
|
457
|
+
nxt = stops[i + 1]
|
|
458
|
+
if nxt.chapter != stop.chapter:
|
|
459
|
+
then = "Next: the first call, from the entry point"
|
|
460
|
+
elif len(nxt.stack) < depth or (len(nxt.stack) == depth and nxt.stack[-1] != stop.stack[-1]):
|
|
461
|
+
back = nxt.stack[-1][0] if nxt.stack else ""
|
|
462
|
+
then = f"Next: back in {back}, line {nxt.step.line}" if nxt.step.line else f"Next: back in {back}"
|
|
463
|
+
else:
|
|
464
|
+
then = "That's the last step."
|
|
465
|
+
return (
|
|
466
|
+
f'<div class="wc"><div class="wnav">{"".join(nav)}</div>'
|
|
467
|
+
f'<div class="whead">{headline}</div>'
|
|
468
|
+
f'<div class="wstk"><span class="win">In</span>{where}{line}</div>'
|
|
469
|
+
f'<div class="wsay">{_says(stops, i, first, docs, over is not None)}{_facts_html(stop.step, said)}</div>'
|
|
470
|
+
+ (f'<div class="wthen">{_esc(then)}</div>' if then else "")
|
|
471
|
+
+ "</div>"
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _frame_rows(frame: _Frame, lines: list[str], stops: list[_Stop]) -> list[tuple[int, str]]:
|
|
476
|
+
"""
|
|
477
|
+
The frame's lines, (number, HTML): from its file when it was read, else just its steps' lines. A long one keeps
|
|
478
|
+
only the lines around its steps, with a (0, '') row where lines are left out.
|
|
479
|
+
"""
|
|
480
|
+
if not lines:
|
|
481
|
+
shown = {line: _python_html(stops[i].step.code) for line, i in frame.stops.items()}
|
|
482
|
+
return sorted(shown.items())
|
|
483
|
+
start, end = max(1, frame.start), min(len(lines), frame.end if frame.end else len(lines))
|
|
484
|
+
while start > 1 and lines[start - 2].lstrip().startswith("@"): # its decorators, over its def
|
|
485
|
+
start -= 1
|
|
486
|
+
if frame.stops: # a step outside where its function was thought to end
|
|
487
|
+
start, end = min(start, min(frame.stops)), max(end, min(len(lines), max(frame.stops)))
|
|
488
|
+
while end > start and end not in frame.stops and not lines[end - 1].strip():
|
|
489
|
+
end -= 1
|
|
490
|
+
numbers = range(start, end + 1)
|
|
491
|
+
if len(numbers) > _FRAME_LINES:
|
|
492
|
+
near = {k for at in [frame.start, *frame.stops] for k in range(at - _FRAME_AROUND, at + _FRAME_AROUND + 1)}
|
|
493
|
+
numbers = [k for k in numbers if k in near]
|
|
494
|
+
rows: list[tuple[int, str]] = []
|
|
495
|
+
for k in numbers:
|
|
496
|
+
if rows and rows[-1][0] and k > rows[-1][0] + 1:
|
|
497
|
+
rows.append((0, ""))
|
|
498
|
+
rows.append((k, lines[k - 1]))
|
|
499
|
+
return rows
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _frame_html(index: int, frame: _Frame, rows: list[tuple[int, str]]) -> str:
|
|
503
|
+
"""
|
|
504
|
+
A frame's code, its lines numbered, each line that's a step a link to it, and the bar that lights the current one.
|
|
505
|
+
"""
|
|
506
|
+
body = []
|
|
507
|
+
for number, code in rows:
|
|
508
|
+
at = frame.stops.get(number)
|
|
509
|
+
if not number:
|
|
510
|
+
body.append('<span class="sl gap"><span class="sn">⋯</span></span>')
|
|
511
|
+
elif at is None:
|
|
512
|
+
body.append(f'<span class="sl"><span class="sn">{number}</span>{code}</span>')
|
|
513
|
+
else:
|
|
514
|
+
body.append(f'<label class="sl" for="cw{at}"><span class="sn">{number}</span>{code}</label>')
|
|
515
|
+
where = (f"line {rows[0][0]}" if len(rows) == 1 else f"lines {rows[0][0]}–{rows[-1][0]}") if rows else ""
|
|
516
|
+
if any(not number for number, _ in rows):
|
|
517
|
+
where += ", around its steps"
|
|
518
|
+
name = "" if frame.name == frame.file else f"<b>{_esc(frame.name)}</b>"
|
|
519
|
+
return (
|
|
520
|
+
f'<div class="wf wf{index}" style="--n:{len(rows)}"><div class="wfh">{name}<span>{_esc(frame.file)}'
|
|
521
|
+
f'</span><span class="wfl">{where}</span></div><div class="wfw"><div class="wfp"><div class="wbar"></div>'
|
|
522
|
+
f"{''.join(body)}</div></div></div>"
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _item_html(stops: list[_Stop], i: int, card: str) -> str:
|
|
527
|
+
"""
|
|
528
|
+
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
|
+
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.
|
|
531
|
+
"""
|
|
532
|
+
stop = stops[i]
|
|
533
|
+
step = stop.step
|
|
534
|
+
if step.kind in ("def", "module"):
|
|
535
|
+
name = step.file if step.kind == "module" else _name_of(step)
|
|
536
|
+
where = "runs when it's imported" if step.kind == "module" else step.file
|
|
537
|
+
text = (
|
|
538
|
+
f'<span class="wtx"><b>{_esc(name)}</b><span class="wof">'
|
|
539
|
+
f"{_esc('again, shown before' if step.seen else where)}</span></span>"
|
|
540
|
+
)
|
|
541
|
+
else:
|
|
542
|
+
headline, said = _headline(stop)
|
|
543
|
+
dot = f'<span class="wdot d-{said}" title="{_esc(_NOTE_TIPS.get(said, ""))}"></span>' if said in _DOTS else ""
|
|
544
|
+
text = f'<span class="wtx">{headline}</span>{dot}'
|
|
545
|
+
checked = " checked" if i == 0 else ""
|
|
546
|
+
return (
|
|
547
|
+
f'<input type="radio" class="wr" name="{_GROUP}" id="cw{i}"{checked}><label class="wil k-{_esc(step.kind)}" '
|
|
548
|
+
f'for="cw{i}" style="--d:{stop.depth}"><span class="wno">{i + 1}</span><span class="wx">{text}</span>'
|
|
549
|
+
f"</label>{card}"
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
@_draws(_Walk, "html")
|
|
554
|
+
def _walk_html(block: _Walk, report: _Report) -> str:
|
|
555
|
+
"""
|
|
556
|
+
The list of steps, each function's (or module's) steps in a box of their own with its code after them: the CSS
|
|
557
|
+
shows the card after the checked radio button, and the code of the box it's in (`.wr:checked ~ .wf`). No :has(),
|
|
558
|
+
so a step costs the same however many there are; the one rule a step adds says which line of its code to light.
|
|
559
|
+
"""
|
|
560
|
+
title = f"<h4>{_prose(block.title)}</h4>" if block.title else ""
|
|
561
|
+
walker = _Walker(block)
|
|
562
|
+
stops, frames = walker.stops, walker.frames
|
|
563
|
+
if not stops:
|
|
564
|
+
return title + f'<div class="more">{_esc(block.empty)}</div>'
|
|
565
|
+
root = report.root or _ROOT
|
|
566
|
+
highlighted: dict[str, list[str]] = {}
|
|
567
|
+
first: dict[str, int] = {}
|
|
568
|
+
for i, stop in enumerate(stops):
|
|
569
|
+
if stop.step.kind == "def" and not stop.step.seen:
|
|
570
|
+
first.setdefault(stop.step.name, i)
|
|
571
|
+
docs = {f.name: f.doc for f in block.functions if f.doc}
|
|
572
|
+
drawn, rows_of = [], []
|
|
573
|
+
for k, frame in enumerate(frames):
|
|
574
|
+
if frame.file in block.sources and frame.file not in highlighted:
|
|
575
|
+
highlighted[frame.file] = _python_lines(block.sources[frame.file])
|
|
576
|
+
rows = _frame_rows(frame, highlighted.get(frame.file, []), stops)
|
|
577
|
+
rows_of.append({number: n for n, (number, _) in enumerate(rows) if number})
|
|
578
|
+
drawn.append(_frame_html(k, frame, rows))
|
|
579
|
+
out: list[str] = []
|
|
580
|
+
opened: list[int] = [] # the frames whose boxes are open, innermost last
|
|
581
|
+
chapter = -1
|
|
582
|
+
for i, stop in enumerate(stops):
|
|
583
|
+
if stop.chapter != chapter:
|
|
584
|
+
while opened:
|
|
585
|
+
out.append(drawn[opened.pop()] + "</div>")
|
|
586
|
+
chapter = stop.chapter
|
|
587
|
+
if chapter == 0:
|
|
588
|
+
out.append(f'<div class="wch">{_esc(block.startup_title)}</div>')
|
|
589
|
+
elif block.startup:
|
|
590
|
+
out.append(f'<div class="wch">{_esc(block.calls_title)}</div>')
|
|
591
|
+
while opened and opened[-1] != stop.frame and stop.frame in opened: # back from a call
|
|
592
|
+
out.append(drawn[opened.pop()] + "</div>")
|
|
593
|
+
if not opened or opened[-1] != stop.frame: # into a call
|
|
594
|
+
out.append('<div class="wfn">')
|
|
595
|
+
opened.append(stop.frame)
|
|
596
|
+
out.append(_item_html(stops, i, _card_html(stops, i, first, docs, block.editor)))
|
|
597
|
+
at = rows_of[stop.frame].get(stop.lit)
|
|
598
|
+
if at is not None: # the line its code lights
|
|
599
|
+
report.rules.add(f".{root} #cw{i}:checked~.wf{stop.frame}{{--k:{at}}}")
|
|
600
|
+
while opened:
|
|
601
|
+
out.append(drawn[opened.pop()] + "</div>")
|
|
602
|
+
sub = (
|
|
603
|
+
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.</div>"
|
|
605
|
+
)
|
|
606
|
+
return (
|
|
607
|
+
f'{title}{sub}<div class="walk"><div class="wl" role="radiogroup" aria-label="The steps">{"".join(out)}'
|
|
608
|
+
'</div><div class="ws"></div></div>'
|
|
609
|
+
)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
@_draws(_Walk, "text")
|
|
613
|
+
def _walk_lines(block: _Walk, report: _Report) -> list[str]:
|
|
614
|
+
out: list[str] = []
|
|
615
|
+
if block.startup:
|
|
616
|
+
out += _outline_lines(_Outline(block.startup, block.startup_title), report)
|
|
617
|
+
return out + _outline_lines(_Outline(block.steps, block.title, empty=block.empty), report)
|
|
618
|
+
|
|
619
|
+
|
|
620
|
+
def walk_stops(block: _Walk) -> list[tuple[int, str, list[str]]]:
|
|
621
|
+
"""
|
|
622
|
+
The walk's stops in the order they're stepped through: (line, code, the calls it's inside, outermost first).
|
|
623
|
+
"""
|
|
624
|
+
return [(s.step.line, s.step.code, [name for name, _ in s.stack]) for s in _Walker(block).stops]
|