From ac34108253c3029956adc34023f00d381ab26843 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 16 Jul 2026 08:36:19 -0700 Subject: [PATCH] [library] auto-rename single-use names in add_source --- masque/library/build.py | 4 +++- masque/library/overlay.py | 13 +++++++++++-- masque/test/test_build_library.py | 13 +++++++++++++ masque/test/test_gdsii_lazy.py | 2 +- masque/test/test_library.py | 20 ++++++++++++++++++++ 5 files changed, 48 insertions(+), 4 deletions(-) diff --git a/masque/library/build.py b/masque/library/build.py index c4b7f8f..e69fa9a 100644 --- a/masque/library/build.py +++ b/masque/library/build.py @@ -287,7 +287,7 @@ class LibraryBuilder(INameView): self, source: Mapping[str, Pattern] | ILibraryView, *, - rename_theirs: Callable[[INameView, str], str] | None = None, + rename_theirs: Callable[[INameView, str], str] | None = _rename_patterns, rename_when: Literal['conflict', 'always'] = 'conflict', ) -> dict[str, str]: """ @@ -309,6 +309,8 @@ class LibraryBuilder(INameView): rename_theirs: Function used to choose visible names for imported source cells. Its `INameView` argument contains existing and previously reserved names, but does not support pattern lookup. + By default, conflicting single-use names are made unique; + pass `None` to reject every conflict. rename_when: If `'conflict'`, only conflicting names are renamed. If `'always'`, every imported source name is passed through `rename_theirs`. diff --git a/masque/library/overlay.py b/masque/library/overlay.py index 698a457..529684b 100644 --- a/masque/library/overlay.py +++ b/masque/library/overlay.py @@ -10,7 +10,14 @@ from ..error import LibraryError from ..pattern import Pattern, map_layers, map_targets from .base import ILibrary, ILibraryView from .capabilities import IBorrowing, IMaterializable -from .utils import INameView, dangling_mode_t, _plan_source_names, _source_rename_map, _validate_dangling_mode +from .utils import ( + INameView, + dangling_mode_t, + _plan_source_names, + _rename_patterns, + _source_rename_map, + _validate_dangling_mode, + ) from .mapping import LibraryView if TYPE_CHECKING: @@ -304,7 +311,7 @@ class OverlayLibrary(ILibrary, IMaterializable, IBorrowing): self, source: Mapping[str, Pattern] | ILibraryView, *, - rename_theirs: Callable[[INameView, str], str] | None = None, + rename_theirs: Callable[[INameView, str], str] | None = _rename_patterns, rename_when: Literal['conflict', 'always'] = 'conflict', ) -> dict[str, str]: """ @@ -317,6 +324,8 @@ class OverlayLibrary(ILibrary, IMaterializable, IBorrowing): rename_theirs: Function used to choose visible names for imported source cells. Its `INameView` argument contains existing and previously reserved names, but does not support pattern lookup. + By default, conflicting single-use names are made unique; + pass `None` to reject every conflict. rename_when: If `'conflict'`, only conflicting names are renamed. If `'always'`, every imported source name is passed through `rename_theirs`. diff --git a/masque/test/test_build_library.py b/masque/test/test_build_library.py index 36dd05d..e020912 100644 --- a/masque/test/test_build_library.py +++ b/masque/test/test_build_library.py @@ -437,6 +437,19 @@ def test_build_library_add_source_can_rename_every_source_cell() -> None: assert report.provenance["mapped_child"].requested_name == "child" +def test_build_library_add_source_renames_conflicting_single_use_name_by_default() -> None: + source = Library({'_myCellName$F': Pattern(), 'source_top': Pattern()}) + source['source_top'].ref('_myCellName$F') + builder = LibraryBuilder() + builder['_myCellName$F'] = Pattern() + + rename_map = builder.add_source(source) + built, _report = builder.build() + + assert rename_map == {'_myCellName$F': '_myCellName'} + assert set(built['source_top'].refs) == {'_myCellName'} + + def test_library_builder_adds_an_ordinary_view_eagerly() -> None: child = Pattern() top = Pattern() diff --git a/masque/test/test_gdsii_lazy.py b/masque/test/test_gdsii_lazy.py index d155d74..610d743 100644 --- a/masque/test/test_gdsii_lazy.py +++ b/masque/test/test_gdsii_lazy.py @@ -382,7 +382,7 @@ def test_gdsii_lazy_overlay_add_source_rename_when_validation() -> None: src = _make_lazy_port_library() with pytest.raises(TypeError, match='rename_theirs'): - OverlayLibrary().add_source(src, rename_when='always') + OverlayLibrary().add_source(src, rename_theirs=None, rename_when='always') with pytest.raises(ValueError, match='rename mode'): OverlayLibrary().add_source(src, rename_when='sometimes') # type: ignore[arg-type] diff --git a/masque/test/test_library.py b/masque/test/test_library.py index c205ec1..adc6f3c 100644 --- a/masque/test/test_library.py +++ b/masque/test/test_library.py @@ -621,6 +621,26 @@ def test_overlay_add_source_callback_sees_earlier_name_reservations() -> None: assert set(rename_map.values()) <= set(overlay) +def test_overlay_add_source_renames_conflicting_single_use_name_by_default() -> None: + overlay = OverlayLibrary() + overlay['_myCellName$F'] = Pattern() + source = Library({'_myCellName$F': Pattern(), 'source_top': Pattern()}) + source['source_top'].ref('_myCellName$F') + + rename_map = overlay.add_source(source) + + assert rename_map == {'_myCellName$F': '_myCellName'} + assert set(overlay['source_top'].refs) == {'_myCellName'} + + +def test_overlay_add_source_can_explicitly_disable_default_renaming() -> None: + overlay = OverlayLibrary() + overlay['_helper$A'] = Pattern() + + with pytest.raises(LibraryError, match='Conflicting name'): + overlay.add_source(Library({'_helper$A': Pattern()}), rename_theirs=None) + + def test_port_load_view_detaches_already_materialized_overlay_pattern() -> None: overlay = OverlayLibrary() overlay.add_source(Library({"top": Pattern()}))