742 lines
28 KiB
Python
742 lines
28 KiB
Python
"""Two-phase build library implementation."""
|
|
from __future__ import annotations
|
|
|
|
from collections import defaultdict
|
|
from contextvars import ContextVar
|
|
from dataclasses import dataclass, replace
|
|
from functools import wraps
|
|
from pprint import pformat
|
|
from typing import TYPE_CHECKING, Any, Literal, Self, cast
|
|
import copy
|
|
|
|
from ..error import BuildError, LibraryError
|
|
from .base import ILibrary, ILibraryView
|
|
from .utils import TreeView, dangling_mode_t, _plan_source_names, _rename_patterns, _source_rename_map
|
|
from .mapping import Library, LibraryView
|
|
from .overlay import OverlayLibrary
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable, Iterator, Mapping, Sequence
|
|
|
|
from ..abstract import Abstract
|
|
from ..pattern import Pattern
|
|
|
|
|
|
_ACTIVE_BUILD_SESSIONS: ContextVar[dict[int, _BuildSessionLibrary] | None] = ContextVar(
|
|
'masque_active_build_sessions',
|
|
default=None,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CellProvenance:
|
|
"""
|
|
Provenance record for one cell in a completed build output.
|
|
|
|
Each output name in a `BuildReport` maps to one `CellProvenance`. The
|
|
record captures both where the cell came from and how its visible name was
|
|
chosen.
|
|
|
|
Attributes:
|
|
requested_name: First name requested for this cell during the build.
|
|
kind: Whether the cell came from a declaration, helper emission, or an
|
|
imported source library.
|
|
owner_declared_name: Declared cell responsible for this output cell, if
|
|
any. Imported source cells leave this as `None`.
|
|
build_chain: Declared-cell dependency chain that was active when the
|
|
cell was emitted.
|
|
"""
|
|
requested_name: str
|
|
kind: Literal['declared', 'helper', 'source']
|
|
owner_declared_name: str | None
|
|
build_chain: tuple[str, ...]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BuildReport:
|
|
"""
|
|
Immutable summary of one `BuildLibrary.validate()` or `.build()` run.
|
|
|
|
The report is designed to answer two questions after a build completes:
|
|
which declared cells depended on which other declared cells, and where each
|
|
output cell came from.
|
|
|
|
Attributes:
|
|
requested_roots: Roots explicitly requested for the run. A full
|
|
`build()` uses all declared cells.
|
|
provenance: Mapping from final output name to provenance metadata.
|
|
dependency_graph: Declared-cell dependency graph discovered through
|
|
library-mediated reads and explicit recipe hints.
|
|
"""
|
|
requested_roots: tuple[str, ...]
|
|
provenance: Mapping[str, CellProvenance]
|
|
dependency_graph: Mapping[str, frozenset[str]]
|
|
|
|
@dataclass
|
|
class _BuildRecipe:
|
|
""" Captured deferred call to a pattern factory. """
|
|
func: Callable[..., Pattern]
|
|
args: tuple[Any, ...]
|
|
kwargs: dict[str, Any]
|
|
explicit_dependencies: tuple[str, ...] = ()
|
|
|
|
def depends_on(self, *names: str) -> _BuildRecipe:
|
|
self.explicit_dependencies += tuple(names)
|
|
return self
|
|
|
|
|
|
def cell(func: Callable[..., Pattern]) -> Callable[..., _BuildRecipe]:
|
|
"""
|
|
Wrap a plain pattern factory so calls return deferred build recipes.
|
|
|
|
Use as either `cell(fn)(...)` or `@cell`.
|
|
"""
|
|
@wraps(func)
|
|
def wrapper(*args: Any, **kwargs: Any) -> _BuildRecipe:
|
|
return _BuildRecipe(func=func, args=args, kwargs=kwargs)
|
|
|
|
return wrapper
|
|
|
|
|
|
class BuildCellsView:
|
|
"""
|
|
Attribute-based declaration namespace for `BuildLibrary`.
|
|
|
|
This is the ergonomic authoring surface exposed as `builder.cells`. It is
|
|
intentionally write-focused: attribute assignment and deletion register
|
|
declarations, while attribute reads fail with guidance to build first and
|
|
use the returned library.
|
|
"""
|
|
|
|
def __init__(self, library: BuildLibrary) -> None:
|
|
object.__setattr__(self, '_library', library)
|
|
|
|
def __getattr__(self, name: str) -> Pattern:
|
|
raise BuildError(
|
|
f'BuildLibrary.cells.{name} is write-only during authoring. '
|
|
'Call build() and index the returned library instead.'
|
|
)
|
|
|
|
def __setattr__(self, name: str, value: Pattern | _BuildRecipe) -> None:
|
|
if name.startswith('_'):
|
|
object.__setattr__(self, name, value)
|
|
return
|
|
self._library[name] = value
|
|
|
|
def __delattr__(self, name: str) -> None:
|
|
if name.startswith('_'):
|
|
raise AttributeError(name)
|
|
del self._library[name]
|
|
|
|
|
|
class BuildLibrary(ILibrary):
|
|
"""
|
|
Two-phase declaration surface for mixed imported/generated libraries.
|
|
|
|
A `BuildLibrary` collects three kinds of inputs:
|
|
- direct declared `Pattern` objects
|
|
- deferred recipes created with `cell(...)`
|
|
- imported source-backed library views added with `add_source(...)`
|
|
|
|
The builder itself is not a normal readable library during authoring.
|
|
Instead, `validate()` and `build()` create a temporary build-session library
|
|
that recipes can read from and write helper cells into while dependencies
|
|
are resolved. `build()` then freezes the builder on success and returns the
|
|
built library plus a `BuildReport`.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self.cells = BuildCellsView(self)
|
|
self._frozen = False
|
|
self._declarations: dict[str, Pattern | _BuildRecipe] = {}
|
|
self._sources: list[tuple[ILibraryView, dict[str, str]]] = []
|
|
self._names: dict[str, None] = {}
|
|
|
|
def _active_session(self) -> _BuildSessionLibrary | None:
|
|
sessions = _ACTIVE_BUILD_SESSIONS.get()
|
|
if sessions is None:
|
|
return None
|
|
return sessions.get(id(self))
|
|
|
|
def _require_active_session(self, operation: str) -> _BuildSessionLibrary:
|
|
session = self._active_session()
|
|
if session is None:
|
|
raise BuildError(
|
|
f'BuildLibrary.{operation}() is only available while validate() or build() is running. '
|
|
'Use the built output library for reads.'
|
|
)
|
|
return session
|
|
|
|
def _assert_editable(self) -> None:
|
|
if self._frozen:
|
|
raise BuildError('This BuildLibrary has already been built successfully and is now frozen.')
|
|
|
|
def __iter__(self) -> Iterator[str]:
|
|
session = self._active_session()
|
|
if session is not None:
|
|
return iter(session)
|
|
return iter(self._names)
|
|
|
|
def __len__(self) -> int:
|
|
session = self._active_session()
|
|
if session is not None:
|
|
return len(session)
|
|
return len(self._names)
|
|
|
|
def __contains__(self, key: object) -> bool:
|
|
session = self._active_session()
|
|
if session is not None:
|
|
return key in session
|
|
return key in self._names
|
|
|
|
def __getitem__(self, key: str) -> Pattern:
|
|
return self._require_active_session('__getitem__')[key]
|
|
|
|
def __setitem__(
|
|
self,
|
|
key: str,
|
|
value: Pattern | _BuildRecipe,
|
|
) -> None:
|
|
session = self._active_session()
|
|
if session is not None:
|
|
session[key] = value
|
|
return
|
|
|
|
self._assert_editable()
|
|
if key in self._names:
|
|
raise LibraryError(f'"{key}" already exists in the builder. Overwriting is not allowed!')
|
|
|
|
if isinstance(value, _BuildRecipe):
|
|
declaration = value
|
|
else:
|
|
if callable(value):
|
|
raise TypeError('BuildLibrary recipes must be wrapped with cell(fn)(...) or @cell.')
|
|
declaration = value
|
|
|
|
self._declarations[key] = declaration
|
|
self._names[key] = None
|
|
|
|
def __delitem__(self, key: str) -> None:
|
|
session = self._active_session()
|
|
if session is not None:
|
|
del session[key]
|
|
return
|
|
|
|
self._assert_editable()
|
|
if key not in self._declarations:
|
|
raise KeyError(key)
|
|
del self._declarations[key]
|
|
del self._names[key]
|
|
|
|
def _merge(self, key_self: str, other: Mapping[str, Pattern], key_other: str) -> None:
|
|
session = self._active_session()
|
|
if session is not None:
|
|
session._merge(key_self, other, key_other)
|
|
return
|
|
self[key_self] = copy.deepcopy(other[key_other])
|
|
|
|
def add(
|
|
self,
|
|
other: Mapping[str, Pattern],
|
|
rename_theirs: Callable[[ILibraryView, str], str] = _rename_patterns,
|
|
mutate_other: bool = False,
|
|
) -> dict[str, str]:
|
|
from ..pattern import map_targets # noqa: PLC0415
|
|
|
|
session = self._active_session()
|
|
if session is not None:
|
|
return session.add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
|
|
|
self._assert_editable()
|
|
|
|
source_backed = isinstance(other, ILibraryView) and not isinstance(other, Library | LibraryView)
|
|
if source_backed:
|
|
if mutate_other:
|
|
raise BuildError('BuildLibrary.add(..., mutate_other=True) is not supported for source-backed inputs.')
|
|
return self.add_source(
|
|
other,
|
|
rename_theirs = rename_theirs,
|
|
rename_when = 'conflict',
|
|
)
|
|
|
|
source_order = tuple(other.keys())
|
|
source_to_visible = _plan_source_names(
|
|
self,
|
|
source_order,
|
|
self._names,
|
|
rename_theirs = rename_theirs,
|
|
rename_when = 'conflict',
|
|
)
|
|
rename_map = _source_rename_map(source_to_visible)
|
|
|
|
if mutate_other:
|
|
temp = other
|
|
else:
|
|
temp = Library(copy.deepcopy(dict(other)))
|
|
|
|
for source_name in source_order:
|
|
visible_name = source_to_visible[source_name]
|
|
pattern = temp[source_name]
|
|
if rename_map:
|
|
pattern.refs = map_targets(
|
|
pattern.refs,
|
|
lambda target: cast('dict[str | None, str | None]', rename_map).get(target, target),
|
|
)
|
|
self[visible_name] = pattern
|
|
|
|
return rename_map
|
|
|
|
def __lshift__(self, other: TreeView) -> str:
|
|
session = self._active_session()
|
|
if session is not None:
|
|
return session << other
|
|
|
|
self._assert_editable()
|
|
if len(other) == 1:
|
|
name = next(iter(other))
|
|
elif isinstance(other, ILibraryView) and not isinstance(other, Library | LibraryView):
|
|
source_order = other.source_order()
|
|
child_graph = other.child_graph(dangling='include')
|
|
referenced = set().union(*child_graph.values()) if child_graph else set()
|
|
tops = [candidate for candidate in source_order if candidate not in referenced]
|
|
if len(tops) != 1:
|
|
raise LibraryError(f'Asked for the single topcell, but found the following: {pformat(tops)}')
|
|
name = tops[0]
|
|
else:
|
|
return super().__lshift__(other)
|
|
|
|
rename_map = self.add(other)
|
|
return rename_map.get(name, name)
|
|
|
|
def __le__(self, other: Mapping[str, Pattern]) -> Abstract:
|
|
if self._active_session() is not None:
|
|
return super().__le__(other)
|
|
raise BuildError('BuildLibrary.__le__() is only available while validate() or build() is running.')
|
|
|
|
def rename(
|
|
self,
|
|
old_name: str,
|
|
new_name: str,
|
|
move_references: bool = False,
|
|
) -> Self:
|
|
"""
|
|
Rename a helper cell during an active build session.
|
|
|
|
During authoring, declared cells must be registered under their
|
|
intended final names and imported source cells must be renamed through
|
|
`add_source(...)`.
|
|
"""
|
|
session = self._active_session()
|
|
if session is not None:
|
|
session.rename(old_name, new_name, move_references=move_references)
|
|
return self
|
|
|
|
self._assert_editable()
|
|
if old_name == new_name:
|
|
return self
|
|
if old_name in self._declarations:
|
|
raise BuildError(
|
|
f'Cannot rename declared build cell "{old_name}" during authoring. '
|
|
'Register it under the intended final name instead.'
|
|
)
|
|
if old_name not in self._names:
|
|
raise LibraryError(f'"{old_name}" does not exist in the builder.')
|
|
raise BuildError(
|
|
f'Cannot rename imported source cell "{old_name}" during authoring. '
|
|
'Choose visible source names with add_source(..., rename_theirs=..., rename_when=...).'
|
|
)
|
|
|
|
def abstract(self, name: str) -> Abstract:
|
|
return self._require_active_session('abstract').abstract(name)
|
|
|
|
def resolve(
|
|
self,
|
|
other: Abstract | str | Pattern | TreeView,
|
|
append: bool = False,
|
|
) -> Abstract | Pattern:
|
|
return self._require_active_session('resolve').resolve(other, append=append)
|
|
|
|
def add_source(
|
|
self,
|
|
source: Mapping[str, Pattern] | ILibraryView,
|
|
*,
|
|
rename_theirs: Callable[[ILibraryView, str], str] | None = None,
|
|
rename_when: Literal['conflict', 'always'] = 'conflict',
|
|
) -> dict[str, str]:
|
|
"""
|
|
Register an imported source-backed library with the builder.
|
|
|
|
The source is not materialized immediately. Its names are scanned once
|
|
to reserve visible builder names, then the source is read again when a
|
|
build session starts. The source's cell membership must not be
|
|
structurally mutated between `add_source()` and `build()`/`validate()`.
|
|
Source cells may be renamed on entry to avoid collisions with existing
|
|
declarations or other imported sources.
|
|
|
|
Args:
|
|
rename_theirs: Function used to choose visible names for imported
|
|
source cells.
|
|
rename_when: If `'conflict'`, only conflicting names are renamed.
|
|
If `'always'`, every imported source name is passed through
|
|
`rename_theirs`.
|
|
|
|
Returns:
|
|
Mapping of `{source_name: visible_name}` for imported names that
|
|
were renamed while being added.
|
|
"""
|
|
if self._active_session() is not None:
|
|
raise BuildError('BuildLibrary.add_source() is only available while authoring, not during validate() or build().')
|
|
self._assert_editable()
|
|
|
|
view = source if isinstance(source, ILibraryView) else LibraryView(source)
|
|
source_order = tuple(view.source_order())
|
|
source_to_visible = _plan_source_names(
|
|
self,
|
|
source_order,
|
|
self._names,
|
|
rename_theirs = rename_theirs,
|
|
rename_when = rename_when,
|
|
)
|
|
|
|
self._sources.append((view, dict(source_to_visible)))
|
|
for source_name in source_order:
|
|
visible = source_to_visible[source_name]
|
|
self._names[visible] = None
|
|
return _source_rename_map(source_to_visible)
|
|
|
|
def validate(
|
|
self,
|
|
names: Sequence[str] | None = None,
|
|
*,
|
|
allow_dangling: bool = False,
|
|
) -> BuildReport:
|
|
"""
|
|
Run the full build logic and return a `BuildReport` without producing output.
|
|
|
|
This is a dry run over the same dependency resolution and recipe
|
|
execution path used by `build()`. Any generated library is discarded
|
|
after validation completes.
|
|
"""
|
|
_session, report = self._run_build(names=names, allow_dangling=allow_dangling)
|
|
return report
|
|
|
|
def build(
|
|
self,
|
|
*,
|
|
output: Literal['overlay', 'library'] = 'overlay',
|
|
allow_dangling: bool = False,
|
|
) -> tuple[ILibrary, BuildReport]:
|
|
"""
|
|
Materialize declarations and return a usable output library plus report.
|
|
|
|
Args:
|
|
output: `'overlay'` preserves imported source-backed cells where
|
|
possible, while `'library'` eagerly materializes the full
|
|
result.
|
|
allow_dangling: If `False`, fail the build when the completed
|
|
library still contains dangling references.
|
|
"""
|
|
if output not in ('overlay', 'library'):
|
|
raise ValueError(f'Unknown build output mode: {output!r}')
|
|
self._assert_editable()
|
|
session, report = self._run_build(names=None, allow_dangling=allow_dangling)
|
|
if output == 'library':
|
|
built_output = session.to_library()
|
|
else:
|
|
built_output = session.to_overlay()
|
|
self._frozen = True
|
|
return built_output, report
|
|
|
|
def _run_build(
|
|
self,
|
|
*,
|
|
names: Sequence[str] | None,
|
|
allow_dangling: bool,
|
|
) -> tuple[_BuildSessionLibrary, BuildReport]:
|
|
roots = tuple(dict.fromkeys(names if names is not None else self._declarations.keys()))
|
|
unknown = [name for name in roots if name not in self._names]
|
|
if unknown:
|
|
raise BuildError(f'Unknown build roots requested: {unknown}')
|
|
|
|
session = _BuildSessionLibrary(self)
|
|
sessions = dict(_ACTIVE_BUILD_SESSIONS.get() or {})
|
|
sessions[id(self)] = session
|
|
token = _ACTIVE_BUILD_SESSIONS.set(sessions)
|
|
try:
|
|
session.materialize_many(roots)
|
|
if not allow_dangling:
|
|
session.child_graph(dangling='error')
|
|
finally:
|
|
_ACTIVE_BUILD_SESSIONS.reset(token)
|
|
|
|
report = session.build_report(roots)
|
|
return session, report
|
|
|
|
|
|
class _BuildSessionLibrary(ILibrary):
|
|
"""
|
|
Internal overlay-backed library used while a `BuildLibrary` is executing.
|
|
|
|
This object provides the mutable-library surface that recipes expect while
|
|
also tracking declared-cell dependencies, helper-cell provenance, and
|
|
imported source cells. It exists only for the duration of a validation or
|
|
build run.
|
|
"""
|
|
|
|
def __init__(self, builder: BuildLibrary) -> None:
|
|
self._builder = builder
|
|
self._overlay = OverlayLibrary()
|
|
self._built: set[str] = set()
|
|
self._declared_stack: list[str] = []
|
|
self._names = dict(builder._names)
|
|
self._provenance: dict[str, CellProvenance] = {}
|
|
self._dependency_graph: defaultdict[str, set[str]] = defaultdict(set)
|
|
self._install_sources()
|
|
|
|
def _install_sources(self) -> None:
|
|
for source_library, source_to_visible in self._builder._sources:
|
|
source_order = source_library.source_order()
|
|
expected_names = set(source_to_visible)
|
|
actual_names = set(source_order)
|
|
if actual_names != expected_names:
|
|
added_names = sorted(actual_names - expected_names)
|
|
removed_names = sorted(expected_names - actual_names)
|
|
detail = []
|
|
if added_names:
|
|
detail.append(f'added={added_names}')
|
|
if removed_names:
|
|
detail.append(f'removed={removed_names}')
|
|
raise BuildError(
|
|
'Imported source library changed after add_source() was called '
|
|
f'({", ".join(detail)}). '
|
|
'Do not structurally mutate source libraries between add_source() and build()/validate().'
|
|
)
|
|
|
|
def rename_source(_lib: ILibraryView, name: str, *, mapping: Mapping[str, str] = source_to_visible) -> str:
|
|
return mapping[name]
|
|
|
|
self._overlay.add_source(
|
|
source_library,
|
|
rename_theirs = rename_source,
|
|
rename_when = 'always',
|
|
)
|
|
|
|
for source_name in source_order:
|
|
visible_name = source_to_visible[source_name]
|
|
self._provenance[visible_name] = CellProvenance(
|
|
requested_name = source_name,
|
|
kind = 'source',
|
|
owner_declared_name = None,
|
|
build_chain = (),
|
|
)
|
|
|
|
def __iter__(self) -> Iterator[str]:
|
|
return iter(self._names)
|
|
|
|
def __len__(self) -> int:
|
|
return len(self._names)
|
|
|
|
def __contains__(self, key: object) -> bool:
|
|
return key in self._names
|
|
|
|
def _current_declared(self) -> str | None:
|
|
if not self._declared_stack:
|
|
return None
|
|
return self._declared_stack[-1]
|
|
|
|
def _record_dependency(self, target: str) -> None:
|
|
current = self._current_declared()
|
|
if current is None or current == target or target not in self._builder._declarations:
|
|
return
|
|
self._dependency_graph[current].add(target)
|
|
|
|
def _guard_mutable_output_name(self, key: str, *, operation: str) -> None:
|
|
if key in self._builder._declarations:
|
|
raise BuildError(f'Cannot {operation} declared build cell "{key}" during an active build session.')
|
|
|
|
provenance = self._provenance.get(key)
|
|
if provenance is not None and provenance.kind == 'source':
|
|
raise BuildError(f'Cannot {operation} imported source cell "{key}" during an active build session.')
|
|
|
|
def rename(
|
|
self,
|
|
old_name: str,
|
|
new_name: str,
|
|
move_references: bool = False,
|
|
) -> Self:
|
|
if old_name == new_name:
|
|
return self
|
|
if old_name not in self._overlay:
|
|
if old_name in self._builder._declarations:
|
|
self._guard_mutable_output_name(old_name, operation='rename')
|
|
raise LibraryError(f'"{old_name}" does not exist in the library.')
|
|
|
|
self._guard_mutable_output_name(old_name, operation='rename')
|
|
if new_name in self._names:
|
|
raise LibraryError(f'"{new_name}" already exists in the library.')
|
|
|
|
self._overlay.rename(old_name, new_name, move_references=move_references)
|
|
self._names = {
|
|
new_name if name == old_name else name: None
|
|
for name in self._names
|
|
}
|
|
|
|
provenance = self._provenance.pop(old_name)
|
|
self._provenance[new_name] = provenance
|
|
return self
|
|
|
|
def __getitem__(self, key: str) -> Pattern:
|
|
if key in self._builder._declarations:
|
|
self._record_dependency(key)
|
|
self._ensure_declared(key)
|
|
return self._overlay[key]
|
|
|
|
def __setitem__(
|
|
self,
|
|
key: str,
|
|
value: Pattern | Callable[[], Pattern],
|
|
) -> None:
|
|
if key in self._overlay:
|
|
raise LibraryError(f'"{key}" already exists in the library. Overwriting is not allowed!')
|
|
current = self._current_declared()
|
|
if key in self._builder._declarations and key != current:
|
|
raise LibraryError(f'"{key}" is reserved for a declared cell and cannot be used as a helper name.')
|
|
|
|
pattern = value() if callable(value) else value
|
|
self._overlay[key] = pattern
|
|
self._names.setdefault(key, None)
|
|
|
|
kind: Literal['declared', 'helper']
|
|
if current is not None and key == current:
|
|
kind = 'declared'
|
|
else:
|
|
kind = 'helper'
|
|
|
|
self._provenance[key] = CellProvenance(
|
|
requested_name = key,
|
|
kind = kind,
|
|
owner_declared_name = current if kind == 'helper' else key,
|
|
build_chain = tuple(self._declared_stack),
|
|
)
|
|
|
|
def __delitem__(self, key: str) -> None:
|
|
if key not in self._overlay:
|
|
if key in self._builder._declarations:
|
|
self._guard_mutable_output_name(key, operation='delete')
|
|
raise KeyError(key)
|
|
|
|
self._guard_mutable_output_name(key, operation='delete')
|
|
if key in self._overlay:
|
|
del self._overlay[key]
|
|
self._names.pop(key, None)
|
|
self._provenance.pop(key, None)
|
|
|
|
def _merge(self, key_self: str, other: Mapping[str, Pattern], key_other: str) -> None:
|
|
self[key_self] = copy.deepcopy(other[key_other])
|
|
|
|
def add(
|
|
self,
|
|
other: Mapping[str, Pattern],
|
|
rename_theirs: Callable[[ILibraryView, str], str] = _rename_patterns,
|
|
mutate_other: bool = False,
|
|
) -> dict[str, str]:
|
|
rename_map = super().add(other, rename_theirs=rename_theirs, mutate_other=mutate_other)
|
|
|
|
current = self._current_declared()
|
|
for old_name, new_name in rename_map.items():
|
|
if new_name in self._provenance:
|
|
self._provenance[new_name] = replace(
|
|
self._provenance[new_name],
|
|
requested_name = old_name,
|
|
owner_declared_name = current if current is not None else self._provenance[new_name].owner_declared_name,
|
|
)
|
|
return rename_map
|
|
|
|
def _wrap_error(self, name: str, exc: Exception) -> BuildError:
|
|
chain = tuple(self._declared_stack)
|
|
msg = [f'Failed while building declared cell "{name}"']
|
|
if chain:
|
|
msg.append(f'Dependency chain: {" -> ".join(chain)}')
|
|
msg.append(f'Cause: {exc}')
|
|
return BuildError('\n'.join(msg))
|
|
|
|
def _ensure_named(self, name: str) -> None:
|
|
if name in self._builder._declarations:
|
|
self._record_dependency(name)
|
|
self._ensure_declared(name)
|
|
return
|
|
if name in self._overlay:
|
|
return
|
|
raise BuildError(f'Missing dependency "{name}"')
|
|
|
|
def _ensure_declared(self, name: str) -> None:
|
|
from ..pattern import Pattern # noqa: PLC0415
|
|
|
|
if name in self._built:
|
|
return
|
|
if name in self._declared_stack:
|
|
chain = ' -> '.join(self._declared_stack + [name])
|
|
raise BuildError(f'Cycle detected while building declared cells: {chain}')
|
|
|
|
declaration = self._builder._declarations[name]
|
|
self._declared_stack.append(name)
|
|
try:
|
|
if isinstance(declaration, _BuildRecipe):
|
|
for dep in declaration.explicit_dependencies:
|
|
self._ensure_named(dep)
|
|
pattern = declaration.func(*declaration.args, **declaration.kwargs)
|
|
if not isinstance(pattern, Pattern):
|
|
raise BuildError(f'Recipe for "{name}" returned {type(pattern).__name__}, expected Pattern') # noqa: TRY301
|
|
else:
|
|
pattern = declaration.deepcopy()
|
|
|
|
if name in self._overlay:
|
|
if self._overlay[name] is not pattern:
|
|
raise BuildError( # noqa: TRY301
|
|
f'Recipe for "{name}" wrote a different pattern into the session under its own name.'
|
|
)
|
|
else:
|
|
self[name] = pattern
|
|
self._built.add(name)
|
|
except Exception as exc:
|
|
raise self._wrap_error(name, exc) from exc
|
|
finally:
|
|
self._declared_stack.pop()
|
|
|
|
def materialize_many(self, names: Sequence[str]) -> None:
|
|
for name in dict.fromkeys(names):
|
|
self._ensure_named(name)
|
|
|
|
def source_order(self) -> tuple[str, ...]:
|
|
return self._overlay.source_order()
|
|
|
|
def child_graph(
|
|
self,
|
|
dangling: dangling_mode_t = 'error',
|
|
) -> dict[str, set[str]]:
|
|
return self._overlay.child_graph(dangling=dangling)
|
|
|
|
def parent_graph(
|
|
self,
|
|
dangling: dangling_mode_t = 'error',
|
|
) -> dict[str, set[str]]:
|
|
return self._overlay.parent_graph(dangling=dangling)
|
|
|
|
def build_report(self, requested_roots: Sequence[str]) -> BuildReport:
|
|
dependency_graph = {
|
|
name: frozenset(self._dependency_graph.get(name, set()))
|
|
for name in self._builder._declarations
|
|
if name in self._dependency_graph or name in requested_roots
|
|
}
|
|
return BuildReport(
|
|
requested_roots = tuple(dict.fromkeys(requested_roots)),
|
|
provenance = dict(self._provenance),
|
|
dependency_graph = dependency_graph,
|
|
)
|
|
|
|
def to_overlay(self) -> ILibrary:
|
|
return self._overlay
|
|
|
|
def to_library(self) -> Library:
|
|
mapping = {name: self._overlay[name] for name in self._overlay.source_order()}
|
|
return Library(mapping)
|