From 60c155f8f92e3d806f377ea9658c99a1f1eff96b Mon Sep 17 00:00:00 2001 From: Ken Kroenlein Date: Mon, 28 Sep 2026 16:56:20 -0600 Subject: [PATCH] Normalize attribute template/bounds pairs to tuples Return the (template, bounds) pair from BaseTemplate._homogenize_ranges as a tuple rather than a list, and introduce type aliases so every annotation in the template modules states what the code actually accepts and returns. Each module now declares the same four aliases: the template or its link, the canonical pair as a tuple, the same pair spelled as a list, and the union of everything the setters take. Collapsing the nested unions into these aliases removes the six-line annotations that the formatter had exploded. Correct three annotations that the aliases exposed. The conditions and parameters getters claimed to return bare templates when they return pairs. The input annotations omitted the list form, which is not merely tolerated: JSON arrays deserialize to lists, so every load path supplies the pair that way. This changes observable behavior. Pairs read back from a template are now immutable, so code that assigned to one element must replace the pair instead: template.conditions[0][1] = None # before template.conditions[0] = (template.conditions[0][0], None) # after Objects built by earlier versions hold list pairs and do not compare equal to objects built by this one. Data persisted as JSON is unaffected, since the pair is rebuilt on load. The serialized form is byte-identical either way. Co-Authored-By: Claude Opus 5 (1M context) --- gemd/__version__.py | 2 +- gemd/entity/template/base_template.py | 20 +++++++++---------- .../template/has_condition_templates.py | 20 +++++++------------ .../template/has_parameter_templates.py | 20 +++++++------------ .../entity/template/has_property_templates.py | 20 +++++++------------ .../template/test_measurement_template.py | 8 ++++---- 6 files changed, 36 insertions(+), 54 deletions(-) diff --git a/gemd/__version__.py b/gemd/__version__.py index f1e49f6..55e4709 100644 --- a/gemd/__version__.py +++ b/gemd/__version__.py @@ -1 +1 @@ -__version__ = "2.2.5" +__version__ = "2.3.0" diff --git a/gemd/entity/template/base_template.py b/gemd/entity/template/base_template.py index c6e9fdb..0dbab71 100644 --- a/gemd/entity/template/base_template.py +++ b/gemd/entity/template/base_template.py @@ -1,6 +1,6 @@ """Base template.""" -from typing import Iterable, Mapping, Union +from typing import Iterable, List, Mapping, Optional, Tuple, Union from gemd.entity.base_entity import BaseEntity from gemd.entity.bounds.base_bounds import BaseBounds @@ -8,6 +8,10 @@ from gemd.entity.template.attribute_template import AttributeTemplate __all__ = ["BaseTemplate"] +_AttributeTemplateType = Union[AttributeTemplate, LinkByUID] +_AttributePairType = Tuple[_AttributeTemplateType, Optional[BaseBounds]] +_AttributeListPairType = List[Optional[Union[_AttributeTemplateType, BaseBounds]]] +_AttributeInputType = Union[_AttributeTemplateType, _AttributePairType, _AttributeListPairType] class BaseTemplate(BaseEntity): @@ -43,12 +47,8 @@ def __init__( self.description = description @staticmethod - def _homogenize_ranges( - template_or_tuple: Union[ - AttributeTemplate, LinkByUID, Iterable[Union[AttributeTemplate, BaseBounds]] - ], - ): - """Take either a template or pair and turn it into a (template, bounds) pair. + def _homogenize_ranges(template_or_tuple: _AttributeInputType) -> _AttributePairType: + """Take either a template or pair and turn it into a (template, bounds) tuple. If no bounds are provided, use the attribute template's default bounds. @@ -60,14 +60,14 @@ def _homogenize_ranges( Returns ------- - List[AttributeTemplate or LinkByUID, BaseBounds] + Tuple[AttributeTemplate or LinkByUID, BaseBounds or None] The attribute template and bounds that should be applied to the attribute when used in the context of **this** object. """ # if given a template only, use None to represent passthrough bounds if isinstance(template_or_tuple, (AttributeTemplate, LinkByUID)): - return [template_or_tuple, None] + return (template_or_tuple, None) # if given a (template, bounds) pair, # check that the bounds is consistent with that of the template elif isinstance(template_or_tuple, (tuple, list)): @@ -78,5 +78,5 @@ def _homogenize_ranges( if isinstance(first, AttributeTemplate) and isinstance(second, BaseBounds): if not first.bounds.contains(second): raise ValueError("Range and template are inconsistent") - return [first, second] + return (first, second) raise TypeError("Expected a template or (template, bounds) tuple") # pragma: no cover diff --git a/gemd/entity/template/has_condition_templates.py b/gemd/entity/template/has_condition_templates.py index da7ae7e..337a1c4 100644 --- a/gemd/entity/template/has_condition_templates.py +++ b/gemd/entity/template/has_condition_templates.py @@ -12,6 +12,10 @@ __all__ = ["HasConditionTemplates"] BaseEntityType = TypeVar("BaseEntityType", bound="BaseEntity") # noqa: F821 ConditionType = TypeVar("ConditionType", bound="Condition") # noqa: F821 +_ConditionTemplateType = Union[ConditionTemplate, LinkByUID] +_ConditionPairType = Tuple[_ConditionTemplateType, Optional[BaseBounds]] +_ConditionListPairType = List[Optional[Union[_ConditionTemplateType, BaseBounds]]] +_ConditionInputType = Union[_ConditionTemplateType, _ConditionPairType, _ConditionListPairType] class HasConditionTemplates(HasDependencies): @@ -27,18 +31,13 @@ class HasConditionTemplates(HasDependencies): def __init__( self, - conditions: Iterable[ - Union[ - Union[ConditionTemplate, LinkByUID], - Tuple[Union[ConditionTemplate, LinkByUID], Optional[BaseBounds]], - ] - ], + conditions: Iterable[_ConditionInputType], ): self._conditions = None self.conditions = conditions @property - def conditions(self) -> List[Union[ConditionTemplate, LinkByUID]]: + def conditions(self) -> List[_ConditionPairType]: """Get the list of condition template/bounds tuples. Returns @@ -52,12 +51,7 @@ def conditions(self) -> List[Union[ConditionTemplate, LinkByUID]]: @conditions.setter def conditions( self, - conditions: Iterable[ - Union[ - Union[ConditionTemplate, LinkByUID], - Tuple[Union[ConditionTemplate, LinkByUID], Optional[BaseBounds]], - ] - ], + conditions: Iterable[_ConditionInputType], ): """Set the list of condition templates. diff --git a/gemd/entity/template/has_parameter_templates.py b/gemd/entity/template/has_parameter_templates.py index ea7b0a7..1782468 100644 --- a/gemd/entity/template/has_parameter_templates.py +++ b/gemd/entity/template/has_parameter_templates.py @@ -12,6 +12,10 @@ __all__ = ["HasParameterTemplates"] ParameterType = TypeVar("ParameterType", bound="Parameter") # noqa: F821 BaseEntityType = TypeVar("BaseEntityType", bound="BaseEntity") # noqa: F821 +_ParameterTemplateType = Union[ParameterTemplate, LinkByUID] +_ParameterPairType = Tuple[_ParameterTemplateType, Optional[BaseBounds]] +_ParameterListPairType = List[Optional[Union[_ParameterTemplateType, BaseBounds]]] +_ParameterInputType = Union[_ParameterTemplateType, _ParameterPairType, _ParameterListPairType] class HasParameterTemplates(HasDependencies): @@ -27,18 +31,13 @@ class HasParameterTemplates(HasDependencies): def __init__( self, - parameters: Iterable[ - Union[ - Union[ParameterTemplate, LinkByUID], - Tuple[Union[ParameterTemplate, LinkByUID], Optional[BaseBounds]], - ] - ], + parameters: Iterable[_ParameterInputType], ): self._parameters = None self.parameters = parameters @property - def parameters(self) -> List[Union[ParameterTemplate, LinkByUID]]: + def parameters(self) -> List[_ParameterPairType]: """Get the list of parameter template/bounds tuples. Returns @@ -52,12 +51,7 @@ def parameters(self) -> List[Union[ParameterTemplate, LinkByUID]]: @parameters.setter def parameters( self, - parameters: Iterable[ - Union[ - Union[ParameterTemplate, LinkByUID], - Tuple[Union[ParameterTemplate, LinkByUID], Optional[BaseBounds]], - ] - ], + parameters: Iterable[_ParameterInputType], ): """Set the list of parameter templates. diff --git a/gemd/entity/template/has_property_templates.py b/gemd/entity/template/has_property_templates.py index 29d6acf..a7224b5 100644 --- a/gemd/entity/template/has_property_templates.py +++ b/gemd/entity/template/has_property_templates.py @@ -16,6 +16,10 @@ "PropertyAndConditionsType", bound="PropertyAndConditions", # noqa: F821 ) +_PropertyTemplateType = Union[PropertyTemplate, LinkByUID] +_PropertyPairType = Tuple[_PropertyTemplateType, Optional[BaseBounds]] +_PropertyListPairType = List[Optional[Union[_PropertyTemplateType, BaseBounds]]] +_PropertyInputType = Union[_PropertyTemplateType, _PropertyPairType, _PropertyListPairType] class HasPropertyTemplates(HasDependencies): @@ -31,18 +35,13 @@ class HasPropertyTemplates(HasDependencies): def __init__( self, - properties: Iterable[ - Union[ - Union[PropertyTemplate, LinkByUID], - Tuple[Union[PropertyTemplate, LinkByUID], Optional[BaseBounds]], - ] - ], + properties: Iterable[_PropertyInputType], ): self._properties = None self.properties = properties @property - def properties(self) -> List[Tuple[Union[PropertyTemplate, LinkByUID], Optional[BaseBounds]]]: + def properties(self) -> List[_PropertyPairType]: """Get the list of property template/bounds tuples. Returns @@ -56,12 +55,7 @@ def properties(self) -> List[Tuple[Union[PropertyTemplate, LinkByUID], Optional[ @properties.setter def properties( self, - properties: Iterable[ - Union[ - Union[PropertyTemplate, LinkByUID], - Tuple[Union[PropertyTemplate, LinkByUID], Optional[BaseBounds]], - ] - ], + properties: Iterable[_PropertyInputType], ): """Set the list of property templates. diff --git a/tests/entity/template/test_measurement_template.py b/tests/entity/template/test_measurement_template.py index cce55b2..4dad93e 100644 --- a/tests/entity/template/test_measurement_template.py +++ b/tests/entity/template/test_measurement_template.py @@ -58,7 +58,7 @@ def test_mixins(): for x in (obj.properties, obj.conditions, obj.parameters): assert isinstance(x, ValidList) for y in x: - assert isinstance(y, list) + assert isinstance(y, tuple) assert len(y) == 2 assert isinstance(y[0], AttributeTemplate) if y[1] is not None: @@ -125,9 +125,9 @@ def test_mixins(): "Unmatched property and bad value didn't validate." ) - second.conditions[0][1] = None - second.parameters[0][1] = None - second.properties[0][1] = None + second.conditions[0] = (second.conditions[0][0], None) + second.parameters[0] = (second.parameters[0][0], None) + second.properties[0] = (second.properties[0][0], None) assert second.validate_condition(Condition("Name", value=good_val)), ( "Condition and good value with passthrough didn't validate." )