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.
Files changed (178) hide show
  1. code_node/__init__.py +40 -0
  2. code_node/__main__.py +7 -0
  3. code_node/analysis/__init__.py +3 -0
  4. code_node/analysis/aws_model.py +197 -0
  5. code_node/analysis/findings.py +462 -0
  6. code_node/analysis/graph.py +780 -0
  7. code_node/analysis/mapping.py +114 -0
  8. code_node/analysis/model.py +289 -0
  9. code_node/analysis/reader.py +1926 -0
  10. code_node/analysis/syntax.py +220 -0
  11. code_node/analysis/terms.py +505 -0
  12. code_node/cli.py +316 -0
  13. code_node/deploys/__init__.py +321 -0
  14. code_node/deploys/sam.py +371 -0
  15. code_node/deploys/serverless.py +117 -0
  16. code_node/kit/__init__.py +4 -0
  17. code_node/kit/blocks.py +184 -0
  18. code_node/kit/fmt.py +389 -0
  19. code_node/kit/highlight.py +131 -0
  20. code_node/kit/render.py +1066 -0
  21. code_node/kit/text.py +81 -0
  22. code_node/libraries/__init__.py +333 -0
  23. code_node/libraries/llm.py +124 -0
  24. code_node/libraries/mongo.py +169 -0
  25. code_node/libraries/redis.py +253 -0
  26. code_node/libraries/sql.py +232 -0
  27. code_node/pages.py +92 -0
  28. code_node/platforms/__init__.py +203 -0
  29. code_node/platforms/aws_lambda.py +290 -0
  30. code_node/platforms/web.py +301 -0
  31. code_node/project.py +402 -0
  32. code_node/report/__init__.py +3 -0
  33. code_node/report/brands.py +244 -0
  34. code_node/report/build.py +119 -0
  35. code_node/report/editor.py +40 -0
  36. code_node/report/graph.py +1411 -0
  37. code_node/report/logos/README.md +8 -0
  38. code_node/report/logos/anthropic-icon.svg +1 -0
  39. code_node/report/logos/aws-amplify.svg +1 -0
  40. code_node/report/logos/aws-api-gateway.svg +1 -0
  41. code_node/report/logos/aws-appflow.svg +1 -0
  42. code_node/report/logos/aws-appsync.svg +1 -0
  43. code_node/report/logos/aws-athena.svg +1 -0
  44. code_node/report/logos/aws-backup.svg +1 -0
  45. code_node/report/logos/aws-batch.svg +1 -0
  46. code_node/report/logos/aws-certificate-manager.svg +1 -0
  47. code_node/report/logos/aws-cloudformation.svg +1 -0
  48. code_node/report/logos/aws-cloudfront.svg +1 -0
  49. code_node/report/logos/aws-cloudtrail.svg +1 -0
  50. code_node/report/logos/aws-cloudwatch.svg +1 -0
  51. code_node/report/logos/aws-codebuild.svg +1 -0
  52. code_node/report/logos/aws-codecommit.svg +1 -0
  53. code_node/report/logos/aws-codedeploy.svg +1 -0
  54. code_node/report/logos/aws-codepipeline.svg +1 -0
  55. code_node/report/logos/aws-cognito.svg +1 -0
  56. code_node/report/logos/aws-config.svg +1 -0
  57. code_node/report/logos/aws-documentdb.svg +1 -0
  58. code_node/report/logos/aws-dynamodb.svg +1 -0
  59. code_node/report/logos/aws-ec2.svg +1 -0
  60. code_node/report/logos/aws-ecs.svg +1 -0
  61. code_node/report/logos/aws-eks.svg +1 -0
  62. code_node/report/logos/aws-elasticache.svg +1 -0
  63. code_node/report/logos/aws-elb.svg +1 -0
  64. code_node/report/logos/aws-eventbridge.svg +1 -0
  65. code_node/report/logos/aws-glacier.svg +1 -0
  66. code_node/report/logos/aws-glue.svg +1 -0
  67. code_node/report/logos/aws-iam.svg +1 -0
  68. code_node/report/logos/aws-keyspaces.svg +1 -0
  69. code_node/report/logos/aws-kinesis.svg +1 -0
  70. code_node/report/logos/aws-kms.svg +1 -0
  71. code_node/report/logos/aws-lake-formation.svg +1 -0
  72. code_node/report/logos/aws-lambda.svg +1 -0
  73. code_node/report/logos/aws-lightsail.svg +1 -0
  74. code_node/report/logos/aws-mq.svg +1 -0
  75. code_node/report/logos/aws-msk.svg +1 -0
  76. code_node/report/logos/aws-neptune.svg +1 -0
  77. code_node/report/logos/aws-open-search.svg +1 -0
  78. code_node/report/logos/aws-quicksight.svg +1 -0
  79. code_node/report/logos/aws-rds.svg +1 -0
  80. code_node/report/logos/aws-redshift.svg +1 -0
  81. code_node/report/logos/aws-route53.svg +1 -0
  82. code_node/report/logos/aws-s3.svg +1 -0
  83. code_node/report/logos/aws-secrets-manager.svg +1 -0
  84. code_node/report/logos/aws-ses.svg +1 -0
  85. code_node/report/logos/aws-shield.svg +1 -0
  86. code_node/report/logos/aws-sns.svg +1 -0
  87. code_node/report/logos/aws-sqs.svg +1 -0
  88. code_node/report/logos/aws-step-functions.svg +1 -0
  89. code_node/report/logos/aws-systems-manager.svg +1 -0
  90. code_node/report/logos/aws-timestream.svg +1 -0
  91. code_node/report/logos/aws-waf.svg +1 -0
  92. code_node/report/logos/aws-xray.svg +1 -0
  93. code_node/report/logos/aws.svg +1 -0
  94. code_node/report/logos/azure-icon.svg +1 -0
  95. code_node/report/logos/django-icon.svg +1 -0
  96. code_node/report/logos/elasticsearch.svg +1 -0
  97. code_node/report/logos/fastapi-icon.svg +1 -0
  98. code_node/report/logos/firebase-icon.svg +1 -0
  99. code_node/report/logos/flask.svg +1 -0
  100. code_node/report/logos/google-cloud.svg +1 -0
  101. code_node/report/logos/kafka-icon.svg +1 -0
  102. code_node/report/logos/mariadb-icon.svg +1 -0
  103. code_node/report/logos/mongodb-icon.svg +1 -0
  104. code_node/report/logos/mysql-icon.svg +1 -0
  105. code_node/report/logos/openai-icon.svg +1 -0
  106. code_node/report/logos/postgresql.svg +1 -0
  107. code_node/report/logos/python.svg +1 -0
  108. code_node/report/logos/rabbitmq-icon.svg +1 -0
  109. code_node/report/logos/redis.svg +1 -0
  110. code_node/report/logos/serverless.svg +1 -0
  111. code_node/report/logos/slack-icon.svg +1 -0
  112. code_node/report/logos/small/aws-api-gateway.svg +1 -0
  113. code_node/report/logos/small/aws-appflow.svg +1 -0
  114. code_node/report/logos/small/aws-appsync.svg +1 -0
  115. code_node/report/logos/small/aws-athena.svg +1 -0
  116. code_node/report/logos/small/aws-backup.svg +1 -0
  117. code_node/report/logos/small/aws-batch.svg +1 -0
  118. code_node/report/logos/small/aws-certificate-manager.svg +1 -0
  119. code_node/report/logos/small/aws-cloudformation.svg +1 -0
  120. code_node/report/logos/small/aws-cloudfront.svg +1 -0
  121. code_node/report/logos/small/aws-cloudtrail.svg +1 -0
  122. code_node/report/logos/small/aws-cloudwatch.svg +1 -0
  123. code_node/report/logos/small/aws-codebuild.svg +1 -0
  124. code_node/report/logos/small/aws-codecommit.svg +1 -0
  125. code_node/report/logos/small/aws-codedeploy.svg +1 -0
  126. code_node/report/logos/small/aws-codepipeline.svg +1 -0
  127. code_node/report/logos/small/aws-cognito.svg +1 -0
  128. code_node/report/logos/small/aws-config.svg +1 -0
  129. code_node/report/logos/small/aws-documentdb.svg +1 -0
  130. code_node/report/logos/small/aws-dynamodb.svg +1 -0
  131. code_node/report/logos/small/aws-ec2.svg +1 -0
  132. code_node/report/logos/small/aws-ecs.svg +1 -0
  133. code_node/report/logos/small/aws-eks.svg +1 -0
  134. code_node/report/logos/small/aws-elasticache.svg +1 -0
  135. code_node/report/logos/small/aws-elb.svg +1 -0
  136. code_node/report/logos/small/aws-eventbridge.svg +1 -0
  137. code_node/report/logos/small/aws-glacier.svg +1 -0
  138. code_node/report/logos/small/aws-glue.svg +1 -0
  139. code_node/report/logos/small/aws-iam.svg +1 -0
  140. code_node/report/logos/small/aws-keyspaces.svg +1 -0
  141. code_node/report/logos/small/aws-kinesis.svg +1 -0
  142. code_node/report/logos/small/aws-kms.svg +1 -0
  143. code_node/report/logos/small/aws-lake-formation.svg +1 -0
  144. code_node/report/logos/small/aws-lambda.svg +1 -0
  145. code_node/report/logos/small/aws-mq.svg +1 -0
  146. code_node/report/logos/small/aws-msk.svg +1 -0
  147. code_node/report/logos/small/aws-neptune.svg +1 -0
  148. code_node/report/logos/small/aws-open-search.svg +1 -0
  149. code_node/report/logos/small/aws-quicksight.svg +1 -0
  150. code_node/report/logos/small/aws-rds.svg +1 -0
  151. code_node/report/logos/small/aws-redshift.svg +1 -0
  152. code_node/report/logos/small/aws-route53.svg +1 -0
  153. code_node/report/logos/small/aws-s3.svg +1 -0
  154. code_node/report/logos/small/aws-secrets-manager.svg +1 -0
  155. code_node/report/logos/small/aws-ses.svg +1 -0
  156. code_node/report/logos/small/aws-shield.svg +1 -0
  157. code_node/report/logos/small/aws-sns.svg +1 -0
  158. code_node/report/logos/small/aws-sqs.svg +1 -0
  159. code_node/report/logos/small/aws-step-functions.svg +1 -0
  160. code_node/report/logos/small/aws-systems-manager.svg +1 -0
  161. code_node/report/logos/small/aws-timestream.svg +1 -0
  162. code_node/report/logos/small/aws-waf.svg +1 -0
  163. code_node/report/logos/small/aws-xray.svg +1 -0
  164. code_node/report/logos/snowflake-icon.svg +1 -0
  165. code_node/report/logos/sqlite.svg +1 -0
  166. code_node/report/logos/stripe.svg +1 -0
  167. code_node/report/logos/supabase-icon.svg +1 -0
  168. code_node/report/logos/twilio-icon.svg +1 -0
  169. code_node/report/outline.py +219 -0
  170. code_node/report/style.py +358 -0
  171. code_node/report/tabs.py +73 -0
  172. code_node/report/walk.py +624 -0
  173. code_node/serve.py +277 -0
  174. code_node_graph-0.1.0.dist-info/METADATA +165 -0
  175. code_node_graph-0.1.0.dist-info/RECORD +178 -0
  176. code_node_graph-0.1.0.dist-info/WHEEL +4 -0
  177. code_node_graph-0.1.0.dist-info/entry_points.txt +2 -0
  178. code_node_graph-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -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]