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
tan/core/global_flags.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""The single place a command opts into the oracle's global-argument surface
|
|
3
|
+
(tan-cli#261).
|
|
4
|
+
|
|
5
|
+
The v0.4.1 oracle's clap `GlobalArgs` (`crates/tan-cli/src/cli.rs` lines
|
|
6
|
+
24-73) marks every field `#[arg(long, global = true, ...)]`: clap attaches
|
|
7
|
+
the WHOLE struct to every subcommand, so `tan <any-command> --verbose` parses
|
|
8
|
+
on the oracle even for a command whose own Rust handler never reads
|
|
9
|
+
`args.verbose`. Measured against this port (tan-cli#261, re-measured before
|
|
10
|
+
this file existed): 99 registration sites across 17 already-ported commands
|
|
11
|
+
raised Click's own "No such option" instead -- not because the flag did the
|
|
12
|
+
wrong thing, but because nobody had declared it there at all, an
|
|
13
|
+
eighteenth-command-repeats-the-mistake defect this module exists to remove
|
|
14
|
+
structurally rather than patch site by site.
|
|
15
|
+
|
|
16
|
+
`accept_global_flags` closes exactly that gap and nothing else: it adds a
|
|
17
|
+
`typer.Option` for whichever of `_GLOBAL_FLAG_SPECS` a command's own
|
|
18
|
+
signature does not ALREADY declare, detected by the CLI flag STRING itself
|
|
19
|
+
(each parameter's `typer.Option(...).param_decls`) rather than by Python
|
|
20
|
+
parameter name, which already varies command to command for the identical
|
|
21
|
+
flag (`all_cores` in `clean_cmd.clean`, `all_targets` in `build_cmd.build`,
|
|
22
|
+
both `--all`). A flag a command already reads for real keeps doing exactly
|
|
23
|
+
that -- this module never touches an existing parameter.
|
|
24
|
+
|
|
25
|
+
Every flag this module ADDS -- arity-0 or value-carrying alike -- is accepted
|
|
26
|
+
and then DROPPED before the wrapped command runs. That was always true for a
|
|
27
|
+
boolean (an accepted-and-ignored `--verbose` changes nothing a caller can
|
|
28
|
+
observe); tan-cli#398 tried to narrow it for a value-carrying flag instead,
|
|
29
|
+
refusing the flag outright the moment a value was actually supplied
|
|
30
|
+
(`typer.BadParameter`, exit 2), reasoning that dropping the value silently
|
|
31
|
+
serves the command's own default in its place -- measured harmful on exactly
|
|
32
|
+
one flag, `model`'s injected `--board-yaml` (see `model_cmd.py`'s own
|
|
33
|
+
`--board`, which now declares `--board-yaml` as a second spelling of the SAME
|
|
34
|
+
option instead, so it is never one of the flags this module injects for
|
|
35
|
+
`model` at all; `_declared_flags` below already skips anything a command
|
|
36
|
+
declares for real).
|
|
37
|
+
|
|
38
|
+
Measured against the oracle (tan-cli#403 postmortem), the refusal was wrong
|
|
39
|
+
everywhere else it fired: 17 other (command, flag) pairs -- `--board-yaml` on
|
|
40
|
+
`examples`/`explain`/`sdk`, `--target` on 14 more -- accept the SAME flag on
|
|
41
|
+
`target/debug/tan.exe` and ignore it (it is `global = true` there too, but
|
|
42
|
+
each command's own Rust handler simply never reads it), so refusing it here
|
|
43
|
+
was a NEW divergence: `tan doctor --target zephyr` went from matching the
|
|
44
|
+
oracle's exit 4 to a `typer.BadParameter` exit 2 with the WRONG command in the
|
|
45
|
+
JSON envelope (`main`'s generic `command: "cli"` fallback, since the refusal
|
|
46
|
+
happens before the wrapped command -- and therefore before `emit()` -- ever
|
|
47
|
+
runs). A command that genuinely computes a wrong answer from a dropped value
|
|
48
|
+
needs the SAME fix `model` got -- read the real flag, whatever its
|
|
49
|
+
oracle-parity alias is called -- not a blanket refusal that punishes every
|
|
50
|
+
command the oracle itself treats as accept-and-ignore.
|
|
51
|
+
|
|
52
|
+
This is the same "declared once, accepted everywhere, read only where a
|
|
53
|
+
command already reads it" shape `clean_cmd.clean` hand-wrote for six flags
|
|
54
|
+
before this module existed -- see its own comment there: "the shared fix
|
|
55
|
+
(one decorator for every command) belongs with whoever owns the global-flag
|
|
56
|
+
surface." This is that decorator, generalised and applied to the rest of the
|
|
57
|
+
surface.
|
|
58
|
+
|
|
59
|
+
`--format` is deliberately NOT one of `_GLOBAL_FLAG_SPECS`: unlike the
|
|
60
|
+
arity-0 flags here, a command that merely ACCEPTED `--format json` without
|
|
61
|
+
reading it would silently run in text mode for a caller who asked for JSON --
|
|
62
|
+
exit 0, human text on stderr, nothing on stdout, an envelope-less
|
|
63
|
+
`--format json` run.
|
|
64
|
+
`--format` is instead made global by RELOCATION (`RELOCATABLE_FLAG_ARITY`
|
|
65
|
+
below, tan-cli#378) -- every registered command already declares and READS its
|
|
66
|
+
own `--format`, so moving the token past the subcommand name hands it to a
|
|
67
|
+
parameter that acts on it rather than to one that drops it.
|
|
68
|
+
"""
|
|
69
|
+
from __future__ import annotations
|
|
70
|
+
|
|
71
|
+
import inspect
|
|
72
|
+
import typing
|
|
73
|
+
from collections.abc import Callable
|
|
74
|
+
|
|
75
|
+
import typer
|
|
76
|
+
|
|
77
|
+
from tan.core.inert import PARITY, inert_help
|
|
78
|
+
|
|
79
|
+
#: One entry per oracle `GlobalArgs` field this port must be able to PARSE on
|
|
80
|
+
#: every command (`crates/tan-cli/src/cli.rs:24-73`), as
|
|
81
|
+
#: `(flag, python_name, is_bool, metavar)`. `metavar` is `None` for a bool
|
|
82
|
+
#: flag (arity 0). `--project`/`--sdk-root` are included even though every
|
|
83
|
+
#: command touched by tan-cli#261 already declares them (measured) -- the
|
|
84
|
+
#: NEXT command to be added is exactly who this guards.
|
|
85
|
+
_GLOBAL_FLAG_SPECS: tuple[tuple[str, str, bool, str | None], ...] = (
|
|
86
|
+
("--project", "project", False, "PATH"),
|
|
87
|
+
("--board-yaml", "board_yaml", False, "PATH"),
|
|
88
|
+
("--sdk-root", "sdk_root", False, "PATH"),
|
|
89
|
+
("--target", "target", False, "EMIT"),
|
|
90
|
+
("--all", "all_", True, None),
|
|
91
|
+
("--verbose", "verbose", True, None),
|
|
92
|
+
("--quiet", "quiet", True, None),
|
|
93
|
+
("--no-color", "no_color", True, None),
|
|
94
|
+
("--non-interactive", "non_interactive", True, None),
|
|
95
|
+
("--ci", "ci", True, None),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
#: `{flag: arity}` -- `cli.py`'s `_reorder_global_flags` reads this (arity 1
|
|
99
|
+
#: = takes a value, 0 = boolean) to relocate a leading global flag across the
|
|
100
|
+
#: subcommand boundary. Derived from `_GLOBAL_FLAG_SPECS` so the reorder
|
|
101
|
+
#: table and this module's own injection list cannot drift apart the way
|
|
102
|
+
#: tan-cli#261's own second comment flagged `_GLOBAL_FLAG_ARITY` could.
|
|
103
|
+
GLOBAL_FLAG_ARITY: dict[str, int] = {
|
|
104
|
+
flag: (0 if is_bool else 1) for flag, _name, is_bool, _metavar in _GLOBAL_FLAG_SPECS
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#: Every flag this module knows how to inject -- what the port-wide gate
|
|
108
|
+
#: (`tests/gates/test_global_flags_gate.py`) walks to assert the whole
|
|
109
|
+
#: registered command surface accepts it.
|
|
110
|
+
GLOBAL_FLAGS: tuple[str, ...] = tuple(flag for flag, *_rest in _GLOBAL_FLAG_SPECS)
|
|
111
|
+
|
|
112
|
+
#: What `cli.py`'s `_reorder_global_flags` actually relocates across the
|
|
113
|
+
#: subcommand boundary: the injectable set above PLUS `--format` (tan-cli#378).
|
|
114
|
+
#:
|
|
115
|
+
#: `--format` is `#[arg(long, global = true, ...)]` on the oracle exactly like
|
|
116
|
+
#: the ten above, so `tan --format json <anything>` parses there; the port used
|
|
117
|
+
#: to instead declare `--format` on the ROOT callback and hand-list which
|
|
118
|
+
#: commands were allowed to precede it, which shipped as global on 12 of 32
|
|
119
|
+
#: commands and a `cli.parse-error` (exit 2, `command: "cli"` -- the WRONG
|
|
120
|
+
#: command in the envelope) on the other 20. Relocating is what makes it
|
|
121
|
+
#: uniform with no list to forget: the flag lands on the command's own
|
|
122
|
+
#: `--format` parameter, the position every golden and every command's
|
|
123
|
+
#: validation already uses.
|
|
124
|
+
#:
|
|
125
|
+
#: Separate from `_GLOBAL_FLAG_SPECS` on purpose, NOT merged into it: that
|
|
126
|
+
#: tuple is the INJECTION list, and injecting `--format` would give a future
|
|
127
|
+
#: command that does not declare one an accepted-and-dropped `--format json`
|
|
128
|
+
#: -- the silent-text-mode defect the module docstring rules out. Keeping the
|
|
129
|
+
#: two apart also leaves `GLOBAL_FLAGS`/`GLOBAL_FLAG_ARITY` (and the gate that
|
|
130
|
+
#: walks them) meaning exactly what they meant before.
|
|
131
|
+
RELOCATABLE_FLAG_ARITY: dict[str, int] = {**GLOBAL_FLAG_ARITY, "--format": 1}
|
|
132
|
+
|
|
133
|
+
#: Shown nowhere (every injected option is `hidden=True`, matching the
|
|
134
|
+
#: precedent `clean_cmd.clean` already set for its six) -- kept as a real
|
|
135
|
+
#: string anyway so a `--help -v` or future un-hiding does not surface a bare
|
|
136
|
+
#: `None`.
|
|
137
|
+
_ACCEPTED_NOT_READ_HELP = inert_help(
|
|
138
|
+
"Accepted for oracle parity; not read by this command.", PARITY, "tan-cli#261"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _declared_flags(func: Callable[..., object]) -> set[str]:
|
|
143
|
+
"""Every CLI flag string `func` already declares, keyed by the flag
|
|
144
|
+
string itself rather than by Python parameter name -- the same fact
|
|
145
|
+
under a different name (`all_cores` vs `all_targets` for `--all`) must
|
|
146
|
+
still count as "already declared", or this would hand a command a
|
|
147
|
+
second, silently-ignored `--all` behind its own real one."""
|
|
148
|
+
declared: set[str] = set()
|
|
149
|
+
for param in inspect.signature(func).parameters.values():
|
|
150
|
+
decls = getattr(param.default, "param_decls", None)
|
|
151
|
+
if decls:
|
|
152
|
+
declared.update(decls)
|
|
153
|
+
return declared
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def accept_global_flags(func: Callable[..., object]) -> Callable[..., object]:
|
|
157
|
+
"""Return a callable Typer can register whose declared options are
|
|
158
|
+
`func`'s own plus whichever of `_GLOBAL_FLAG_SPECS` it was missing, each
|
|
159
|
+
of the added ones accepted and then dropped before `func` ever runs.
|
|
160
|
+
|
|
161
|
+
A command that already declares the full set is returned UNCHANGED
|
|
162
|
+
(`func` itself, not a wrapper) -- the common case once every command in
|
|
163
|
+
tan-cli#261 has been swept once.
|
|
164
|
+
|
|
165
|
+
Call this right where the command function is defined, in its own
|
|
166
|
+
`*_cmd.py` module (`validate = accept_global_flags(validate)`), not as a
|
|
167
|
+
decorator on the `app.command(...)` call in `cli.py`: by the time
|
|
168
|
+
`cli.py` runs, the click `Command` Typer builds from the function is
|
|
169
|
+
already final, and wrapping there would scatter the fix away from the
|
|
170
|
+
command it changes.
|
|
171
|
+
"""
|
|
172
|
+
sig = inspect.signature(func)
|
|
173
|
+
existing = _declared_flags(func)
|
|
174
|
+
to_add = [spec for spec in _GLOBAL_FLAG_SPECS if spec[0] not in existing]
|
|
175
|
+
if not to_add:
|
|
176
|
+
return func
|
|
177
|
+
|
|
178
|
+
# Resolve every EXISTING parameter's annotation to the real type object,
|
|
179
|
+
# not whatever bare STRING `from __future__ import annotations` leaves it
|
|
180
|
+
# as: `inspect.signature` alone never evaluates PEP 563 postponed
|
|
181
|
+
# annotations, and every one of these 17 `*_cmd.py` modules has that
|
|
182
|
+
# import at the top. Typer normally resolves the string itself via
|
|
183
|
+
# `typing.get_type_hints(callback)`, using `callback.__globals__` --
|
|
184
|
+
# `func`'s module, where `str`/`bool`/`typer.Context`/... are in scope.
|
|
185
|
+
# Once `func` is wrapped below, Typer would instead resolve hints
|
|
186
|
+
# against `wrapper.__globals__` -- THIS module's globals, not `func`'s --
|
|
187
|
+
# and get nothing back for a name it cannot see; the fallback path then
|
|
188
|
+
# feeds `get_click_type` the literal string `'str'` instead of the type
|
|
189
|
+
# `str`, which fails with `RuntimeError: Type not yet supported: str`
|
|
190
|
+
# (measured: `explain --target zephyr-board` after a first version of
|
|
191
|
+
# this function skipped this step). Resolving here, once, against the
|
|
192
|
+
# ORIGINAL `func` -- exactly what Typer would have done directly -- is
|
|
193
|
+
# what keeps a wrapped command's pre-existing options working at all.
|
|
194
|
+
resolved_hints = typing.get_type_hints(func)
|
|
195
|
+
params = [
|
|
196
|
+
param.replace(annotation=resolved_hints[name]) if name in resolved_hints else param
|
|
197
|
+
for name, param in sig.parameters.items()
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
#: Every injected flag's python param name -- arity-0 or value-carrying
|
|
201
|
+
#: alike, all dropped before `func` runs (see the module docstring for why
|
|
202
|
+
#: refusing a value-carrying one, tried in tan-cli#398, is not this
|
|
203
|
+
#: module's job any more).
|
|
204
|
+
dropped: list[str] = []
|
|
205
|
+
for flag, name, is_bool, metavar in to_add:
|
|
206
|
+
if is_bool:
|
|
207
|
+
option = typer.Option(False, flag, hidden=True, help=_ACCEPTED_NOT_READ_HELP)
|
|
208
|
+
annotation: type = bool
|
|
209
|
+
else:
|
|
210
|
+
option = typer.Option(
|
|
211
|
+
None, flag, metavar=metavar, hidden=True, help=_ACCEPTED_NOT_READ_HELP
|
|
212
|
+
)
|
|
213
|
+
annotation = str
|
|
214
|
+
dropped.append(name)
|
|
215
|
+
params.append(
|
|
216
|
+
inspect.Parameter(
|
|
217
|
+
name, inspect.Parameter.KEYWORD_ONLY, default=option, annotation=annotation
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def wrapper(*args: object, **kwargs: object) -> object:
|
|
222
|
+
for name in dropped:
|
|
223
|
+
kwargs.pop(name, None)
|
|
224
|
+
return func(*args, **kwargs)
|
|
225
|
+
|
|
226
|
+
wrapper.__doc__ = func.__doc__
|
|
227
|
+
wrapper.__name__ = getattr(func, "__name__", "wrapper")
|
|
228
|
+
return_annotation = resolved_hints.get("return", sig.return_annotation)
|
|
229
|
+
wrapper.__signature__ = inspect.Signature(params, return_annotation=return_annotation)
|
|
230
|
+
return wrapper
|
tan/core/host_python.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""ONE host-interpreter resolver, shared by `tan doctor`'s `hostPython` check
|
|
3
|
+
and `tan build`'s `-DPython3_EXECUTABLE`.
|
|
4
|
+
|
|
5
|
+
tan-cli#1317. Doctor probed `python3`/`python` off PATH and passed on a 3.14
|
|
6
|
+
interpreter while `tan build` (no workspace venv) baked the BARE name
|
|
7
|
+
`python3` into the Zephyr slice, so CMake ran its own `FindPython3` search
|
|
8
|
+
and picked a shadowing `~/.local/bin/python3.10`. Two resolvers meant the two
|
|
9
|
+
commands could disagree about which Python the build uses. This module is the
|
|
10
|
+
single answer; both call it.
|
|
11
|
+
|
|
12
|
+
`resolve_tool` finds each candidate, the candidate is RUN, and the absolute
|
|
13
|
+
`sys.executable` it reports is what a build bakes in -- never a bare name.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
|
|
21
|
+
from tan.core.probe import probe
|
|
22
|
+
from tan.core.tool_lookup import resolve_tool
|
|
23
|
+
|
|
24
|
+
# Security (code execution via module hijack): `-c` puts the CWD on `sys.path`,
|
|
25
|
+
# so `core.probe` runs every `python -c` probe from a fresh EMPTY directory, and
|
|
26
|
+
# the code below also strips '' / the CWD from `sys.path` before LOCATING
|
|
27
|
+
# `west` (`find_spec('west.version')` imports the parent `west` package from
|
|
28
|
+
# the interpreter's own site dirs, never from the CWD). `-I` is deliberately
|
|
29
|
+
# NOT used: it would hide a legitimately installed west in the user site
|
|
30
|
+
# (`~/.local/lib/pythonX.Y/site-packages`, where `pip install --user west`
|
|
31
|
+
# lands). `-P` (3.11+) is not used either: the probe must run under 3.10
|
|
32
|
+
# candidates too, and the explicit strip does the same job there.
|
|
33
|
+
_PROBE_CODE = (
|
|
34
|
+
"import sys,os;sys.path[:]=[p for p in sys.path if p not in ('',os.getcwd(),os.curdir)];"
|
|
35
|
+
"import importlib.util as u\n"
|
|
36
|
+
"def f(n):\n"
|
|
37
|
+
" try:\n"
|
|
38
|
+
" s=u.find_spec(n)\n"
|
|
39
|
+
" return s is not None and s.origin is not None\n"
|
|
40
|
+
" except Exception:\n"
|
|
41
|
+
" return False\n"
|
|
42
|
+
"print('%d.%d' % sys.version_info[:2]);print(sys.executable);"
|
|
43
|
+
"print('west=%d' % (f('west') and f('west.version')))"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class HostPython:
|
|
49
|
+
display: str #: how the candidate is spelled (`python3`, `py -3`)
|
|
50
|
+
version: tuple[int, int]
|
|
51
|
+
resolved: str #: PATH-resolved path of the candidate's first argv word
|
|
52
|
+
interpreter: str #: absolute `sys.executable` the interpreter reported
|
|
53
|
+
has_west: bool = False #: `import west.version` succeeds under it
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def python_candidates() -> list[list[str]]:
|
|
57
|
+
"""Windows leads with the `py` launcher (a machine can have a good 3.12
|
|
58
|
+
with no bare `python`, and bare `python.exe` is often the Store alias)."""
|
|
59
|
+
if os.name == "nt":
|
|
60
|
+
return [["py", "-3"], ["python"], ["python3"]]
|
|
61
|
+
return [["python3"], ["python"]]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _posix_path_pythons(environ) -> list[str]:
|
|
65
|
+
"""Every `python3`/`python`/`python3.N` file along PATH, in PATH order (a
|
|
66
|
+
venv's `bin/` first on PATH must not hide a better interpreter behind it).
|
|
67
|
+
An unreadable PATH entry is skipped, never raised."""
|
|
68
|
+
out: list[str] = []
|
|
69
|
+
seen: set[tuple[str, str]] = set()
|
|
70
|
+
for d in (environ.get("PATH") or "").split(os.pathsep):
|
|
71
|
+
if not d or not os.path.isabs(d):
|
|
72
|
+
continue
|
|
73
|
+
try:
|
|
74
|
+
names = os.listdir(d)
|
|
75
|
+
except OSError:
|
|
76
|
+
continue
|
|
77
|
+
minors = sorted(
|
|
78
|
+
(n for n in names if re.fullmatch(r"python3\.\d+", n)),
|
|
79
|
+
key=lambda n: -int(n.split(".")[1]),
|
|
80
|
+
)
|
|
81
|
+
for name in ["python3", "python", *minors]:
|
|
82
|
+
full = os.path.join(d, name)
|
|
83
|
+
if not (os.path.isfile(full) and os.access(full, os.X_OK)):
|
|
84
|
+
continue
|
|
85
|
+
# Dedupe on (realpath(dir), realpath(file)): folds `/bin` vs
|
|
86
|
+
# `/usr/bin` under usrmerge, but keeps a venv's `bin/python3`
|
|
87
|
+
# (a symlink to the system python, in a DIFFERENT dir and a
|
|
88
|
+
# different environment) separate.
|
|
89
|
+
key = (os.path.realpath(d), os.path.realpath(full))
|
|
90
|
+
if key in seen:
|
|
91
|
+
continue
|
|
92
|
+
seen.add(key)
|
|
93
|
+
out.append(full)
|
|
94
|
+
return out
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _parse(raw: str) -> tuple[tuple[int, int], str | None, bool] | None:
|
|
98
|
+
"""Version from the LAST version-shaped line (noise may precede it); the
|
|
99
|
+
next lines are `sys.executable` and `west=0|1`."""
|
|
100
|
+
lines = [ln.strip() for ln in raw.splitlines() if ln.strip()]
|
|
101
|
+
for i in range(len(lines) - 1, -1, -1):
|
|
102
|
+
m = re.fullmatch(r"(\d+)\.(\d+)", lines[i])
|
|
103
|
+
if m is not None:
|
|
104
|
+
exe = lines[i + 1] if i + 1 < len(lines) else None
|
|
105
|
+
west = i + 2 < len(lines) and lines[i + 2] == "west=1"
|
|
106
|
+
return (int(m.group(1)), int(m.group(2))), exe, west
|
|
107
|
+
if lines:
|
|
108
|
+
m = re.search(r"(\d+)\.(\d+)", lines[-1])
|
|
109
|
+
if m is not None:
|
|
110
|
+
return (int(m.group(1)), int(m.group(2))), None, False
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def probe_all_host_pythons(env=None) -> list[HostPython]:
|
|
115
|
+
"""Every distinct interpreter that RUNS, in discovery order."""
|
|
116
|
+
environ = os.environ if env is None else env
|
|
117
|
+
if os.name == "nt":
|
|
118
|
+
cands = [(c, resolve_tool(c[0], environ).resolved) for c in python_candidates()]
|
|
119
|
+
else:
|
|
120
|
+
# argv[0] is the ABSOLUTE path: a bare name makes Python re-search PATH for
|
|
121
|
+
# `sys.executable`, misreporting a venv's python as whatever is later on PATH.
|
|
122
|
+
cands = [([p], p) for p in _posix_path_pythons(environ)]
|
|
123
|
+
seen: set[str] = set()
|
|
124
|
+
found: list[HostPython] = []
|
|
125
|
+
for candidate, resolved in cands:
|
|
126
|
+
if resolved is None:
|
|
127
|
+
continue
|
|
128
|
+
out = probe([*candidate, "-c", _PROBE_CODE], executable=resolved)
|
|
129
|
+
parsed = _parse(out) if out is not None else None
|
|
130
|
+
if parsed is None:
|
|
131
|
+
continue
|
|
132
|
+
version, exe, west = parsed
|
|
133
|
+
interpreter = exe if exe and os.path.isabs(exe) else resolved
|
|
134
|
+
if interpreter in seen:
|
|
135
|
+
continue
|
|
136
|
+
seen.add(interpreter)
|
|
137
|
+
found.append(HostPython(" ".join(candidate) if os.name == "nt" else os.path.basename(candidate[0]), version, resolved, interpreter, west))
|
|
138
|
+
return found
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def select_host_python(
|
|
142
|
+
found: list[HostPython], floor: tuple[int, int], *, need_west: bool = False
|
|
143
|
+
) -> HostPython | None:
|
|
144
|
+
"""Best of `found`: >=floor AND west-capable, then >=floor (unless
|
|
145
|
+
`need_west`), then the first that merely ran (so a too-old message can name
|
|
146
|
+
a real version)."""
|
|
147
|
+
for pred in (
|
|
148
|
+
lambda h: h.version >= floor and h.has_west,
|
|
149
|
+
lambda h: h.version >= floor and not need_west,
|
|
150
|
+
lambda h: True,
|
|
151
|
+
):
|
|
152
|
+
for h in found:
|
|
153
|
+
if pred(h):
|
|
154
|
+
return h
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def probe_host_python(
|
|
159
|
+
floor: tuple[int, int], env=None, *, need_west: bool = False
|
|
160
|
+
) -> HostPython | None:
|
|
161
|
+
return select_host_python(probe_all_host_pythons(env), floor, need_west=need_west)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def describe_unsuitable(found: list[HostPython], floor: tuple[int, int]) -> str:
|
|
165
|
+
"""Name every candidate and why it does not qualify."""
|
|
166
|
+
want = f"{floor[0]}.{floor[1]}"
|
|
167
|
+
if not found:
|
|
168
|
+
return (
|
|
169
|
+
"no runnable Python interpreter was found on PATH; "
|
|
170
|
+
f"Zephyr needs Python {want}+ with `west` importable."
|
|
171
|
+
)
|
|
172
|
+
parts = []
|
|
173
|
+
for h in found:
|
|
174
|
+
why = []
|
|
175
|
+
if h.version < floor:
|
|
176
|
+
why.append(f"too old (below {want})")
|
|
177
|
+
if not h.has_west:
|
|
178
|
+
why.append("no `west` module")
|
|
179
|
+
parts.append(f"`{h.interpreter}` {h.version[0]}.{h.version[1]}: " + ", ".join(why or ["ok"]))
|
|
180
|
+
return (
|
|
181
|
+
f"no host Python is usable for the Zephyr build -- Zephyr needs Python {want}+ "
|
|
182
|
+
"that can import `west`. Found: " + "; ".join(parts) + "."
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def build_interpreter(
|
|
187
|
+
venv_python: str | None,
|
|
188
|
+
fallback: str,
|
|
189
|
+
plan_uses_python_token: bool,
|
|
190
|
+
floor: tuple[int, int],
|
|
191
|
+
*,
|
|
192
|
+
probe_all=probe_all_host_pythons,
|
|
193
|
+
) -> tuple[str, str | None]:
|
|
194
|
+
"""The `${PYTHON}` a build bakes in, as `(interpreter, refusal)`.
|
|
195
|
+
|
|
196
|
+
A workspace venv wins unchanged. Otherwise, when the plan uses `${PYTHON}`
|
|
197
|
+
(every Zephyr slice's `-DPython3_EXECUTABLE`), the interpreter is the
|
|
198
|
+
absolute one the shared resolver picks: >= `floor` (the doctor's EFFECTIVE
|
|
199
|
+
floor) and able to import `west` -- never the bare `python3` (tan-cli#1317).
|
|
200
|
+
Forward-slashed, because a Windows backslash in a CMake `-D` is an escape
|
|
201
|
+
(alp-sdk#849, `tan.core.venv.venv_python`). `refusal` names each candidate
|
|
202
|
+
when none qualifies.
|
|
203
|
+
"""
|
|
204
|
+
if venv_python is not None or not plan_uses_python_token:
|
|
205
|
+
return (venv_python or fallback), None
|
|
206
|
+
found = probe_all()
|
|
207
|
+
best = select_host_python(found, floor, need_west=True)
|
|
208
|
+
if best is not None and best.version >= floor and best.has_west:
|
|
209
|
+
return best.interpreter.replace("\\", "/"), None
|
|
210
|
+
return fallback, describe_unsuitable(found, floor)
|