diff --git a/masque/file/gdsii_lazy_core.py b/masque/file/gdsii_lazy_core.py index f5dc994..bf4cde0 100644 --- a/masque/file/gdsii_lazy_core.py +++ b/masque/file/gdsii_lazy_core.py @@ -17,7 +17,7 @@ Both the classic and Arrow-backed lazy GDS readers rely on these helpers. from __future__ import annotations from dataclasses import dataclass -from typing import IO, Any, cast +from typing import IO, Any, Literal, cast from collections import defaultdict from collections.abc import Callable, Iterator, Mapping, Sequence import copy @@ -303,7 +303,23 @@ class OverlayLibrary(ILibrary): source: Mapping[str, Pattern] | ILibraryView, *, rename_theirs: Callable[[ILibraryView, str], str] | None = None, + rename_when: Literal['conflict', 'always'] = 'conflict', ) -> dict[str, str]: + """ + Add a source-backed library layer. + + 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`. + """ + if rename_when not in ('conflict', 'always'): + raise ValueError(f'Unknown source rename mode: {rename_when!r}') + if rename_when == 'always' and rename_theirs is None: + raise TypeError('rename_theirs is required when rename_when="always"') + view = _coerce_library_view(source) source_order = list(view.source_order()) child_graph = view.child_graph(dangling='include') @@ -314,12 +330,20 @@ class OverlayLibrary(ILibrary): for name in source_order: visible = name - if visible in self._entries or visible in visible_to_source: + renamed = False + if rename_when == 'always': + visible = cast('Callable[[ILibraryView, str], str]', rename_theirs)(self, name) + renamed = True + elif visible in self._entries or visible in visible_to_source: if rename_theirs is None: raise LibraryError(f'Conflicting name while adding source: {name!r}') visible = rename_theirs(self, name) - if visible in self._entries or visible in visible_to_source: - raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}') + renamed = True + if visible in self._entries or visible in visible_to_source: + if not renamed: + raise LibraryError(f'Conflicting name while adding source: {name!r}') + raise LibraryError(f'Unresolved duplicate key encountered while adding source: {name!r} -> {visible!r}') + if visible != name: rename_map[name] = visible source_to_visible[name] = visible visible_to_source[visible] = name diff --git a/masque/test/test_gdsii_lazy.py b/masque/test/test_gdsii_lazy.py index 6855783..0134d6e 100644 --- a/masque/test/test_gdsii_lazy.py +++ b/masque/test/test_gdsii_lazy.py @@ -1,6 +1,7 @@ from pathlib import Path import numpy +import pytest from numpy.testing import assert_allclose from ..file import gdsii, gdsii_lazy @@ -82,6 +83,35 @@ def test_gdsii_lazy_overlay_add_source_stays_lazy_for_processed_view(tmp_path: P assert set(abstract.ports) == {'A'} +def test_gdsii_lazy_overlay_add_source_can_rename_every_source_cell() -> None: + src = _make_lazy_port_library() + overlay = gdsii_lazy.OverlayLibrary() + + rename_map = overlay.add_source( + src, + rename_theirs=lambda _lib, name: f'mapped_{name}', + rename_when='always', + ) + + assert rename_map == { + 'leaf': 'mapped_leaf', + 'child': 'mapped_child', + 'top': 'mapped_top', + } + assert tuple(overlay.keys()) == ('mapped_leaf', 'mapped_child', 'mapped_top') + assert 'mapped_leaf' in overlay['mapped_child'].refs + + +def test_gdsii_lazy_overlay_add_source_rename_when_validation() -> None: + src = _make_lazy_port_library() + + with pytest.raises(TypeError, match='rename_theirs'): + gdsii_lazy.OverlayLibrary().add_source(src, rename_when='always') + + with pytest.raises(ValueError, match='rename mode'): + gdsii_lazy.OverlayLibrary().add_source(src, rename_when='sometimes') # type: ignore[arg-type] + + def test_gdsii_lazy_processed_write_roundtrips_without_explicit_units(tmp_path: Path) -> None: gds_file = tmp_path / 'lazy_roundtrip.gds' src = _make_lazy_port_library()