exlab_wizard.template.resolution#

Read-side template resolver (generalizes Backend Spec §5.0).

The project / run wizards do not offer a single flat template list: a template defined nearer the work (per-project, then per-equipment) should override a same-named template defined further out (the global paths.templates_dir). This module turns a config plus a wizard context (template_type + optional equipment_id / project_path) into the ordered, de-duplicated list of templates the wizard should offer, nearest scope first, nearest scope winning on a name collision.

Directory layout searched (design spec §2):

  • Globalpaths.templates_dir (flat: templates sit directly inside, all types mixed, filtered by _exlab_type).

  • Per-equipment<local_root>/<equipment_id>/.exlab-wizard/templates/<type>/ (type-segregated).

  • Per-project<project_path>/.exlab-wizard/templates/<type>/ (type-segregated).

The per-instance directory scanning + manifest parsing is delegated to exlab_wizard.ui.pages.templates.list_templates(); this module only composes the search chain and merges the results.

Functions

instance_template_dir(instance_dir, ...)

Return the per-instance template directory for one scope.

project_dir(config, equipment_id, project_name)

Return the absolute project directory, or None when not derivable.

reconcile_selection(choices, selected_name)

Reconcile a prior template selection against freshly-resolved choices.

resolve_template_chain(config, *, template_type)

Resolve the merged template list a wizard should offer.

search_dirs(config, *, template_type[, ...])

Return the template search directories, highest precedence first.

Classes

TemplateChoices([names, questions, paths])

The templates a wizard offers for one resolution context.

class exlab_wizard.template.resolution.TemplateChoices(names=<factory>, questions=<factory>, paths=<factory>)[source]#

Bases: object

The templates a wizard offers for one resolution context.

Bundles the three parallel views the wizard’s template + variables steps need so a single on_resolve(equipment_id, project_name) call returns everything for the current selection:

names#

Offered template names, nearest scope first.

questions#

Per-template parsed copier.yml questions (drives the dynamic Variables step). Missing entry == no variables.

paths#

Per-template absolute source path of the resolved template, so on_submit renders the exact file the wizard listed rather than re-deriving templates_dir / name (which would ignore a per-instance override). Keyed by template name.

Parameters:
names: list[str]#
paths: dict[str, Path]#
questions: dict[str, list[TemplateQuestion]]#
exlab_wizard.template.resolution.instance_template_dir(instance_dir, template_type)[source]#

Return the per-instance template directory for one scope.

A per-instance (per-equipment / per-project) template store is <instance_dir>/.exlab-wizard/templates/<template_type>/ – the .exlab-wizard cache dir, a templates sub-dir, then a child sub-dir per template type so the project / run stores never collide.

Parameters:
  • instance_dir (Path) – The equipment or project root directory.

  • template_type (str) – The child template type ("project" / "run") whose sub-dir is returned.

Return type:

Path

Returns:

The (possibly non-existent) per-instance template directory.

exlab_wizard.template.resolution.project_dir(config, equipment_id, project_name)[source]#

Return the absolute project directory, or None when not derivable.

A run’s per-project template store lives under <local_root>/<equipment_id>/<project_name>/ (Backend Spec §3.2). The wizard knows the equipment id and the parent project’s folder name, so this composes the path the run-wizard resolver passes as project_path. Returns None when local_root, equipment_id, or project_name is missing – the caller then resolves without the per-project scope (per-equipment + global only).

Parameters:
Return type:

Path | None

exlab_wizard.template.resolution.reconcile_selection(choices, selected_name)[source]#

Reconcile a prior template selection against freshly-resolved choices.

Called after a wizard re-resolves its template chain (the operator changed equipment / project). Returns (name, path, dropped):

  • The selected name survives the new context -> its resolved path is re-derived from choices and returned with dropped=False. This is the critical case: a same-named per-instance template shadows the global one at a different path, and a wizard ui.select that keeps its value does not re-fire its change handler – so the stored path must be refreshed here or submit would render the stale source.

  • The selected name is gone (or was None) -> returns (None, None, True) so the caller clears the selection and its now-orphaned variables.

Parameters:
  • choices (TemplateChoices) – The freshly-resolved templates for the new context.

  • selected_name (str | None) – The template name selected under the old context.

Return type:

tuple[str | None, Path | None, bool]

Returns:

(name, path, dropped) – the reconciled selection name, its re-derived absolute path (or None), and whether the prior selection was dropped.

exlab_wizard.template.resolution.resolve_template_chain(config, *, template_type, equipment_id=None, project_path=None, run_scope=None)[source]#

Resolve the merged template list a wizard should offer.

Walks search_dirs() nearest-first, scanning each directory with list_templates(), and merges the results by template name keeping the first (nearest-scope) occurrence – so a per-project template shadows a same-named per-equipment or global one. Nearest-first order is preserved in the result.

template_type is always passed to list_templates(): the global dir is flat (mixed types) so the filter is required, and per-instance <type>/ dirs pass it defensively so a misfiled template of the wrong type is skipped rather than offered.

For run templates, run_scope additionally narrows by the template’s declared scope: when given ("experimental" / "test") a run template is kept only when its run_scope equals that scope or is "both". run_scope=None keeps every run template. The parameter is ignored for non-run template types.

Parameters:
  • config (Config) – The loaded config.

  • template_type (str) – One of "project" / "run" / "equipment".

  • equipment_id (str | None) – The equipment the wizard runs under, if any.

  • project_path (Path | None) – The absolute project directory, if any (run wizard).

  • run_scope (str | None) – Optional run-scope filter (run templates only).

Return type:

list[TemplateSummary]

Returns:

The merged, de-duplicated list of templates, nearest scope first.

exlab_wizard.template.resolution.search_dirs(config, *, template_type, equipment_id=None, project_path=None)[source]#

Return the template search directories, highest precedence first.

The precedence chain narrows from the work outwards: a per-project store beats a per-equipment store, which beats the global paths.templates_dir. Which scopes apply depends on the template type the wizard is offering:

  • "run" – per-project, then per-equipment, then global.

  • "project" – per-equipment, then global (a project has no children of its own to scope project templates by).

  • "equipment" – global only.

A scope is included only when its backing config value is present: the global dir is skipped when paths.templates_dir is empty, the per-equipment dir when equipment_id is None (or local_root is empty), and the per-project dir when project_path is None. Returned directories need not exist – list_templates() tolerates a missing directory by returning no templates.

Parameters:
  • config (Config) – The loaded config (supplies paths.templates_dir and paths.local_root).

  • template_type (str) – One of "project" / "run" / "equipment".

  • equipment_id (str | None) – The equipment the wizard runs under, if any. Gates the per-equipment scope.

  • project_path (Path | None) – The absolute project directory, if any. Gates the per-project scope (run wizard only).

Return type:

list[Path]

Returns:

The ordered list of search directories, nearest scope first.