Compare commits
No commits in common. "b44c962e079882f119a6b524660349134fa1ae63" and "c2ef3e42174cea0fb7c3c35625189d95aff97169" have entirely different histories.
b44c962e07
...
c2ef3e4217
7 changed files with 25 additions and 227 deletions
|
|
@ -75,8 +75,7 @@ def preflight(
|
||||||
raise PatternError('Non-numeric layers found:' + pformat(named_layers))
|
raise PatternError('Non-numeric layers found:' + pformat(named_layers))
|
||||||
|
|
||||||
if prune_empty_patterns:
|
if prune_empty_patterns:
|
||||||
prune_dangling = 'error' if allow_dangling_refs is False else 'ignore'
|
pruned = lib.prune_empty()
|
||||||
pruned = lib.prune_empty(dangling=prune_dangling)
|
|
||||||
if pruned:
|
if pruned:
|
||||||
logger.info(f'Preflight pruned {len(pruned)} empty patterns')
|
logger.info(f'Preflight pruned {len(pruned)} empty patterns')
|
||||||
logger.debug('Pruned: ' + pformat(pruned))
|
logger.debug('Pruned: ' + pformat(pruned))
|
||||||
|
|
|
||||||
|
|
@ -59,9 +59,6 @@ TreeView: TypeAlias = Mapping[str, 'Pattern']
|
||||||
Tree: TypeAlias = MutableMapping[str, 'Pattern']
|
Tree: TypeAlias = MutableMapping[str, 'Pattern']
|
||||||
""" A mutable name-to-`Pattern` mapping which is expected to have only one top-level cell """
|
""" A mutable name-to-`Pattern` mapping which is expected to have only one top-level cell """
|
||||||
|
|
||||||
dangling_mode_t: TypeAlias = Literal['error', 'ignore', 'include']
|
|
||||||
""" How helpers should handle refs whose targets are not present in the library. """
|
|
||||||
|
|
||||||
|
|
||||||
SINGLE_USE_PREFIX = '_'
|
SINGLE_USE_PREFIX = '_'
|
||||||
"""
|
"""
|
||||||
|
|
@ -421,21 +418,6 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
return self[self.top()]
|
return self[self.top()]
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _dangling_refs_error(dangling: set[str], context: str) -> LibraryError:
|
|
||||||
dangling_list = sorted(dangling)
|
|
||||||
return LibraryError(f'Dangling refs found while {context}: ' + pformat(dangling_list))
|
|
||||||
|
|
||||||
def _raw_child_graph(self) -> tuple[dict[str, set[str]], set[str]]:
|
|
||||||
existing = set(self.keys())
|
|
||||||
graph: dict[str, set[str]] = {}
|
|
||||||
dangling: set[str] = set()
|
|
||||||
for name, pat in self.items():
|
|
||||||
children = {child for child, refs in pat.refs.items() if child is not None and refs}
|
|
||||||
graph[name] = children
|
|
||||||
dangling |= children - existing
|
|
||||||
return graph, dangling
|
|
||||||
|
|
||||||
def dfs(
|
def dfs(
|
||||||
self,
|
self,
|
||||||
pattern: 'Pattern',
|
pattern: 'Pattern',
|
||||||
|
|
@ -541,88 +523,46 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
|
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def child_graph(
|
def child_graph(self) -> dict[str, set[str | None]]:
|
||||||
self,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[str, set[str]]:
|
|
||||||
"""
|
"""
|
||||||
Return a mapping from pattern name to a set of all child patterns
|
Return a mapping from pattern name to a set of all child patterns
|
||||||
(patterns it references).
|
(patterns it references).
|
||||||
|
|
||||||
Only non-empty ref lists with non-`None` targets are treated as graph edges.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
dangling: How refs to missing targets are handled. `'error'` raises,
|
|
||||||
`'ignore'` drops those edges, and `'include'` exposes them as
|
|
||||||
synthetic leaf nodes.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Mapping from pattern name to a set of all pattern names it references.
|
Mapping from pattern name to a set of all pattern names it references.
|
||||||
"""
|
"""
|
||||||
graph, dangling_refs = self._raw_child_graph()
|
graph = {name: set(pat.refs.keys()) for name, pat in self.items()}
|
||||||
if dangling == 'error':
|
|
||||||
if dangling_refs:
|
|
||||||
raise self._dangling_refs_error(dangling_refs, 'building child graph')
|
|
||||||
return graph
|
|
||||||
if dangling == 'ignore':
|
|
||||||
existing = set(graph)
|
|
||||||
return {name: {child for child in children if child in existing} for name, children in graph.items()}
|
|
||||||
|
|
||||||
for target in dangling_refs:
|
|
||||||
graph.setdefault(target, set())
|
|
||||||
return graph
|
return graph
|
||||||
|
|
||||||
def parent_graph(
|
def parent_graph(self) -> dict[str, set[str]]:
|
||||||
self,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[str, set[str]]:
|
|
||||||
"""
|
"""
|
||||||
Return a mapping from pattern name to a set of all parent patterns
|
Return a mapping from pattern name to a set of all parent patterns
|
||||||
(patterns which reference it).
|
(patterns which reference it).
|
||||||
|
|
||||||
Args:
|
|
||||||
dangling: How refs to missing targets are handled. `'error'` raises,
|
|
||||||
`'ignore'` drops those targets, and `'include'` adds them as
|
|
||||||
synthetic keys whose values are their existing parents.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Mapping from pattern name to a set of all patterns which reference it.
|
Mapping from pattern name to a set of all patterns which reference it.
|
||||||
"""
|
"""
|
||||||
child_graph, dangling_refs = self._raw_child_graph()
|
igraph: dict[str, set[str]] = {name: set() for name in self}
|
||||||
if dangling == 'error' and dangling_refs:
|
for name, pat in self.items():
|
||||||
raise self._dangling_refs_error(dangling_refs, 'building parent graph')
|
for child, reflist in pat.refs.items():
|
||||||
|
if reflist and child is not None:
|
||||||
existing = set(child_graph)
|
igraph[child].add(name)
|
||||||
igraph: dict[str, set[str]] = {name: set() for name in existing}
|
|
||||||
for parent, children in child_graph.items():
|
|
||||||
for child in children:
|
|
||||||
if child in existing:
|
|
||||||
igraph[child].add(parent)
|
|
||||||
elif dangling == 'include':
|
|
||||||
igraph.setdefault(child, set()).add(parent)
|
|
||||||
return igraph
|
return igraph
|
||||||
|
|
||||||
def child_order(
|
def child_order(self) -> list[str]:
|
||||||
self,
|
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> list[str]:
|
|
||||||
"""
|
"""
|
||||||
Return a topologically sorted list of graph node names.
|
Return a topologically sorted list of all contained pattern names.
|
||||||
Child (referenced) patterns will appear before their parents.
|
Child (referenced) patterns will appear before their parents.
|
||||||
|
|
||||||
Args:
|
|
||||||
dangling: Passed to `child_graph()`.
|
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
Topologically sorted list of pattern names.
|
Topologically sorted list of pattern names.
|
||||||
"""
|
"""
|
||||||
return cast('list[str]', list(TopologicalSorter(self.child_graph(dangling=dangling)).static_order()))
|
return cast('list[str]', list(TopologicalSorter(self.child_graph()).static_order()))
|
||||||
|
|
||||||
def find_refs_local(
|
def find_refs_local(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
parent_graph: dict[str, set[str]] | None = None,
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[str, list[NDArray[numpy.float64]]]:
|
) -> dict[str, list[NDArray[numpy.float64]]]:
|
||||||
"""
|
"""
|
||||||
Find the location and orientation of all refs pointing to `name`.
|
Find the location and orientation of all refs pointing to `name`.
|
||||||
|
|
@ -635,8 +575,6 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
The provided graph may be for a superset of `self` (i.e. it may
|
The provided graph may be for a superset of `self` (i.e. it may
|
||||||
contain additional patterns which are not present in self; they
|
contain additional patterns which are not present in self; they
|
||||||
will be ignored).
|
will be ignored).
|
||||||
dangling: How refs to missing targets are handled if `parent_graph`
|
|
||||||
is not provided. `'include'` also allows querying missing names.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Mapping of {parent_name: transform_list}, where transform_list
|
Mapping of {parent_name: transform_list}, where transform_list
|
||||||
|
|
@ -645,18 +583,8 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
"""
|
"""
|
||||||
instances = defaultdict(list)
|
instances = defaultdict(list)
|
||||||
if parent_graph is None:
|
if parent_graph is None:
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
parent_graph = self.parent_graph()
|
||||||
parent_graph = self.parent_graph(dangling=graph_mode)
|
for parent in parent_graph[name]:
|
||||||
|
|
||||||
if name not in self:
|
|
||||||
if name not in parent_graph:
|
|
||||||
return instances
|
|
||||||
if dangling == 'error':
|
|
||||||
raise self._dangling_refs_error({name}, f'finding local refs for {name!r}')
|
|
||||||
if dangling == 'ignore':
|
|
||||||
return instances
|
|
||||||
|
|
||||||
for parent in parent_graph.get(name, set()):
|
|
||||||
if parent not in self: # parent_graph may be a for a superset of self
|
if parent not in self: # parent_graph may be a for a superset of self
|
||||||
continue
|
continue
|
||||||
for ref in self[parent].refs[name]:
|
for ref in self[parent].refs[name]:
|
||||||
|
|
@ -669,7 +597,6 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
name: str,
|
name: str,
|
||||||
order: list[str] | None = None,
|
order: list[str] | None = None,
|
||||||
parent_graph: dict[str, set[str]] | None = None,
|
parent_graph: dict[str, set[str]] | None = None,
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> dict[tuple[str, ...], NDArray[numpy.float64]]:
|
) -> dict[tuple[str, ...], NDArray[numpy.float64]]:
|
||||||
"""
|
"""
|
||||||
Find the absolute (top-level) location and orientation of all refs (including
|
Find the absolute (top-level) location and orientation of all refs (including
|
||||||
|
|
@ -686,28 +613,18 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
The provided graph may be for a superset of `self` (i.e. it may
|
The provided graph may be for a superset of `self` (i.e. it may
|
||||||
contain additional patterns which are not present in self; they
|
contain additional patterns which are not present in self; they
|
||||||
will be ignored).
|
will be ignored).
|
||||||
dangling: How refs to missing targets are handled if `order` or
|
|
||||||
`parent_graph` are not provided. `'include'` also allows
|
|
||||||
querying missing names.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Mapping of `{hierarchy: transform_list}`, where `hierarchy` is a tuple of the form
|
Mapping of `{hierarchy: transform_list}`, where `hierarchy` is a tuple of the form
|
||||||
`(toplevel_pattern, lvl1_pattern, ..., name)` and `transform_list` is an Nx4 ndarray
|
`(toplevel_pattern, lvl1_pattern, ..., name)` and `transform_list` is an Nx4 ndarray
|
||||||
with rows `(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
with rows `(x_offset, y_offset, rotation_ccw_rad, mirror_across_x)`.
|
||||||
"""
|
"""
|
||||||
graph_mode = 'ignore' if dangling == 'ignore' else 'include'
|
|
||||||
if order is None:
|
|
||||||
order = self.child_order(dangling=graph_mode)
|
|
||||||
if parent_graph is None:
|
|
||||||
parent_graph = self.parent_graph(dangling=graph_mode)
|
|
||||||
|
|
||||||
if name not in self:
|
if name not in self:
|
||||||
if name not in parent_graph:
|
return {}
|
||||||
return {}
|
if order is None:
|
||||||
if dangling == 'error':
|
order = self.child_order()
|
||||||
raise self._dangling_refs_error({name}, f'finding global refs for {name!r}')
|
if parent_graph is None:
|
||||||
if dangling == 'ignore':
|
parent_graph = self.parent_graph()
|
||||||
return {}
|
|
||||||
|
|
||||||
self_keys = set(self.keys())
|
self_keys = set(self.keys())
|
||||||
|
|
||||||
|
|
@ -716,16 +633,16 @@ class ILibraryView(Mapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
NDArray[numpy.float64]
|
NDArray[numpy.float64]
|
||||||
]]]
|
]]]
|
||||||
transforms = defaultdict(list)
|
transforms = defaultdict(list)
|
||||||
for parent, vals in self.find_refs_local(name, parent_graph=parent_graph, dangling=dangling).items():
|
for parent, vals in self.find_refs_local(name, parent_graph=parent_graph).items():
|
||||||
transforms[parent] = [((name,), numpy.concatenate(vals))]
|
transforms[parent] = [((name,), numpy.concatenate(vals))]
|
||||||
|
|
||||||
for next_name in order:
|
for next_name in order:
|
||||||
if next_name not in transforms:
|
if next_name not in transforms:
|
||||||
continue
|
continue
|
||||||
if not parent_graph.get(next_name, set()) & self_keys:
|
if not parent_graph[next_name] & self_keys:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
outers = self.find_refs_local(next_name, parent_graph=parent_graph, dangling=dangling)
|
outers = self.find_refs_local(next_name, parent_graph=parent_graph)
|
||||||
inners = transforms.pop(next_name)
|
inners = transforms.pop(next_name)
|
||||||
for parent, outer in outers.items():
|
for parent, outer in outers.items():
|
||||||
for path, inner in inners:
|
for path, inner in inners:
|
||||||
|
|
@ -1202,19 +1119,17 @@ class ILibrary(ILibraryView, MutableMapping[str, 'Pattern'], metaclass=ABCMeta):
|
||||||
def prune_empty(
|
def prune_empty(
|
||||||
self,
|
self,
|
||||||
repeat: bool = True,
|
repeat: bool = True,
|
||||||
dangling: dangling_mode_t = 'error',
|
|
||||||
) -> set[str]:
|
) -> set[str]:
|
||||||
"""
|
"""
|
||||||
Delete any empty patterns (i.e. where `Pattern.is_empty` returns `True`).
|
Delete any empty patterns (i.e. where `Pattern.is_empty` returns `True`).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
repeat: Also recursively delete any patterns which only contain(ed) empty patterns.
|
repeat: Also recursively delete any patterns which only contain(ed) empty patterns.
|
||||||
dangling: Passed to `parent_graph()`.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A set containing the names of all deleted patterns
|
A set containing the names of all deleted patterns
|
||||||
"""
|
"""
|
||||||
parent_graph = self.parent_graph(dangling=dangling)
|
parent_graph = self.parent_graph()
|
||||||
empty = {name for name, pat in self.items() if pat.is_empty()}
|
empty = {name for name, pat in self.items() if pat.is_empty()}
|
||||||
trimmed = set()
|
trimmed = set()
|
||||||
while empty:
|
while empty:
|
||||||
|
|
|
||||||
|
|
@ -1411,9 +1411,7 @@ class Pattern(PortList, AnnotatableImpl, Mirrorable):
|
||||||
other_copy.translate_elements(offset)
|
other_copy.translate_elements(offset)
|
||||||
self.append(other_copy)
|
self.append(other_copy)
|
||||||
else:
|
else:
|
||||||
if isinstance(other, Pattern):
|
assert not isinstance(other, Pattern)
|
||||||
raise PatternError('Must provide an `Abstract` (not a `Pattern`) when creating a reference. '
|
|
||||||
'Use `append=True` if you intended to append the full geometry.')
|
|
||||||
ref = Ref(mirrored=mirrored)
|
ref = Ref(mirrored=mirrored)
|
||||||
ref.rotate_around(pivot, rotation)
|
ref.rotate_around(pivot, rotation)
|
||||||
ref.translate(offset)
|
ref.translate(offset)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import pytest
|
import pytest
|
||||||
from typing import cast, TYPE_CHECKING
|
from typing import cast, TYPE_CHECKING
|
||||||
from numpy.testing import assert_allclose
|
|
||||||
from ..library import Library, LazyLibrary
|
from ..library import Library, LazyLibrary
|
||||||
from ..pattern import Pattern
|
from ..pattern import Pattern
|
||||||
from ..error import LibraryError, PatternError
|
from ..error import LibraryError, PatternError
|
||||||
from ..ports import Port
|
from ..ports import Port
|
||||||
from ..repetition import Grid
|
from ..repetition import Grid
|
||||||
from ..file.utils import preflight
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ..shapes import Polygon
|
from ..shapes import Polygon
|
||||||
|
|
@ -43,79 +41,6 @@ def test_library_dangling() -> None:
|
||||||
assert lib.dangling_refs() == {"missing"}
|
assert lib.dangling_refs() == {"missing"}
|
||||||
|
|
||||||
|
|
||||||
def test_library_dangling_graph_modes() -> None:
|
|
||||||
lib = Library()
|
|
||||||
lib["parent"] = Pattern()
|
|
||||||
lib["parent"].ref("missing")
|
|
||||||
|
|
||||||
with pytest.raises(LibraryError, match="Dangling refs found"):
|
|
||||||
lib.child_graph()
|
|
||||||
with pytest.raises(LibraryError, match="Dangling refs found"):
|
|
||||||
lib.parent_graph()
|
|
||||||
with pytest.raises(LibraryError, match="Dangling refs found"):
|
|
||||||
lib.child_order()
|
|
||||||
|
|
||||||
assert lib.child_graph(dangling="ignore") == {"parent": set()}
|
|
||||||
assert lib.parent_graph(dangling="ignore") == {"parent": set()}
|
|
||||||
assert lib.child_order(dangling="ignore") == ["parent"]
|
|
||||||
|
|
||||||
assert lib.child_graph(dangling="include") == {"parent": {"missing"}, "missing": set()}
|
|
||||||
assert lib.parent_graph(dangling="include") == {"parent": set(), "missing": {"parent"}}
|
|
||||||
assert lib.child_order(dangling="include") == ["missing", "parent"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_find_refs_with_dangling_modes() -> None:
|
|
||||||
lib = Library()
|
|
||||||
lib["target"] = Pattern()
|
|
||||||
|
|
||||||
mid = Pattern()
|
|
||||||
mid.ref("target", offset=(2, 0))
|
|
||||||
lib["mid"] = mid
|
|
||||||
|
|
||||||
top = Pattern()
|
|
||||||
top.ref("mid", offset=(5, 0))
|
|
||||||
top.ref("missing", offset=(9, 0))
|
|
||||||
lib["top"] = top
|
|
||||||
|
|
||||||
assert lib.find_refs_local("missing", dangling="ignore") == {}
|
|
||||||
assert lib.find_refs_global("missing", dangling="ignore") == {}
|
|
||||||
|
|
||||||
local_missing = lib.find_refs_local("missing", dangling="include")
|
|
||||||
assert set(local_missing) == {"top"}
|
|
||||||
assert_allclose(local_missing["top"][0], [[9, 0, 0, 0, 1]])
|
|
||||||
|
|
||||||
global_missing = lib.find_refs_global("missing", dangling="include")
|
|
||||||
assert_allclose(global_missing[("top", "missing")], [[9, 0, 0, 0, 1]])
|
|
||||||
|
|
||||||
with pytest.raises(LibraryError, match="missing"):
|
|
||||||
lib.find_refs_local("missing")
|
|
||||||
with pytest.raises(LibraryError, match="missing"):
|
|
||||||
lib.find_refs_global("missing")
|
|
||||||
|
|
||||||
global_target = lib.find_refs_global("target")
|
|
||||||
assert_allclose(global_target[("top", "mid", "target")], [[7, 0, 0, 0, 1]])
|
|
||||||
|
|
||||||
|
|
||||||
def test_preflight_prune_empty_preserves_dangling_policy(caplog: pytest.LogCaptureFixture) -> None:
|
|
||||||
def make_lib() -> Library:
|
|
||||||
lib = Library()
|
|
||||||
lib["empty"] = Pattern()
|
|
||||||
lib["top"] = Pattern()
|
|
||||||
lib["top"].ref("missing")
|
|
||||||
return lib
|
|
||||||
|
|
||||||
caplog.set_level("WARNING")
|
|
||||||
warned = preflight(make_lib(), allow_dangling_refs=None, prune_empty_patterns=True)
|
|
||||||
assert "empty" not in warned
|
|
||||||
assert any("Dangling refs found" in record.message for record in caplog.records)
|
|
||||||
|
|
||||||
allowed = preflight(make_lib(), allow_dangling_refs=True, prune_empty_patterns=True)
|
|
||||||
assert "empty" not in allowed
|
|
||||||
|
|
||||||
with pytest.raises(LibraryError, match="Dangling refs found"):
|
|
||||||
preflight(make_lib(), allow_dangling_refs=False, prune_empty_patterns=True)
|
|
||||||
|
|
||||||
|
|
||||||
def test_library_flatten() -> None:
|
def test_library_flatten() -> None:
|
||||||
lib = Library()
|
lib = Library()
|
||||||
child = Pattern()
|
child = Pattern()
|
||||||
|
|
|
||||||
|
|
@ -126,14 +126,6 @@ def test_pattern_flatten_repeated_ref_with_ports_raises() -> None:
|
||||||
parent.flatten({"child": child}, flatten_ports=True)
|
parent.flatten({"child": child}, flatten_ports=True)
|
||||||
|
|
||||||
|
|
||||||
def test_pattern_place_requires_abstract_for_reference() -> None:
|
|
||||||
parent = Pattern()
|
|
||||||
child = Pattern()
|
|
||||||
|
|
||||||
with pytest.raises(PatternError, match='Must provide an `Abstract`'):
|
|
||||||
parent.place(child)
|
|
||||||
|
|
||||||
|
|
||||||
def test_pattern_interface() -> None:
|
def test_pattern_interface() -> None:
|
||||||
source = Pattern()
|
source = Pattern()
|
||||||
source.ports["A"] = Port((10, 20), 0, ptype="test")
|
source.ports["A"] = Port((10, 20), 0, ptype="test")
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import numpy
|
||||||
from numpy.testing import assert_equal, assert_allclose
|
from numpy.testing import assert_equal, assert_allclose
|
||||||
from numpy import pi
|
from numpy import pi
|
||||||
|
|
||||||
from ..utils import remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points, rotation_matrix_2d, apply_transforms, DeferredDict
|
from ..utils import remove_duplicate_vertices, remove_colinear_vertices, poly_contains_points, rotation_matrix_2d, apply_transforms
|
||||||
|
|
||||||
|
|
||||||
def test_remove_duplicate_vertices() -> None:
|
def test_remove_duplicate_vertices() -> None:
|
||||||
|
|
@ -86,21 +86,3 @@ def test_apply_transforms_advanced() -> None:
|
||||||
# 2. rotate by outer rotation (pi/2): (10, 0) -> (0, 10)
|
# 2. rotate by outer rotation (pi/2): (10, 0) -> (0, 10)
|
||||||
# 3. add outer offset (0, 0) -> (0, 10)
|
# 3. add outer offset (0, 0) -> (0, 10)
|
||||||
assert_allclose(combined[0], [0, 10, pi / 2, 1, 1], atol=1e-10)
|
assert_allclose(combined[0], [0, 10, pi / 2, 1, 1], atol=1e-10)
|
||||||
|
|
||||||
|
|
||||||
def test_deferred_dict_accessors_resolve_values_once() -> None:
|
|
||||||
calls = 0
|
|
||||||
|
|
||||||
def make_value() -> int:
|
|
||||||
nonlocal calls
|
|
||||||
calls += 1
|
|
||||||
return 7
|
|
||||||
|
|
||||||
deferred = DeferredDict[str, int]()
|
|
||||||
deferred["x"] = make_value
|
|
||||||
|
|
||||||
assert deferred.get("missing", 9) == 9
|
|
||||||
assert deferred.get("x") == 7
|
|
||||||
assert list(deferred.values()) == [7]
|
|
||||||
assert list(deferred.items()) == [("x", 7)]
|
|
||||||
assert calls == 1
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import TypeVar, Generic
|
from typing import TypeVar, Generic
|
||||||
from collections.abc import Callable, Iterator
|
from collections.abc import Callable
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -41,19 +41,6 @@ class DeferredDict(dict, Generic[Key, Value]):
|
||||||
def __getitem__(self, key: Key) -> Value:
|
def __getitem__(self, key: Key) -> Value:
|
||||||
return dict.__getitem__(self, key)()
|
return dict.__getitem__(self, key)()
|
||||||
|
|
||||||
def get(self, key: Key, default: Value | None = None) -> Value | None:
|
|
||||||
if key not in self:
|
|
||||||
return default
|
|
||||||
return self[key]
|
|
||||||
|
|
||||||
def items(self) -> Iterator[tuple[Key, Value]]:
|
|
||||||
for key in self.keys():
|
|
||||||
yield key, self[key]
|
|
||||||
|
|
||||||
def values(self) -> Iterator[Value]:
|
|
||||||
for key in self.keys():
|
|
||||||
yield self[key]
|
|
||||||
|
|
||||||
def update(self, *args, **kwargs) -> None:
|
def update(self, *args, **kwargs) -> None:
|
||||||
"""
|
"""
|
||||||
Update the DeferredDict. If a value is callable, it is used as a generator.
|
Update the DeferredDict. If a value is callable, it is used as a generator.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue