pyflightstream 0.2.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 (71) hide show
  1. pyflightstream/__init__.py +27 -0
  2. pyflightstream/cases/__init__.py +332 -0
  3. pyflightstream/cases/matrix_legacy.py +337 -0
  4. pyflightstream/commands/__init__.py +544 -0
  5. pyflightstream/commands/_meta.yaml +18 -0
  6. pyflightstream/commands/actuators.yaml +167 -0
  7. pyflightstream/commands/advanced_settings.yaml +136 -0
  8. pyflightstream/commands/aeroelastic_coupling.yaml +172 -0
  9. pyflightstream/commands/boundary_conditions.yaml +153 -0
  10. pyflightstream/commands/ccs_wing_mesh.yaml +74 -0
  11. pyflightstream/commands/coordinate_systems.yaml +123 -0
  12. pyflightstream/commands/file_io.yaml +82 -0
  13. pyflightstream/commands/mesh_import_export.yaml +72 -0
  14. pyflightstream/commands/motion_definitions.yaml +185 -0
  15. pyflightstream/commands/probe_points.yaml +116 -0
  16. pyflightstream/commands/runtime_settings.yaml +160 -0
  17. pyflightstream/commands/scenes.yaml +14 -0
  18. pyflightstream/commands/script_controls.yaml +39 -0
  19. pyflightstream/commands/simulation_controls.yaml +30 -0
  20. pyflightstream/commands/solver_analysis.yaml +118 -0
  21. pyflightstream/commands/solver_export.yaml +138 -0
  22. pyflightstream/commands/solver_initialization.yaml +95 -0
  23. pyflightstream/commands/solver_settings.yaml +120 -0
  24. pyflightstream/commands/streamlines.yaml +63 -0
  25. pyflightstream/commands/surface_sections.yaml +145 -0
  26. pyflightstream/commands/sweeper.yaml +115 -0
  27. pyflightstream/commands/unsteady_solver.yaml +68 -0
  28. pyflightstream/commands/volume_sections.yaml +148 -0
  29. pyflightstream/farfield/__init__.py +613 -0
  30. pyflightstream/files/__init__.py +375 -0
  31. pyflightstream/fsi/__init__.py +57 -0
  32. pyflightstream/fsi/beam.py +417 -0
  33. pyflightstream/fsi/centrifugal.py +418 -0
  34. pyflightstream/fsi/cli.py +206 -0
  35. pyflightstream/fsi/config.py +316 -0
  36. pyflightstream/fsi/driver.py +501 -0
  37. pyflightstream/fsi/kinematics.py +153 -0
  38. pyflightstream/fsi/loads.py +740 -0
  39. pyflightstream/fsi/nodes.py +463 -0
  40. pyflightstream/fsi/state.py +181 -0
  41. pyflightstream/post/__init__.py +18 -0
  42. pyflightstream/post/writers.py +195 -0
  43. pyflightstream/probes/__init__.py +453 -0
  44. pyflightstream/probes/geometry.py +281 -0
  45. pyflightstream/probes/planar.py +477 -0
  46. pyflightstream/qa/__init__.py +59 -0
  47. pyflightstream/qa/cli.py +364 -0
  48. pyflightstream/qa/compat.py +285 -0
  49. pyflightstream/qa/drift.py +406 -0
  50. pyflightstream/qa/geometry.py +421 -0
  51. pyflightstream/qa/physics.py +1744 -0
  52. pyflightstream/qa/probes.py +1023 -0
  53. pyflightstream/qa/references/PHY-01.yaml +38 -0
  54. pyflightstream/qa/references/PHY-02.yaml +28 -0
  55. pyflightstream/qa/references/PHY-05.yaml +29 -0
  56. pyflightstream/qa/references/PHY-06.yaml +90 -0
  57. pyflightstream/qa/references/SMI-01.yaml +28 -0
  58. pyflightstream/qa/references/SMI-02.yaml +28 -0
  59. pyflightstream/qa/specs.py +1405 -0
  60. pyflightstream/reference.py +465 -0
  61. pyflightstream/results/__init__.py +547 -0
  62. pyflightstream/run/__init__.py +677 -0
  63. pyflightstream/script/__init__.py +474 -0
  64. pyflightstream/script/helpers.py +844 -0
  65. pyflightstream/versions.py +191 -0
  66. pyflightstream-0.2.0.dist-info/METADATA +88 -0
  67. pyflightstream-0.2.0.dist-info/RECORD +71 -0
  68. pyflightstream-0.2.0.dist-info/WHEEL +5 -0
  69. pyflightstream-0.2.0.dist-info/entry_points.txt +3 -0
  70. pyflightstream-0.2.0.dist-info/licenses/LICENSE +21 -0
  71. pyflightstream-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,474 @@
1
+ """The validating FlightStream script builder.
2
+
3
+ Pipeline role: turns typed Python calls into the ASCII script text the
4
+ solver executes, validating every emission against the per-version
5
+ command database before a single line reaches FlightStream. Errors
6
+ happen at build time with manual citations, because solver-side
7
+ failures are silent or cryptic.
8
+
9
+ A :class:`Script` is an ordinary object bound to one FlightStream
10
+ version; two scripts coexist safely, and there is no module-level
11
+ state. Emission is checked in order: command exists in the version,
12
+ argument binding and types, enum membership, count-versus-list
13
+ consistency, phase ordering, and cross references. The ``raw()``
14
+ escape hatch bypasses validation and flags the script for the run
15
+ manifest.
16
+
17
+ Cross references (SAD Section 4.2): the script counts the local
18
+ coordinate systems, actuators, and motions it creates, and rejects a
19
+ command citing an index that does not exist yet, because FlightStream
20
+ expects auxiliary definitions before they are referenced and fails
21
+ silently otherwise. Frames carried by an opened project file are
22
+ declared with :meth:`Script.declare_existing`.
23
+
24
+ The two gaps of the first cut are closed: the per-surface lines of
25
+ INITIALIZE_SOLVER when SURFACES is not -1 (``surface_toggles``) and
26
+ the PERIODIC symmetry copy count (``symmetry_copies``) are regular
27
+ database arguments now, emitted comfortably through the curated
28
+ helper layer in :mod:`pyflightstream.script.helpers` (SAD Section
29
+ 4.3).
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import os
35
+ from collections.abc import Sequence
36
+
37
+ from pyflightstream.commands import (
38
+ ArgSpec,
39
+ ArgType,
40
+ CommandEntry,
41
+ CommandRegistry,
42
+ Layout,
43
+ ListSeparator,
44
+ Phase,
45
+ )
46
+ from pyflightstream.versions import FsVersion
47
+
48
+ _ORDERED_PHASES = (
49
+ Phase.GEOMETRY,
50
+ Phase.SETUP,
51
+ Phase.INIT,
52
+ Phase.EXEC,
53
+ Phase.ANALYSIS,
54
+ Phase.EXPORT,
55
+ )
56
+
57
+ _COUNT_ARG_NAMES = {
58
+ "count",
59
+ "surfaces",
60
+ "numpts",
61
+ "num_boundaries",
62
+ "num_variables",
63
+ "num_frames",
64
+ "num_sections",
65
+ }
66
+
67
+ # Cross-reference ledger (SAD Section 4.2): commands that create an
68
+ # indexed auxiliary object, commands that delete one, and the argument
69
+ # names that cite one. Frame index 1 is the reference frame and always
70
+ # exists (SRC-003 p.329); created local frames take indices 2 upward.
71
+ _CREATION_COMMANDS = {
72
+ "CREATE_NEW_COORDINATE_SYSTEM": "frames",
73
+ "CREATE_NEW_ACTUATOR": "actuators",
74
+ "CREATE_NEW_MOTION": "motions",
75
+ }
76
+ _DELETION_COMMANDS = {
77
+ "DELETE_ACTUATOR": "actuators",
78
+ "DELETE_MOTION": "motions",
79
+ }
80
+ _SCALAR_REFERENCE_ARGS = {
81
+ "frame": "frames",
82
+ "load_frame": "frames",
83
+ "coordinate_system_id": "frames",
84
+ "actuator_index": "actuators",
85
+ "motion_id": "motions",
86
+ }
87
+ _LIST_REFERENCE_ARGS = {
88
+ "frame_indices": "frames",
89
+ }
90
+ _REFERENCE_NOUNS = {
91
+ "frames": "local coordinate system",
92
+ "actuators": "actuator",
93
+ "motions": "motion",
94
+ }
95
+
96
+
97
+ class ScriptOrderError(ValueError):
98
+ """A command was emitted after its phase had already passed.
99
+
100
+ The script builder tracks the highest phase reached (geometry,
101
+ setup, init, exec, analysis, export); FlightStream expects
102
+ auxiliary definitions such as coordinate systems, actuators, and
103
+ motions before solver initialization. Control commands are exempt.
104
+ """
105
+
106
+
107
+ class CommandArgumentError(ValueError):
108
+ """An emitted argument does not satisfy the database specification.
109
+
110
+ The message names the command, the argument, the expectation, and
111
+ the manual citation of the entry, so the fix can be checked against
112
+ the manual directly.
113
+ """
114
+
115
+
116
+ class ScriptReferenceError(ValueError):
117
+ """A command cites a frame, actuator, or motion not created yet.
118
+
119
+ FlightStream resolves these indices at execution time and fails
120
+ silently or cryptically when they do not exist; the builder counts
121
+ the objects the script creates and rejects the citation at build
122
+ time instead (SAD Section 4.2). Objects already present in the
123
+ opened project file are declared with
124
+ :meth:`Script.declare_existing`.
125
+ """
126
+
127
+
128
+ def _type_error(entry: CommandEntry, spec: ArgSpec, expected: str, value: object) -> None:
129
+ raise CommandArgumentError(
130
+ f"{entry.name}: argument {spec.name!r} expects {expected}, got {value!r} "
131
+ f"({entry.manual_ref})"
132
+ )
133
+
134
+
135
+ def _match_enum(entry: CommandEntry, spec: ArgSpec, value: object) -> str:
136
+ if isinstance(value, str):
137
+ for member in spec.values:
138
+ if member.upper() == value.upper():
139
+ return member
140
+ _type_error(entry, spec, f"one of {', '.join(spec.values)}", value)
141
+ raise AssertionError("unreachable")
142
+
143
+
144
+ def _check_scalar(entry: CommandEntry, spec: ArgSpec, value: object) -> object:
145
+ if spec.type is ArgType.INT:
146
+ if isinstance(value, bool) or not isinstance(value, int):
147
+ _type_error(entry, spec, "an integer", value)
148
+ return value
149
+ if spec.type is ArgType.FLOAT:
150
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
151
+ _type_error(entry, spec, "a real number", value)
152
+ return value
153
+ if spec.type is ArgType.PATH:
154
+ if not isinstance(value, (str, os.PathLike)):
155
+ _type_error(entry, spec, "a path", value)
156
+ return str(value)
157
+ if spec.type is ArgType.STR:
158
+ if not isinstance(value, str):
159
+ _type_error(entry, spec, "a string", value)
160
+ return value
161
+ if spec.type is ArgType.ENUM:
162
+ return _match_enum(entry, spec, value)
163
+ if spec.type is ArgType.BOOL:
164
+ if not isinstance(value, bool):
165
+ _type_error(entry, spec, "True or False", value)
166
+ return value
167
+ raise AssertionError(f"unhandled scalar type {spec.type}")
168
+
169
+
170
+ def _check_list(entry: CommandEntry, spec: ArgSpec, value: object) -> list:
171
+ if isinstance(value, str) or not isinstance(value, Sequence):
172
+ _type_error(entry, spec, "a sequence of values", value)
173
+ items = list(value)
174
+ if spec.type is ArgType.INT_LIST:
175
+ for item in items:
176
+ if isinstance(item, bool) or not isinstance(item, int):
177
+ _type_error(entry, spec, "a sequence of integers", value)
178
+ return items
179
+ if spec.type is ArgType.FLOAT_LIST:
180
+ for item in items:
181
+ if isinstance(item, bool) or not isinstance(item, (int, float)):
182
+ _type_error(entry, spec, "a sequence of real numbers", value)
183
+ return items
184
+ if spec.type is ArgType.STR_LIST:
185
+ for item in items:
186
+ if not isinstance(item, str):
187
+ _type_error(entry, spec, "a sequence of strings", value)
188
+ return items
189
+ return [_match_enum(entry, spec, item) for item in items]
190
+
191
+
192
+ class Script:
193
+ """One FlightStream script under construction, bound to one version.
194
+
195
+ Parameters
196
+ ----------
197
+ version : str or FsVersion
198
+ Target FlightStream version, canonical or alias; every
199
+ emission is validated against this version's command view.
200
+ registry : CommandRegistry, optional
201
+ Alternative database, used by tests; defaults to the packaged
202
+ one.
203
+
204
+ Attributes
205
+ ----------
206
+ version : FsVersion
207
+ The resolved target version.
208
+ raw_flag : bool
209
+ True once ``raw()`` was used; recorded in the run manifest so
210
+ unvalidated scripts stay identifiable (FR-07).
211
+ """
212
+
213
+ def __init__(self, version: str | FsVersion, registry: CommandRegistry | None = None):
214
+ view = (registry or CommandRegistry.load()).for_version(version)
215
+ self._view = view
216
+ self.version: FsVersion = view.version
217
+ self.raw_flag = False
218
+ self._lines: list[str] = []
219
+ self._phase_index: int | None = None
220
+ self._phase_setter: tuple[str, int] | None = None
221
+ self._created = {"frames": 0, "actuators": 0, "motions": 0}
222
+
223
+ @property
224
+ def num_local_frames(self) -> int:
225
+ """Local coordinate systems the script created or declared."""
226
+ return self._created["frames"]
227
+
228
+ @property
229
+ def num_actuators(self) -> int:
230
+ """Actuators the script created or declared."""
231
+ return self._created["actuators"]
232
+
233
+ @property
234
+ def num_motions(self) -> int:
235
+ """Motions the script created or declared."""
236
+ return self._created["motions"]
237
+
238
+ def declare_existing(self, *, frames: int = 0, actuators: int = 0, motions: int = 0) -> None:
239
+ """Declare auxiliary objects already present in the opened project.
240
+
241
+ A simulation file loaded with OPEN can carry local coordinate
242
+ systems, actuators, and motions saved earlier; the builder
243
+ cannot see inside the file, so scripts citing those objects
244
+ declare them here to satisfy the cross-reference check.
245
+
246
+ Parameters
247
+ ----------
248
+ frames : int
249
+ Local coordinate systems in the project, beyond the
250
+ reference frame (index 1), which always exists.
251
+ actuators : int
252
+ Actuators in the project.
253
+ motions : int
254
+ Motions in the project.
255
+ """
256
+ for kind, extra in (("frames", frames), ("actuators", actuators), ("motions", motions)):
257
+ if extra < 0:
258
+ raise ValueError(f"declared {kind} must be zero or positive, got {extra}")
259
+ self._created[kind] += extra
260
+
261
+ def emit(self, name: str, /, *args: object, **kwargs: object) -> None:
262
+ """Validate and append one command.
263
+
264
+ Parameters
265
+ ----------
266
+ name : str
267
+ Command name as in the database; positional-only, so a
268
+ command argument may itself be called ``name`` (for
269
+ example CREATE_NEW_ACTUATOR).
270
+ *args, **kwargs
271
+ Argument values, positional in database order or by
272
+ argument name. Optional arguments may be omitted.
273
+
274
+ Raises
275
+ ------
276
+ CommandNotInVersionError
277
+ If the command does not exist in this version; the message
278
+ carries the manual citation and successor when known.
279
+ CommandArgumentError
280
+ If an argument violates the typed specification.
281
+ ScriptOrderError
282
+ If the command's phase precedes the phase already reached.
283
+ ScriptReferenceError
284
+ If the command cites a frame, actuator, or motion index the
285
+ script has not created or declared yet.
286
+ """
287
+ entry = self._view[name]
288
+ bound = self._bind(entry, args, kwargs)
289
+ self._check_phase(entry)
290
+ self._check_references(entry, bound)
291
+ block, multiline = self._render_command(entry, bound)
292
+ self._lines.extend(block)
293
+ if multiline:
294
+ self._lines.append("")
295
+ if entry.name in _CREATION_COMMANDS:
296
+ self._created[_CREATION_COMMANDS[entry.name]] += 1
297
+ elif entry.name in _DELETION_COMMANDS:
298
+ self._created[_DELETION_COMMANDS[entry.name]] -= 1
299
+
300
+ def raw(self, text: str) -> None:
301
+ """Append unvalidated script text and flag the script (FR-07)."""
302
+ self.raw_flag = True
303
+ self._lines.extend(text.splitlines())
304
+
305
+ def comment(self, text: str) -> None:
306
+ """Append a comment line; FlightStream ignores lines starting with #."""
307
+ self._lines.append(f"# {text}")
308
+
309
+ def render(self) -> str:
310
+ """Return the complete script text, newline terminated."""
311
+ return "\n".join(self._lines) + "\n"
312
+
313
+ def _bind(self, entry: CommandEntry, args: tuple, kwargs: dict) -> dict[str, object]:
314
+ specs = entry.args
315
+ if len(args) > len(specs):
316
+ raise CommandArgumentError(
317
+ f"{entry.name} takes at most {len(specs)} arguments, got {len(args)} "
318
+ f"({entry.manual_ref})"
319
+ )
320
+ bound: dict[str, object] = {}
321
+ for spec, value in zip(specs, args, strict=False):
322
+ bound[spec.name] = value
323
+ known = {spec.name for spec in specs}
324
+ for key, value in kwargs.items():
325
+ if key not in known:
326
+ raise CommandArgumentError(
327
+ f"{entry.name} has no argument {key!r}; arguments are "
328
+ f"{', '.join(sorted(known)) or 'none'} ({entry.manual_ref})"
329
+ )
330
+ if key in bound:
331
+ raise CommandArgumentError(
332
+ f"{entry.name}: argument {key!r} given twice ({entry.manual_ref})"
333
+ )
334
+ bound[key] = value
335
+ checked: dict[str, object] = {}
336
+ for spec in specs:
337
+ if spec.name not in bound:
338
+ if spec.required:
339
+ raise CommandArgumentError(
340
+ f"{entry.name} requires argument {spec.name!r} ({entry.manual_ref})"
341
+ )
342
+ continue
343
+ value = bound[spec.name]
344
+ if spec.is_list:
345
+ checked[spec.name] = _check_list(entry, spec, value)
346
+ else:
347
+ checked[spec.name] = _check_scalar(entry, spec, value)
348
+ self._check_counts(entry, checked)
349
+ return checked
350
+
351
+ def _check_counts(self, entry: CommandEntry, bound: dict[str, object]) -> None:
352
+ count_value: object | None = None
353
+ for spec in entry.args:
354
+ if spec.name in _COUNT_ARG_NAMES and spec.name in bound:
355
+ count_value = bound[spec.name]
356
+ elif spec.is_list and spec.name in bound and isinstance(count_value, int):
357
+ if count_value >= 0 and count_value != len(bound[spec.name]):
358
+ raise CommandArgumentError(
359
+ f"{entry.name}: the declared count is {count_value} but "
360
+ f"{spec.name!r} holds {len(bound[spec.name])} values "
361
+ f"({entry.manual_ref})"
362
+ )
363
+
364
+ def _check_phase(self, entry: CommandEntry) -> None:
365
+ if entry.phase is Phase.CONTROL:
366
+ return
367
+ index = _ORDERED_PHASES.index(entry.phase)
368
+ if self._phase_index is not None and index < self._phase_index:
369
+ setter_name, setter_line = self._phase_setter
370
+ current = _ORDERED_PHASES[self._phase_index]
371
+ raise ScriptOrderError(
372
+ f"{entry.name} is a {entry.phase} command, but the script already "
373
+ f"reached the {current} phase ({setter_name} at line {setter_line}). "
374
+ "Auxiliary definitions such as coordinate systems, actuators, and "
375
+ "motions must precede solver initialization; the phase order is "
376
+ "geometry, setup, init, exec, analysis, export."
377
+ )
378
+ if self._phase_index is None or index > self._phase_index:
379
+ self._phase_index = index
380
+ self._phase_setter = (entry.name, len(self._lines) + 1)
381
+
382
+ def _check_references(self, entry: CommandEntry, bound: dict[str, object]) -> None:
383
+ for spec in entry.args:
384
+ if spec.name not in bound:
385
+ continue
386
+ kind = _SCALAR_REFERENCE_ARGS.get(spec.name)
387
+ if kind is not None:
388
+ self._check_one_reference(entry, spec.name, kind, bound[spec.name])
389
+ kind = _LIST_REFERENCE_ARGS.get(spec.name)
390
+ if kind is not None:
391
+ for value in bound[spec.name]:
392
+ if value != -1:
393
+ self._check_one_reference(entry, spec.name, kind, value)
394
+
395
+ def _check_one_reference(self, entry: CommandEntry, arg: str, kind: str, value: object) -> None:
396
+ limit = self._created[kind] + (1 if kind == "frames" else 0)
397
+ if isinstance(value, int) and 1 <= value <= limit:
398
+ return
399
+ noun = _REFERENCE_NOUNS[kind]
400
+ if kind == "frames":
401
+ available = (
402
+ f"the reference frame is index 1 and the script has created or declared "
403
+ f"{self._created[kind]} local frame(s), so valid indices run 1 to {limit}"
404
+ )
405
+ else:
406
+ available = (
407
+ f"the script has created or declared {self._created[kind]} {noun}(s), "
408
+ f"so valid indices run 1 to {limit}"
409
+ )
410
+ raise ScriptReferenceError(
411
+ f"{entry.name}: argument {arg!r} cites {noun} {value!r}, but {available}. "
412
+ "FlightStream expects auxiliary definitions before they are referenced; "
413
+ "create the object earlier in the script, or declare objects carried by "
414
+ f"the opened project with declare_existing(). ({entry.manual_ref})"
415
+ )
416
+
417
+ def _format_scalar(self, value: object) -> str:
418
+ return str(value)
419
+
420
+ def _list_lines(self, spec: ArgSpec, items: list) -> list[str]:
421
+ rendered = [self._format_scalar(item) for item in items]
422
+ if spec.separator is ListSeparator.NEWLINE:
423
+ return rendered
424
+ joiner = "," if spec.separator is ListSeparator.COMMA else " "
425
+ return [joiner.join(rendered)]
426
+
427
+ def _render_command(self, entry: CommandEntry, bound: dict) -> tuple[list[str], bool]:
428
+ provided = [(spec, bound[spec.name]) for spec in entry.args if spec.name in bound]
429
+ if entry.layout is Layout.BARE:
430
+ return [entry.name], False
431
+ if entry.layout is Layout.INLINE:
432
+ inline_parts = [entry.name]
433
+ tail_lines: list[str] = []
434
+ for spec, value in provided:
435
+ if spec.own_line:
436
+ tail_lines.append(self._format_scalar(value))
437
+ elif spec.is_list:
438
+ inline_parts.append(" ".join(self._format_scalar(item) for item in value))
439
+ else:
440
+ inline_parts.append(self._format_scalar(value))
441
+ return [" ".join(inline_parts), *tail_lines], bool(tail_lines)
442
+ if entry.layout is Layout.PAYLOAD_LINES:
443
+ inline_parts = [entry.name]
444
+ tail_lines = []
445
+ for spec, value in provided:
446
+ if spec.is_list:
447
+ tail_lines.extend(self._list_lines(spec, value))
448
+ else:
449
+ inline_parts.append(self._format_scalar(value))
450
+ return [" ".join(inline_parts), *tail_lines], True
451
+ if entry.layout is Layout.PARAM_LINES:
452
+ lines = [entry.name]
453
+ for spec, value in provided:
454
+ if spec.is_list:
455
+ lines.extend(self._list_lines(spec, value))
456
+ elif spec.type is ArgType.PATH:
457
+ lines.append(self._format_scalar(value))
458
+ else:
459
+ lines.append(f"{spec.name.upper()} {self._format_scalar(value)}")
460
+ return lines, True
461
+ lines = [entry.name]
462
+ for spec, value in provided:
463
+ if spec.is_list:
464
+ lines.extend(self._list_lines(spec, value))
465
+ elif spec.type is ArgType.BOOL:
466
+ # Presence keyword (SRC-003 p.307): True is the bare keyword
467
+ # line, False emits nothing.
468
+ if value:
469
+ lines.append(spec.name.upper())
470
+ elif spec.joins_previous:
471
+ lines[-1] += f" {self._format_scalar(value)}"
472
+ else:
473
+ lines.append(f"{spec.name.upper()} {self._format_scalar(value)}")
474
+ return lines, True