tan-cli 0.7.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.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""A comment-preserving, text-level append to `board.yaml`'s `models:` list
|
|
3
|
+
(tan-cli#1286, `tan model add`).
|
|
4
|
+
|
|
5
|
+
Customers hand-write and annotate `board.yaml`; a `yaml.safe_load` /
|
|
6
|
+
`yaml.dump` round-trip deletes every comment. Rather than take a new
|
|
7
|
+
dependency (`ruamel.yaml`), this splices TEXT: it finds the one top-level
|
|
8
|
+
`models:` list with a small line-oriented scan, inserts the new entry's lines
|
|
9
|
+
right after that list's last content line, and returns every other byte of the
|
|
10
|
+
file untouched -- the same targeted-splice shape `tan.core.scaffold.
|
|
11
|
+
splice_companion_cores` uses for `cores:`.
|
|
12
|
+
|
|
13
|
+
**It refuses rather than guesses.** `BoardEditRefused` is raised for every
|
|
14
|
+
shape the scan cannot place an insertion in with certainty: a flow-style
|
|
15
|
+
`models: [...]`/`{...}` (only the empty `[]` is rewritten), an anchor/alias/tag
|
|
16
|
+
or scalar on the `models:` line, a quoted `models` key, a second top-level
|
|
17
|
+
`models:`, a multi-document file, a tab in the list's indentation, or a list
|
|
18
|
+
whose lines do not read as a block sequence. A final guard re-parses the old
|
|
19
|
+
and the new text with `yaml.safe_load` and refuses unless the new document is
|
|
20
|
+
EXACTLY the old one plus the one appended entry -- so a shape the scan
|
|
21
|
+
misjudged cannot reach disk as a silently different document.
|
|
22
|
+
|
|
23
|
+
The only bytes that ever change: the inserted lines, the `[]` of an empty
|
|
24
|
+
flow list, and a missing final newline on the line the insertion follows.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import copy
|
|
30
|
+
import re
|
|
31
|
+
from collections.abc import Mapping
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
_LINE_RE = re.compile(r"[^\n]*\n|[^\n]+")
|
|
35
|
+
_MODELS_KEY_RE = re.compile(r"^models[ ]*:(?P<rest>([ \t].*)?)$")
|
|
36
|
+
_QUOTED_MODELS_RE = re.compile(r"""^["']models["']\s*:""")
|
|
37
|
+
_DOC_START_RE = re.compile(r"^---(\s|$)")
|
|
38
|
+
_DOC_END_RE = re.compile(r"^\.\.\.(\s|$)")
|
|
39
|
+
_COMPLEX_KEY_RE = re.compile(r"^\?(\s|$)")
|
|
40
|
+
_EMPTY_VALUES = ("[]", "~", "null", "Null", "NULL")
|
|
41
|
+
_BOM = "\ufeff"
|
|
42
|
+
|
|
43
|
+
#: Indent for a `models:` list this module creates itself.
|
|
44
|
+
DEFAULT_ITEM_INDENT = 2
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class BoardEditRefused(Exception):
|
|
48
|
+
"""The `models:` list cannot be appended to safely; `str(err)` says why."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _split_lines(text: str) -> list[str]:
|
|
52
|
+
# Not `str.splitlines`: it also breaks on form feed, NEL and U+2028, none
|
|
53
|
+
# of which are YAML line breaks.
|
|
54
|
+
return _LINE_RE.findall(text)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _content(line: str) -> str:
|
|
58
|
+
return line.rstrip("\r\n")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _text(lines: list[str], n: int) -> str:
|
|
62
|
+
"""Line `n` without its line ending, and (line 0 only) without a leading
|
|
63
|
+
BOM -- YAML ignores a BOM, so a column-0 key behind one is still column 0."""
|
|
64
|
+
line = _content(lines[n]).rstrip("\r")
|
|
65
|
+
return line[1:] if n == 0 and line.startswith(_BOM) else line
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _strip_comment(rest: str) -> tuple[str, str]:
|
|
69
|
+
"""`(value, comment)` of the text after a `key:` -- `comment` keeps its
|
|
70
|
+
leading `#` (and the whitespace before it is dropped from `value`)."""
|
|
71
|
+
match = re.search(r"(^|\s)#", rest)
|
|
72
|
+
if match is None:
|
|
73
|
+
return rest.strip(), ""
|
|
74
|
+
return rest[: match.start()].strip(), rest[match.start() :].strip()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _is_seq_item(stripped: str) -> bool:
|
|
78
|
+
return stripped == "-" or stripped.startswith("- ")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _scalar(value: Any) -> str:
|
|
82
|
+
import yaml # noqa: PLC0415 (declared dependency)
|
|
83
|
+
|
|
84
|
+
if isinstance(value, bool):
|
|
85
|
+
return "true" if value else "false"
|
|
86
|
+
if isinstance(value, int):
|
|
87
|
+
return str(value)
|
|
88
|
+
if isinstance(value, str):
|
|
89
|
+
out = yaml.safe_dump(value, default_flow_style=True, width=2**30, allow_unicode=True)
|
|
90
|
+
out = out.rstrip("\n")
|
|
91
|
+
if out.endswith("\n..."):
|
|
92
|
+
out = out[:-4]
|
|
93
|
+
if "\n" in out:
|
|
94
|
+
raise BoardEditRefused(f"cannot render {value!r} as a single-line YAML scalar")
|
|
95
|
+
return out
|
|
96
|
+
raise BoardEditRefused(f"cannot render a {type(value).__name__} value in a `models:` entry")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _render_mapping(mapping: Mapping, indent: int, first_prefix: str | None) -> list[str]:
|
|
100
|
+
"""Block-style lines for `mapping`; `first_prefix` (`"- "`) replaces the
|
|
101
|
+
first line's leading indent for a sequence item."""
|
|
102
|
+
lines: list[str] = []
|
|
103
|
+
pad = " " * indent
|
|
104
|
+
for n, (key, value) in enumerate(mapping.items()):
|
|
105
|
+
lead = first_prefix if (n == 0 and first_prefix is not None) else pad
|
|
106
|
+
k = _scalar(key)
|
|
107
|
+
if isinstance(value, Mapping):
|
|
108
|
+
if not value:
|
|
109
|
+
raise BoardEditRefused(f"cannot render empty mapping under `{key}`")
|
|
110
|
+
lines.append(f"{lead}{k}:")
|
|
111
|
+
lines.extend(_render_mapping(value, indent + 2, None))
|
|
112
|
+
elif isinstance(value, (list, tuple)):
|
|
113
|
+
if not all(isinstance(v, (int, str)) and not isinstance(v, bool) for v in value):
|
|
114
|
+
raise BoardEditRefused(f"cannot render the list under `{key}`")
|
|
115
|
+
lines.append(f"{lead}{k}: [{', '.join(_scalar(v) for v in value)}]")
|
|
116
|
+
else:
|
|
117
|
+
lines.append(f"{lead}{k}: {_scalar(value)}")
|
|
118
|
+
return lines
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def render_entry(entry: Mapping, item_indent: int = DEFAULT_ITEM_INDENT) -> list[str]:
|
|
122
|
+
"""The block-sequence lines (no line endings) for one `models:` entry."""
|
|
123
|
+
return _render_mapping(entry, item_indent + 2, " " * item_indent + "- ")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _scan(lines: list[str]) -> tuple[int | None, int | None]:
|
|
127
|
+
"""`(models key line | None, line of a trailing `...` | None)`. Refuses a
|
|
128
|
+
complex `? key`, a quoted `models` key, a duplicate key and a second YAML
|
|
129
|
+
document (a second `---`, or content after `...`); a leading `%` directive
|
|
130
|
+
plus one `---`, and a trailing `...`, are fine."""
|
|
131
|
+
seen_content = False
|
|
132
|
+
seen_start = False
|
|
133
|
+
end_marker: int | None = None
|
|
134
|
+
found: list[int] = []
|
|
135
|
+
for n in range(len(lines)):
|
|
136
|
+
line = _text(lines, n)
|
|
137
|
+
if line[:1] in (" ", "\t", "#", ""):
|
|
138
|
+
continue
|
|
139
|
+
if end_marker is not None:
|
|
140
|
+
raise BoardEditRefused("board.yaml has content after a `...` document end")
|
|
141
|
+
if line[0] == "%" and not seen_content and not seen_start:
|
|
142
|
+
continue
|
|
143
|
+
if _DOC_START_RE.match(line):
|
|
144
|
+
if seen_content or seen_start:
|
|
145
|
+
raise BoardEditRefused("board.yaml has more than one YAML document")
|
|
146
|
+
seen_start = True
|
|
147
|
+
continue
|
|
148
|
+
if _DOC_END_RE.match(line):
|
|
149
|
+
end_marker = n
|
|
150
|
+
continue
|
|
151
|
+
seen_content = True
|
|
152
|
+
if _COMPLEX_KEY_RE.match(line):
|
|
153
|
+
raise BoardEditRefused("board.yaml uses a complex `? key`")
|
|
154
|
+
if _QUOTED_MODELS_RE.match(line):
|
|
155
|
+
raise BoardEditRefused("board.yaml quotes its `models` key")
|
|
156
|
+
if _MODELS_KEY_RE.match(line):
|
|
157
|
+
found.append(n)
|
|
158
|
+
if len(found) > 1:
|
|
159
|
+
raise BoardEditRefused("board.yaml has more than one top-level `models:` key")
|
|
160
|
+
return (found[0] if found else None), end_marker
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _list_extent(lines: list[str], key_n: int) -> tuple[int, int | None]:
|
|
164
|
+
"""`(last content line, item indent)` of the block list under line
|
|
165
|
+
`key_n`; `(key_n, None)` for an empty one. Trailing blank/comment lines
|
|
166
|
+
are not part of the list, so the insertion lands before them."""
|
|
167
|
+
last = key_n
|
|
168
|
+
item_indent: int | None = None
|
|
169
|
+
for n in range(key_n + 1, len(lines)):
|
|
170
|
+
line = _text(lines, n)
|
|
171
|
+
stripped = line.lstrip(" ")
|
|
172
|
+
if stripped == "" or stripped.startswith("#"):
|
|
173
|
+
continue
|
|
174
|
+
ind = len(line) - len(stripped)
|
|
175
|
+
if stripped[:1] == "\t" or "\t" in line[:ind]:
|
|
176
|
+
raise BoardEditRefused("`models:` list is indented with a tab")
|
|
177
|
+
if ind == 0 and not _is_seq_item(stripped):
|
|
178
|
+
break
|
|
179
|
+
if item_indent is None:
|
|
180
|
+
if not _is_seq_item(stripped):
|
|
181
|
+
raise BoardEditRefused("`models:` is not a block list")
|
|
182
|
+
item_indent = ind
|
|
183
|
+
elif ind < item_indent or (ind == item_indent and not _is_seq_item(stripped)):
|
|
184
|
+
raise BoardEditRefused("`models:` list has inconsistent indentation")
|
|
185
|
+
last = n
|
|
186
|
+
return last, item_indent
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def append_models_entry(text: str, entry: Mapping) -> str:
|
|
190
|
+
"""`text` (a `board.yaml`) with `entry` appended to its `models:` list,
|
|
191
|
+
created at the end of the file when absent. Raises `BoardEditRefused`."""
|
|
192
|
+
old_doc = _strict_load(text, "board.yaml")
|
|
193
|
+
if old_doc is None:
|
|
194
|
+
old_doc = {}
|
|
195
|
+
if not isinstance(old_doc, dict):
|
|
196
|
+
raise BoardEditRefused("board.yaml is not a YAML mapping")
|
|
197
|
+
|
|
198
|
+
lines = _split_lines(text)
|
|
199
|
+
eol = "\r\n" if "\r\n" in text else "\n"
|
|
200
|
+
key_n, end_n = _scan(lines)
|
|
201
|
+
new_lines = list(lines)
|
|
202
|
+
if key_n is None:
|
|
203
|
+
at = len(new_lines) if end_n is None else end_n
|
|
204
|
+
if at and not new_lines[at - 1].endswith("\n"):
|
|
205
|
+
new_lines[at - 1] += eol
|
|
206
|
+
block = ["models:", *render_entry(entry, DEFAULT_ITEM_INDENT)]
|
|
207
|
+
new_lines[at:at] = [ln + eol for ln in block]
|
|
208
|
+
else:
|
|
209
|
+
key_raw = lines[key_n]
|
|
210
|
+
rest = _MODELS_KEY_RE.match(_text(lines, key_n)).group("rest") # type: ignore[union-attr]
|
|
211
|
+
value, comment = _strip_comment(rest)
|
|
212
|
+
if value in _EMPTY_VALUES:
|
|
213
|
+
new_key = ("\ufeff" if key_n == 0 and key_raw.startswith(_BOM) else "") + "models:"
|
|
214
|
+
new_lines[key_n] = (
|
|
215
|
+
new_key + (f" {comment}" if comment else "") + key_raw[len(_content(key_raw)) :]
|
|
216
|
+
)
|
|
217
|
+
elif value:
|
|
218
|
+
raise BoardEditRefused(f"`models:` is written as `{value}`, not a block list")
|
|
219
|
+
last, item_indent = _list_extent(lines, key_n)
|
|
220
|
+
if value in _EMPTY_VALUES and item_indent is not None:
|
|
221
|
+
raise BoardEditRefused(f"`models: {value}` is followed by list items")
|
|
222
|
+
if not new_lines[last].endswith("\n"):
|
|
223
|
+
new_lines[last] += eol
|
|
224
|
+
indent = DEFAULT_ITEM_INDENT if item_indent is None else item_indent
|
|
225
|
+
new_lines[last + 1 : last + 1] = [ln + eol for ln in render_entry(entry, indent)]
|
|
226
|
+
|
|
227
|
+
new_text = "".join(new_lines)
|
|
228
|
+
_verify(old_doc, new_text, entry)
|
|
229
|
+
return new_text
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _strict_load(text: str, what: str) -> Any:
|
|
233
|
+
"""`yaml.safe_load` that also rejects a repeated mapping key -- the check
|
|
234
|
+
`tan build`'s own strict loader applies, which plain `safe_load` (last
|
|
235
|
+
key wins) hides. Refuses on any failure, recursion included."""
|
|
236
|
+
import yaml # noqa: PLC0415 (declared dependency)
|
|
237
|
+
|
|
238
|
+
class Strict(yaml.SafeLoader):
|
|
239
|
+
pass
|
|
240
|
+
|
|
241
|
+
def construct(loader: Any, node: Any) -> dict:
|
|
242
|
+
seen: set = set()
|
|
243
|
+
for key_node, _ in node.value:
|
|
244
|
+
if key_node.tag == "tag:yaml.org,2002:merge":
|
|
245
|
+
continue
|
|
246
|
+
key = loader.construct_object(key_node, deep=True)
|
|
247
|
+
if key in seen:
|
|
248
|
+
raise BoardEditRefused(f"{what} repeats the mapping key {key!r}")
|
|
249
|
+
seen.add(key)
|
|
250
|
+
loader.flatten_mapping(node)
|
|
251
|
+
return {
|
|
252
|
+
loader.construct_object(k, deep=True): loader.construct_object(v, deep=True)
|
|
253
|
+
for k, v in node.value
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
Strict.add_constructor(yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, construct)
|
|
257
|
+
try:
|
|
258
|
+
return yaml.load(text, Loader=Strict) # noqa: S506 (SafeLoader subclass)
|
|
259
|
+
except BoardEditRefused:
|
|
260
|
+
raise
|
|
261
|
+
except (yaml.YAMLError, RecursionError, TypeError) as err:
|
|
262
|
+
raise BoardEditRefused(f"{what} does not parse cleanly: {err}") from err
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _same(a: Any, b: Any) -> bool:
|
|
266
|
+
"""Structural equality that treats NaN as equal to NaN (`.nan` in a board
|
|
267
|
+
would otherwise make every edit look like a corruption)."""
|
|
268
|
+
if isinstance(a, float) and isinstance(b, float) and a != a and b != b:
|
|
269
|
+
return True
|
|
270
|
+
if isinstance(a, dict) and isinstance(b, dict):
|
|
271
|
+
return a.keys() == b.keys() and all(_same(a[k], b[k]) for k in a)
|
|
272
|
+
if isinstance(a, list) and isinstance(b, list):
|
|
273
|
+
return len(a) == len(b) and all(_same(x, y) for x, y in zip(a, b))
|
|
274
|
+
return type(a) is type(b) and a == b
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _verify(old_doc: dict, new_text: str, entry: Mapping) -> None:
|
|
278
|
+
import yaml # noqa: PLC0415 (declared dependency)
|
|
279
|
+
|
|
280
|
+
new_doc = _strict_load(new_text, "the edited board.yaml")
|
|
281
|
+
expected = copy.deepcopy(old_doc)
|
|
282
|
+
existing = expected.get("models")
|
|
283
|
+
if existing is None:
|
|
284
|
+
existing = []
|
|
285
|
+
if not isinstance(existing, list):
|
|
286
|
+
raise BoardEditRefused("`models:` is not a list")
|
|
287
|
+
expected["models"] = [*existing, yaml.safe_load(yaml.safe_dump(dict(entry)))]
|
|
288
|
+
try:
|
|
289
|
+
same = _same(new_doc, expected)
|
|
290
|
+
except RecursionError:
|
|
291
|
+
same = False
|
|
292
|
+
if not same:
|
|
293
|
+
raise BoardEditRefused(
|
|
294
|
+
"the edit would change more than the appended entry (unusual `models:` layout)"
|
|
295
|
+
)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""Position-aware YAML loader for board.yaml diagnostics.
|
|
3
|
+
|
|
4
|
+
PORTED from alp-sdk `scripts/alp_cli/yaml_pos.py` (pinned by
|
|
5
|
+
`tests/gates/test_planner_relocation_freshness.py::HAND_PORT_HASHES`).
|
|
6
|
+
The body is unchanged; only this docstring is tan's.
|
|
7
|
+
|
|
8
|
+
Subclasses PyYAML's SafeLoader so every constructed mapping and
|
|
9
|
+
sequence carries ``__line__``, ``__column__``, ``__end_line__``,
|
|
10
|
+
``__end_column__`` (1-based) and a per-key ``__keys__`` table that
|
|
11
|
+
maps key name -> (line, col, value_line, value_col).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Literal
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _PosLoader(yaml.SafeLoader):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _construct_mapping(loader: _PosLoader, node: yaml.MappingNode) -> dict[str, Any]:
|
|
27
|
+
# Detect duplicates against the node's OWN keys, BEFORE flatten_mapping().
|
|
28
|
+
#
|
|
29
|
+
# flatten_mapping() splices the merged mapping's keys into node.value, so
|
|
30
|
+
# a spec-legal override of a merged key --
|
|
31
|
+
#
|
|
32
|
+
# base: &b {a: 1}
|
|
33
|
+
# derived: {<<: *b, a: 9}
|
|
34
|
+
#
|
|
35
|
+
# -- would appear as `a` twice afterwards and be rejected. Overriding a
|
|
36
|
+
# merged key is the entire point of the merge construct (YAML 1.1 merge:
|
|
37
|
+
# the explicit key wins), and `dev` accepts it, so rejecting it here would
|
|
38
|
+
# refuse customer board.yaml files that build fine today.
|
|
39
|
+
seen: set[Any] = set()
|
|
40
|
+
for key_node, _value_node in node.value:
|
|
41
|
+
if key_node.tag == "tag:yaml.org,2002:merge":
|
|
42
|
+
continue
|
|
43
|
+
key = loader.construct_object(key_node, deep=True)
|
|
44
|
+
if key in seen:
|
|
45
|
+
mark = key_node.start_mark
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"duplicate key {key!r} at line {mark.line + 1}, "
|
|
48
|
+
f"column {mark.column + 1}"
|
|
49
|
+
)
|
|
50
|
+
seen.add(key)
|
|
51
|
+
|
|
52
|
+
loader.flatten_mapping(node)
|
|
53
|
+
mapping: dict[str, Any] = {}
|
|
54
|
+
keys: dict[str, dict[str, int]] = {}
|
|
55
|
+
for key_node, value_node in node.value:
|
|
56
|
+
key = loader.construct_object(key_node, deep=True)
|
|
57
|
+
# Last-wins is correct AFTER flattening: PyYAML prepends the merged
|
|
58
|
+
# keys, so an explicit key encountered later overrides the merged one.
|
|
59
|
+
value = loader.construct_object(value_node, deep=True)
|
|
60
|
+
mapping[key] = value
|
|
61
|
+
keys[key] = {
|
|
62
|
+
"line": key_node.start_mark.line + 1,
|
|
63
|
+
"col": key_node.start_mark.column + 1,
|
|
64
|
+
"value_line": value_node.start_mark.line + 1,
|
|
65
|
+
"value_col": value_node.start_mark.column + 1,
|
|
66
|
+
"value_end_line": value_node.end_mark.line + 1,
|
|
67
|
+
"value_end_col": value_node.end_mark.column + 1,
|
|
68
|
+
}
|
|
69
|
+
mapping["__line__"] = node.start_mark.line + 1
|
|
70
|
+
mapping["__column__"] = node.start_mark.column + 1
|
|
71
|
+
mapping["__end_line__"] = node.end_mark.line + 1
|
|
72
|
+
mapping["__end_column__"] = node.end_mark.column + 1
|
|
73
|
+
mapping["__keys__"] = keys
|
|
74
|
+
return mapping
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _construct_sequence(loader: _PosLoader, node: yaml.SequenceNode) -> list[Any]:
|
|
78
|
+
items: list[Any] = []
|
|
79
|
+
for child in node.value:
|
|
80
|
+
item = loader.construct_object(child, deep=True)
|
|
81
|
+
if isinstance(item, dict):
|
|
82
|
+
item.setdefault("__line__", child.start_mark.line + 1)
|
|
83
|
+
item.setdefault("__column__", child.start_mark.column + 1)
|
|
84
|
+
item.setdefault("__end_line__", child.end_mark.line + 1)
|
|
85
|
+
item.setdefault("__end_column__", child.end_mark.column + 1)
|
|
86
|
+
items.append(item)
|
|
87
|
+
return items
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
_PosLoader.add_constructor(
|
|
91
|
+
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, _construct_mapping
|
|
92
|
+
)
|
|
93
|
+
_PosLoader.add_constructor(
|
|
94
|
+
yaml.resolver.BaseResolver.DEFAULT_SEQUENCE_TAG, _construct_sequence
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load_with_positions(text: str, source: str | Path) -> dict[str, Any]:
|
|
99
|
+
"""Parse a YAML document, attaching position metadata to mappings."""
|
|
100
|
+
data = yaml.load(text, Loader=_PosLoader)
|
|
101
|
+
if not isinstance(data, dict):
|
|
102
|
+
raise ValueError(f"{source}: top-level YAML is not a mapping")
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def node_position(
|
|
107
|
+
mapping: dict[str, Any],
|
|
108
|
+
key: str,
|
|
109
|
+
target: Literal["key", "value"] = "key",
|
|
110
|
+
) -> tuple[int, int]:
|
|
111
|
+
"""Return (line, column), 1-based, for a key or its value."""
|
|
112
|
+
table = mapping.get("__keys__")
|
|
113
|
+
if not table or key not in table:
|
|
114
|
+
return (mapping.get("__line__", 1), mapping.get("__column__", 1))
|
|
115
|
+
entry = table[key]
|
|
116
|
+
if target == "value":
|
|
117
|
+
return (entry["value_line"], entry["value_col"])
|
|
118
|
+
return (entry["line"], entry["col"])
|