From 4121cd0451bc4e560ff4c4a0a8c58d2eca498cd1 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Wed, 8 Jul 2026 17:35:45 +1000 Subject: [PATCH 01/54] Implement list fixes Signed-off-by: Boris Filin # Conflicts: # src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py # src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py # src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py # test/dataclass_descriptors/test_dataclass_base.py # test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py # Conflicts: # src/zepben/ewb/model/cim/iec61970/base/core/feeder.py # src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py # src/zepben/ewb/model/cim/iec61970/base/core/substation.py --- .../sqlite/network/network_database_reader.py | 2 +- .../dataclass_descriptors/dataclass_base.py | 130 ++++-- .../dataclass_descriptors/descriptor_fix.py | 92 +---- .../ewb/dataclass_descriptors/lazy_list.py | 193 +++++++++ .../ewb/dataclass_descriptors/mrid_list.py | 195 +++++++++ .../ewb/dataclass_descriptors/mrid_map.py | 145 +++++++ src/zepben/ewb/dataclass_descriptors/tst.py | 162 ++++++++ .../metering/pan_demand_reponse_function.py | 4 - .../extensions/iec61970/base/feeder/loop.py | 223 ++++------- .../iec61970/base/feeder/lv_feeder.py | 252 ++++-------- .../iec61970/base/feeder/lv_substation.py | 256 +++++------- .../protection/protection_relay_function.py | 201 ++++------ .../protection/protection_relay_scheme.py | 76 ++-- .../protection/protection_relay_system.py | 76 ++-- .../assetinfo/power_transformer_info.py | 78 ++-- .../assetinfo/transformer_tank_info.py | 78 ++-- .../ewb/model/cim/iec61968/assets/asset.py | 136 +++---- .../model/cim/iec61968/customers/customer.py | 75 ++-- .../iec61968/customers/customer_agreement.py | 80 ++-- .../iec61968/customers/pricing_structure.py | 74 ++-- .../iec61968/infiec61968/infassets/pole.py | 74 ++-- .../iec61968/metering/controlled_appliance.py | 2 +- .../model/cim/iec61968/metering/end_device.py | 151 +++---- .../cim/iec61968/metering/usage_point.py | 181 ++++----- .../operations/operational_restriction.py | 74 ++-- .../base/auxiliaryequipment/sensor.py | 76 ++-- .../ewb/model/cim/iec61970/base/core/curve.py | 165 ++++---- .../model/cim/iec61970/base/core/equipment.py | 288 +++++--------- .../iec61970/base/core/equipment_container.py | 149 +++---- .../model/cim/iec61970/base/core/feeder.py | 374 ++++++------------ .../iec61970/base/core/geographical_region.py | 63 ++- .../ewb/model/cim/iec61970/base/core/name.py | 2 - .../model/cim/iec61970/base/core/name_type.py | 3 +- .../base/core/power_system_resource.py | 74 ++-- .../base/core/sub_geographical_region.py | 62 +-- .../cim/iec61970/base/core/substation.py | 273 +++++-------- .../iec61970/base/diagramlayout/diagram.py | 100 ++--- .../generation/production/battery_unit.py | 108 +++-- .../iec61970/base/wires/ac_line_segment.py | 248 +++++------- .../base/wires/ac_line_segment_phase.py | 5 - .../iec61970/base/wires/energy_consumer.py | 65 ++- .../base/wires/energy_consumer_phase.py | 5 - .../cim/iec61970/base/wires/energy_source.py | 60 ++- .../base/wires/energy_source_phase.py | 5 - .../base/wires/per_length_phase_impedance.py | 127 +++--- .../wires/power_electronics_connection.py | 192 ++++----- .../iec61970/base/wires/power_transformer.py | 170 ++++---- .../iec61970/base/wires/protected_switch.py | 80 ++-- .../iec61970/base/wires/regulating_cond_eq.py | 4 - .../iec61970/base/wires/regulating_control.py | 75 ++-- .../base/wires/synchronous_machine.py | 81 ++-- .../iec61970/base/wires/transformer_end.py | 5 - .../iec61970/infiec61970/feeder/circuit.py | 142 +++---- .../test_basic_descriptor.py | 8 +- .../test_dataclass_base.py | 22 +- .../test_network_trace_step_path_provider.py | 1 + 56 files changed, 2706 insertions(+), 3336 deletions(-) create mode 100644 src/zepben/ewb/dataclass_descriptors/lazy_list.py create mode 100644 src/zepben/ewb/dataclass_descriptors/mrid_list.py create mode 100644 src/zepben/ewb/dataclass_descriptors/mrid_map.py create mode 100644 src/zepben/ewb/dataclass_descriptors/tst.py diff --git a/src/zepben/ewb/database/sqlite/network/network_database_reader.py b/src/zepben/ewb/database/sqlite/network/network_database_reader.py index 3f9ba1765..ad1005b02 100644 --- a/src/zepben/ewb/database/sqlite/network/network_database_reader.py +++ b/src/zepben/ewb/database/sqlite/network/network_database_reader.py @@ -139,7 +139,7 @@ def _validate_equipment_containers(self): for it in missing_containers: count_by_class[type(it).__name__] += 1 - for (className, count) in count_by_class: + for (className, count) in count_by_class.items(): self._logger.warning(f"{count} {className}s were missing an equipment container.") if count_by_class: diff --git a/src/zepben/ewb/dataclass_descriptors/dataclass_base.py b/src/zepben/ewb/dataclass_descriptors/dataclass_base.py index 58067ae4a..ceed79f33 100644 --- a/src/zepben/ewb/dataclass_descriptors/dataclass_base.py +++ b/src/zepben/ewb/dataclass_descriptors/dataclass_base.py @@ -2,7 +2,8 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from dataclasses import dataclass, fields, MISSING +from dataclasses import dataclass, fields, MISSING, Field +from typing import TypeVar def _is_set(obj: object, name: str) -> bool: @@ -24,44 +25,121 @@ class DataclassBase: For more motivation, refer to `MANIFESTO.md` """ def __init__(self, **kwargs) -> None: - # Assign all of the kwargs manually. - # str-based setattr triggers descriptors, allowing us to intercept __set__. - for attr, value in kwargs.items(): - setattr(self, attr, value) # Manually assign defaults in dataclass fields for f in fields(type(self)): # We cannot just check kwargs because fields could be set in subclass __init__'s - if _is_set(self, f.name): + if _is_set(self, f.name) or f.name in kwargs: continue - # Set field defaults - if f.default is not MISSING: - setattr(self, f.name, f.default) - elif f.default_factory is not MISSING: - setattr(self, f.name, f.default_factory()) + resolve_default(self, f) - # Ignore custom descriptors - elif hasattr(f, '__get__'): - continue + # Assign all of the kwargs manually. + # str-based setattr triggers descriptors, allowing us to intercept __set__. + for attr, value in kwargs.items(): + setattr(self, attr, value) - # Mimic default Python missing arg error - else: - raise TypeError( - f"Missing required field {f.name!r} " - f"for {type(self).__name__}" - ) # Currently post init implementation requires a lot of extra logic, # which would mess with readability. If you require it - add it. if callable(getattr(self, "__post_init__", None)): raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") +def resolve_default(instance, field: Field): + # Set field defaults + if field.default is not MISSING: + setattr(instance, field.name, field.default) + elif field.default_factory is not MISSING: + setattr(instance, field.name, field.default_factory()) + + # Ignore custom descriptors + elif hasattr(field, '__get__'): + return + + # Mimic default Python missing arg error + else: + raise TypeError( + f"Missing required field {field.name!r} " + f"for {type(instance).__name__}" + ) + +T = TypeVar("T", bound=type) + +def remove_descriptor_annotations(cls: T) -> T: + """ + Remove annotations for class attributes that are data descriptors. + + This is intended for descriptor attributes that should remain class-level + descriptors (classes defining ``__get__``/``__set__`` methods, controlling attribute access), + rather than becoming dataclass fields/slots. Dataclasses decide + which fields to create from ``__annotations__``. By removing annotations for + descriptor-backed attributes before ``@dataclass`` runs, those attributes are + left alone and can continue to behave as descriptors. + + Use this decorator below ``@dataclass`` so that it is called first. Python + applies decorators from the bottom up:: + + @dataclass + class MyClass: + _x: int = field(default=0) -# Alias for dataclass with params. Makes it easier to edit params for all of CIM at once. -zb_dataclass = dataclass(init=False, eq=False, slots=True, repr=False) -""" -Shorthand alias for ``@dataclass(init=False,eq=False,slots=True,repr=False)`` -Allows us to modify dataclass parameters for all of CIM from a single reference point -""" + x: int = MyDescriptor("_x") + In the example above, ``x`` is annotated, but its class value is a descriptor. + Without ``remove_descriptor_annotations``, ``@dataclass`` would treat ``x`` as + a dataclass field and may try to include it in generated fields, slots, init, + repr, etc. With this decorator, the annotation for ``x`` is removed before + dataclass processing, while normal fields such as ``_x`` are left intact. + + Dataclass ``Field`` instances are themselves descriptors and are thus skipped. + In the example above, the slot for ``_x`` is still created. + + Before decoration:: + + MyClass.__annotations__ == { + "_x": int, + "x": int, + } + + After ``remove_descriptor_annotations`` runs:: + + MyClass.__annotations__ == { + "_x": int, + } + + The class is then passed to ``@dataclass`` with only real dataclass fields + remaining in ``__annotations__``. + """ + # Get editable annotations + original_annotations = dict(getattr(cls, "__annotations__", {})) + tweaked_annotations = dict(original_annotations) + + # Get the current values of class fields. Before @dataclass is run, we see Descriptor instances here + cls_dict = vars(cls) + + for name in list(tweaked_annotations): + try: + value = cls_dict[name] + # Skip dataclass fields - they need annotations + if isinstance(value, Field): + continue + # Any descriptor needs to implement get or set - most likely both. + if hasattr(value, "__get__") or hasattr(value, "__set__"): + tweaked_annotations.pop(name) + # Values without defaults will error out - definitely not descriptors + except KeyError: + pass + + # Update annotations on the class + cls.__annotations__ = tweaked_annotations + + return cls + + +def zb_dataclass(cls: type[object]): + """ + Shorthand alias for ``@dataclass(init=False,eq=False,slots=True,repr=False)`` + Allows us to modify dataclass parameters for all of CIM from a single reference point + """ + cls = remove_descriptor_annotations(cls) + return dataclass(init=False, eq=False, slots=True, repr=False)(cls) diff --git a/src/zepben/ewb/dataclass_descriptors/descriptor_fix.py b/src/zepben/ewb/dataclass_descriptors/descriptor_fix.py index 1ecf6d965..6b4afbb91 100644 --- a/src/zepben/ewb/dataclass_descriptors/descriptor_fix.py +++ b/src/zepben/ewb/dataclass_descriptors/descriptor_fix.py @@ -12,7 +12,6 @@ from dataclasses import Field from typing import TypeVar -T = TypeVar("T", bound=type) @@ -24,89 +23,26 @@ class BackedDescriptor: """ def __init__(self, private_field): - if not isinstance(private_field, Field): - raise TypeError("private_field parameter of the Descriptor constructor has to be an instance of dataclass Field.") - self.private_field: Field = private_field + if not isinstance(private_field, Field) and not isinstance(private_field, BackedDescriptor): + raise TypeError(f"private_field parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {private_field}") + self.private_field: Field | 'BackedDescriptor' = private_field + self.backing_name: str | None = None self.name = None def __set_name__(self, owner, name): - self.name = name + if name is None: + return + self.__name__ = self.name = name + if not self.backing_name: + self.backing_name = self.private_field.name def __get__(self, instance, _): - return getattr(instance, self.private_field.name) + return getattr(instance, self.backing_name) def __set__(self, instance, value): - return setattr(instance, self.private_field.name, value) + return setattr(instance, self.backing_name, value) +Alias = BackedDescriptor - -def remove_descriptor_annotations(cls: T) -> T: - """ - Remove annotations for class attributes that are data descriptors. - - This is intended for descriptor attributes that should remain class-level - descriptors (classes defining ``__get__``/``__set__`` methods, controlling attribute access), - rather than becoming dataclass fields/slots. Dataclasses decide - which fields to create from ``__annotations__``. By removing annotations for - descriptor-backed attributes before ``@dataclass`` runs, those attributes are - left alone and can continue to behave as descriptors. - - Use this decorator below ``@dataclass`` so that it is called first. Python - applies decorators from the bottom up:: - - @dataclass - @remove_descriptor_annotations - class MyClass: - _x: int = field(default=0) - - x: int = MyDescriptor("_x") - - In the example above, ``x`` is annotated, but its class value is a descriptor. - Without ``remove_descriptor_annotations``, ``@dataclass`` would treat ``x`` as - a dataclass field and may try to include it in generated fields, slots, init, - repr, etc. With this decorator, the annotation for ``x`` is removed before - dataclass processing, while normal fields such as ``_x`` are left intact. - - Dataclass ``Field`` instances are themselves descriptors and are thus skipped. - In the example above, the slot for ``_x`` is still created. - - Before decoration:: - - MyClass.__annotations__ == { - "_x": int, - "x": int, - } - - After ``remove_descriptor_annotations`` runs:: - - MyClass.__annotations__ == { - "_x": int, - } - - The class is then passed to ``@dataclass`` with only real dataclass fields - remaining in ``__annotations__``. - """ - # Get editable annotations - original_annotations = dict(getattr(cls, "__annotations__", {})) - tweaked_annotations = dict(original_annotations) - - # Get the current values of class fields. Before @dataclass is run, we see Descriptor instances here - cls_dict = vars(cls) - - for name in list(tweaked_annotations): - try: - value = cls_dict[name] - # Skip dataclass fields - they need annotations - if isinstance(value, Field): - continue - # Any descriptor needs to implement get or set - most likely both. - if hasattr(value, "__get__") or hasattr(value, "__set__"): - tweaked_annotations.pop(name) - # Values without defaults will error out - definitely not descriptors - except KeyError: - pass - - # Update annotations on the class - cls.__annotations__ = tweaked_annotations - - return cls +# Alias for dataclass with params. Makes it easier to edit params for all of CIM at once. +# zb_dataclass = dataclass(init=False, eq=False, slots=True, repr=False) diff --git a/src/zepben/ewb/dataclass_descriptors/lazy_list.py b/src/zepben/ewb/dataclass_descriptors/lazy_list.py new file mode 100644 index 000000000..c4980e993 --- /dev/null +++ b/src/zepben/ewb/dataclass_descriptors/lazy_list.py @@ -0,0 +1,193 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC +from collections.abc import Collection +from dataclasses import field +from typing import TypeVar, Protocol, Any, Iterable + +from typing_extensions import deprecated, Self + +from zepben.ewb import BackedDescriptor, remove_descriptor_annotations, resolve_default, DataclassBase, zb_dataclass + + +# from descriptor_fix import BackedDescriptor, remove_descriptor_annotations + + +class HasMrid(Protocol): + mrid: str + +T = TypeVar("T") +S = TypeVar("S", bound=HasMrid) + +class MutableCollection(Collection[T], Protocol[T]): + def append(self, item: T) -> None: ... + + def remove(self, item: T) -> None: ... + + def clear(self) -> None: ... + + def extend(self, items: Collection[T]) -> None: + for item in items: + self.append(item) + + def __next__(self): + # TODO: Remove test hack in separate PR + return next(iter(self)) + + def __eq__(self, other): + return isinstance(other, Iterable) and all(a == b for a, b in zip(self, other)) + + +class IndexedMutableCollection(MutableCollection, ABC): + def __getitem__(self, idx: int) -> T: ... + + + +class _Wrapper(BackedDescriptor): + + instance: Any + backing_name: Any + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + """ + This __new__ stores the init arguments, and otherwise acts as default. + We need this to efficiently and truly re-create the wrapper and bind it to an instance upon a get call. + """ + obj = super().__new__(cls) + obj._init_args = args + obj._init_kwargs = kwargs.copy() + return obj + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.instance = None + self.backing_name = None + + def __get__(self, instance, _=None) -> Self: + """ + This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. + From there on, the wrapper functions as a lazy list. + """ + if instance is None: + return self + obj = type(self)(*self._init_args, **self._init_kwargs) + obj.instance = instance + obj.backing_name = self.private_field.name + return obj + +class _WrapperFgetFix(_Wrapper): + """ + This class exists to fix the tests that rely on the old lists being @property. + TODO: Remove in a separate PR fixing tests + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + def fget(instance): + return self.__get__(instance) + self.fget = fget + + def __set_name__(self, owner, name): + super().__set_name__(owner, name) + self.fget.__name__ = name + self.fget.__qualname__ = f"{owner.__qualname__}.{name}" + + @deprecated("Lists are no longer a property") + def fget(self, instance): ... + +class _IterableWrapper(_WrapperFgetFix, IndexedMutableCollection, ABC): + """ + This class allows us to assign lists at init time to avoid special-case handling. + """ + def __set__(self, instance, value): + if instance is None: + return + if not hasattr(instance, self.backing_name): + resolve_default(instance, self.private_field) + elif getattr(instance, self.backing_name): + raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") + if value is not None: + self.__get__(instance).extend(value) + else: + self.__get__(instance).clear() + +class LazyValidatedList(_IterableWrapper, IndexedMutableCollection[T]): + def __init__(self, + private_field, + validate=None, + sort_by=None): + super().__init__(private_field) + self.validate = validate + self.sort_by = sort_by + + def _get(self): + return getattr(self.instance, self.backing_name) + + def _get_or_empty(self) -> list[T]: + return getattr(self.instance, self.backing_name) or [] + + # clearIfEmpty is inlined for performance + + def append(self, item): + if self.validate is not None: + self.validate(self.instance, item) + + existing = getattr(self.instance, self.backing_name) + if existing is None: + existing = [item] + setattr(self.instance, self.backing_name, existing) + else: + existing.append(item) + + if self.sort_by is not None: + existing.sort(key=self.sort_by) + + def __len__(self): + return len(self._get_or_empty()) + + def __iter__(self): + return iter(self._get_or_empty()) + + def __contains__(self, item): + return item in (self._get_or_empty()) + + def remove(self, item): + existing = self._get_or_empty() + existing.remove(item) + if not existing: + self.clear() + + def clear(self): + setattr(self.instance, self.backing_name, None) + + def __repr__(self): + if self.instance is None: + return self.__class__.__repr__() + return str(self._get_or_empty()) + + def __str__(self): + return self.__repr__() + + def __getitem__(self, item): + return (self._get_or_empty())[item] + + +if __name__ == '__main__': + @zb_dataclass + class C(DataclassBase): + _xs: list[int] | None = field(default=None) + xs: IndexedMutableCollection[int] = LazyValidatedList( + _xs, + validate=lambda self, it: print("validate", it), + sort_by=lambda it: -it + ) + + c = C(xs=[24, 42]) + print(c._xs) + c.xs.clear() + print(c._xs) + c.xs.append(1) + c.xs.append(2) + c.xs.append(3) + print(c._xs) diff --git a/src/zepben/ewb/dataclass_descriptors/mrid_list.py b/src/zepben/ewb/dataclass_descriptors/mrid_list.py new file mode 100644 index 000000000..2774ce025 --- /dev/null +++ b/src/zepben/ewb/dataclass_descriptors/mrid_list.py @@ -0,0 +1,195 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import abstractmethod, ABC +from dataclasses import dataclass, field, Field +from typing import Any, TypeVar, Protocol + +import pytest +from typing_extensions import Self + +from zepben.ewb import remove_descriptor_annotations, BackedDescriptor +from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList, MutableCollection, IndexedMutableCollection, _IterableWrapper + + +class HasMrid(Protocol): + mrid: str + +T = TypeVar("T") +S = TypeVar("S", bound=HasMrid) + +class MridCollection(MutableCollection[S], ABC): + instance: Any + element_description: str + + @abstractmethod + def _safe_get_by_mrid(self, mrid: str) -> S | None: ... + + def get_by_mrid(self, mrid: str) -> S: + res = self._safe_get_by_mrid(mrid) + if res is None: + raise KeyError + return res + + def _can_add_by_mrid(self, element: S) -> bool: + existing = self._safe_get_by_mrid(element.mrid) + + if existing is None: + return True + + if existing is not element: + raise ValueError( + f"{self.element_description} with mRID {element.mrid} " + f"already exists in {self.instance}." + ) + + return False + + +class LazyMridList(LazyValidatedList, MridCollection[S]): + def __init__(self, + private_field, + element_description: str, + validate=None, + sort_by=None): + super().__init__(private_field, validate, sort_by) + self.element_description = element_description + + def _safe_get_by_mrid(self, mrid: str) -> S | None: + existing = self._get() + if existing is None: + return None + found = next((element for element in existing if element.mrid == mrid), None) + return found + + def append(self, item: T): + if not self._can_add_by_mrid(item): + return + + super().append(item) + + +class MridList(_IterableWrapper, IndexedMutableCollection[S], MridCollection[S]): + def __init__(self, + private_field, + element_description: str, + validate=None, + sort_by=None): + super().__init__(private_field) + self.element_description = element_description + self.validate = validate + self.sort_by = sort_by + self.backing_list = None + + def __get__(self, instance, owner=None) -> Self: + obj = super().__get__(instance, owner) + if obj is not self: + # noinspection PyUnresolvedReferences + obj.__post_init__() + return obj + + def __post_init__(self): + self.backing_list = getattr(self.instance, self.backing_name) + + def _safe_get_by_mrid(self, mrid: str) -> S | None: + found = next((element for element in self.backing_list if element.mrid == mrid), None) + return found + + def append(self, item: S): + if not self._can_add_by_mrid(item): + return + + if self.validate is not None: + self.validate(self.instance, item) + + self.backing_list.append(item) + + if self.sort_by is not None: + self.backing_list.sort(key=self.sort_by) + + def __len__(self): + return len(self.backing_list) + + def __iter__(self): + return iter(self.backing_list) + + def __contains__(self, item): + return item in self.backing_list + + def remove(self, item): + self.backing_list.remove(item) + + def clear(self): + self.backing_list.clear() + + def __repr__(self): + return str(self.backing_list) + + def __str__(self): + return self.__repr__() + + def __getitem__(self, item): + return self.backing_list[item] + + +# +# class Backfill(Protocol[S]): +# def __init__(self, backref_field: Any): +# if not isinstance(backref_field, Field) and not isinstance(backref_field, BackedDescriptor): +# raise TypeError(f"backref_field parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {backref_field}") +# self.backref_field = backref_field +# +# def apply(self, element: S, owner: Any): +# setattr(element, self.backref_field.__name__, ) + + + + + +if __name__ == '__main__': + @dataclass(slots=True) + class Ido: + mrid: str + n: int = 42 + + @dataclass(slots=True) + class A: + # _x: list[Ido] = field(default_factory=list) + _x: list[Ido] = field(default=None) + # x: MridCollection[Ido] = MridList( + x: MridCollection[Ido] = LazyMridList( + _x, + "A Thingo", + validate=(lambda self, it: self.validate_x(it)), + sort_by=(lambda it: it.n), + ) + + def validate_x(self, item): + print(f"validating {item}") + + def __hash__(self): + return object.__hash__(self) + + o1 = Ido("1") + o2 = Ido("2", 24) + oX = Ido("2", 32) + + a = A() + print(a, a.x) + a.x.append(o1) + print(a, a.x) + a.x.append(o2) + print(a, a.x) + for t in a.x: + print(t) + a.x.remove(o1) + print(a, a.x) + a.x.remove(o2) + print(a, a.x) + + a.x.append(o2) + print(a, a.x) + with pytest.raises(ValueError): + a.x.append(oX) + print(a, a.x) diff --git a/src/zepben/ewb/dataclass_descriptors/mrid_map.py b/src/zepben/ewb/dataclass_descriptors/mrid_map.py new file mode 100644 index 000000000..c74c40fa6 --- /dev/null +++ b/src/zepben/ewb/dataclass_descriptors/mrid_map.py @@ -0,0 +1,145 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from dataclasses import dataclass, field +from typing import Protocol, TypeVar + +import pytest + +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.lazy_list import _IterableWrapper +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection + + +class HasMrid(Protocol): + mrid: str + +T = TypeVar("T") +S = TypeVar("S", bound=HasMrid) + + +class LazyMridMap(_IterableWrapper[S], MridCollection[S]): + def __init__( + self, + private_field, + element_description: str, + validate=None): + super().__init__(private_field) + self.validate = validate + self.element_description = element_description + + def _get(self): + return getattr(self.instance, self.backing_name) + + def _get_or_empty(self) -> dict[str, S]: + return getattr(self.instance, self.backing_name) or {} + + def _safe_get_by_mrid(self, mrid: str) -> S | None: + return self._get_or_empty().get(mrid, None) + + def get_by_mrid(self, mrid: str) -> S: + return self._get_or_empty()[mrid] + + def _can_add_by_mrid(self, element: S) -> bool: + existing = self._safe_get_by_mrid(element.mrid) + + if existing is None: + return True + + if existing is not element: + raise ValueError( + f"{self.element_description} with mRID {element.mrid} " + f"already exists in {self.instance}." + ) + + return False + + def append(self, element: S): + if not self._can_add_by_mrid(element): + return + + if self.validate is not None: + self.validate(self.instance, element) + + existing = getattr(self.instance, self.backing_name) + if existing is None: + existing = {element.mrid: element} + setattr(self.instance, self.backing_name, existing) + else: + existing[element.mrid] = element + + def __len__(self): + return len(self._get_or_empty()) + + def __iter__(self): + return iter(self._get_or_empty().values()) + + def __contains__(self, item: S): + return self._get_or_empty().get(item.mrid, None) == item + + def remove(self, item): + existing = self._get_or_empty() + + del existing[item.mrid] + if not existing: + self.clear() + + def clear(self): + setattr(self.instance, self.backing_name, None) + + def __repr__(self): + return str(self._get_or_empty()) + + def __str__(self): + return self.__repr__() + + def __getitem__(self, item): + return (self._get_or_empty())[item] + + +if __name__ == '__main__': + @dataclass(slots=True) + class Ido: + mrid: str + n: int = 42 + + @dataclass(slots=True) + class A: + _x: list[Ido] | None = field(default=None) + x: MridCollection[Ido] = LazyMridMap( + _x, + "A Thingo", + validate=(lambda self, it: self.validate_x(it)), + # cached=False + ) + + def validate_x(self, item): + print(f"{self} validating {item}") + + + def __hash__(self): + return object.__hash__(self) + + o1 = Ido("1") + o2 = Ido("2", 24) + oX = Ido("2", 32) + + a = A() + print(a, a.x) + a.x.append(o1) + print(a, a.x) + a.x.append(o2) + print(a, a.x) + for t in a.x: + print(t) + a.x.remove(o1) + print(a, a.x) + a.x.remove(o2) + print(a, a.x) + + a.x.append(o2) + print(a, a.x) + with pytest.raises(ValueError): + a.x.append(oX) + print(a, a.x) diff --git a/src/zepben/ewb/dataclass_descriptors/tst.py b/src/zepben/ewb/dataclass_descriptors/tst.py new file mode 100644 index 000000000..55ed4ce86 --- /dev/null +++ b/src/zepben/ewb/dataclass_descriptors/tst.py @@ -0,0 +1,162 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +import copy +import time +from abc import abstractmethod, ABC +from dataclasses import dataclass, field +from typing import Any +from typing_extensions import Self, deprecated +from zepben.ewb import remove_descriptor_annotations, BackedDescriptor, resolve_default + + +class _Wrapper(BackedDescriptor): + + instance: Any + backing_name: Any + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + """ + This __new__ stores the init arguments, and otherwise acts as default. + We need this to efficiently and truly re-create the wrapper and bind it to an instance upon a get call. + """ + obj = super().__new__(cls) + obj._init_args = args + obj._init_kwargs = kwargs.copy() + return obj + + def __get__(self, instance, _=None) -> Self: + """ + This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. + From there on, the wrapper functions as a lazy list. + """ + obj = type(self)(*self._init_args, **self._init_kwargs) + obj.instance = instance + obj.backing_name = self.private_field.name + return obj + +class _WrapperFgetFix(_Wrapper): + """ + This class exists to fix the tests that rely on the old lists being @property. + TODO: Remove in a separate PR fixing tests + """ + def __set_name__(self, owner, name): + super().__set_name__(owner, name) + + def fget(instance): + return self.__get__(instance) + + fget.__name__ = name + fget.__qualname__ = f"{owner.__qualname__}.{name}" + + self.fget = fget + + @deprecated("Lists are no longer a property") + def fget(self, instance): + # TODO: Remove test hack in separate PR + raise NotImplementedError() + +class _IterableWrapper(_WrapperFgetFix, ABC): + """ + This class allows us to assign lists at init time to avoid special-case handling. + """ + def __set__(self, instance, value): + if instance is None: + return + if not hasattr(instance, self.backing_name): + resolve_default(instance, self.private_field) + elif getattr(instance, self.backing_name): + raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") + if value is not None: + self.__get__(instance).extend(value) + else: + self.__get__(instance).clear() + + @abstractmethod + def extend(self, items): ... + + @abstractmethod + def clear(self): ... + + +class ListWrapper(_IterableWrapper): + + def clear(self): + self._set(None) + + def extend(self, items): + for item in items: + self.append(item) + + def __init__( + self, + private_field: Any, + validate = None, + sort_by = None + ): + super().__init__(private_field) + self.validate = validate + self.sort_by = sort_by + + def _get(self): + if self.instance is None: + return self + return getattr(self.instance, self.backing_name) + + def _set(self, value): + if self.instance is None: + return + setattr(self.instance, self.backing_name, value) + + def __str__(self): + return str(self._get()) + + def append(self, item): + l = self._get() + if l is None: + self._set([item]) + else: + l.append(item) + + + + +@dataclass +class C: + _x: list[int] | None = field(default=None) + x: ListWrapper = ListWrapper(_x, sort_by=lambda it: -it) + + +if __name__ == '__main__': + c = C([1, 2, 3]) + print(c) + print(c.x) + print(c.x.sort_by) + c.x.clear() + c.x.append(42) + print(c) + print(c.x) + + # class C: + # def __init__(self, x, f1, f2): + # self.x = x + # self.y = x + x + # self.f1 = f1 + # self.f2 = f2 + # + # N = 1_000_000 + # t = time.time() + # for _ in range(N): + # tmp = C("blahblahblah", lambda : 42, lambda : 24) + # print(time.time() - t) + # + # c = C("blahblahblah", lambda : 42, lambda : 24) + # t = time.time() + # for _ in range(N): + # tmp = copy.copy(c) + # print(time.time() - t) + ... \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py b/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py index de788d02f..934f21fed 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py @@ -27,10 +27,6 @@ class PanDemandResponseFunction(EndDeviceFunction): _appliance_bitmask: Optional[int] = None - def __init__(self, *args, appliance: Union[int, ControlledAppliance] = None, **kwargs): - super(PanDemandResponseFunction, self).__init__(*args, **kwargs) - if appliance is not None: - self.appliance = appliance @property def appliance(self) -> Optional[ControlledAppliance]: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py index f45014aa1..79f3e77fc 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py @@ -8,11 +8,15 @@ __all__ = ["Loop"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import safe_remove, ngen, nlen, get_by_mrid from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.substation import Substation @@ -28,182 +32,121 @@ class Loop(IdentifiedObject): to many customers for more than a short time. """ - _circuits: Optional[List[Circuit]] = None - _substations: Optional[List[Substation]] = None - _energizing_substations: Optional[List[Substation]] = None - - def __init__(self, *args, circuits: List[Circuit] = None, substations: List[Substation] = None, energizing_substations: List[Substation] = None, **kwargs): - super(Loop, self).__init__(*args, **kwargs) - if circuits: - for term in circuits: - self.add_circuit(term) - - if substations: - for sub in substations: - self.add_substation(sub) - - if energizing_substations: - for sub in energizing_substations: - self.add_energizing_substation(sub) - - @property - def circuits(self) -> Generator[Circuit, None, None]: - """ - [ZBEX] Sub-transmission `Circuit`s that form part of this loop. - """ - return ngen(self._circuits) - - @property - def substations(self) -> Generator[Substation, None, None]: - """ - [ZBEX] The `Substation`s that are powered by this `Loop`. - """ - return ngen(self._substations) - - @property - def energizing_substations(self) -> Generator[Substation, None, None]: - """ - [ZBEX] The `Substation`s that normally energize this `Loop`. - """ - return ngen(self._energizing_substations) + _circuits: Optional[List[Circuit]] = field(default=None) + _substations: Optional[List[Substation]] = field(default=None) + _energizing_substations: Optional[List[Substation]] = field(default=None) + + circuits: MridCollection[Circuit] = LazyMridList( + _circuits, + "An Circuit", + ) + + substations: MridCollection[Substation] = LazyMridList( + _substations, + "An Substation", + ) + + energizing_substations: MridCollection[Substation] = LazyMridList( + _energizing_substations, + "An Substation", + ) + + + + + + + + + + + + + + + + + # region deprecated list boilerplate + # region circuits boilerplate + + @deprecated("Use len(obj.circuits) instead.") def num_circuits(self): - """Return the number of end `Circuit`s associated with this `Loop`""" - return nlen(self._circuits) + return len(self.circuits) + @deprecated("Use obj.circuits.get_by_mrid(mrid) instead.") def get_circuit(self, mrid: str) -> Circuit: - """ - Get the `Circuit` for this `Loop` identified by `mrid` - - `mrid` the mRID of the required `Circuit` - Returns The `Circuit` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._circuits, mrid) + return self.circuits.get_by_mrid(mrid) + @deprecated("Use obj.circuits.append(circuit) instead.") def add_circuit(self, circuit: Circuit) -> Loop: - """ - Associate an `Circuit` with this `Loop` - - `circuit` the `Circuit` to associate with this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if another `Circuit` with the same `mrid` already exists for this `Loop`. - """ - if self._validate_reference(circuit, self.get_circuit, "An Circuit"): - return self - self._circuits = list() if self._circuits is None else self._circuits - self._circuits.append(circuit) + self.circuits.append(circuit) return self + @deprecated("Use obj.circuits.remove(circuit) instead.") def remove_circuit(self, circuit: Circuit) -> Loop: - """ - Disassociate `circuit` from this `Loop` - - `circuit` the `Circuit` to disassociate from this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if `circuit` was not associated with this `Loop`. - """ - self._circuits = safe_remove(self._circuits, circuit) + self.circuits.remove(circuit) return self + @deprecated("Use obj.circuits.clear() instead.") def clear_circuits(self) -> Loop: - """ - Clear all end circuits. - Returns A reference to this `Loop` to allow fluent use. - """ - self._circuits = None + self.circuits.clear() return self + # endregion circuits boilerplate + + # region substations boilerplate + + @deprecated("Use len(obj.substations) instead.") def num_substations(self): - """Return the number of end `Substation`s associated with this `Loop`""" - return nlen(self._substations) + return len(self.substations) + @deprecated("Use obj.substations.get_by_mrid(mrid) instead.") def get_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `Loop` identified by `mrid` - - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._substations, mrid) + return self.substations.get_by_mrid(mrid) + @deprecated("Use obj.substations.append(substation) instead.") def add_substation(self, substation: Substation) -> Loop: - """ - Associate an `Substation` with this `Loop` - - `substation` the `Substation` to associate with this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `Loop`. - """ - if self._validate_reference(substation, self.get_substation, "An Substation"): - return self - self._substations = list() if self._substations is None else self._substations - self._substations.append(substation) + self.substations.append(substation) return self + @deprecated("Use obj.substations.remove(substation) instead.") def remove_substation(self, substation: Substation) -> Loop: - """ - Disassociate `substation` from this `Loop` - - `substation` the `Substation` to disassociate from this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `Loop`. - """ - self._substations = safe_remove(self._substations, substation) + self.substations.remove(substation) return self + @deprecated("Use obj.substations.clear() instead.") def clear_substations(self) -> Loop: - """ - Clear all end substations. - Returns A reference to this `Loop` to allow fluent use. - """ - self._substations = None + self.substations.clear() return self + # endregion substations boilerplate + + # region energizing_substations boilerplate + + @deprecated("Use len(obj.energizing_substations) instead.") def num_energizing_substations(self): - """Return the number of end `Substation`s associated with this `Loop`""" - return nlen(self._energizing_substations) + return len(self.energizing_substations) + @deprecated("Use obj.energizing_substations.get_by_mrid(mrid) instead.") def get_energizing_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `Loop` identified by `mrid` - - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energizing_substations, mrid) + return self.energizing_substations.get_by_mrid(mrid) + @deprecated("Use obj.energizing_substations.append(substation) instead.") def add_energizing_substation(self, substation: Substation) -> Loop: - """ - Associate an `Substation` with this `Loop` - - `substation` the `Substation` to associate with this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `Loop`. - """ - if self._validate_reference(substation, self.get_energizing_substation, "An Substation"): - return self - self._energizing_substations = list() if self._energizing_substations is None else self._energizing_substations - self._energizing_substations.append(substation) + self.energizing_substations.append(substation) return self + @deprecated("Use obj.energizing_substations.remove(substation) instead.") def remove_energizing_substation(self, substation: Substation) -> Loop: - """ - Disassociate `substation` from this `Loop` - - `substation` the `Substation` to disassociate from this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `Loop`. - """ - self._energizing_substations = safe_remove(self._energizing_substations, substation) + self.energizing_substations.remove(substation) return self + @deprecated("Use obj.energizing_substations.clear() instead.") def clear_energizing_substations(self) -> Loop: - """ - Clear all end energizing_substations. - Returns A reference to this `Loop` to allow fluent use. - """ - self._energizing_substations = None + self.energizing_substations.clear() return self + + # endregion energizing_substations boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py index e41f23369..1ea96f249 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py @@ -8,8 +8,15 @@ __all__ = ["LvFeeder"] import typing +from dataclasses import field from typing import Generator, Optional, Dict, List +from pygments.lexer import default +from typing_extensions import deprecated + +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.util import safe_remove_by_id, nlen, ngen @@ -40,39 +47,35 @@ class LvFeeder(EquipmentContainer): _normal_head_terminal: Terminal | None = None """[ZBEX] The normal head terminal or terminals of this LvFeeder""" - _normal_energizing_feeders: Dict[str, Feeder] | None = None + _normal_energizing_feeders_by_id: Dict[str, Feeder] | None = field(default=None) """[ZBEX] The feeders that energize this LV feeder in the normal state of the network.""" - _current_equipment: Dict[str, Equipment] | None = None + _current_equipment_by_id: Dict[str, Equipment] | None = field(default=None) """[ZBEX] The equipment contained in this LvFeeder in the current state of the network.""" - _current_energizing_feeders: Dict[str, Feeder] | None = None + _current_energizing_feeders_by_id: Dict[str, Feeder] | None = field(default=None) """[ZBEX] The feeders that energize this LV feeder in the current state of the network.""" normal_energizing_lv_substation: 'LvSubstation | None' = None """[ZBEX] The normally energizing LvSubstation for this LvFeeder""" - def __init__( - self, - *args, - normal_head_terminal: Terminal = None, - normal_energizing_feeders: List[Feeder] = None, - current_equipment: List[Equipment] = None, - current_energizing_feeders: List[Feeder] = None, - **kwargs - ): - super(LvFeeder, self).__init__(*args, **kwargs) - if normal_head_terminal: - self.normal_head_terminal = normal_head_terminal - if normal_energizing_feeders: - for feeder in normal_energizing_feeders: - self.add_normal_energizing_feeder(feeder) - if current_equipment: - for eq in current_equipment: - self.add_current_equipment(eq) - if current_energizing_feeders: - for feeder in current_energizing_feeders: - self.add_current_energizing_feeder(feeder) + current_equipment: MridCollection[Equipment] = LazyMridMap( + _current_equipment_by_id, + "A current Equipment", + ) + """Contained `Equipment` using the current state of the network.""" + + normal_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _normal_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that normally energize this LV feeder.""" + + current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _current_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that currently energize this LV feeder.""" @property def normal_head_terminal(self) -> Optional[Terminal]: @@ -88,185 +91,84 @@ def normal_head_terminal(self, term: Optional[Terminal]): else: raise ValueError(f"normal_head_terminal for {str(self)} has already been set to {self._normal_head_terminal}, cannot reset this field to {term}") - @property - def normal_energizing_feeders(self) -> Generator[Feeder, None, None]: - """ - [ZBEX] The HV/MV feeders that normally energize this LV feeder. - """ - return ngen(self._normal_energizing_feeders) - - @property - def current_energizing_feeders(self) -> Generator[Feeder, None, None]: - """ - [ZBEX] The HV/MV feeders that currently energize this LV feeder. - """ - return ngen(self._current_energizing_feeders) - @property - def current_equipment(self) -> Generator[Equipment, None, None]: - """ - Contained `Equipment` using the current state of the network. - """ - return ngen(self._current_equipment) + # region deprecated list boilerplate + # region current_equipment boilerplate + @deprecated("Use len(current_equipment) instead") def num_current_equipment(self): - """ - Returns The number of `Equipment` associated with this `LvFeeder` in the current state of the network. - """ - return nlen(self._current_equipment) + return len(self.current_equipment) + @deprecated("Use current_equipment.get_by_mrid(mrid) instead") def get_current_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` contained in this `LvFeeder` in the current state of the network, identified by `mrid` + return self.current_equipment.get_by_mrid(mrid) - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - if not self._current_equipment: - raise KeyError(mrid) - try: - return self._current_equipment[mrid] - except AttributeError: - raise KeyError(mrid) - - def add_current_equipment(self, equipment: Equipment) -> LvFeeder: - """ - Associate `equipment` with this `LvFeeder` in the current state of the network. - - `equipment` the `Equipment` to associate with this `LvFeeder` in the current state of the network. - Returns A reference to this `LvFeeder` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `LvFeeder`. - """ - if self._validate_reference(equipment, self.get_current_equipment, "An Equipment"): - return self - self._current_equipment = dict() if self._current_equipment is None else self._current_equipment - self._current_equipment[equipment.mrid] = equipment + @deprecated("Use current_equipment.append(equipment) instead") + def add_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.append(equipment) return self - def remove_current_equipment(self, equipment: Equipment) -> LvFeeder: - """ - Disassociate `equipment` from this `LvFeeder` in the current state of the network. - - `equipment` The `Equipment` to disassociate from this `LvFeeder` in the current state of the network. - Returns A reference to this `LvFeeder` to allow fluent use. - Raises `KeyError` if `equipment` was not associated with this `LvFeeder`. - """ - self._current_equipment = safe_remove_by_id(self._current_equipment, equipment) + @deprecated("Use current_equipment.remove(equipment) instead") + def remove_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.remove(equipment) return self - def clear_current_equipment(self) -> LvFeeder: - """ - Clear all `Equipment` from this `LvFeeder` in the current state of the network. - Returns A reference to this `LvFeeder` to allow fluent use. - """ - self._current_equipment = None + @deprecated("Use current_equipment.clear() instead") + def clear_current_equipment(self) -> EquipmentContainer: + self.current_equipment.clear() return self - def num_normal_energizing_feeders(self) -> int: - """ - Get the number of HV/MV feeders that normally energize this LV feeder. - """ - return nlen(self._normal_energizing_feeders) + # endregion + # region normal_energizing_feeders boilerplate - def get_normal_energizing_feeder(self, mrid: str) -> Feeder: - """ - Energizing feeder using the normal state of the network. + @deprecated("Use len(normal_energizing_feeders) instead") + def num_normal_energizing_feeders(self): + return len(self.normal_energizing_feeders) - @param mrid: The mrid of the `Feeder`. - @return A matching `Feeder` that energizes this `LvFeeder` in the normal state of the network. - @raise A `KeyError` if no matching `Feeder` was found. - """ - if not self._normal_energizing_feeders: - raise KeyError(mrid) - try: - return self._normal_energizing_feeders[mrid] - except AttributeError: - raise KeyError(mrid) - - def add_normal_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Associate this `LvFeeder` with a `Feeder` in the normal state of the network. + @deprecated("Use normal_energizing_feeders.get_by_mrid(mrid) instead") + def get_normal_energizing_feeder(self, mrid: str) -> Feeder: + return self.normal_energizing_feeders.get_by_mrid(mrid) - @param feeder: the HV/MV feeder to associate with this LV feeder in the normal state of the network. - @return: This `LvFeeder` for fluent use. - """ - if self._validate_reference(feeder, self.get_normal_energizing_feeder, "A Feeder"): - return self - self._normal_energizing_feeders = dict() if self._normal_energizing_feeders is None else self._normal_energizing_feeders - self._normal_energizing_feeders[feeder.mrid] = feeder + @deprecated("Use normal_energizing_feeders.append(lv_feeder) instead") + def add_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.normal_energizing_feeders.append(lv_feeder) return self - def remove_normal_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Disassociate this `LvFeeder` from a `Feeder` in the normal state of the network. - - @param feeder: the HV/MV feeder to disassociate from this LV feeder in the normal state of the network. - @return: This `LvFeeder` for fluent use. - @raise: A `ValueError` if `feeder` is not found in the normal energizing feeders collection. - """ - self._normal_energizing_feeders = safe_remove_by_id(self._normal_energizing_feeders, feeder) + @deprecated("Use normal_energizing_feeders.remove(lv_feeder) instead") + def remove_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.normal_energizing_feeders.remove(lv_feeder) return self + @deprecated("Use normal_energizing_feeders.clear() instead") def clear_normal_energizing_feeders(self) -> LvFeeder: - """ - Clear all `Feeder`s associated with `LvFeeder` in the normal state of the network. - - @return: This `LvFeeder` for fluent use. - """ - self._normal_energizing_feeders = None + self.normal_energizing_feeders.clear() return self - def num_current_energizing_feeders(self) -> int: - """ - Get the number of HV/MV feeders that currently energize this LV feeder. - """ - return nlen(self._current_energizing_feeders) + # endregion + # region current_energizing_feeders boilerplate - def get_current_energizing_feeder(self, mrid: str) -> Feeder: - """ - Energizing feeder using the current state of the network. + @deprecated("Use len(current_energizing_feeders) instead") + def num_current_energizing_feeders(self): + return len(self.current_energizing_feeders) - @param mrid: The mrid of the `Feeder`. - @return A matching `Feeder` that energizes this `LvFeeder` in the current state of the network. - @raise A `KeyError` if no matching `Feeder` was found. - """ - if not self._current_energizing_feeders: - raise KeyError(mrid) - try: - return self._current_energizing_feeders[mrid] - except AttributeError: - raise KeyError(mrid) - - def add_current_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Associate this `LvFeeder` with a `Feeder` in the current state of the network. + @deprecated("Use current_energizing_feeders.get_by_mrid(mrid) instead") + def get_current_energizing_feeder(self, mrid: str) -> Feeder: + return self.current_energizing_feeders.get_by_mrid(mrid) - @param feeder: the HV/MV feeder to associate with this LV feeder in the current state of the network. - @return: This `LvFeeder` for fluent use. - """ - if self._validate_reference(feeder, self.get_current_energizing_feeder, "A Feeder"): - return self - self._current_energizing_feeders = dict() if self._current_energizing_feeders is None else self._current_energizing_feeders - self._current_energizing_feeders[feeder.mrid] = feeder + @deprecated("Use current_energizing_feeders.append(lv_feeder) instead") + def add_current_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.current_energizing_feeders.append(lv_feeder) return self - def remove_current_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Disassociate this `LvFeeder` from a `Feeder` in the current state of the network. - - @param feeder: the HV/MV feeder to disassociate from this LV feeder in the current state of the network. - @return: This `LvFeeder` for fluent use. - @raise: A `ValueError` if `feeder` is not found in the current energizing feeders collection. - """ - self._current_energizing_feeders = safe_remove_by_id(self._current_energizing_feeders, feeder) + @deprecated("Use current_energizing_feeders.remove(lv_feeder) instead") + def remove_current_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.current_energizing_feeders.remove(lv_feeder) return self + @deprecated("Use current_energizing_feeders.clear() instead") def clear_current_energizing_feeders(self) -> LvFeeder: - """ - Clear all `Feeder`s associated with `LvFeeder` in the current state of the network. - - @return: This `LvFeeder` for fluent use. - """ - self._current_energizing_feeders = None + self.current_energizing_feeders.clear() return self + + # endregion + # endregion diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index 8d203e09a..bf51f54d4 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -3,11 +3,18 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from __future__ import annotations + __all__ = ['LvSubstation'] +from dataclasses import field from typing import Generator, TYPE_CHECKING -from zepben.ewb import ngen, nlen, safe_remove_by_id, get_by_mrid +from typing_extensions import deprecated + +from zepben.ewb import ngen, nlen, safe_remove_by_id, get_by_mrid, remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder @@ -28,198 +35,115 @@ class LvSubstation(EquipmentContainer): :var current_energizing_feeders: [ZBEX] the feeders that currently energize the substation. also used for naming purposes. """ - _normal_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = None - _current_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = None - _normal_energized_lv_feeders_by_id: dict[str | None, LvFeeder] | None = None - - def __init__( - self, - *args, - normal_energizing_feeders: list['Feeder'] | None = None, - current_energizing_feeders: list['Feeder'] | None = None, - normal_energized_lv_feeders: list[LvFeeder] | None = None, - **kwargs - ): - super(LvSubstation, self).__init__(*args, **kwargs) - if normal_energizing_feeders: - for lv_feeder in normal_energizing_feeders: - self.add_normal_energizing_feeder(lv_feeder) - if current_energizing_feeders: - for lv_feeder in current_energizing_feeders: - self.add_current_energizing_feeder(lv_feeder) - if normal_energized_lv_feeders: - for lv_feeder in normal_energized_lv_feeders: - self.add_normal_energized_lv_feeder(lv_feeder) - - @zbex - @property - def normal_energizing_feeders(self) -> Generator["Feeder", None, None]: - """[ZBEX] The HV/MV feeders that normally energize this ``LvSubstation``.""" - return ngen(self._normal_energizing_feeders_by_id) - - @zbex - @property - def normal_energized_lv_feeders(self) -> Generator[LvFeeder, None, None]: - """[ZBEX] the ``LvFeeders`` that are normally energized by this ``LvSubstation``.""" - return ngen(self._normal_energized_lv_feeders_by_id) - - @zbex - @property - def current_energizing_feeders(self) -> Generator['Feeder', None, None]: - """ - [ZBEX] The HV/MV feeders that currently energize this LV substation. - """ - return ngen(self._current_energizing_feeders_by_id) - - def normal_energized_lv_switch_feeders(self) -> Generator[LvFeeder, None, None]: - """ - Retrieves all normally energized LvFeeders that represent low voltage network connected below a switch on the edge of this LvSubstation. This is all - LvFeeders in the normalEnergizedLvFeeders that has a normalHeadTerminal attached to a Switch. - """ - # NOTE: import exists here due to a circular import problem - from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch + _normal_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = field(default=None) + _current_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = field(default=None) + _normal_energized_lv_feeders_by_id: dict[str | None, LvFeeder] | None = field(default=None) - for lv_feeder in self.normal_energized_lv_feeders: - if (it := lv_feeder.normal_head_terminal) is not None and isinstance(it.conducting_equipment, Switch): - yield lv_feeder + normal_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _normal_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that normally energize this ``LvSubstation``.""" - def num_normal_energizing_feeders(self) -> int: - """Get the number of entries in the normal ``Feeder`` collection.""" - return nlen(self._normal_energizing_feeders_by_id) + current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _current_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that currently energize this LV substation.""" - def get_normal_energizing_feeder(self, mrid: str) -> 'Feeder | None': - """ - Energizing feeder using the normal state of the network. - :param mrid: the mRID of the required normal ``Feeder`` - :returns: The ``Feeder`` with the specified ``mrid`` if it exists, otherwise null - """ - return get_by_mrid(self._normal_energizing_feeders_by_id, mrid) + normal_energized_lv_feeders: MridCollection[LvFeeder] = LazyMridMap( + _normal_energized_lv_feeders_by_id, + "An LvFeeder", + ) + """[ZBEX] the ``LvFeeders`` that are normally energized by this ``LvSubstation``.""" - def add_normal_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Associate this ``LvSubstation`` with a ``Feeder`` in the normal state of the network. + def num_normal_energized_lv_feeders(self) -> int: + """Get the number of entries in the normal ``LvFeeder`` collection.""" + return nlen(self._normal_energized_lv_feeders_by_id) - :param feeder: the HV/MV feeder to associate with this ``LvSubstation`` in the normal state of the network. - :returns: This ``LvSubstation`` for fluent use. - """ - if self._validate_reference(feeder, self.get_normal_energizing_feeder, "A Feeder"): - return self + for lv_feeder in self.normal_energized_lv_feeders: + if (it := lv_feeder.normal_head_terminal) is not None and isinstance(it.conducting_equipment, Switch): + yield lv_feeder - if self._normal_energizing_feeders_by_id is None: - self._normal_energizing_feeders_by_id = dict() - self._normal_energizing_feeders_by_id[feeder.mrid] = feeder - return self - def remove_normal_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Disassociate this ``LvSubstation`` from a ``Feeder`` in the normal state of the network. + # region deprecated list boilerplate + # region normal_energizing_feeders boilerplate - :param feeder: the HV/MV feeder to disassociate from this ``LvSubstation`` in the normal state of the network. - :returns: true if a matching feeder is removed from the collection. - """ - self._normal_energizing_feeders_by_id = safe_remove_by_id(self._normal_energizing_feeders_by_id, feeder) - return self + @deprecated("Use len(normal_energizing_feeders) instead") + def num_normal_energizing_feeders(self): + return len(self.normal_energizing_feeders) - def clear_normal_energizing_feeders(self) -> "LvSubstation": - """ - Clear all ``Feeder``'s associated with this ``LvSubstation`` in the normal state of the network. + @deprecated("Use normal_energizing_feeders.get_by_mrid(mrid) instead") + def get_normal_energizing_feeder(self, mrid: str) -> Feeder: + return self.normal_energizing_feeders.get_by_mrid(mrid) - :returns: This ``LvSubstation`` for fluent use. - """ - self._normal_energizing_feeders_by_id = None + @deprecated("Use normal_energizing_feeders.append(lv_feeder) instead") + def add_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.normal_energizing_feeders.append(lv_feeder) return self - def num_current_energizing_feeders(self) -> int: - """ - Get the number of entries in the current ``Feeder`` collection. - """ - return nlen(self._current_energizing_feeders_by_id) - - def get_current_energizing_feeder(self, mrid: str) -> 'Feeder | None': - """ - Retrieve an energizing feeder using the current state of the network. - - :param mrid: the mRID of the required current ``Feeder`` - :returns: The ``Feeder`` with the specified ``mRID`` if it exists, otherwise null - """ - return get_by_mrid(self._current_energizing_feeders_by_id, mrid) - - def add_current_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Associate this ``LvSubstation`` with a ``Feeder`` in the current state of the network. - - :param feeder: the HV/MV feeder to associate with this ``LvSubstation`` in the current state of the network. - :returns: This ``LvSubstation`` for fluent use. - """ - if self._validate_reference(feeder, self.get_current_energizing_feeder, "A Feeder"): - return self - if self._current_energizing_feeders_by_id is None: - self._current_energizing_feeders_by_id = dict() - self._current_energizing_feeders_by_id[feeder.mrid] = feeder + @deprecated("Use normal_energizing_feeders.remove(lv_feeder) instead") + def remove_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.normal_energizing_feeders.remove(lv_feeder) return self - def remove_current_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Disassociate this ``LvSubstation`` from a ``Feeder`` in the current state of the network. - - :param feeder: the HV/MV feeder to disassociate from this LvSubstation the current state of the network. - :returns: true if a matching feeder is removed from the collection. - """ - self._current_energizing_feeders_by_id = safe_remove_by_id(self._current_energizing_feeders_by_id, feeder) + @deprecated("Use normal_energizing_feeders.clear() instead") + def clear_normal_energizing_feeders(self) -> LvSubstation: + self.normal_energizing_feeders.clear() return self - def clear_current_energizing_feeders(self) -> "LvSubstation": - """ - Clear all ``Feeder``'s associated with this ``LvSubstation`` in the current state of the network. + # endregion + # region current_energizing_feeders boilerplate - :returns: This ``LvSubstation`` for fluent use. - """ - self._current_energizing_feeders_by_id = None - return self + @deprecated("Use len(current_energizing_feeders) instead") + def num_current_energizing_feeders(self): + return len(self.current_energizing_feeders) - def num_normal_energized_lv_feeders(self) -> int: - """Get the number of entries in the normal ``LvFeeder`` collection.""" - return nlen(self._normal_energized_lv_feeders_by_id) + @deprecated("Use current_energizing_feeders.get_by_mrid(mrid) instead") + def get_current_energizing_feeder(self, mrid: str) -> Feeder: + return self.current_energizing_feeders.get_by_mrid(mrid) - def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder | None: - """ - Retrieve an energized ``LvFeeder`` using the normal state of the network. + @deprecated("Use current_energizing_feeders.append(lv_feeder) instead") + def add_current_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.current_energizing_feeders.append(lv_feeder) + return self - :param mrid: the mRID of the required normal ``LvFeeder`` - :returns: The ``LvFeeder`` with the specified ``mRID`` if it exists, otherwise null - """ - return get_by_mrid(self._normal_energized_lv_feeders_by_id, mrid) + @deprecated("Use current_energizing_feeders.remove(lv_feeder) instead") + def remove_current_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.current_energizing_feeders.remove(lv_feeder) + return self - def add_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> "LvSubstation": - """ - Associate this ``LvSubstation`` with an ``LvFeeder`` in the normal state of the network. + @deprecated("Use current_energizing_feeders.clear() instead") + def clear_current_energizing_feeders(self) -> LvSubstation: + self.current_energizing_feeders.clear() + return self - :param lv_feeder: the ``LvFeeder`` to associate with this feeder in the normal state of the network. - :returns: This ``LvSubstation`` for fluent use. - """ - if self._validate_reference(lv_feeder, self.get_normal_energized_lv_feeder, "An LvFeeder"): - return self + # endregion + # region normal_energized_lv_feeders boilerplate - if self._normal_energized_lv_feeders_by_id is None: - self._normal_energized_lv_feeders_by_id = dict() - self._normal_energized_lv_feeders_by_id[lv_feeder.mrid] = lv_feeder - return self + @deprecated("Use len(normal_energized_lv_feeders) instead") + def num_normal_energized_lv_feeders(self): + return len(self.normal_energized_lv_feeders) - def remove_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> "LvSubstation": - """ - Disassociate this ``LvSubstation`` from an ``LvFeeder`` in the normal state of the network. + @deprecated("Use normal_energized_lv_feeders.get_by_mrid(mrid) instead") + def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder: + return self.normal_energized_lv_feeders.get_by_mrid(mrid) - :param lv_feeder: the ``LvFeeder`` to disassociate from this HV/MV feeder in the normal state of the network. - """ - self._normal_energized_lv_feeders_by_id = safe_remove_by_id(self._normal_energized_lv_feeders_by_id, lv_feeder) + @deprecated("Use normal_energized_lv_feeders.append(lv_feeder) instead") + def add_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> LvSubstation: + self.normal_energized_lv_feeders.append(lv_feeder) return self - def clear_normal_energized_lv_feeders(self) -> "LvSubstation": - """ - Clear all ``LvFeeder``'s associated with this ``LvSubstation`` in the normal state of the network. + @deprecated("Use normal_energized_lv_feeders.remove(lv_feeder) instead") + def remove_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> LvSubstation: + self.normal_energized_lv_feeders.remove(lv_feeder) + return self - :returns: This ``LvSubstation`` for fluent use. - """ - self._normal_energized_lv_feeders_by_id = None + @deprecated("Use normal_energized_lv_feeders.clear() instead") + def clear_normal_energized_lv_feeders(self) -> LvSubstation: + self.normal_energized_lv_feeders.clear() return self + + # endregion + # endregion diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 0dc128bfb..66cea87a5 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -15,6 +15,7 @@ from warnings import deprecated else: from typing_extensions import deprecated +from dataclasses import field from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb.model.cim.extensions.iec61970.base.protection.power_direction_kind import PowerDirectionKind @@ -22,6 +23,8 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.util import require, nlen, ngen, safe_remove, get_by_mrid +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61968.assetinfo.relay_info import RelayInfo @@ -59,11 +62,11 @@ class ProtectionRelayFunction(PowerSystemResource, metaclass=ABCMeta): power_direction: PowerDirectionKind = PowerDirectionKind.UNKNOWN """[ZBEX] The flow of the power direction used by this ProtectionRelayFunction.""" - _sensors: Optional[List[Sensor]] = None + _sensors: Optional[List[Sensor]] = field(default=None) - _protected_switches: Optional[List[ProtectedSwitch]] = None + _protected_switches: Optional[List[ProtectedSwitch]] = field(default=None) - _schemes: Optional[List[ProtectionRelayScheme]] = None + _schemes: Optional[List[ProtectionRelayScheme]] = field(default=None) _time_limits: Optional[List[float]] = None @@ -312,155 +315,121 @@ def clear_time_limits(self) -> ProtectionRelayFunction: self._time_limits = None return self - def num_sensors(self) -> int: - """ - Get the number of :class:`Sensors` for this :class:`ProtectionRelayFunction`. + sensors: MridCollection[Sensor] = LazyMridList( + _sensors, + "A Sensor", + ) - :return: The number of :class:`Sensors` for this :class:`ProtectionRelayFunction`. - """ - return nlen(self._sensors) - def get_sensor(self, mrid: str) -> Sensor: - """ - Get a sensor :class:`Sensor` for this :class:`ProtectionRelayFunction` by its mrid. - :param mrid: The mrid of the desired :class:`Sensor`. - :returns: The :class:`Sensor` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._sensors, mrid) - def add_sensor(self, sensor: Sensor) -> ProtectionRelayFunction: - """ - Associate this :class:`ProtectionRelayFunction` with a :class:`Sensor`. - :param sensor: The :class:`Sensor` to associate with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if self._validate_reference(sensor, self.get_sensor, "A Sensor"): - return self - self._sensors = list() if self._sensors is None else self._sensors - self._sensors.append(sensor) - return self - def remove_sensor(self, sensor: Optional[Sensor]) -> ProtectionRelayFunction: - """ - Disassociate this :class:`ProtectionRelayFunction` from a :class:`Sensor`. + protected_switches: MridCollection[ProtectedSwitch] = LazyMridList( + _protected_switches, + "A ProtectedSwitch", + ) - :param sensor: The :class:`Sensor` to disassociate from this :class:`ProtectionRelayFunction`. - :raises ValueError: If sensor was not associated with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._sensors = safe_remove(self._sensors, sensor) - return self - def clear_sensors(self) -> ProtectionRelayFunction: + + + def remove_protected_switch(self, protected_switch: Optional[ProtectedSwitch]) -> ProtectionRelayFunction: """ - Disassociate all :class:`Sensors` from this :class:`ProtectionRelayFunction`. + Disassociate this :class:`ProtectionRelayFunction` from a :class:`ProtectedSwitch`. + :param protected_switch: The :class:`ProtectedSwitch` to disassociate from this :class:`ProtectionRelayFunction`. + :raises ValueError: If protected_switch was not associated with this :class:`ProtectionRelayFunction`. :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. """ - self._sensors = None + self._sensors = safe_remove(self._protected_switches, protected_switch) return self - def num_protected_switches(self) -> int: - """ - Get the number of :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. - :return: The number of :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. - """ - return nlen(self._protected_switches) + schemes: MridCollection[ProtectionRelayScheme] = LazyMridList( + _schemes, + "A ProtectionRelayScheme", + ) - def get_protected_switch(self, mrid: str) -> ProtectedSwitch: - """ - Get a :class:`ProtectedSwitch` operated by this :class:`ProtectionRelayFunction` by its mrid. - :param mrid: The mrid of the desired :class:`ProtectedSwitch`. - :returns: The :class:`ProtectedSwitch` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._protected_switches, mrid) - def add_protected_switch(self, protected_switch: ProtectedSwitch) -> ProtectionRelayFunction: - """ - Associate this :class:`ProtectionRelayFunction` with a :class:`ProtectedSwitch` it operates. - :param protected_switch: The :class:`ProtectedSwitch` to associate with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if self._validate_reference(protected_switch, self.get_protected_switch, "A ProtectedSwitch"): - return self - self._protected_switches = list() if self._protected_switches is None else self._protected_switches - self._protected_switches.append(protected_switch) + + + # region deprecated list boilerplate + # region sensors boilerplate + + @deprecated("Use len(obj.sensors) instead.") + def num_sensors(self) -> int: + return len(self.sensors) + + @deprecated("Use obj.sensors.get_by_mrid(mrid) instead.") + def get_sensor(self, mrid: str) -> Sensor: + return self.sensors.get_by_mrid(mrid) + + @deprecated("Use obj.sensors.append(sensor) instead.") + def add_sensor(self, sensor: Sensor) -> ProtectionRelayFunction: + self.sensors.append(sensor) return self - def remove_protected_switch(self, protected_switch: Optional[ProtectedSwitch]) -> ProtectionRelayFunction: - """ - Disassociate this :class:`ProtectionRelayFunction` from a :class:`ProtectedSwitch`. + @deprecated("Use obj.sensors.remove(sensor) instead.") + def remove_sensor(self, sensor: Optional[Sensor]) -> ProtectionRelayFunction: + self.sensors.remove(sensor) + return self - :param protected_switch: The :class:`ProtectedSwitch` to disassociate from this :class:`ProtectionRelayFunction`. - :raises ValueError: If protected_switch was not associated with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._sensors = safe_remove(self._protected_switches, protected_switch) + @deprecated("Use obj.sensors.clear() instead.") + def clear_sensors(self) -> ProtectionRelayFunction: + self.sensors.clear() return self - def clear_protected_switches(self) -> ProtectionRelayFunction: - """ - Disassociate all :class:`ProtectedSwitches` from this :class:`ProtectionRelayFunction`. + # endregion sensors boilerplate - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._protected_switches = None + # region protected_switches boilerplate + + @deprecated("Use len(obj.protected_switches) instead.") + def num_protected_switches(self) -> int: + return len(self.protected_switches) + + @deprecated("Use obj.protected_switches.get_by_mrid(mrid) instead.") + def get_protected_switch(self, mrid: str) -> ProtectedSwitch: + return self.protected_switches.get_by_mrid(mrid) + + @deprecated("Use obj.protected_switches.append(protected_switch) instead.") + def add_protected_switch(self, protected_switch: ProtectedSwitch) -> ProtectionRelayFunction: + self.protected_switches.append(protected_switch) return self - def num_schemes(self) -> int: - """ - Get the number of :class:`ProtectionRelaySchemes` this :class:`ProtectionRelayFunction` operates under. + @deprecated("Use obj.protected_switches.clear() instead.") + def clear_protected_switches(self) -> ProtectionRelayFunction: + self.protected_switches.clear() + return self - :return: The number of:class:`ProtectionRelaySchemes` operated by this :class:`ProtectionRelayFunction`. - """ - return nlen(self._schemes) + # endregion protected_switches boilerplate - def get_scheme(self, mrid: str) -> ProtectionRelayScheme: - """ - Get a :class:`ProtectionRelayScheme` this :class:`ProtectionRelayFunction` operates under by its mRID. + # region schemes boilerplate - :param mrid: The mRID of the desired :class:`ProtectionRelayScheme`. - :returns: The :class:`ProtectionRelayScheme` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._schemes, mrid) + @deprecated("Use len(obj.schemes) instead.") + def num_schemes(self) -> int: + return len(self.schemes) - def add_scheme(self, scheme: ProtectionRelayScheme) -> ProtectionRelayFunction: - """ - Associate this :class:`ProtectionRelayFunction` with a :class:`ProtectionRelayScheme` it operates under. + @deprecated("Use obj.schemes.get_by_mrid(mrid) instead.") + def get_scheme(self, mrid: str) -> ProtectionRelayScheme: + return self.schemes.get_by_mrid(mrid) - :param scheme: The :class:`ProtectionRelayScheme` to associate with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if self._validate_reference(scheme, self.get_scheme, "A ProtectionRelayScheme"): - return self - self._schemes = list() if self._schemes is None else self._schemes - self._schemes.append(scheme) + @deprecated("Use obj.schemes.append(scheme) instead.") + def add_scheme(self, scheme: ProtectionRelayScheme) -> ProtectionRelayFunction: + self.schemes.append(scheme) return self + @deprecated("Use obj.schemes.remove(scheme) instead.") def remove_scheme(self, scheme: Optional[ProtectionRelayScheme]) -> ProtectionRelayFunction: - """ - Disassociate this :class:`ProtectionRelayFunction` from a :class:`ProtectionRelayScheme`. - - :param scheme: The :class:`ProtectionRelayScheme` to disassociate from this :class:`ProtectionRelayFunction`. - :raises ValueError: If scheme was not associated with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._schemes = safe_remove(self._schemes, scheme) + self.schemes.remove(scheme) return self + @deprecated("Use obj.schemes.clear() instead.") def clear_schemes(self) -> ProtectionRelayFunction: - """ - Disassociate all :class:`ProtectionRelaySchemes` from this :class:`ProtectionRelayFunction`. - - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._schemes = None + self.schemes.clear() return self + + # endregion schemes boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py index 8895badfe..dbeb9cf07 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py @@ -8,11 +8,15 @@ __all__ = ["ProtectionRelayScheme"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import ngen, get_by_mrid, nlen, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_system import ProtectionRelaySystem @@ -30,70 +34,40 @@ class ProtectionRelayScheme(IdentifiedObject): system: Optional[ProtectionRelaySystem] = None """[ZBEX] The system this scheme belongs to.""" - _functions: Optional[List[ProtectionRelayFunction]] = None + _functions: Optional[List[ProtectionRelayFunction]] = field(default=None) - def __init__(self, *args, functions: Optional[List[ProtectionRelayFunction]] = None, **kwargs): - super(ProtectionRelayScheme, self).__init__(*args, **kwargs) - if functions is not None: - for function in functions: - self.add_function(function) + functions: MridCollection[ProtectionRelayFunction] = LazyMridList( + _functions, + "A ProtectionRelayFunction", + ) - @property - def functions(self) -> Generator[ProtectionRelayFunction, None, None]: - """ - [ZBEX] 6Yields all the functions operated as part of this :class:`ProtectionRelayScheme`. - :return: A generator that iterates over all functions operated as part of this :class:`ProtectionRelayScheme`. - """ - return ngen(self._functions) + # region deprecated list boilerplate + # region functions boilerplate + @deprecated("Use len(obj.functions) instead.") def num_functions(self) -> int: - """ - Get the number of :class:`ProtectionRelayFunctions` operated as part of this :class:`ProtectionRelayScheme`. - - :return: The number of :class:`ProtectionRelayFunctions` operated as part of this :class:`ProtectionRelayScheme`. - """ - return nlen(self._functions) + return len(self.functions) + @deprecated("Use obj.functions.get_by_mrid(mrid) instead.") def get_function(self, mrid: str) -> ProtectionRelayFunction: - """ - Get a :class:`ProtectionRelayFunction` operated as part of this :class:`ProtectionRelayScheme`. - - :param mrid: The mrid of the desired :class:`ProtectionRelayFunction`. - :returns: The :class:`ProtectionRelayFunction` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._functions, mrid) + return self.functions.get_by_mrid(mrid) + @deprecated("Use obj.functions.append(function) instead.") def add_function(self, function: ProtectionRelayFunction) -> ProtectionRelayScheme: - """ - Associate a :class:`ProtectionRelayFunction` with this :class:`ProtectionRelayScheme`. - - :param function: The :class:`ProtectionRelayFunction` to associate with this :class:`ProtectionRelayScheme`. - :return: A reference to this :class:`ProtectionRelayScheme` for fluent use. - """ - if self._validate_reference(function, self.get_function, "A ProtectionRelayFunction"): - return self - self._functions = list() if self._functions is None else self._functions - self._functions.append(function) + self.functions.append(function) return self + @deprecated("Use obj.functions.remove(function) instead.") def remove_function(self, function: Optional[ProtectionRelayFunction]) -> ProtectionRelayScheme: - """ - Disassociate this :class:`ProtectionRelayScheme` from a :class:`ProtectionRelayFunction`. - - :param function: The :class:`ProtectionRelayFunction` to disassociate from this :class:`ProtectionRelayScheme`. - :raises ValueError: If function was not associated with this :class:`ProtectionRelayScheme`. - :return: A reference to this :class:`ProtectionRelayScheme` for fluent use. - """ - self._functions = safe_remove(self._functions, function) + self.functions.remove(function) return self + @deprecated("Use obj.functions.clear() instead.") def clear_function(self) -> ProtectionRelayScheme: - """ - Disassociate all :class:`ProtectionRelayFunctions` from this :class:`ProtectionRelayScheme`. - - :return: A reference to this :class:`ProtectionRelayScheme` for fluent use. - """ - self._functions = None + self.functions.clear() return self + + # endregion functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py index 83c1ae601..49276fd6d 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py @@ -8,12 +8,16 @@ __all__ = ["ProtectionRelaySystem"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.util import ngen, get_by_mrid, nlen, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_scheme import ProtectionRelayScheme @@ -30,70 +34,40 @@ class ProtectionRelaySystem(Equipment): protection_kind: ProtectionKind = ProtectionKind.UNKNOWN """[ZBEX] The kind of protection being provided by this protection equipment.""" - _schemes: Optional[List[ProtectionRelayScheme]] = None + _schemes: Optional[List[ProtectionRelayScheme]] = field(default=None) - def __init__(self, *args, schemes: Optional[List[ProtectionRelayScheme]] = None, **kwargs): - super(ProtectionRelaySystem, self).__init__(*args, **kwargs) - if schemes is not None: - for scheme in schemes: - self.add_scheme(scheme) + schemes: MridCollection[ProtectionRelayScheme] = LazyMridList( + _schemes, + "A ProtectionRelayScheme", + ) - @property - def schemes(self) -> Generator[ProtectionRelayScheme, None, None]: - """ - [ZBEX] Yields all the schemes implemented by this :class:`ProtectionRelaySystem`. - :return: A generator that iterates over all the schemes implemented by this :class:`ProtectionRelaySystem`. - """ - return ngen(self._schemes) + # region deprecated list boilerplate + # region schemes boilerplate + @deprecated("Use len(obj.schemes) instead.") def num_schemes(self) -> int: - """ - Get the number of :class:`ProtectionRelaySchemes` for this :class:`ProtectionRelaySystem`. - - :return: The number of :class:`ProtectionRelaySchemes` for this :class:`ProtectionRelaySystem`. - """ - return nlen(self._schemes) + return len(self.schemes) + @deprecated("Use obj.schemes.get_by_mrid(mrid) instead.") def get_scheme(self, mrid: str) -> ProtectionRelayScheme: - """ - Get a :class:`ProtectionRelayScheme` for this :class:`ProtectionRelaySystem` by its mRID. - - :param mrid: The mRID of the desired :class:`ProtectionRelayScheme`. - :returns: The :class:`ProtectionRelayScheme` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._schemes, mrid) + return self.schemes.get_by_mrid(mrid) + @deprecated("Use obj.schemes.append(scheme) instead.") def add_scheme(self, scheme: ProtectionRelayScheme) -> ProtectionRelaySystem: - """ - Add a :class:`ProtectionRelayScheme` to this :class:`ProtectionRelaySystem`. - - :param scheme: The :class:`ProtectionRelayScheme` to add. - :return: A reference to this :class:`ProtectionRelaySystem` for fluent use. - """ - if self._validate_reference(scheme, self.get_scheme, "A ProtectionRelayScheme"): - return self - self._schemes = list() if self._schemes is None else self._schemes - self._schemes.append(scheme) + self.schemes.append(scheme) return self + @deprecated("Use obj.schemes.remove(scheme) instead.") def remove_scheme(self, scheme: Optional[ProtectionRelayScheme]) -> ProtectionRelaySystem: - """ - Remove a :class:`ProtectionRelayScheme` from this :class:`ProtectionRelaySystem`. - - :param scheme: The :class:`ProtectionRelayScheme` to remove. - :raises ValueError: If scheme was not associated with this :class:`ProtectionRelaySystem`. - :return: A reference to this :class:`ProtectionRelaySystem` for fluent use. - """ - self._schemes = safe_remove(self._schemes, scheme) + self.schemes.remove(scheme) return self + @deprecated("Use obj.schemes.clear() instead.") def clear_scheme(self) -> ProtectionRelaySystem: - """ - Remove all :class:`ProtectionRelaySchemes` from this :class:`ProtectionRelaySystem`. - - :return: A reference to this :class:`ProtectionRelaySystem` for fluent use. - """ - self._schemes = None + self.schemes.clear() return self + + # endregion schemes boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py index eb939df8f..e966a3e14 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py @@ -8,11 +8,15 @@ __all__ = ["PowerTransformerInfo"] from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.transformer_tank_info import TransformerTankInfo @@ -22,21 +26,14 @@ class PowerTransformerInfo(AssetInfo): """Set of power transformer data, from an equipment library.""" - _transformer_tank_infos: Optional[List[TransformerTankInfo]] = None + _transformer_tank_infos: Optional[List[TransformerTankInfo]] = field(default=None) """Data for all the tanks described by this power transformer data.""" - def __init__(self, *args, transformer_tank_infos: List[TransformerTankInfo] = None, **kwargs): - super(PowerTransformerInfo, self).__init__(*args, **kwargs) - if transformer_tank_infos: - for ti in transformer_tank_infos: - self.add_transformer_tank_info(ti) + transformer_tank_infos: MridCollection[TransformerTankInfo] = LazyMridList( + _transformer_tank_infos, + "A TransformerTankInfo", + ) - @property - def transformer_tank_infos(self) -> Generator[TransformerTankInfo, None, None]: - """ - The `TransformerTankInfo`s of this `PowerTransformerInfo`. - """ - return ngen(self._transformer_tank_infos) def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance]: """ @@ -51,55 +48,32 @@ def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance] else: return None + # region deprecated list boilerplate + # region transformer_tank_infos boilerplate + + @deprecated("Use len(obj.transformer_tank_infos) instead.") def num_transformer_tank_infos(self): - """ - Get the number of `TransformerTankInfo`s associated with this `PowerTransformerInfo`. - """ - return nlen(self._transformer_tank_infos) + return len(self.transformer_tank_infos) + @deprecated("Use obj.transformer_tank_infos.get_by_mrid(mrid) instead.") def get_transformer_tank_info(self, mrid: str) -> TransformerTankInfo: - """ - Get the `TransformerTankInfo` for this `PowerTransformerInfo` identified by `mrid`. - - `mrid` the mRID of the required `TransformerTankInfo` - Returns The `TransformerTankInfo` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._transformer_tank_infos, mrid) + return self.transformer_tank_infos.get_by_mrid(mrid) + @deprecated("Use obj.transformer_tank_infos.append(tti) instead.") def add_transformer_tank_info(self, tti: TransformerTankInfo) -> PowerTransformerInfo: - """ - `tti` The `TransformerTankInfo` to - associate with this `PowerTransformerInfo`. - - Returns A reference to this `PowerTransformerInfo` to allow fluent use. - - Raises `ValueError` if another `TransformerTankInfo` with the same `mrid` already - exists in this `PowerTransformerInfo` - """ - if self._validate_reference(tti, self.get_transformer_tank_info, "A TransformerTankInfo"): - return self - - self._transformer_tank_infos = list() if self._transformer_tank_infos is None else self._transformer_tank_infos - self._transformer_tank_infos.append(tti) + self.transformer_tank_infos.append(tti) return self + @deprecated("Use obj.transformer_tank_infos.remove(tti) instead.") def remove_transformer_tank_info(self, tti: TransformerTankInfo) -> PowerTransformerInfo: - """ - Disassociate an `TransformerTankInfo` from this `PowerTransformerInfo`. - - `tti` the `TransformerTankInfo` to - disassociate with this `PowerTransformerInfo`. - Raises `ValueError` if `tti` was not associated with this `PowerTransformerInfo`. - Returns A reference to this `Asset` to allow fluent use. - """ - self._transformer_tank_infos = safe_remove(self._transformer_tank_infos, tti) + self.transformer_tank_infos.remove(tti) return self + @deprecated("Use obj.transformer_tank_infos.clear() instead.") def clear_transformer_tank_infos(self) -> PowerTransformerInfo: - """ - Clears all `TransformerTankInfo`. - Returns self - """ - self._transformer_tank_infos = None + self.transformer_tank_infos.clear() return self + + # endregion transformer_tank_infos boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py index 79d84f371..a0e0c406f 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py @@ -8,11 +8,15 @@ __all__ = ["TransformerTankInfo"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.util import nlen, ngen, safe_remove, get_by_mrid from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo @@ -26,21 +30,14 @@ class TransformerTankInfo(AssetInfo): power_transformer_info: Optional[PowerTransformerInfo] = None """Power transformer data that this tank description is part of.""" - _transformer_end_infos: Optional[List[TransformerEndInfo]] = None + _transformer_end_infos: Optional[List[TransformerEndInfo]] = field(default=None) """Data for all the ends described by this transformer tank data.""" - def __init__(self, *args, transformer_end_infos: List[TransformerEndInfo] = None, **kwargs): - super(TransformerTankInfo, self).__init__(*args, **kwargs) - if transformer_end_infos: - for tei in transformer_end_infos: - self.add_transformer_end_info(tei) + transformer_end_infos: MridCollection[TransformerEndInfo] = LazyMridList( + _transformer_end_infos, + "A TransformerEndInfo", + ) - @property - def transformer_end_infos(self) -> Generator[TransformerEndInfo, None, None]: - """ - The `TransformerEndInfo`s of this `TransformerTankInfo`. - """ - return ngen(self._transformer_end_infos) def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance]: """ @@ -56,55 +53,32 @@ def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance] else: return None + # region deprecated list boilerplate + # region transformer_end_infos boilerplate + + @deprecated("Use len(obj.transformer_end_infos) instead.") def num_transformer_end_infos(self): - """ - Get the number of `TransformerEndInfo`s associated with this `TransformerTankInfo`. - """ - return nlen(self._transformer_end_infos) + return len(self.transformer_end_infos) + @deprecated("Use obj.transformer_end_infos.get_by_mrid(mrid) instead.") def get_transformer_end_info(self, mrid: str) -> TransformerEndInfo: - """ - Get the `TransformerEndInfo` for this `TransformerTankInfo` identified by `mrid`. - - `mrid` the mRID of the required `TransformerEndInfo` - Returns The `TransformerEndInfo` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._transformer_end_infos, mrid) + return self.transformer_end_infos.get_by_mrid(mrid) + @deprecated("Use obj.transformer_end_infos.append(tei) instead.") def add_transformer_end_info(self, tei: TransformerEndInfo) -> TransformerTankInfo: - """ - `tei` The `TransformerEndInfo` to - associate with this `TransformerTankInfo`. - - Returns A reference to this `TransformerTankInfo` to allow fluent use. - - Raises `ValueError` if another `TransformerEndInfo` with the same `mrid` already - exists in this `TransformerTankInfo` - """ - if self._validate_reference(tei, self.get_transformer_end_info, "A TransformerEndInfo"): - return self - - self._transformer_end_infos = list() if self._transformer_end_infos is None else self._transformer_end_infos - self._transformer_end_infos.append(tei) + self.transformer_end_infos.append(tei) return self + @deprecated("Use obj.transformer_end_infos.remove(tei) instead.") def remove_transformer_end_info(self, tei: TransformerEndInfo) -> TransformerTankInfo: - """ - Disassociate an `TransformerEndInfo` from this `TransformerTankInfo`. - - `tei` the `TransformerEndInfo` to - disassociate with this `TransformerTankInfo`. - Raises `ValueError` if `tei` was not associated with this `TransformerTankInfo`. - Returns A reference to this `TransformerTankInfo` to allow fluent use. - """ - self._transformer_end_infos = safe_remove(self._transformer_end_infos, tei) + self.transformer_end_infos.remove(tei) return self + @deprecated("Use obj.transformer_end_infos.clear() instead.") def clear_transformer_end_infos(self) -> TransformerTankInfo: - """ - Clears all `TransformerEndInfo`. - Returns self - """ - self._transformer_end_infos = None + self.transformer_end_infos.clear() return self + + # endregion transformer_end_infos boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset.py b/src/zepben/ewb/model/cim/iec61968/assets/asset.py index ce0e138f4..afb538c2c 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset.py @@ -9,10 +9,14 @@ from typing import Optional, Generator, List, TYPE_CHECKING from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset_organisation_role import AssetOrganisationRole @@ -32,126 +36,74 @@ class Asset(IdentifiedObject, metaclass=ABCMeta): location: Optional[Location] = None """`zepben.ewb.model.cim.iec61968.common.location.Location` of this asset""" - _organisation_roles: Optional[List[AssetOrganisationRole]] = None + _organisation_roles: Optional[List[AssetOrganisationRole]] = field(default=None) - _power_system_resources: Optional[List[PowerSystemResource]] = None + _power_system_resources: Optional[List[PowerSystemResource]] = field(default=None) - def __init__(self, *args, organisation_roles: List[AssetOrganisationRole] = None, power_system_resources: List[PowerSystemResource] = None, **kwargs): - super(Asset, self).__init__(*args, **kwargs) - if organisation_roles: - for role in organisation_roles: - self.add_organisation_role(role) + organisation_roles: MridCollection[AssetOrganisationRole] = LazyMridList( + _organisation_roles, + "An AssetOrganisationRole", + ) - if power_system_resources: - for resource in power_system_resources: - self.add_power_system_resource(resource) + power_system_resources: MridCollection[PowerSystemResource] = LazyMridList( + _power_system_resources, + "An PowerSystemResource", + ) - @property - def organisation_roles(self) -> Generator[AssetOrganisationRole, None, None]: - """ - The `AssetOrganisationRole`s of this `Asset`. - """ - return ngen(self._organisation_roles) - @property - def power_system_resources(self) -> Generator[PowerSystemResource, None, None]: - """ - The `PowerSystemResource`s of this `Asset`. - """ - return ngen(self._power_system_resources) + # region deprecated list boilerplate + # region organisation_roles boilerplate + @deprecated("Use len(obj.organisation_roles) instead.") def num_organisation_roles(self) -> int: - """ - Get the number of `AssetOrganisationRole`s associated with this `Asset`. - """ - return nlen(self._organisation_roles) + return len(self.organisation_roles) + @deprecated("Use obj.organisation_roles.get_by_mrid(mrid) instead.") def get_organisation_role(self, mrid: str) -> AssetOrganisationRole: - """ - Get the `AssetOrganisationRole` for this asset identified by `mrid`. - - `mrid` the mRID of the required `AssetOrganisationRole` - Returns The `AssetOrganisationRole` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._organisation_roles, mrid) + return self.organisation_roles.get_by_mrid(mrid) + @deprecated("Use obj.organisation_roles.append(role) instead.") def add_organisation_role(self, role: AssetOrganisationRole) -> Asset: - """ - `role` The `AssetOrganisationRole` to associate with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - Raises `ValueError` if another `AssetOrganisationRole` with the same `mrid` already exists in this `Asset` - """ - if self._validate_reference(role, self.get_organisation_role, "An AssetOrganisationRole"): - return self - - self._organisation_roles = list() if self._organisation_roles is None else self._organisation_roles - self._organisation_roles.append(role) + self.organisation_roles.append(role) return self + @deprecated("Use obj.organisation_roles.remove(role) instead.") def remove_organisation_role(self, role: AssetOrganisationRole) -> Asset: - """ - Disassociate an `AssetOrganisationRole` from this `Asset`. - - `role` the `AssetOrganisationRole` to disassociate from this `Asset`. - Raises `ValueError` if `role` was not associated with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - """ - self._organisation_roles = safe_remove(self._organisation_roles, role) + self.organisation_roles.remove(role) return self + @deprecated("Use obj.organisation_roles.clear() instead.") def clear_organisation_roles(self) -> Asset: - """ - Clear all organisation roles. - Returns self - """ - self._organisation_roles = None + self.organisation_roles.clear() return self + # endregion organisation_roles boilerplate + + # region power_system_resources boilerplate + + @deprecated("Use len(obj.power_system_resources) instead.") def num_power_system_resources(self) -> int: - """ - Get the number of `PowerSystemResource`s associated with this `Asset`. - """ - return nlen(self._power_system_resources) + return len(self.power_system_resources) + @deprecated("Use obj.power_system_resources.get_by_mrid(mrid) instead.") def get_power_system_resource(self, mrid: str) -> PowerSystemResource: - """ - Get the `PowerSystemResource` for this asset identified by `mrid`. - - `mrid` the mRID of the required `PowerSystemResource` - Returns The `PowerSystemResource` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_system_resources, mrid) + return self.power_system_resources.get_by_mrid(mrid) + @deprecated("Use obj.power_system_resources.append(resource) instead.") def add_power_system_resource(self, resource: PowerSystemResource) -> Asset: - """ - `resource` The `PowerSystemResource` to associate with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - Raises `ValueError` if another `PowerSystemResource` with the same `mrid` already exists in this `Asset` - """ - if self._validate_reference(resource, self.get_power_system_resource, "An PowerSystemResource"): - return self - - self._power_system_resources = list() if self._power_system_resources is None else self._power_system_resources - self._power_system_resources.append(resource) + self.power_system_resources.append(resource) return self + @deprecated("Use obj.power_system_resources.remove(resource) instead.") def remove_power_system_resource(self, resource: PowerSystemResource) -> Asset: - """ - Disassociate an `PowerSystemResource` from this `Asset`. - - `resource` the `PowerSystemResource` to disassociate from this `Asset`. - Raises `ValueError` if `resource` was not associated with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - """ - self._power_system_resources = safe_remove(self._power_system_resources, resource) + self.power_system_resources.remove(resource) return self + @deprecated("Use obj.power_system_resources.clear() instead.") def clear_power_system_resources(self) -> Asset: - """ - Clear all power system resources. - Returns self - """ - self._power_system_resources = None + self.power_system_resources.clear() return self + + # endregion power_system_resources boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index 1fd03ab43..9787f551d 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -8,11 +8,15 @@ __all__ = ["Customer"] from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.organisation_role import OrganisationRole from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer_agreement import CustomerAgreement @@ -30,66 +34,41 @@ class Customer(OrganisationRole): special_need: Optional[str] = None """A special service need such as life support, hospitals, etc.""" - _customer_agreements: Optional[List[CustomerAgreement]] = None + _customer_agreements: Optional[List[CustomerAgreement]] = field(default=None) - def __init__(self, *args, customer_agreements: List[CustomerAgreement] = None, **kwargs): - super(Customer, self).__init__(*args, **kwargs) - if customer_agreements: - for agreement in customer_agreements: - self.add_agreement(agreement) + agreements: MridCollection[CustomerAgreement] = LazyMridList( + _customer_agreements, + "A CustomerAgreement", + ) + customer_agreements = Alias(agreements) - @property - def agreements(self) -> Generator[CustomerAgreement, None, None]: - """ - The `CustomerAgreement`s for this `Customer`. - """ - return ngen(self._customer_agreements) + # region deprecated list boilerplate + # region agreements boilerplate + + @deprecated("Use len(obj.agreements) instead.") def num_agreements(self) -> int: - """ - Get the number of `CustomerAgreement`s associated with this `Customer`. - """ - return nlen(self._customer_agreements) + return len(self.agreements) + @deprecated("Use obj.agreements.get_by_mrid(mrid) instead.") def get_agreement(self, mrid: str) -> CustomerAgreement: - """ - Get the `CustomerAgreement` for this `Customer` identified by `mrid`. - - `mrid` the mRID of the required `customer_agreement.CustomerAgreement` - Returns the `CustomerAgreement` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._customer_agreements, mrid) + return self.agreements.get_by_mrid(mrid) + @deprecated("Use obj.agreements.append(customer_agreement) instead.") def add_agreement(self, customer_agreement: CustomerAgreement) -> Customer: - """ - Associate a `CustomerAgreement` with this `Customer`. - `customer_agreement` The `customer_agreement.CustomerAgreement` to associate with this `Customer`. - Returns A reference to this `Customer` to allow fluent use. - Raises `ValueError` if another `CustomerAgreement` with the same `mrid` already exists for this `Customer` - """ - if self._validate_reference(customer_agreement, self.get_agreement, "A CustomerAgreement"): - return self - - self._customer_agreements = list() if self._customer_agreements is None else self._customer_agreements - self._customer_agreements.append(customer_agreement) + self.agreements.append(customer_agreement) return self + @deprecated("Use obj.agreements.remove(customer_agreement) instead.") def remove_agreement(self, customer_agreement: CustomerAgreement) -> Customer: - """ - Disassociate `customer_agreement` from this `Customer`. - - `customer_agreement` the `customer_agreement.CustomerAgreement` to disassociate with this `Customer`. - Returns A reference to this `Customer` to allow fluent use. - Raises `ValueError` if `customer_agreement` was not associated with this `Customer`. - """ - self._customer_agreements = safe_remove(self._customer_agreements, customer_agreement) + self.agreements.remove(customer_agreement) return self + @deprecated("Use obj.agreements.clear() instead.") def clear_agreements(self) -> Customer: - """ - Clear all customer agreements. - Returns self - """ - self._customer_agreements = None + self.agreements.clear() return self + + # endregion agreements boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py index 836c36b02..9d6b0a4e0 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py @@ -8,10 +8,14 @@ __all__ = ["CustomerAgreement"] from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.agreement import Agreement from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer import Customer @@ -29,15 +33,7 @@ class CustomerAgreement(Agreement): _customer: Optional[Customer] = None """The `zepben.ewb.model.cim.iec61968.customers.customer.Customer` that has this `CustomerAgreement`.""" - _pricing_structures: Optional[List[PricingStructure]] = None - - def __init__(self, *args, customer: Customer = None, pricing_structures: List[PricingStructure] = None, **kwargs): - super(CustomerAgreement, self).__init__(*args, **kwargs) - if customer: - self.customer = customer - if pricing_structures: - for ps in pricing_structures: - self.add_pricing_structure(ps) + _pricing_structures: Optional[List[PricingStructure]] = field(default=None) @property def customer(self): @@ -51,59 +47,39 @@ def customer(self, cust): else: raise ValueError(f"customer for {str(self)} has already been set to {self._customer}, cannot reset this field to {cust}") - @property - def pricing_structures(self) -> Generator[PricingStructure, None, None]: - """ - The `PricingStructure`s of this `CustomerAgreement`. - """ - return ngen(self._pricing_structures) + pricing_structures: MridCollection[PricingStructure] = LazyMridList( + _pricing_structures, + "A PricingStructure", + ) + + + # region deprecated list boilerplate + # region pricing_structures boilerplate + + @deprecated("Use len(obj.pricing_structures) instead.") def num_pricing_structures(self): - """ - The number of `PricingStructure`s associated with this `CustomerAgreement` - """ - return nlen(self._pricing_structures) + return len(self.pricing_structures) + @deprecated("Use obj.pricing_structures.get_by_mrid(mrid) instead.") def get_pricing_structure(self, mrid: str) -> PricingStructure: - """ - Get the `PricingStructure` for this `CustomerAgreement` identified by `mrid` - - `mrid` the mRID of the required `PricingStructure` - Returns the `PricingStructure` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._pricing_structures, mrid) + return self.pricing_structures.get_by_mrid(mrid) + @deprecated("Use obj.pricing_structures.append(ps) instead.") def add_pricing_structure(self, ps: PricingStructure) -> CustomerAgreement: - """ - Associate `ps` with this `CustomerAgreement` - - `ps` the `PricingStructure` to associate with this `CustomerAgreement`. - Returns A reference to this `CustomerAgreement` to allow fluent use. - Raises `ValueError` if another `PricingStructure` with the same `mrid` already exists for this `CustomerAgreement` - """ - if self._validate_reference(ps, self.get_pricing_structure, "A PricingStructure"): - return self - - self._pricing_structures = list() if self._pricing_structures is None else self._pricing_structures - self._pricing_structures.append(ps) + self.pricing_structures.append(ps) return self + @deprecated("Use obj.pricing_structures.remove(ps) instead.") def remove_pricing_structure(self, ps: PricingStructure) -> CustomerAgreement: - """ - Disassociate `ps` from this `CustomerAgreement` - - `ps` the `PricingStructure` to disassociate from this `CustomerAgreement`. - Returns A reference to this `CustomerAgreement` to allow fluent use. - Raises `ValueError` if `ps` was not associated with this `CustomerAgreement`. - """ - self._pricing_structures = safe_remove(self._pricing_structures, ps) + self.pricing_structures.remove(ps) return self + @deprecated("Use obj.pricing_structures.clear() instead.") def clear_pricing_structures(self) -> CustomerAgreement: - """ - Clear all pricing structures. - Returns a reference to this `CustomerAgreement` to allow fluent use. - """ - self._pricing_structures = None + self.pricing_structures.clear() return self + + # endregion pricing_structures boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py index dc8202907..be237a622 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py @@ -8,10 +8,14 @@ __all__ = ["PricingStructure"] from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.tariff import Tariff @@ -29,68 +33,42 @@ class PricingStructure(Document): :var code: Unique user-allocated key for this pricing structure, used by company representatives to identify the correct price structure for allocating to a customer. For rate schedules it is often prefixed by a state code. """ - _tariffs: Optional[List[Tariff]] = None + _tariffs: Optional[List[Tariff]] = field(default=None) code: str | None = None - def __init__(self, *args, tariffs: List[Tariff] = None, **kwargs): - super(PricingStructure, self).__init__(*args, **kwargs) - if tariffs: - for tariff in tariffs: - self.add_tariff(tariff) + tariffs: MridCollection[Tariff] = LazyMridList( + _tariffs, + "A Tariff", + ) - @property - def tariffs(self) -> Generator[Tariff, None, None]: - """ - The `Tariff`s of this `PricingStructure`. - """ - return ngen(self._tariffs) + # region deprecated list boilerplate + # region tariffs boilerplate + + @deprecated("Use len(obj.tariffs) instead.") def num_tariffs(self): - """ - Returns The number of `Tariff`s associated with this `PricingStructure` - """ - return nlen(self._tariffs) + return len(self.tariffs) + @deprecated("Use obj.tariffs.get_by_mrid(mrid) instead.") def get_tariff(self, mrid: str) -> Tariff: - """ - Get the `Tariff` for this `PricingStructure` identified by `mrid` - - `mrid` the mRID of the required `Tariff` - Returns The `Tariff` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._tariffs, mrid) + return self.tariffs.get_by_mrid(mrid) + @deprecated("Use obj.tariffs.append(tariff) instead.") def add_tariff(self, tariff: Tariff) -> PricingStructure: - """ - Associate a `Tariff` with this `PricingStructure`. - - `tariff` the `Tariff` to associate with this `PricingStructure`. - Returns A reference to this `PricingStructure` to allow fluent use. - Raises `ValueError` if another `Tariff` with the same `mrid` already exists for this `PricingStructure`. - """ - if self._validate_reference(tariff, self.get_tariff, "A Tariff"): - return self - self._tariffs = list() if self._tariffs is None else self._tariffs - self._tariffs.append(tariff) + self.tariffs.append(tariff) return self + @deprecated("Use obj.tariffs.remove(tariff) instead.") def remove_tariff(self, tariff: Tariff) -> PricingStructure: - """ - Disassociate `tariff` from this `PricingStructure`. - - `tariff` the `Tariff` to disassociate from this `PricingStructure`. - Returns A reference to this `PricingStructure` to allow fluent use. - Raises `ValueError` if `tariff` was not associated with this `PricingStructure`. - """ - self._tariffs = safe_remove(self._tariffs, tariff) + self.tariffs.remove(tariff) return self + @deprecated("Use obj.tariffs.clear() instead.") def clear_tariffs(self) -> PricingStructure: - """ - Clear all tariffs. - Returns A reference to this `PricingStructure` to allow fluent use. - """ - self._tariffs = None + self.tariffs.clear() return self + + # endregion tariffs boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py index df77d8465..a3ad34d3a 100644 --- a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py +++ b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py @@ -8,10 +8,14 @@ __all__ = ["Pole"] from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.structure import Structure from zepben.ewb.util import get_by_mrid, ngen, nlen, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.streetlight import Streetlight @@ -24,66 +28,40 @@ class Pole(Structure): classification: Optional[str] = None """Pole class: 1, 2, 3, 4, 5, 6, 7, H1, H2, Other, Unknown.""" - _streetlights: Optional[List[Streetlight]] = None + _streetlights: Optional[List[Streetlight]] = field(default=None) - def __init__(self, *args, streetlights: List[Streetlight] = None, **kwargs): - super(Pole, self).__init__(*args, **kwargs) - if streetlights: - for light in streetlights: - self.add_streetlight(light) + streetlights: MridCollection[Streetlight] = LazyMridList( + _streetlights, + "A Streetlight", + ) - @property - def streetlights(self) -> Generator[Streetlight, None, None]: - """ - The `Streetlight`s of this `Pole`. - """ - return ngen(self._streetlights) + # region deprecated list boilerplate + # region streetlights boilerplate + + @deprecated("Use len(obj.streetlights) instead.") def num_streetlights(self) -> int: - """ - Get the number of `Streetlight`s associated with this `Pole`. - """ - return nlen(self._streetlights) + return len(self.streetlights) + @deprecated("Use obj.streetlights.get_by_mrid(mrid) instead.") def get_streetlight(self, mrid: str) -> Streetlight: - """ - Get the `Streetlight` for this asset identified by `mrid`. - - `mrid` the mRID of the required `Streetlight` - Returns The `Streetlight` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._streetlights, mrid) + return self.streetlights.get_by_mrid(mrid) + @deprecated("Use obj.streetlights.append(streetlight) instead.") def add_streetlight(self, streetlight: Streetlight) -> Pole: - """ - Associate a `Streetlight` with this `Pole` - - `streetlight` the `Streetlight` to associate with this `Pole`. - Returns A reference to this `Pole` to allow fluent use. - Raises `ValueError` if another `Streetlight` with the same `mrid` already exists in this `Pole` - """ - if self._validate_reference(streetlight, self.get_streetlight, "A Streetlight"): - return self - - self._streetlights = list() if self._streetlights is None else self._streetlights - self._streetlights.append(streetlight) + self.streetlights.append(streetlight) return self + @deprecated("Use obj.streetlights.remove(streetlight) instead.") def remove_streetlight(self, streetlight: Streetlight) -> Pole: - """ - Disassociate `streetlight` from this `Pole` - `streetlight` the `Streetlight` to disassociate from this `Pole`. - Raises `ValueError` if `streetlight` was not associated with this `Pole`. - Returns A reference to this `Pole` to allow fluent use. - """ - self._streetlights = safe_remove(self._streetlights, streetlight) + self.streetlights.remove(streetlight) return self + @deprecated("Use obj.streetlights.clear() instead.") def clear_streetlights(self) -> Pole: - """ - Clear all Streetlights. - Returns self - """ - self._streetlights = None + self.streetlights.clear() return self + + # endregion streetlights boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py index 943eaebf0..91db333b4 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py @@ -69,7 +69,7 @@ class ControlledAppliance: _bitmask: int - def __init__(self, appliances: Union[int, Appliance, List[Appliance]]): + def __init__(self, appliances: int | Appliance | List[Appliance]): if isinstance(appliances, int): self._bitmask = appliances elif isinstance(appliances, Appliance): diff --git a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py index fcd5210da..bc4e3daff 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py @@ -9,10 +9,14 @@ from typing import Optional, List, Generator, TYPE_CHECKING from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_container import AssetContainer from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location @@ -42,127 +46,74 @@ class EndDevice(AssetContainer, metaclass=ABCMeta): service_location: Optional[Location] = None """Service `zepben.ewb.model.cim.iec61968.common.location.Location` whose service delivery is measured by this `EndDevice`.""" - _usage_points: Optional[List[UsagePoint]] = None - - _functions: Optional[List[EndDeviceFunction]] = None - - def __init__(self, *args, usage_points: List[UsagePoint] = None, functions: List[EndDeviceFunction] = None, **kwargs): - super(EndDevice, self).__init__(*args, **kwargs) - if usage_points: - for up in usage_points: - self.add_usage_point(up) - if functions: - for edf in functions: - self.add_function(edf) - - @property - def usage_points(self) -> Generator[UsagePoint, None, None]: - """ - The `UsagePoint`s associated with this `EndDevice` - """ - return ngen(self._usage_points) - - @property - def functions(self) -> Generator[EndDeviceFunction, None, None]: - """ - The `EndDeviceFunction`s associated with this `EndDevice` - """ - return ngen(self._functions) + _usage_points: Optional[List[UsagePoint]] = field(default=None) + _functions: Optional[List[EndDeviceFunction]] = field(default=None) + + usage_points: MridCollection[UsagePoint] = LazyMridList( + _usage_points, + "A UsagePoint", + ) + + functions: MridCollection[EndDeviceFunction] = LazyMridList( + _functions, + "An EndDeviceFunction", + ) + + + # region deprecated list boilerplate + # region usage_points boilerplate + + @deprecated("Use len(obj.usage_points) instead.") def num_usage_points(self): - """ - Returns The number of `UsagePoint`s associated with this `EndDevice` - """ - return nlen(self._usage_points) + return len(self.usage_points) + @deprecated("Use obj.usage_points.get_by_mrid(mrid) instead.") def get_usage_point(self, mrid: str) -> UsagePoint: - """ - Get the `UsagePoint` for this `EndDevice` identified by `mrid` - - `mrid` the mRID of the required `UsagePoint` - Returns The `UsagePoint` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._usage_points, mrid) + return self.usage_points.get_by_mrid(mrid) + @deprecated("Use obj.usage_points.append(up) instead.") def add_usage_point(self, up: UsagePoint) -> EndDevice: - """ - Associate `up` to this `EndDevice`. - - `up` the `UsagePoint` to associate with this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if another `UsagePoint` with the same `mrid` already exists for this `EndDevice`. - """ - if self._validate_reference(up, self.get_usage_point, "A UsagePoint"): - return self - self._usage_points = list() if self._usage_points is None else self._usage_points - self._usage_points.append(up) + self.usage_points.append(up) return self + @deprecated("Use obj.usage_points.remove(up) instead.") def remove_usage_point(self, up: UsagePoint) -> EndDevice: - """ - Disassociate `up` from this `EndDevice` - - `up` the `UsagePoint` to disassociate from this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `EndDevice`. - """ - self._usage_points = safe_remove(self._usage_points, up) + self.usage_points.remove(up) return self + @deprecated("Use obj.usage_points.clear() instead.") def clear_usage_points(self) -> EndDevice: - """ - Clear all usage_points. - Returns A reference to this `EndDevice` to allow fluent use. - """ - self._usage_points = None + self.usage_points.clear() return self + # endregion usage_points boilerplate + + # region functions boilerplate + + @deprecated("Use len(obj.functions) instead.") def num_functions(self): - """ - Returns The number of `EndDeviceFunction`s associated with this `EndDevice` - """ - return nlen(self._functions) + return len(self.functions) + @deprecated("Use obj.functions.get_by_mrid(mrid) instead.") def get_function(self, mrid: str) -> EndDeviceFunction: - """ - Get the `EndDeviceFunction` for this `EndDevice` identified by `mrid` - - `mrid` the mRID of the required `EndDeviceFunction` - Returns The `EndDeviceFunction` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._functions, mrid) - - def add_function(self, edf: EndDeviceFunction) -> EndDevice: - """ - Associate `edf` to this `EndDevice`. - - `edf` the `EndDeviceFunction` to associate with this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if another `EndDeviceFunction` with the same `mrid` already exists for this `EndDevice`. - """ - if self._validate_reference(edf, self.get_function, "An EndDeviceFunction"): - return self - self._functions = list() if self._functions is None else self._functions - self._functions.append(edf) + return self.functions.get_by_mrid(mrid) + + @deprecated("Use obj.functions.append(edf) instead.") + def add_function(self, edf: 'EndDeviceFunction') -> 'EndDevice': + self.functions.append(edf) return self + @deprecated("Use obj.functions.remove(edf) instead.") def remove_function(self, edf: EndDeviceFunction) -> EndDevice: - """ - Disassociate `edf` from this `EndDevice` - - `up` the `EndDeviceFunction` to disassociate from this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `EndDevice`. - """ - self._functions = safe_remove(self._functions, edf) + self.functions.remove(edf) return self + @deprecated("Use obj.functions.clear() instead.") def clear_functions(self) -> EndDevice: - """ - Clear all end_device_functions. - Returns A reference to this `EndDevice` to allow fluent use. - """ - self._functions = None + self.functions.clear() return self + + # endregion functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index 764e395cf..dc8235b9b 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -8,12 +8,16 @@ __all__ = ["UsagePoint"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location @@ -56,125 +60,27 @@ class UsagePoint(IdentifiedObject): four-wire, s12n (splitSecondary12N) is single-phase, three-wire, and s1n and s2n are single-phase, two-wire. """ - _equipment: list[Equipment] | None = None - _end_devices: list[EndDevice] | None = None + _equipment: list[Equipment] | None = field(default=None) + _end_devices: list[EndDevice] | None = field(default=None) _contacts: list[ContactDetails] | None = None - def __init__(self, *args, equipment: List[Equipment] = None, end_devices: List[EndDevice] = None, contacts: List[ContactDetails] = None, **kwargs): + def __init__(self, *args, contacts: List[ContactDetails] = None, **kwargs): super(UsagePoint, self).__init__(*args, **kwargs) - if equipment: - for eq in equipment: - self.add_equipment(eq) - if end_devices: - for ed in end_devices: - self.add_end_device(ed) if contacts: for c in contacts: self.add_contact(c) - @property - def end_devices(self) -> Generator[EndDevice, None, None]: - """ - The `EndDevice`'s (Meter's) associated with this `UsagePoint`. - """ - return ngen(self._end_devices) - - @property - def equipment(self) -> Generator[Equipment, None, None]: - """ - The `zepben.model.Equipment` associated with this `UsagePoint`. - """ - return ngen(self._equipment) - def is_metered(self): - """ - Check whether this `UsagePoint` is metered. A `UsagePoint` is metered if it's associated with at least one `EndDevice`. - Returns True if this `UsagePoint` has an `EndDevice`, False otherwise. - """ - return nlen(self._end_devices) > 0 - - @property - def contacts(self) -> Generator[ContactDetails, None, None]: - """[ZBEX] All contact details for this `UsagePoint`""" - return ngen(self._contacts) - - def num_contacts(self): - """Get the number of entries in the `ContactDetails` collection""" - return nlen(self._contacts) - - def get_contact(self, _id: str) -> ContactDetails: - """All End devices at this usage point.""" # TODO: again, lol, also jvmsdk - try: - return next((it for it in self.contacts if it.id == _id)) - except StopIteration: - raise KeyError(_id) - - def add_contact(self, contact: ContactDetails) -> UsagePoint: - """Add a `ContactDetails` to this `UsagePoint`""" - if self._validate_reference(contact, self.get_contact, "A ContactDetails"): - return self - - if self._contacts is None: - self._contacts = list() - self._contacts.append(contact) - return self + end_devices: MridCollection[EndDevice] = LazyMridList( + _end_devices, + "An EndDevice", + ) - def remove_contact(self, contact: ContactDetails) -> UsagePoint: - self._contacts = safe_remove(self._contacts, contact) - return self - - def clear_contacts(self) -> UsagePoint: - self._contacts = None - return self - - def num_end_devices(self): - """ - Returns The number of `EndDevice`s associated with this `UsagePoint` - """ - return nlen(self._end_devices) - - def get_end_device(self, mrid: str) -> EndDevice: - """ - Get the `EndDevice` for this `UsagePoint` identified by `mrid` - - `mrid` The mRID of the required `EndDevice` - Returns The `EndDevice` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._end_devices, mrid) - - def add_end_device(self, end_device: EndDevice) -> UsagePoint: - """ - Associate an `EndDevice` with this `UsagePoint` - - `end_device` The `EndDevice` to associate with this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if another `EndDevice` with the same `mrid` already exists for this `UsagePoint`. - """ - if self._validate_reference(end_device, self.get_end_device, "An EndDevice"): - return self - self._end_devices = list() if self._end_devices is None else self._end_devices - self._end_devices.append(end_device) - return self - - def remove_end_device(self, end_device: EndDevice) -> UsagePoint: - """ - Disassociate `end_device` from this `UsagePoint`. - - `end_device` The `EndDevice` to disassociate from this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if `end_device` was not associated with this `UsagePoint`. - """ - self._end_devices = safe_remove(self._end_devices, end_device) - return self + equipment: MridCollection[Equipment] = LazyMridList( + _equipment, + "An Equipment", + ) - def clear_end_devices(self) -> UsagePoint: - """ - Clear all end_devices. - Returns A reference to this `UsagePoint` to allow fluent use. - """ - self._end_devices = None - return self def num_equipment(self): """ @@ -225,3 +131,60 @@ def clear_equipment(self) -> UsagePoint: """ self._equipment = None return self + + # region deprecated list boilerplate + # region end_devices boilerplate + + @deprecated("Use len(obj.end_devices) instead.") + def num_end_devices(self): + return len(self.end_devices) + + @deprecated("Use obj.end_devices.get_by_mrid(mrid) instead.") + def get_end_device(self, mrid: str) -> EndDevice: + return self.end_devices.get_by_mrid(mrid) + + @deprecated("Use obj.end_devices.append(end_device) instead.") + def add_end_device(self, end_device: EndDevice) -> UsagePoint: + self.end_devices.append(end_device) + return self + + @deprecated("Use obj.end_devices.remove(end_device) instead.") + def remove_end_device(self, end_device: EndDevice) -> UsagePoint: + self.end_devices.remove(end_device) + return self + + @deprecated("Use obj.end_devices.clear() instead.") + def clear_end_devices(self) -> UsagePoint: + self.end_devices.clear() + return self + + # endregion end_devices boilerplate + + # region equipment boilerplate + + @deprecated("Use len(obj.equipment) instead.") + def num_equipment(self): + return len(self.equipment) + + @deprecated("Use obj.equipment.get_by_mrid(mrid) instead.") + def get_equipment(self, mrid: str) -> Equipment: + return self.equipment.get_by_mrid(mrid) + + @deprecated("Use obj.equipment.append(equipment) instead.") + def add_equipment(self, equipment: Equipment) -> UsagePoint: + self.equipment.append(equipment) + return self + + @deprecated("Use obj.equipment.remove(equipment) instead.") + def remove_equipment(self, equipment: Equipment) -> UsagePoint: + self.equipment.remove(equipment) + return self + + @deprecated("Use obj.equipment.clear() instead.") + def clear_equipment(self) -> UsagePoint: + self.equipment.clear() + return self + + # endregion equipment boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py index ec66ae712..5b79d57ee 100644 --- a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py +++ b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py @@ -8,10 +8,14 @@ __all__ = ["OperationalRestriction"] from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment @@ -29,66 +33,40 @@ class OperationalRestriction(Document): They then apply operational restrictions in the operational systems to warn operators of potential problems. After appropriate inspection and maintenance, the operational restrictions may be removed. """ - _equipment: Optional[List[Equipment]] = None + _equipment: Optional[List[Equipment]] = field(default=None) - def __init__(self, *args, equipment: List[Equipment] = None, **kwargs): - super(OperationalRestriction, self).__init__(*args, **kwargs) - if equipment: - for eq in equipment: - self.add_equipment(eq) + equipment: MridCollection[Equipment] = LazyMridList( + _equipment, + "An Equipment", + ) - @property - def equipment(self) -> Generator[Equipment, None, None]: - """ - The `Equipment` to which this `OperationalRestriction` applies. - """ - return ngen(self._equipment) + # region deprecated list boilerplate + # region equipment boilerplate + + @deprecated("Use len(obj.equipment) instead.") def num_equipment(self): - """ - Returns the number of `Equipment` associated with this `OperationalRestriction` - """ - return nlen(self._equipment) + return len(self.equipment) + @deprecated("Use obj.equipment.get_by_mrid(mrid) instead.") def get_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `OperationalRestriction` identified by `mrid` - - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._equipment, mrid) + return self.equipment.get_by_mrid(mrid) + @deprecated("Use obj.equipment.append(equipment) instead.") def add_equipment(self, equipment: Equipment) -> OperationalRestriction: - """ - Associate an `Equipment` with this `OperationalRestriction` - - `equipment` The `Equipment` to associate with this `OperationalRestriction`. - Returns A reference to this `OperationalRestriction` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `OperationalRestriction`. - """ - if self._validate_reference(equipment, self.get_equipment, "An Equipment"): - return self - self._equipment = list() if self._equipment is None else self._equipment - self._equipment.append(equipment) + self.equipment.append(equipment) return self + @deprecated("Use obj.equipment.remove(equipment) instead.") def remove_equipment(self, equipment: Equipment) -> OperationalRestriction: - """ - Disassociate `equipment` from this `OperationalRestriction`. - - `equipment` The `Equipment` to disassociate from this `OperationalRestriction`. - Returns A reference to this `OperationalRestriction` to allow fluent use. - Raises `ValueError` if `equipment` was not associated with this `OperationalRestriction`. - """ - self._equipment = safe_remove(self._equipment, equipment) + self.equipment.remove(equipment) return self + @deprecated("Use obj.equipment.clear() instead.") def clear_equipment(self) -> OperationalRestriction: - """ - Clear all equipment. - Returns A reference to this `OperationalRestriction` to allow fluent use. - """ - self._equipment = None + self.equipment.clear() return self + + # endregion equipment boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py index 8c65ffb3e..e8370543a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py @@ -9,10 +9,14 @@ from typing import Generator, Optional, List, TYPE_CHECKING, Iterable from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction @@ -25,71 +29,41 @@ class Sensor(AuxiliaryEquipment, metaclass=ABCMeta): used in control or be recorded. """ - _relay_functions: Optional[List[ProtectionRelayFunction]] = None + _relay_functions: Optional[List[ProtectionRelayFunction]] = field(default=None) """The relay functions influenced by this [Sensor].""" - def __init__(self, *args, relay_functions: Iterable[ProtectionRelayFunction] = None, **kwargs): - super(Sensor, self).__init__(*args, **kwargs) - if relay_functions is not None: - for relay_function in relay_functions: - self.add_relay_function(relay_function) + relay_functions: MridCollection[ProtectionRelayFunction] = LazyMridList( + _relay_functions, + "A ProtectionRelayFunction", + ) - @property - def relay_functions(self) -> Generator[ProtectionRelayFunction, None, None]: - """ - Yields all the :class:`ProtectionRelayFunction` that are influenced by this :class:`Sensor`. - :return: A generator that iterates over all ProtectionRelayFunction influenced by this Sensor. - """ - return ngen(self._relay_functions) + # region deprecated list boilerplate + # region relay_functions boilerplate + @deprecated("Use len(obj.relay_functions) instead.") def num_relay_functions(self) -> int: - """ - Get the number of :class:`ProtectionRelayFunction` that are influenced by this :class:`Sensor`. - - :return: The number of ProtectionRelayFunction influenced by this Sensor. - """ - return nlen(self._relay_functions) + return len(self.relay_functions) + @deprecated("Use obj.relay_functions.get_by_mrid(mrid) instead.") def get_relay_function(self, mrid: str) -> ProtectionRelayFunction: - """ - Get a :class:`ProtectionRelayFunction` that are influenced by this :class:`Sensor`. - - :param mrid: The mRID of the desired ProtectionRelayFunction - :return: The ProtectionRelayFunction with the specified mRID if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._relay_functions, mrid) + return self.relay_functions.get_by_mrid(mrid) + @deprecated("Use obj.relay_functions.append(protection_relay_function) instead.") def add_relay_function(self, protection_relay_function: ProtectionRelayFunction) -> Sensor: - """ - Associate this :class:`Sensor` with a :class:`ProtectionRelayFunction` it is influencing. - - :param protection_relay_function: The ProtectionRelayFunction to associate with this Sensor. - :return: A reference to this Sensor for fluent use. - """ - if self._validate_reference(protection_relay_function, self.get_relay_function, "A ProtectionRelayFunction"): - return self - - self._relay_functions = list() if self._relay_functions is None else self._relay_functions - self._relay_functions.append(protection_relay_function) + self.relay_functions.append(protection_relay_function) return self + @deprecated("Use obj.relay_functions.remove(protection_relay_function) instead.") def remove_relay_function(self, protection_relay_function: ProtectionRelayFunction) -> Sensor: - """ - Disassociate this :class:`Sensor` from a :class:`ProtectionRelayFunction` it is influencing. - - :param protection_relay_function: The ProtectionRelayFunction to disassociate from this Sensor. - :return: A reference to this Sensor for fluent use. - """ - self._relay_functions = safe_remove(self._relay_functions, protection_relay_function) + self.relay_functions.remove(protection_relay_function) return self + @deprecated("Use obj.relay_functions.clear() instead.") def clear_relay_function(self) -> Sensor: - """ - Disassociate all :class:`ProtectionRelayFunction` from this :class:`Sensor`. - - :return: A reference to this Sensor for fluent use. - """ - self._relay_functions = None + self.relay_functions.clear() return self + + # endregion relay_functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index 73d4ab90b..821e87af8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -5,22 +5,53 @@ __all__ = ["Curve"] +from dataclasses import field from typing import Optional, List, Generator from abc import ABCMeta +from typing_extensions import deprecated + +from zepben.ewb import remove_descriptor_annotations, zb_dataclass +from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList from zepben.ewb.model.cim.iec61970.base.core.curve_data import CurveData from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import require, ngen, nlen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +class CurveDataList(LazyValidatedList[CurveData]): + + def get(self, x: float) -> CurveData: + """ + Get the :class:`CurveData` identified by its `x_value`. + + :param x: The X value of the required :class:`CurveData`. + :returns: The :class:`CurveData` with the specified `x` if it exists. + :raises KeyError: When no `CurveData` was found with `x`. + """ + curve_data = next((it for it in self if it.x_value == x), None) + if curve_data: + return curve_data + raise KeyError(x) + + def remove_data_at(self, x: float) -> CurveData: + """ + Disassociate a :class:`CurveData` from this collection based on its `x_value`. + + :param x: The :class:`CurveData` to disassociate from this :class:`Curve`. + :returns: A reference to the removed :class:`CurveData`. + :raises IndexError: If no :class:`CurveData` with a value of `x` was not associated with this :class:`Curve`. + """ + data = self.get(x) + self.remove(data) + return data + @zb_dataclass class Curve(IdentifiedObject, metaclass=ABCMeta): """ The Curve class is a multipurpose functional relationship between an independent variable (X-axis) and dependent (Y-axis) variables. """ - _data: Optional[List[CurveData]] = None + _data: list[CurveData] | None = field(default=None) def __init__(self, *args, data: List[CurveData] = None, **kwargs): """ @@ -31,98 +62,68 @@ def __init__(self, *args, data: List[CurveData] = None, **kwargs): for curve_data in data: self.add_curve_data(curve_data) - @property - def data(self) -> Generator[CurveData, None, None]: - """ - The point data values that define this curve, sorted by `x_value` in ascending order. - """ - return ngen(self._data) + data: CurveDataList[CurveData] = CurveDataList( + _data, + validate=lambda self, it: self._validate_data(it), + sort_by=lambda it: it.x_value + ) - def num_data(self): - """Return the number of :class:`CurveData` associated with this :class:`Curve`.""" - return nlen(self._data) + def _validate_data(self, curve_data: CurveData): + require(all([it.x_value != curve_data.x_value for it in self.data]), + lambda: f"Unable to add datapoint to {self}. x_value {curve_data.x_value} is invalid, as data with same x_value already exist in this Curve.") - def get_data(self, x: float) -> CurveData: - """ - Get the :class:`CurveData` for this :class:`Curve` identified by its `x_value`. - :param x: The X value of the required :class:`CurveData`. - :returns: The :class:`CurveData` with the specified `x` if it exists. - :raises KeyError: When no `CurveData` was found with `x`. - """ - if self._data: - curve_data = next((it for it in self._data if it.x_value == x), None) - if curve_data: - return curve_data - raise KeyError(x) + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. - def __getitem__(self, x: float) -> CurveData: - """ - Get the :class:`CurveData` for this :class:`Curve` identified by its `x_value`. - - :param x: The X value of the required :class:`CurveData`. - :returns: The :class:`CurveData` with the specified `x` if it exists. - :raises IndexError: When no `CurveData` was found with `x`. - """ - return self.get_data(x) - - def add_data(self, x: float, y1: float, y2: Optional[float], y3: Optional[float]) -> 'Curve': - """ - Add a data point to this :class:`Curve`. + # region data boilerplate - :param x: The data value of the X-axis variable, depending on the X-axis units. - :param y1: The data value of the first Y-axis variable, depending on the Y-axis units. - :param y2: The data value of the second Y-axis variable (if present), depending on the Y-axis units. - :param y3: The data value of the third Y-axis variable (if present), depending on the Y-axis units. - :raises ValueError: if a :class:`CurveData` for the provided `x` value already exists for this :class:`Curve`. - """ - require(all([it.x_value != x for it in self.data]), - lambda: f"Unable to add datapoint to {self}. x_value {x} is invalid, as data with same x_value already exist in this Curve.") + @deprecated("Use len(data) instead.") + def num_data(self): + return len(self.data) - if self._data is None: - self._data = [] - self._data.append(CurveData(x, y1, y2, y3)) - self._data.sort(key=lambda it: it.x_value) + @deprecated("Use data.get(x) instead.") + def get_data(self, x: float) -> CurveData: + return self.data.get(x) + @deprecated("Use data.get(x) instead.") + def __getitem__(self, x: float) -> CurveData: + return self.data.get(x) + + @deprecated("Use data.append(CurveData(x, y1, y2, y3)) instead.") + def add_data( + self, + x: float, + y1: float, + y2: Optional[float], + y3: Optional[float], + ) -> "Curve": + self.data.append(CurveData(x, y1, y2, y3)) return self - def add_curve_data(self, curve_data: CurveData) -> 'Curve': - """ - Associate a :class:`CurveData` with this :class:`Curve`. - - :param curve_data: The :class:`CurveData` to associate with this :class:`Curve`. - :returns: A reference to this :class:`Curve` to allow fluent use. - :raises ValueError: If another :class:`CurveData` with the same `x_value` already exists for this :class:`Curve`. - """ - return self.add_data(curve_data.x_value, curve_data.y1_value, curve_data.y2_value, curve_data.y3_value) - - def remove_data(self, curve_data: CurveData) -> 'Curve': - """ - Disassociate a :class:`CurveData` from this :class:`Curve`. + @deprecated("Use data.append(curve_data) instead.") + def add_curve_data(self, curve_data: CurveData) -> "Curve": + self.data.append(curve_data) + return self - :param curve_data: The :class:`CurveData` to disassociate from this :class:`Curve`. - :returns: A reference to this :class:`Curve` to allow fluent use. - :raises ValueError: If `curve_data` was not associated with this :class:`Curve`. - """ - self._data = safe_remove(self._data, curve_data) + @deprecated("Use data.remove(curve_data) instead.") + def remove_data(self, curve_data: CurveData) -> "Curve": + self.data.remove(curve_data) return self + @deprecated("Use data.remove_data_at(x) instead.") def remove_data_at(self, x: float) -> CurveData: - """ - Disassociate a :class:`CurveData` from this :class:`Curve` based on its `x_value`. - - :param x: The :class:`CurveData` to disassociate from this :class:`Curve`. - :returns: A reference to the removed :class:`CurveData`. - :raises IndexError: If no :class:`CurveData` with a value of `x` was not associated with this :class:`Curve`. - """ - data = self.get_data(x) - self._data = safe_remove(self._data, data) - return data + return self.data.remove_data_at(x) - def clear_data(self) -> 'Curve': - """ - Clear all :class:`CurveData` associated with this :class:`Curve`. - :returns: A reference to this :class:`Curve` to allow fluent use. - """ - self._data = None + @deprecated("Use data.clear() instead.") + def clear_data(self) -> "Curve": + self.data.clear() return self + + # endregion + + # endregion \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index b5443e4bf..fd41ad360 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -10,6 +10,8 @@ import datetime from typing import Optional, Generator, List, TYPE_CHECKING, TypeVar, Type from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_substation import LvSubstation @@ -19,6 +21,8 @@ from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.metering.usage_point import UsagePoint @@ -42,25 +46,32 @@ class Equipment(PowerSystemResource, metaclass=ABCMeta): commissioned_date: Optional[datetime.datetime] = None """The date this equipment was commissioned into service.""" - _usage_points: Optional[List[UsagePoint]] = None - _equipment_containers: Optional[List[EquipmentContainer]] = None - _operational_restrictions: Optional[List[OperationalRestriction]] = None - _current_containers: Optional[List[EquipmentContainer]] = None - - def __init__(self, *args, usage_points: List[UsagePoint] = None, equipment_containers: List[EquipmentContainer] = None, operational_restrictions: List[OperationalRestriction] = None, current_containers: List[EquipmentContainer] = None, **kwargs): - super(Equipment, self).__init__(*args, **kwargs) - if usage_points: - for up in usage_points: - self.add_usage_point(up) - if equipment_containers: - for container in equipment_containers: - self.add_container(container) - if operational_restrictions: - for restriction in operational_restrictions: - self.add_operational_restriction(restriction) - if current_containers: - for cf in current_containers: - self.add_current_container(cf) + _usage_points: Optional[List[UsagePoint]] = field(default=None) + _equipment_containers: Optional[List[EquipmentContainer]] = field(default=None) + _operational_restrictions: Optional[List[OperationalRestriction]] = field(default=None) + _current_containers: Optional[List[EquipmentContainer]] = field(default=None) + + containers: MridCollection[EquipmentContainer] = LazyMridList( + _equipment_containers, + "An EquipmentContainer", + ) + equipment_containers = Alias(containers) + + current_containers: MridCollection[EquipmentContainer] = LazyMridList( + _current_containers, + "A current EquipmentContainer", + ) + + usage_points: MridCollection[UsagePoint] = LazyMridList( + _usage_points, + "A UsagePoint", + ) + + operational_restrictions: MridCollection[OperationalRestriction] = LazyMridList( + _operational_restrictions, + "An OperationalRestriction", + ) + @property def sites(self) -> Generator['Site', None, None]: @@ -129,12 +140,6 @@ def current_lv_feeders(self) -> Generator[LvFeeder, None, None]: """ return ngen(_of_type(self._current_containers, LvFeeder)) - @property - def containers(self) -> Generator[EquipmentContainer, None, None]: - """ - The `EquipmentContainer`s this equipment belongs to. - """ - return ngen(self._equipment_containers) def num_sites(self) -> int: """ @@ -142,235 +147,126 @@ def num_sites(self) -> int: """ return len(list(self.sites)) - @property - def current_containers(self) -> Generator[EquipmentContainer, None, None]: - """ - The `EquipmentContainer`s this equipment belongs to in the current state of the network. - """ - return ngen(self._current_containers) - @property - def usage_points(self) -> Generator[UsagePoint, None, None]: - """ - The `UsagePoint`s for this equipment. - """ - return ngen(self._usage_points) - - @property - def operational_restrictions(self) -> Generator[OperationalRestriction, None, None]: - """ - The `OperationalRestriction`s that this equipment is associated with. - """ - return ngen(self._operational_restrictions) + # region deprecated list boilerplate + # region containers boilerplate + @deprecated("Use len(obj.containers) instead.") def num_containers(self) -> int: - """ - Returns The number of `EquipmentContainer`s associated with this `Equipment` - """ - return nlen(self._equipment_containers) + return len(self.containers) + @deprecated("Use len(obj.containers) instead.") def num_substations(self) -> int: - """ - Returns The number of `zepben.ewb.model.cim.iec61970.base.core.substation.Substation`s associated with this `Equipment` - """ - return len(list(_of_type(self._equipment_containers, Substation))) + return len(self.containers) + @deprecated("Use len(obj.containers) instead.") def num_normal_feeders(self) -> int: - """ - Returns The number of normal `Feeder`s associated with this `Equipment` - """ - return len(list(_of_type(self._equipment_containers, Feeder))) + return len(self.containers) + @deprecated("Use obj.containers.get_by_mrid(mrid) instead.") def get_container(self, mrid: str) -> EquipmentContainer: - """ - Get the `EquipmentContainer` for this `Equipment` identified by `mrid` - - `mrid` The mRID of the required `EquipmentContainer` - Returns The `EquipmentContainer` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._equipment_containers, mrid) + return self.containers.get_by_mrid(mrid) + @deprecated("Use obj.containers.append(ec) instead.") def add_container(self, ec: EquipmentContainer) -> Equipment: - """ - Associate an `EquipmentContainer` with this `Equipment` - - `ec` The `EquipmentContainer` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `EquipmentContainer` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(ec, self.get_container, "An EquipmentContainer"): - return self - self._equipment_containers = list() if self._equipment_containers is None else self._equipment_containers - self._equipment_containers.append(ec) + self.containers.append(ec) return self + @deprecated("Use obj.containers.remove(ec) instead.") def remove_container(self, ec: EquipmentContainer) -> Equipment: - """ - Disassociate `ec` from this `Equipment`. - - `ec` The `EquipmentContainer` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `ec` was not associated with this `Equipment`. - """ - self._equipment_containers = safe_remove(self._equipment_containers, ec) + self.containers.remove(ec) return self + @deprecated("Use obj.containers.clear() instead.") def clear_containers(self) -> Equipment: - """ - Clear all equipment. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._equipment_containers = None + self.containers.clear() return self + # endregion containers boilerplate + + # region current_containers boilerplate + + @deprecated("Use len(obj.current_containers) instead.") def num_current_containers(self) -> int: - """ - Returns The number of `EquipmentContainer`s associated with this `Equipment` - """ - return nlen(self._current_containers) + return len(self.current_containers) + @deprecated("Use obj.current_containers.get_by_mrid(mrid) instead.") def get_current_container(self, mrid: str) -> EquipmentContainer: - """ - Get the `EquipmentContainer` for this `Equipment` in the current state of the network, identified by `mrid` - - `mrid` The mRID of the required `EquipmentContainer` - Returns The `EquipmentContainer` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._current_containers, mrid) + return self.current_containers.get_by_mrid(mrid) + @deprecated("Use obj.current_containers.append(equipment_container) instead.") def add_current_container(self, equipment_container: EquipmentContainer) -> Equipment: - """ - Associate `equipment_container` with this `Equipment` in the current state of the network. - - `equipment_container` The `EquipmentContainer` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `EquipmentContainer` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(equipment_container, self.get_current_container, "A current EquipmentContainer"): - return self - self._current_containers = list() if self._current_containers is None else self._current_containers - self._current_containers.append(equipment_container) + self.current_containers.append(equipment_container) return self + @deprecated("Use obj.current_containers.remove(equipment_container) instead.") def remove_current_container(self, equipment_container: EquipmentContainer) -> Equipment: - """ - Disassociate `equipment_container` from this `Equipment` in the current state of the network. - - `equipment_container` The `EquipmentContainer` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `equipment_container` was not associated with this `Equipment`. - """ - self._current_containers = safe_remove(self._current_containers, equipment_container) + self.current_containers.remove(equipment_container) return self + @deprecated("Use obj.current_containers.clear() instead.") def clear_current_containers(self) -> Equipment: - """ - Clear all current `EquipmentContainer`s in the current state of the network. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._current_containers = None + self.current_containers.clear() return self + # endregion current_containers boilerplate + + # region usage_points boilerplate + + @deprecated("Use len(obj.usage_points) instead.") def num_usage_points(self) -> int: - """ - Returns The number of `UsagePoint`s associated with this `Equipment` - """ - return nlen(self._usage_points) + return len(self.usage_points) + @deprecated("Use obj.usage_points.get_by_mrid(mrid) instead.") def get_usage_point(self, mrid: str) -> UsagePoint: - """ - Get the `UsagePoint` for this `Equipment` identified by `mrid` - - `mrid` The mRID of the required `UsagePoint` - Returns The `UsagePoint` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._usage_points, mrid) + return self.usage_points.get_by_mrid(mrid) + @deprecated("Use obj.usage_points.append(up) instead.") def add_usage_point(self, up: UsagePoint) -> Equipment: - """ - Associate `up` with this `Equipment`. - - `up` the `UsagePoint` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `UsagePoint` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(up, self.get_usage_point, "A UsagePoint"): - return self - self._usage_points = list() if self._usage_points is None else self._usage_points - self._usage_points.append(up) + self.usage_points.append(up) return self + @deprecated("Use obj.usage_points.remove(up) instead.") def remove_usage_point(self, up: UsagePoint) -> Equipment: - """ - Disassociate `up` from this `Equipment`. - - `up` The `UsagePoint` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `Equipment`. - """ - self._usage_points = safe_remove(self._usage_points, up) + self.usage_points.remove(up) return self + @deprecated("Use obj.usage_points.clear() instead.") def clear_usage_points(self) -> Equipment: - """ - Clear all usage_points. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._usage_points = None + self.usage_points.clear() return self + # endregion usage_points boilerplate + + # region operational_restrictions boilerplate + + @deprecated("Use len(obj.operational_restrictions) instead.") def num_operational_restrictions(self) -> int: - """ - Returns The number of `OperationalRestriction`s associated with this `Equipment` - """ - return nlen(self._operational_restrictions) + return len(self.operational_restrictions) + @deprecated("Use obj.operational_restrictions.get_by_mrid(mrid) instead.") def get_operational_restriction(self, mrid: str) -> OperationalRestriction: - """ - Get the `OperationalRestriction` for this `Equipment` identified by `mrid` - - `mrid` The mRID of the required `OperationalRestriction` - Returns The `OperationalRestriction` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._operational_restrictions, mrid) + return self.operational_restrictions.get_by_mrid(mrid) + @deprecated("Use obj.operational_restrictions.append(op) instead.") def add_operational_restriction(self, op: OperationalRestriction) -> Equipment: - """ - Associate `op` with this `Equipment`. - - `op` The `OperationalRestriction` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `OperationalRestriction` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(op, self.get_operational_restriction, "An OperationalRestriction"): - return self - self._operational_restrictions = list() if self._operational_restrictions is None else self._operational_restrictions - self._operational_restrictions.append(op) + self.operational_restrictions.append(op) return self + @deprecated("Use obj.operational_restrictions.remove(op) instead.") def remove_operational_restriction(self, op: OperationalRestriction) -> Equipment: - """ - Disassociate `up` from this `Equipment`. - - `op` The `OperationalRestriction` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `op` was not associated with this `Equipment`. - """ - self._operational_restrictions = safe_remove(self._operational_restrictions, op) + self.operational_restrictions.remove(op) return self + @deprecated("Use obj.operational_restrictions.clear() instead.") def clear_operational_restrictions(self) -> Equipment: - """ - Clear all `OperationalRestrictions`. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._operational_restrictions = None + self.operational_restrictions.clear() return self + # endregion operational_restrictions boilerplate + + # endregion deprecated list boilerplate + def _of_type(containers: Optional[List[EquipmentContainer]], ectype: Type[TEquipmentContainer]) -> Generator[TEquipmentContainer, None, None]: yield from (ec for ec in containers if isinstance(ec, ectype)) if containers is not None else {} diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py index 44c2f5172..481f2c70e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py @@ -7,9 +7,15 @@ __all__ = ['EquipmentContainer'] +from dataclasses import field from typing import Optional, Dict, Generator, List, TYPE_CHECKING, TypeVar, Iterable, Type from abc import ABCMeta +from typing_extensions import deprecated + +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import ConnectivityNodeContainer from zepben.ewb.util import nlen, ngen, safe_remove_by_id from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @@ -32,28 +38,17 @@ class EquipmentContainer(ConnectivityNodeContainer, metaclass=ABCMeta): Unless overridden, all functions operating on currentEquipment simply operate on the equipment collection. i.e. currentEquipment = equipment """ - _equipment: Optional[Dict[str, Equipment]] = None + _equipment_by_id: Dict[str, Equipment] | None = field(default=None) """Map of Equipment in this EquipmentContainer by their mRID""" - def __init__(self, *args, equipment: List[Equipment] = None, **kwargs): - super(EquipmentContainer, self).__init__(*args, **kwargs) - if equipment: - for eq in equipment: - self.add_equipment(eq) + equipment: MridCollection[Equipment] = LazyMridMap( + _equipment_by_id, + "An Equipment", + ) + """The `Equipment` contained in this `EquipmentContainer`""" - @property - def equipment(self) -> Generator[Equipment, None, None]: - """ - The `Equipment` contained in this `EquipmentContainer` - """ - return ngen(self._equipment) - - @property - def current_equipment(self) -> Generator[Equipment, None, None]: - """ - Contained `Equipment` using the current state of the network. - """ - return self.equipment + current_equipment = Alias(equipment) + """Contained `Equipment` using the current state of the network.""" def current_feeders(self) -> Generator[Feeder, None, None]: """ @@ -61,7 +56,7 @@ def current_feeders(self) -> Generator[Feeder, None, None]: Returns the current feeders for all associated feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.current_feeders: if f not in seen: seen.add(f.mrid) @@ -73,7 +68,7 @@ def normal_feeders(self) -> Generator[Feeder, None, None]: Returns the normal feeders for all associated feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.normal_feeders: if f not in seen: seen.add(f.mrid) @@ -85,7 +80,7 @@ def current_lv_feeders(self) -> Generator[LvFeeder, None, None]: Returns the normal LV feeders for all associated LV feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.current_lv_feeders: if f not in seen: seen.add(f.mrid) @@ -97,7 +92,7 @@ def normal_lv_feeders(self) -> Generator[LvFeeder, None, None]: Returns the normal LV feeders for all associated LV feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.normal_lv_feeders: if f not in seen: seen.add(f.mrid) @@ -146,103 +141,59 @@ def edge_terminals(self, state_operator: 'Type[NetworkStateOperators]' = None) - seen.add(ct.from_terminal) yield ct.from_terminal + + + # region deprecated list boilerplate + # region equipment boilerplate + + @deprecated("Use len(self.equipment) instead") def num_equipment(self): - """ - Returns The number of `Equipment` associated with this `EquipmentContainer` - """ - return nlen(self._equipment) + return len(self.equipment) + @deprecated("Use self.equipment.get_by_mrid(mrid) instead") def get_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `EquipmentContainer` identified by `mrid` - - `mrid` the mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - if not self._equipment: - raise KeyError(mrid) - try: - return self._equipment[mrid] - except AttributeError: - raise KeyError(mrid) + return self.equipment.get_by_mrid(mrid) + @deprecated("Use equipment.append(equipment) instead") def add_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Associate `equipment` with this `EquipmentContainer`. - - `equipment` The `Equipment` to associate with this `EquipmentContainer`. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `EquipmentContainer`. - """ - if self._validate_reference(equipment, self.get_equipment, "An Equipment"): - return self - if self._equipment is None: - self._equipment = dict() - self._equipment[equipment.mrid] = equipment + self.equipment.append(equipment) return self + @deprecated("Use equipment.remove(equipment) instead") def remove_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Disassociate `equipment` from this `EquipmentContainer` - - `equipment` The `Equipment` to disassociate with this `EquipmentContainer`. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `KeyError` if `equipment` was not associated with this `EquipmentContainer`. - """ - self._equipment = safe_remove_by_id(self._equipment, equipment) + self.equipment.remove(equipment) return self + @deprecated("Use equipment.clear() instead") def clear_equipment(self) -> EquipmentContainer: - """ - Clear all equipment. - Returns A reference to this `EquipmentContainer` to allow fluent use. - """ - self._equipment = None + self.equipment.clear() return self - def num_current_equipment(self) -> int: - """ - Returns The number of `Equipment` contained in this `EquipmentContainer` in the current state of the network. - """ - return self.num_equipment() + # endregion + # region current_equipment boilerplate - def get_current_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` contained in this `EquipmentContainer` in the current state of the network, identified by `mrid` + @deprecated("Use len(current_equipment) instead") + def num_current_equipment(self): + return len(self.current_equipment) - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return self.get_equipment(mrid) + @deprecated("Use current_equipment.get_by_mrid(mrid) instead") + def get_current_equipment(self, mrid: str) -> Equipment: + return self.current_equipment.get_by_mrid(mrid) + @deprecated("Use current_equipment.append(equipment) instead") def add_current_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Associate `equipment` with this `EquipmentContainer` in the current state of the network. - - `equipment` the `Equipment` to associate with this `EquipmentContainer` in the current state of the network. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `EquipmentContainer`. - """ - self.add_equipment(equipment) + self.current_equipment.append(equipment) return self + @deprecated("Use current_equipment.remove(equipment) instead") def remove_current_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Disassociate `equipment` from this `EquipmentContainer` in the current state of the network. - - `equipment` The `Equipment` to disassociate from this `EquipmentContainer` in the current state of the network. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `KeyError` if `equipment` was not associated with this `EquipmentContainer`. - """ - self.remove_equipment(equipment) + self.current_equipment.remove(equipment) return self + @deprecated("Use current_equipment.clear() instead") def clear_current_equipment(self) -> EquipmentContainer: - """ - Clear all `Equipment` from this `EquipmentContainer` in the current state of the network. - Returns A reference to this `EquipmentContainer` to allow fluent use. - """ - self.clear_equipment() + self.current_equipment.clear() return self + + # endregion + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 8ca0e5aab..bcc00115e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -7,11 +7,14 @@ __all__ = ["Feeder"] +from dataclasses import field from typing import Optional, Dict, List, Generator, TYPE_CHECKING from typing_extensions import deprecated -from zepben.ewb import get_by_mrid +from zepben.ewb import get_by_mrid, remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.util import ngen, nlen, safe_remove_by_id @@ -35,52 +38,21 @@ class Feeder(EquipmentContainer): _normal_head_terminal: Terminal | None = None """The normal head terminal or terminals of the feeder.""" - _normal_energizing_substation: Substation | None = None + normal_energizing_substation: Substation | None = None + """The substation that normally energizes the feeder. Also used for naming purposes.""" - _current_equipment: Dict[str, Equipment] | None = None + _current_equipment_by_id: Dict[str, Equipment] | None = field(default=None) """The equipment contained in this feeder in the current state of the network.""" - _normal_energized_lv_feeders: Dict[str, LvFeeder] | None = None + _normal_energized_lv_feeders_by_id: Dict[str, LvFeeder] | None = field(default=None) """The LV feeders that are energized by this feeder in the normal state of the network.""" - _current_energized_lv_feeders: Dict[str, LvFeeder] | None = None + _current_energized_lv_feeders_by_id: Dict[str, LvFeeder] | None = field(default=None) """The LV feeders that are energized by this feeder in the current state of the network.""" - _normal_energized_lv_substations: Dict[str, 'LvSubstation'] | None = None - _current_energized_lv_substations: Dict[str, 'LvSubstation'] | None = None - - def __init__( - self, - *args, - normal_head_terminal: Terminal = None, - normal_energizing_substation: Substation = None, - current_equipment: List[Equipment] = None, - normal_energized_lv_feeders: List[LvFeeder] = None, - current_energized_lv_feeders: List[LvFeeder] = None, - normal_energized_lv_substations: List[LvSubstation] = None, - current_energized_lv_substations: List[LvSubstation] = None, - **kwargs, - ): - super(Feeder, self).__init__(*args, **kwargs) - if normal_head_terminal: - self.normal_head_terminal = normal_head_terminal - if normal_energizing_substation: - self.normal_energizing_substation = normal_energizing_substation - if normal_energized_lv_feeders: - for lv_feeder in normal_energized_lv_feeders: - self.add_normal_energized_lv_feeder(lv_feeder) - if current_equipment: - for eq in current_equipment: - self.add_current_equipment(eq) - if current_energized_lv_feeders: - for lv_feeder in current_energized_lv_feeders: - self.add_current_energized_lv_feeder(lv_feeder) - if normal_energized_lv_substations: - for lv_substation in normal_energized_lv_substations: - self.add_normal_energized_lv_substation(lv_substation) - if current_energized_lv_substations: - for lv_substation in current_energized_lv_substations: - self.add_current_energized_lv_substation(lv_substation) + _normal_energized_lv_substations_by_id: Dict[str, 'LvSubstation'] | None = field(default=None) + _current_energized_lv_substations_by_id: Dict[str, 'LvSubstation'] | None = field(default=None) + @property def normal_head_terminal(self) -> Optional[Terminal]: @@ -94,6 +66,35 @@ def normal_head_terminal(self, term: Optional[Terminal]): else: raise ValueError(f"Feeder {self.mrid} has equipment assigned to it. Cannot update normalHeadTerminal on a feeder with equipment assigned.") + current_equipment: MridCollection[Equipment] = LazyMridMap( + _current_equipment_by_id, + "A current Equipment", + ) + """Contained `Equipment` using the current state of the network.""" + + normal_energized_lv_feeders: MridCollection[LvFeeder] = LazyMridMap( + _normal_energized_lv_feeders_by_id, + "An LvFeeder", + ) + """The LV feeders that are normally energized by this feeder.""" + + current_energized_lv_feeders: MridCollection[LvFeeder] = LazyMridMap( + _current_energized_lv_feeders_by_id, + "An LvFeeder", + ) + """[ZBEX] The LV feeders that are currently energized by this feeder.""" + + normal_energized_lv_substations: MridCollection[LvSubstation] = LazyMridMap( + _normal_energized_lv_substations_by_id, + "An LvSubstation", + ) + """[ZBEX]""" + + current_energized_lv_substations: MridCollection[LvSubstation] = LazyMridMap( + _current_energized_lv_substations_by_id, + "An LvSubstation", + ) + """[ZBEX]""" @property def normal_energizing_substation(self): """The substation that normally energizes the feeder. Also used for naming purposes.""" @@ -111,270 +112,137 @@ def current_equipment(self) -> Generator[Equipment, None, None]: """ return ngen(self._current_equipment) - @property - def normal_energized_lv_feeders(self) -> Generator[LvFeeder, None, None]: - """ - The LV feeders that are normally energized by this feeder. - """ - return ngen(self._normal_energized_lv_feeders) - - @zbex - @property - def current_energized_lv_feeders(self) -> Generator[LvFeeder, None, None]: - """ - The LV feeders that are currently energized by this feeder. - """ - return ngen(self._current_energized_lv_feeders) - - @zbex - @property - def normal_energized_lv_substations(self) -> Generator['LvSubstation', None, None]: - return ngen(self._normal_energized_lv_substations) - @zbex - @property - def current_energized_lv_substations(self) -> Generator['LvSubstation', None, None]: - return ngen(self._current_energized_lv_substations) + # region deprecated list boilerplate + # region current_equipment boilerplate + @deprecated("Use len(current_equipment) instead") def num_current_equipment(self): - """ - :returns: The number of `Equipment` associated with this `Feeder` - """ - return nlen(self._current_equipment) + return len(self.current_equipment) + @deprecated("Use current_equipment.get_by_mrid(mrid) instead") def get_current_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `Feeder` identified by `mrid` - - `mrid` The mRID of the required `Equipment` - :returns: The `Equipment` with the specified `mrid` if it exists - :raises: `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._current_equipment, mrid) - - def add_current_equipment(self, equipment: Equipment) -> Feeder: - """ - Associate `equipment` with this `Feeder`. + return self.current_equipment.get_by_mrid(mrid) - `equipment` the `Equipment` to associate with this `Feeder`. - :returns: A reference to this `Feeder` to allow fluent use. - :raises: `ValueError` if another `Equipment` with the same `mrid` already exists for this `Feeder`. - """ - if self._validate_reference(equipment, self.get_current_equipment, "An Equipment"): - return self - self._current_equipment = dict() if self._current_equipment is None else self._current_equipment - self._current_equipment[equipment.mrid] = equipment + @deprecated("Use current_equipment.append(equipment) instead") + def add_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.append(equipment) return self - def remove_current_equipment(self, equipment: Equipment) -> Feeder: - """ - Disassociate `equipment` from this `Feeder` - - `equipment` The `Equipment` to disassociate from this `Feeder`. - :returns: A reference to this `Feeder` to allow fluent use. - :raises: `KeyError` if `equipment` was not associated with this `Feeder`. - """ - self._current_equipment = safe_remove_by_id(self._current_equipment, equipment) + @deprecated("Use current_equipment.remove(equipment) instead") + def remove_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.remove(equipment) return self - def clear_current_equipment(self) -> Feeder: - """ - Clear all equipment. - :returns: A reference to this `Feeder` to allow fluent use. - """ - self._current_equipment = None + @deprecated("Use current_equipment.clear() instead") + def clear_current_equipment(self) -> EquipmentContainer: + self.current_equipment.clear() return self - def num_normal_energized_lv_feeders(self) -> int: - """ - Get the number of LV feeders that are normally energized by this feeder. - """ - return nlen(self._normal_energized_lv_feeders) + # endregion + # region normal_energized_lv_feeders boilerplate - def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder: - """ - Energized LvFeeder in the normal state of the network. + @deprecated("Use len(normal_energized_lv_feeders) instead") + def num_normal_energized_lv_feeders(self): + return len(self.normal_energized_lv_feeders) - :param mrid: The mrid of the `LvFeeder`. - :returns: A matching `LvFeeder` that is energized by this `Feeder` in the normal state of the network. - :raise sA `KeyError` if no matching `LvFeeder` was found. - """ - return get_by_mrid(self._normal_energized_lv_feeders, mrid) + @deprecated("Use normal_energized_lv_feeders.get_by_mrid(mrid) instead") + def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder: + return self.normal_energized_lv_feeders.get_by_mrid(mrid) + @deprecated("Use normal_energized_lv_feeders.append(lv_feeder) instead") def add_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Associate this `Feeder` with an `LvFeeder` in the normal state of the network. - - :param lv_feeder: the LV feeder to associate with this feeder in the normal state of the network. - :return: This `Feeder` for fluent use. - """ - if self._validate_reference(lv_feeder, self.get_normal_energized_lv_feeder, "An LvFeeder"): - return self - self._normal_energized_lv_feeders = dict() if self._normal_energized_lv_feeders is None else self._normal_energized_lv_feeders - self._normal_energized_lv_feeders[lv_feeder.mrid] = lv_feeder + self.normal_energized_lv_feeders.append(lv_feeder) return self + @deprecated("Use normal_energized_lv_feeders.remove(lv_feeder) instead") def remove_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Disassociate this `Feeder` from an `LvFeeder` in the normal state of the network. - - :param lv_feeder: the LV feeder to disassociate from this feeder in the normal state of the network. - :return: This `Feeder` for fluent use. - :raises: A `ValueError` if `lv_feeder` is not found in the normal energized lv feeders collection. - """ - self._normal_energized_lv_feeders = safe_remove_by_id(self._normal_energized_lv_feeders, lv_feeder) + self.normal_energized_lv_feeders.remove(lv_feeder) return self + @deprecated("Use normal_energized_lv_feeders.clear() instead") def clear_normal_energized_lv_feeders(self) -> Feeder: - """ - Clear all `LvFeeder`s associated with `Feeder` in the normal state of the network. - - :return: This `Feeder` for fluent use. - """ - self._normal_energized_lv_feeders = None + self.normal_energized_lv_feeders.clear() return self - def num_current_energized_lv_feeders(self) -> int: - """ - Get the number of LV feeders that are currently energized by this feeder. - """ - return nlen(self._current_energized_lv_feeders) + # endregion + # region current_energized_lv_feeders boilerplate - def get_current_energized_lv_feeder(self, mrid: str) -> LvFeeder: - """ - Energized LvFeeder in the current state of the network. + @deprecated("Use len(current_energized_lv_feeders) instead") + def num_current_energized_lv_feeders(self): + return len(self.current_energized_lv_feeders) - :param mrid: The mrid of the `LvFeeder`. - :return: A matching `LvFeeder` that is energized by this `Feeder` in the current state of the network. - :raises: A `KeyError` if no matching `LvFeeder` was found. - """ - return get_by_mrid(self._current_energized_lv_feeders, mrid) + @deprecated("Use current_energized_lv_feeders.get_by_mrid(mrid) instead") + def get_current_energized_lv_feeder(self, mrid: str) -> LvFeeder: + return self.current_energized_lv_feeders.get_by_mrid(mrid) + @deprecated("Use current_energized_lv_feeders.append(lv_feeder) instead") def add_current_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Associate this `Feeder` with an `LvFeeder` in the current state of the network. - - :param lv_feeder: the LV feeder to associate with this feeder in the current state of the network. - :return: This `Feeder` for fluent use. - """ - if self._validate_reference(lv_feeder, self.get_current_energized_lv_feeder, "An LvFeeder"): - return self - self._current_energized_lv_feeders = dict() if self._current_energized_lv_feeders is None else self._current_energized_lv_feeders - self._current_energized_lv_feeders[lv_feeder.mrid] = lv_feeder + self.current_energized_lv_feeders.append(lv_feeder) return self + @deprecated("Use current_energized_lv_feeders.remove(lv_feeder) instead") def remove_current_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Disassociate this `Feeder` from an `LvFeeder` in the current state of the network. - - :param lv_feeder: the LV feeder to disassociate from this feeder in the current state of the network. - :return: This `Feeder` for fluent use. - :raises: A `ValueError` if `lv_feeder` is not found in the current energized lv feeders collection. - """ - self._current_energized_lv_feeders = safe_remove_by_id(self._current_energized_lv_feeders, lv_feeder) + self.current_energized_lv_feeders.remove(lv_feeder) return self + @deprecated("Use current_energized_lv_feeders.clear() instead") def clear_current_energized_lv_feeders(self) -> Feeder: - """ - Clear all `LvFeeder`s associated with `Feeder` in the current state of the network. - - :return: This `Feeder` for fluent use. - """ - self._current_energized_lv_feeders = None + self.current_energized_lv_feeders.clear() return self - def num_normal_energized_lv_substations(self) -> int: - """ - Get the number of entries in the normal [LvSubstation] collection. - """ - return nlen(self._normal_energized_lv_substations) - - def get_normal_energized_lv_substation(self, mrid: str) -> 'LvSubstation | None': - """ - Retrieve an energized LvSubstation using the normal state of the network. + # endregion + # region normal_energized_lv_substations boilerplate - :param mrid: the mRID of the required normal [LvSubstation] - :returns: The [LvSubstation] with the specified [mRID] if it exists, otherwise null - """ - return get_by_mrid(self._normal_energized_lv_substations, mrid) + @deprecated("Use len(normal_energized_lv_substations) instead") + def num_normal_energized_lv_substations(self): + return len(self.normal_energized_lv_substations) - def add_normal_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Associate this [Feeder] with a [LvSubstation] in the normal state of the network. + @deprecated("Use normal_energized_lv_substations.get_by_mrid(mrid) instead") + def get_normal_energized_lv_substation(self, mrid: str) -> LvSubstation: + return self.normal_energized_lv_substations.get_by_mrid(mrid) - :param lv_substation: the [LvSubstation] to associate with this LV feeder in the normal state of the network. - :returns: This [Feeder] for fluent use. - """ - if self._validate_reference(lv_substation, self.get_normal_energized_lv_substation, "An LvSubstation"): - return self - if self._normal_energized_lv_substations is None: - self._normal_energized_lv_substations = dict() - self._normal_energized_lv_substations[lv_substation.mrid] = lv_substation + @deprecated("Use normal_energized_lv_substations.append(lv_substation) instead") + def add_normal_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.normal_energized_lv_substations.append(lv_substation) return self - def remove_normal_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Disassociate this [Feeder] from a [LvSubstation] in the normal state of the network. - - :param lv_substation: the [LvSubstation] to disassociate from this LV feeder in the normal state of the network. - :returns: true if a matching [LvSubstation] is removed from the collection. - """ - self._normal_energized_lv_substations = safe_remove_by_id(self._normal_energized_lv_substations, lv_substation) + @deprecated("Use normal_energized_lv_substations.remove(lv_substation) instead") + def remove_normal_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.normal_energized_lv_substations.remove(lv_substation) return self - def clear_normal_energized_lv_substations(self) -> "Feeder": - """ - Clear all [LvSubstation]'s associated with this [Feeder] in the normal state of the network. - - :returns: This [Feeder] for fluent use. - """ - self._normal_energized_lv_substations = None + @deprecated("Use normal_energized_lv_substations.clear() instead") + def clear_normal_energized_lv_substations(self) -> Feeder: + self.normal_energized_lv_substations.clear() return self - def num_current_energized_lv_substations(self) -> int: - """ - Get the number of entries in the current [LvSubstation] collection. - """ - return nlen(self._current_energized_lv_substations) - - def get_current_energized_lv_substation(self, mrid: str) -> 'LvSubstation | None': - """ - Retrieve an energized LvSubstation using the current state of the network. + # endregion + # region current_energized_lv_substations boilerplate - :param mrid: the mRID of the required current [LvSubstation] - :returns: The [LvSubstation] with the specified [mRID] if it exists, otherwise null - """ - return get_by_mrid(self._current_energized_lv_substations, mrid) + @deprecated("Use len(current_energized_lv_substations) instead") + def num_current_energized_lv_substations(self): + return len(self.current_energized_lv_substations) - def add_current_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Associate this [Feeder] with a [LvSubstation] in the current state of the network. + @deprecated("Use current_energized_lv_substations.get_by_mrid(mrid) instead") + def get_current_energized_lv_substation(self, mrid: str) -> LvSubstation: + return self.current_energized_lv_substations.get_by_mrid(mrid) - :param lv_substation: the [LvSubstation] to associate with this LV feeder in the current state of the network. - :returns: This [Feeder] for fluent use. - """ - if self._validate_reference(lv_substation, self.get_current_energized_lv_substation, "An LvSubstation"): - return self - if self._current_energized_lv_substations is None: - self._current_energized_lv_substations = dict() - self._current_energized_lv_substations[lv_substation.mrid] = lv_substation + @deprecated("Use current_energized_lv_substations.append(lv_substation) instead") + def add_current_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.current_energized_lv_substations.append(lv_substation) return self - def remove_current_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Disassociate this [Feeder] from a [LvSubstation] in the current state of the network. + @deprecated("Use current_energized_lv_substations.remove(lv_substation) instead") + def remove_current_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.current_energized_lv_substations.remove(lv_substation) + return self - :param lv_substation: the [LvSubstation] to disassociate from this LV feeder in the current state of the network. - :returns: true if a matching [LvSubstation] is removed from the collection. - """ - self._current_energized_lv_substations = safe_remove_by_id(self._current_energized_lv_substations, lv_substation) + @deprecated("Use current_energized_lv_substations.clear() instead") + def clear_current_energized_lv_substations(self) -> Feeder: + self.current_energized_lv_substations.clear() return self - def clear_current_energized_lv_substations(self) -> "Feeder": - """ - Clear all [LvSubstation]'s associated with this [Feeder] in the current state of the network. + # endregion + # endregion - :returns: This [Feeder] for fluent use. - """ - self._current_energized_lv_substations = None - return self diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index b0cf54b0a..ac5448eb2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -8,11 +8,15 @@ __all__ = ["GeographicalRegion"] from typing import Optional, List, Generator +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList @zb_dataclass @@ -20,7 +24,7 @@ class GeographicalRegion(IdentifiedObject): """ A geographical region of a power system network phases. """ - _sub_geographical_regions: Optional[List[SubGeographicalRegion]] = None + _sub_geographical_regions: Optional[List[SubGeographicalRegion]] = field(default=None) def __init__(self, *args, sub_geographical_regions: List[SubGeographicalRegion] = None, **kwargs): super(GeographicalRegion, self).__init__(*args, **kwargs) @@ -28,28 +32,12 @@ def __init__(self, *args, sub_geographical_regions: List[SubGeographicalRegion] for sgr in sub_geographical_regions: self.add_sub_geographical_region(sgr) - @property - def sub_geographical_regions(self) -> Generator[SubGeographicalRegion, None, None]: - """ - The `SubGeographicalRegion`s of this `GeographicalRegion`. - """ - return ngen(self._sub_geographical_regions) - - def num_sub_geographical_regions(self) -> int: - """ - Returns The number of `SubGeographicalRegion`s associated with this `GeographicalRegion` - """ - return nlen(self._sub_geographical_regions) - def get_sub_geographical_region(self, mrid: str) -> SubGeographicalRegion: - """ - Get the `SubGeographicalRegion` for this `GeographicalRegion` identified by `mrid` + sub_geographical_regions: MridCollection[SubGeographicalRegion] = LazyMridList( + _sub_geographical_regions, + "A SubGeographicalRegion", + ) - `mrid` The mRID of the required `SubGeographicalRegion` - Returns The `SubGeographicalRegion` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._sub_geographical_regions, mrid) def add_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: """ @@ -73,20 +61,29 @@ def add_sub_geographical_region(self, sub_geographical_region: SubGeographicalRe self._sub_geographical_regions.append(sub_geographical_region) return self + + + # region deprecated list boilerplate + # region sub_geographical_regions boilerplate + + @deprecated("Use len(obj.sub_geographical_regions) instead.") + def num_sub_geographical_regions(self) -> int: + return len(self.sub_geographical_regions) + + @deprecated("Use obj.sub_geographical_regions.get_by_mrid(mrid) instead.") + def get_sub_geographical_region(self, mrid: str) -> SubGeographicalRegion: + return self.sub_geographical_regions.get_by_mrid(mrid) + + @deprecated("Use obj.sub_geographical_regions.remove(sub_geographical_region) instead.") def remove_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: - """ - Disassociate `sub_geographical_region` from this `GeographicalRegion` - `sub_geographical_region` The `SubGeographicalRegion` to disassociate from this `GeographicalRegion`. - Returns A reference to this `GeographicalRegion` to allow fluent use. - Raises `ValueError` if `sub_geographical_region` was not associated with this `GeographicalRegion`. - """ - self._sub_geographical_regions = safe_remove(self._sub_geographical_regions, sub_geographical_region) + self.sub_geographical_regions.remove(sub_geographical_region) return self + @deprecated("Use obj.sub_geographical_regions.clear() instead.") def clear_sub_geographical_regions(self) -> GeographicalRegion: - """ - Clear all SubGeographicalRegions. - Returns A reference to this `GeographicalRegion` to allow fluent use. - """ - self._sub_geographical_regions = None + self.sub_geographical_regions.clear() return self + + # endregion sub_geographical_regions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name.py b/src/zepben/ewb/model/cim/iec61970/base/core/name.py index 9bf14b350..d457580f6 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name.py @@ -10,7 +10,6 @@ from typing import TYPE_CHECKING, Optional from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.descriptor_fix import remove_descriptor_annotations from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable if TYPE_CHECKING: @@ -19,7 +18,6 @@ @zb_dataclass -@remove_descriptor_annotations class Name(Identifiable): """ The Name class provides the means to define any number of human-readable names for an object. A name is **not** to be used for defining inter-object diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py index 78495bd0a..a15d9a1b0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable from zepben.ewb.model.cim.iec61970.base.core.name import Name from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.descriptor_fix import BackedDescriptor, remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.descriptor_fix import BackedDescriptor if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject @@ -21,7 +21,6 @@ @zb_dataclass -@remove_descriptor_annotations class NameType(Identifiable): """ Type of name. Possible values for attribute 'name' are implementation dependent but standard profiles may specify types. An enterprise may have multiple diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py index 8d97f373e..50331de93 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py @@ -9,10 +9,14 @@ from typing import Optional, TYPE_CHECKING, List, Generator, Iterable from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset import Asset @@ -38,13 +42,13 @@ class PowerSystemResource(IdentifiedObject, metaclass=ABCMeta): num_controls: Optional[int] = None """Number of Control's known to associate with this [PowerSystemResource]""" - _assets: Optional[List[Asset]] = None + _assets: Optional[List[Asset]] = field(default=None) + + assets: MridCollection[Asset] = LazyMridList( + _assets, + "An Asset", + ) - def __init__(self, *args, assets: Iterable[Asset] = None, **kwargs): - super(PowerSystemResource, self).__init__(*args, **kwargs) - if assets: - for asset in assets: - self.add_asset(asset) @property def has_controls(self) -> bool: @@ -53,57 +57,33 @@ def has_controls(self) -> bool: """ return nlen(self.num_controls) > 0 - @property - def assets(self) -> Generator[Asset, None, None]: - """ - The `Asset`s of this `PowerSystemResource`. - """ - return ngen(self._assets) + # region deprecated list boilerplate + # region assets boilerplate + + @deprecated("Use len(obj.assets) instead.") def num_assets(self) -> int: - """ - Get the number of `Asset`s associated with this `PowerSystemResource`. - """ - return nlen(self._assets) + return len(self.assets) + @deprecated("Use obj.assets.get_by_mrid(mrid) instead.") def get_asset(self, mrid: str) -> Asset: - """ - Get the `Asset` associated with this `PowerSystemResource` identified by `mrid`. - - `mrid` the mRID of the required `Asset` - Returns The `Asset` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._assets, mrid) + return self.assets.get_by_mrid(mrid) + @deprecated("Use obj.assets.append(asset) instead.") def add_asset(self, asset: Asset) -> PowerSystemResource: - """ - `asset` The `Asset` to associate with this `PowerSystemResource`. - Returns A reference to this `PowerSystemResource` to allow fluent use. - Raises `ValueError` if another `Asset` with the same `mrid` already exists in this `PowerSystemResource` - """ - if self._validate_reference(asset, self.get_asset, "An Asset"): - return self - - self._assets = list() if self._assets is None else self._assets - self._assets.append(asset) + self.assets.append(asset) return self + @deprecated("Use obj.assets.remove(asset) instead.") def remove_asset(self, asset: Asset) -> PowerSystemResource: - """ - Disassociate an `Asset` from this `PowerSystemResource`. - - `asset` the `Asset` to disassociate from this `PowerSystemResource`. - Raises `ValueError` if `asset` was not associated with this `PowerSystemResource`. - Returns A reference to this `PowerSystemResource` to allow fluent use. - """ - self._assets = safe_remove(self._assets, asset) + self.assets.remove(asset) return self + @deprecated("Use obj.assets.clear() instead.") def clear_assets(self) -> PowerSystemResource: - """ - Clear all assets. - Returns self - """ - self._assets = None + self.assets.clear() return self + + # endregion assets boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index 5a567be73..a1b7fd736 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -8,12 +8,14 @@ __all__ = ["SubGeographicalRegion"] from typing import Optional, List, Generator, TYPE_CHECKING - +from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.geographical_region import GeographicalRegion @@ -26,9 +28,10 @@ class SubGeographicalRegion(IdentifiedObject): A subset of a geographical region of a power system network model. """ - _geographical_region: Optional[GeographicalRegion] = None + geographical_region: Optional[GeographicalRegion] = None + """The geographical region to which this sub-geographical region is within.""" - _substations: Optional[List[Substation]] = None + _substations: Optional[List[Substation]] = field(default=None) def __init__(self, *args, substations: List[Substation] = None, **kwargs): super(SubGeographicalRegion, self).__init__(*args, **kwargs) @@ -47,28 +50,17 @@ def geographical_region(self): def geographical_region(self, value): self._geographical_region = value - @property - def substations(self) -> Generator[Substation, None, None]: - """ - All substations belonging to this sub geographical region. - """ - return ngen(self._substations) - def num_substations(self) -> int: """ Returns The number of `Substation`s associated with this `SubGeographicalRegion` """ return nlen(self._substations) - def get_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `SubGeographicalRegion` identified by `mrid` + substations: MridCollection[Substation] = LazyMridList( + _substations, + "A Substation", + ) - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._substations, mrid) def add_substation(self, substation: Substation) -> SubGeographicalRegion: """ @@ -93,21 +85,29 @@ def add_substation(self, substation: Substation) -> SubGeographicalRegion: self._substations.append(substation) return self - def remove_substation(self, substation: Substation) -> SubGeographicalRegion: - """ - Disassociate `substation` from this `GeographicalRegion` - `substation` The `Substation` to disassociate from this `SubGeographicalRegion`. - Returns A reference to this `SubGeographicalRegion` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `SubGeographicalRegion`. - """ - self._substations = safe_remove(self._substations, substation) + + # region deprecated list boilerplate + # region substations boilerplate + + @deprecated("Use len(obj.substations) instead.") + def num_substations(self) -> int: + return len(self.substations) + + @deprecated("Use obj.substations.get_by_mrid(mrid) instead.") + def get_substation(self, mrid: str) -> Substation: + return self.substations.get_by_mrid(mrid) + + @deprecated("Use obj.substations.remove(substation) instead.") + def remove_substation(self, substation: Substation) -> SubGeographicalRegion: + self.substations.remove(substation) return self + @deprecated("Use obj.substations.clear() instead.") def clear_substations(self) -> SubGeographicalRegion: - """ - Clear all `Substations`. - Returns A reference to this `SubGeographicalRegion` to allow fluent use. - """ - self._substations = None + self.substations.clear() return self + + # endregion substations boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index 7aaeead62..41fead9ba 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -8,12 +8,14 @@ __all__ = ["Substation"] from typing import Optional, Generator, List, TYPE_CHECKING - +from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop @@ -31,29 +33,18 @@ class Substation(EquipmentContainer): _sub_geographical_region: Optional[SubGeographicalRegion] = None - _normal_energized_feeders: Optional[List[Feeder]] = None - - _loops: Optional[List[Loop]] = None + _normal_energized_feeders: Optional[List[Feeder]] = field(default=None) - _energized_loops: Optional[List[Loop]] = None + _loops: Optional[List[Loop]] = field(default=None) - _circuits: Optional[List[Circuit]] = None + _energized_loops: Optional[List[Loop]] = field(default=None) - def __init__(self, *args, normal_energized_feeders: List[Feeder] = None, loops: List[Loop] = None, energized_loops: List[Loop] = None, circuits: List[Circuit] = None, **kwargs): - super(Substation, self).__init__(*args, **kwargs) - if normal_energized_feeders: - for feeder in normal_energized_feeders: - self.add_feeder(feeder) - if loops: - for loop in loops: - self.add_loop(loop) - if energized_loops: - for loop in energized_loops: - self.add_energized_loop(loop) - if circuits: - for circuit in circuits: - self.add_circuit(circuit) + _circuits: Optional[List[Circuit]] = field(default=None) + circuits: MridCollection[Circuit] = LazyMridList( + _circuits, + "A Circuit", + ) @property def sub_geographical_region(self): @@ -72,42 +63,23 @@ def circuits(self) -> Generator[Circuit, None, None]: """ return ngen(self._circuits) - @property - def loops(self) -> Generator[Loop, None, None]: - """ - The `Loop` originating from this substation. - """ - return ngen(self._loops) + loops: MridCollection[Loop] = LazyMridList( + _loops, + "A Loop", + ) - @property - def energized_loops(self) -> Generator[Loop, None, None]: - """ - The `Loop`s originating from this substation that are energised. - """ - return ngen(self._energized_loops) + energized_loops: MridCollection[Loop] = LazyMridList( + _energized_loops, + "A Loop", + ) - @property - def feeders(self) -> Generator[Feeder, None, None]: - """ - The normal energized feeders of the substation. Also used for naming purposes. - """ - return ngen(self._normal_energized_feeders) + feeders: MridCollection[Feeder] = LazyMridList( + _normal_energized_feeders, + "A Feeder", + ) + normal_energized_feeders = Alias(feeders) - def num_circuits(self): - """ - Returns The number of `Circuit`s associated with this `Substation` - """ - return nlen(self._circuits) - - def get_circuit(self, mrid: str) -> Circuit: - """ - Get the `Circuit` for this `Substation` identified by `mrid` - `mrid` The mRID of the required `Circuit` - Returns The `Circuit` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._circuits, mrid) def add_circuit(self, circuit: Circuit) -> Substation: """ @@ -123,175 +95,110 @@ def add_circuit(self, circuit: Circuit) -> Substation: self._circuits.append(circuit) return self - def remove_circuit(self, circuit: Circuit) -> Substation: - """ - Disassociate `circuit` from this `Substation` - `circuit` The `Circuit` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `circuit` was not associated with this `Substation`. - """ - self._circuits = safe_remove(self._circuits, circuit) + + # region deprecated list boilerplate + # region circuits boilerplate + + @deprecated("Use len(obj.circuits) instead.") + def num_circuits(self): + return len(self.circuits) + + @deprecated("Use obj.circuits.get_by_mrid(mrid) instead.") + def get_circuit(self, mrid: str) -> Circuit: + return self.circuits.get_by_mrid(mrid) + + @deprecated("Use obj.circuits.append(circuit) instead.") + def add_circuit(self, circuit: Circuit) -> Substation: + self.circuits.append(circuit) + return self + + @deprecated("Use obj.circuits.remove(circuit) instead.") + def remove_circuit(self, circuit: Circuit) -> Substation: + self.circuits.remove(circuit) return self + @deprecated("Use obj.circuits.clear() instead.") def clear_circuits(self) -> Substation: - """ - Clear all current `Circuit`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._circuits = None + self.circuits.clear() return self + # endregion circuits boilerplate + + # region loops boilerplate + + @deprecated("Use len(obj.loops) instead.") def num_loops(self): - """ - Returns The number of `Loop`s associated with this `Substation` - """ - return nlen(self._loops) + return len(self.loops) + @deprecated("Use obj.loops.get_by_mrid(mrid) instead.") def get_loop(self, mrid: str) -> Loop: - """ - Get the `Loop` for this `Substation` identified by `mrid` - - `mrid` The mRID of the required `Loop` - Returns The `Loop` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._loops, mrid) + return self.loops.get_by_mrid(mrid) + @deprecated("Use obj.loops.append(loop) instead.") def add_loop(self, loop: Loop) -> Substation: - """ - Associate a `Loop` with this `Substation` - - `loop` The `Loop` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Loop` with the same `mrid` already exists for this `Substation`. - """ - if self._validate_reference(loop, self.get_loop, "A Loop"): - return self - self._loops = list() if self._loops is None else self._loops - self._loops.append(loop) + self.loops.append(loop) return self + @deprecated("Use obj.loops.remove(loop) instead.") def remove_loop(self, loop: Loop) -> Substation: - """ - Disassociate `loop` from this `Substation` - - `loop` The `Loop` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `loop` was not associated with this `Substation`. - """ - self._loops = safe_remove(self._loops, loop) + self.loops.remove(loop) return self + @deprecated("Use obj.loops.clear() instead.") def clear_loops(self) -> Substation: - """ - Clear all current `Loop`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._loops = None + self.loops.clear() return self + # endregion loops boilerplate + + # region energized_loops boilerplate + + @deprecated("Use len(obj.energized_loops) instead.") def num_energized_loops(self): - """ - Returns The number of `Loop`s associated with this `Substation` - """ - return nlen(self._energized_loops) + return len(self.energized_loops) + @deprecated("Use obj.energized_loops.get_by_mrid(mrid) instead.") def get_energized_loop(self, mrid: str) -> Loop: - """ - Get the `Loop` for this `Substation` identified by `mrid` - - `mrid` The mRID of the required `Loop` - Returns The `Loop` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energized_loops, mrid) + return self.energized_loops.get_by_mrid(mrid) + @deprecated("Use obj.energized_loops.append(loop) instead.") def add_energized_loop(self, loop: Loop) -> Substation: - """ - Associate a `Loop` with this `Substation` - - `loop` The `Loop` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Loop` with the same `mrid` already exists for this `Substation`. - """ - if self._validate_reference(loop, self.get_energized_loop, "A Loop"): - return self - self._energized_loops = list() if self._energized_loops is None else self._energized_loops - self._energized_loops.append(loop) + self.energized_loops.append(loop) return self + @deprecated("Use obj.energized_loops.remove(loop) instead.") def remove_energized_loop(self, loop: Loop) -> Substation: - """ - Disassociate `loop` from this `Substation` - - `loop` The `Loop` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `loop` was not associated with this `Substation`. - """ - self._energized_loops = safe_remove(self._energized_loops, loop) + self.energized_loops.remove(loop) return self + @deprecated("Use obj.energized_loops.clear() instead.") def clear_energized_loops(self) -> Substation: - """ - Clear all current `Loop`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._energized_loops = None + self.energized_loops.clear() return self - def num_feeders(self): - """ - Returns The number of `Feeder`s associated with this `Substation` - """ - return nlen(self._normal_energized_feeders) - - def get_feeder(self, mrid: str) -> Feeder: - """ - Get the `Feeder` for this `Substation` identified by `mrid` - - `mrid` The mRID of the required `Feeder` - Returns The `Feeder` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._normal_energized_feeders, mrid) - - def add_feeder(self, feeder: Feeder) -> Substation: - """ - Associate a `Feeder` with this `Substation` - - `feeder` The `Feeder` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Feeder` with the same `mrid` already exists for this `Substation`, or if - `feeder.normal_energizing_substation` is not this `Substation`. - """ - if self._validate_reference(feeder, self.get_feeder, "A Feeder"): - return self + # endregion energized_loops boilerplate - if feeder.normal_energizing_substation is None: - feeder.normal_energizing_substation = self + # region feeders boilerplate - require(feeder.normal_energizing_substation is self, lambda: f"{feeder} `normal_energizing_substation` property references {feeder.normal_energizing_substation}, expected {self}.") + @deprecated("Use len(obj.feeders) instead.") + def num_feeders(self): + return len(self.feeders) - self._normal_energized_feeders = list() if self._normal_energized_feeders is None else self._normal_energized_feeders - self._normal_energized_feeders.append(feeder) - return self + @deprecated("Use obj.feeders.get_by_mrid(mrid) instead.") + def get_feeder(self, mrid: str) -> Feeder: + return self.feeders.get_by_mrid(mrid) + @deprecated("Use obj.feeders.remove(feeder) instead.") def remove_feeder(self, feeder: Feeder) -> Substation: - """ - Disassociate `feeder` from this `Substation` - - `feeder` The `Feeder` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `feeder` was not associated with this `Substation`. - """ - self._normal_energized_feeders = safe_remove(self._normal_energized_feeders, feeder) + self.feeders.remove(feeder) return self + @deprecated("Use obj.feeders.clear() instead.") def clear_feeders(self) -> Substation: - """ - Clear all current `Feeder`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._normal_energized_feeders = None + self.feeders.clear() return self + + # endregion feeders boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index 7975de84e..8eba60fbe 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -7,13 +7,19 @@ __all__ = ["Diagram"] -from typing import Optional, Dict, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing import Optional, Dict, List, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_style import DiagramStyle from zepben.ewb.model.cim.iec61970.base.diagramlayout.orientation_kind import OrientationKind -from zepben.ewb.util import nlen, ngen, require, safe_remove_by_id -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.util import require if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject @@ -32,80 +38,50 @@ class Diagram(IdentifiedObject): orientation_kind: OrientationKind = OrientationKind.POSITIVE """Coordinate system orientation of the diagram.""" - _diagram_objects: Optional[Dict[str, DiagramObject]] = None - - def __init__(self, *args, diagram_objects: List[DiagramObject] = None, **kwargs): - super(Diagram, self).__init__(*args, **kwargs) - if diagram_objects: - for obj in diagram_objects: - self.add_diagram_object(obj) - - @property - def diagram_objects(self) -> Generator[DiagramObject, None, None]: - """ - The diagram objects belonging to this diagram. - """ - return ngen(self._diagram_objects) - - def num_diagram_objects(self): - """ - Returns The number of `DiagramObject`s associated with this `Diagram` - """ - return nlen(self._diagram_objects) - - def get_diagram_object(self, mrid: str) -> DiagramObject: - """ - Get the `DiagramObject` for this `Diagram` identified by `mrid` + _diagram_objects: Dict[str, DiagramObject] | None = field(default=None) - `mrid` the mRID of the required `DiagramObject` - Returns The `DiagramObject` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - if not self._diagram_objects: - raise KeyError(mrid) - try: - return self._diagram_objects[mrid] - except AttributeError: - raise KeyError(mrid) + diagram_objects: MridCollection[DiagramObject] = LazyMridMap( + _diagram_objects, + "A DiagramObject", + validate=lambda self, it: self._validate_diagram_object(it) + ) + """The diagram objects belonging to this diagram.""" - def add_diagram_object(self, diagram_object: DiagramObject) -> Diagram: + def _validate_diagram_object(self, diagram_object: DiagramObject) -> None: """ - Associate a `DiagramObject` with this `Diagram`. - - `diagram_object` the `DiagramObject` to associate with this `Diagram`. - Returns The previous `DiagramObject` stored by `diagram_object`s mrid, otherwise `diagram_object` is returned - if there was no previous value. - Raises `ValueError` if another `DiagramObject` with the same `mrid` already exists for this `Diagram`, or if `diagram_object.diagram` is not this - `Diagram`. + Fill and validate the `diagram` backref for the `diagram_object` """ if not diagram_object.diagram: diagram_object.diagram = self require(diagram_object.diagram is self, lambda: f"{str(diagram_object)} `diagram` property references " f"{str(diagram_object.diagram)}, expected {str(self)}.") - if self._validate_reference(diagram_object, self.get_diagram_object, "A DiagramObject"): - return self - self._diagram_objects = dict() if self._diagram_objects is None else self._diagram_objects - self._diagram_objects[diagram_object.mrid] = diagram_object + # region deprecated list boilerplate + # region cuts boilerplate + + @deprecated("Use len(diagram_objects) instead") + def num_diagram_objects(self): + return len(self.diagram_objects) + + @deprecated("Use diagram_objects.get_by_mrid(mrid) instead") + def get_diagram_object(self, mrid: str) -> DiagramObject: + return self.diagram_objects.get_by_mrid(mrid) + @deprecated("Use diagram_objects.append(diagram_object) instead") + def add_diagram_object(self, diagram_object: DiagramObject) -> Diagram: + self.diagram_objects.append(diagram_object) return self + @deprecated("Use diagram_objects.remove(diagram_object) instead") def remove_diagram_object(self, diagram_object: DiagramObject) -> Diagram: - """ - Disassociate `diagram_object` from this `Diagram` - - `diagram_object` the `DiagramObject` to disassociate with this `Diagram`. - Returns A reference to this `Diagram` to allow fluent use. - Raises `KeyError` if `diagram_object` was not associated with this `Diagram`. - """ - self._diagram_objects = safe_remove_by_id(self._diagram_objects, diagram_object) + self.diagram_objects.remove(diagram_object) return self + @deprecated("Use diagram_objects.clear() instead") def clear_diagram_objects(self) -> Diagram: - """ - Clear all `DiagramObject`s. - Returns A reference to this `Diagram` to allow fluent use. - """ - self._diagram_objects = None + self.diagram_objects.clear() return self + + # endregion + # endregion \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py index a501e243c..1dd0e62ed 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py +++ b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py @@ -3,30 +3,45 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from __future__ import annotations + __all__ = ["BatteryUnit"] from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control_mode import BatteryControlMode from zepben.ewb.model.cim.iec61970.base.generation.production.battery_state_kind import BatteryStateKind from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control import BatteryControl +class BatteryControlList(LazyMridList): + def get_by_mode(self, control_mode: BatteryControlMode): + """ + Get the `BatteryControl` identified by its `control_mode` + + `control_mode` the `BatteryControlMode` of the desired `BatteryControl` + Returns The `BatteryControl` with the specified `control_mode` if it exists + Raises `KeyError` if a `BatteryControl` with `control_mode` wasn't present. + """ + for control in self: + if control.control_mode == control_mode: + return control + raise IndexError(f"No BatteryControl with a control_mode of {control_mode} was found in BatteryUnit {str(self)}") + + @zb_dataclass class BatteryUnit(PowerElectronicsUnit): """An electrochemical energy storage device.""" - def __init__(self, *args, controls: List['BatteryControl'] = None, **kwargs): - super(BatteryUnit, self).__init__(*args, **kwargs) - if controls: - for bc in controls: - self.add_control(bc) - battery_state: BatteryStateKind = BatteryStateKind.UNKNOWN """The current state of the battery (charging, full, etc.).""" @@ -36,75 +51,46 @@ def __init__(self, *args, controls: List['BatteryControl'] = None, **kwargs): stored_e: Optional[int] = None """Amount of energy currently stored in watt hours (Wh). The attribute shall be a positive value or zero and lower than `rated_e`.""" - _controls: Optional[List['BatteryControl']] = None + _controls: Optional[List['BatteryControl']] = field(default=None) # NOTE: This is called `num_battery_controls` because `num_controls` is already used by `PowerSystemResource`. - @property - def controls(self) -> Generator['BatteryControl', None, None]: - """ - [ZBEX] The `BatteryControl`s associated with this `BatteryUnit` - """ - return ngen(self._controls) + controls: BatteryControlList['BatteryControl'] = BatteryControlList( + _controls, + "A BatteryControl", + ) + + + # region deprecated list boilerplate + # region controls boilerplate + + @deprecated("Use len(obj.controls) instead.") def num_battery_controls(self): - """ - Returns The number of `BatteryControl`s associated with this `BatteryUnit` - """ - return nlen(self._controls) + return len(self.controls) + @deprecated("Use obj.controls.get_by_mrid(mrid) instead.") def get_control(self, mrid: str) -> 'BatteryControl': - """ - Get the `BatteryControl` for this `BatteryUnit` identified by `mrid` - - `mrid` the mRID of the required `BatteryControl` - Returns The `BatteryControl` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._controls, mrid) + return self.controls.get_by_mrid(mrid) + @deprecated("Use obj.controls.get_by_mode(control_mode) instead.") def get_control_by_mode(self, control_mode: BatteryControlMode) -> 'BatteryControl': - """ - Get the `BatteryControl` for this `BatteryUnit` identified by its `control_mode` - - `control_mode` the `BatteryControlMode` of the desired `BatteryControl` - Returns The `BatteryControl` with the specified `control_mode` if it exists - Raises `KeyError` if a `BatteryControl` with `control_mode` wasn't present. - """ - if self._controls: - for control in self._controls: - if control.control_mode == control_mode: - return control - raise IndexError(f"No BatteryControl with a control_mode of {control_mode} was found in BatteryUnit {str(self)}") + return self.controls.get_by_mode(control_mode) + @deprecated("Use obj.controls.append(bc) instead.") def add_control(self, bc: 'BatteryControl') -> 'BatteryUnit': - """ - Associate `bc` to this `BatteryUnit`. - - `bc` the `BatteryControl` to associate with this `BatteryUnit`. - Returns A reference to this `BatteryUnit` to allow fluent use. - Raises `ValueError` if another `BatteryControl` with the same `mrid` already exists for this `BatteryUnit`. - """ - if self._validate_reference(bc, self.get_control, "A BatteryControl"): - return self - self._controls = list() if self._controls is None else self._controls - self._controls.append(bc) + self.controls.append(bc) return self + @deprecated("Use obj.controls.remove(bc) instead.") def remove_control(self, bc: 'BatteryControl') -> 'BatteryUnit': - """ - Disassociate `bc` from this `BatteryUnit` - - `bc` the `BatteryControl` to disassociate from this `BatteryUnit`. - Returns A reference to this `BatteryUnit` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `BatteryUnit`. - """ - self._controls = safe_remove(self._controls, bc) + self.controls.remove(bc) return self + @deprecated("Use obj.controls.clear() instead.") def clear_controls(self) -> 'BatteryUnit': - """ - Clear all battery_controls. - Returns A reference to this `BatteryUnit` to allow fluent use. - """ - self._controls = None + self.controls.clear() return self + + # endregion controls boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index c98ed592d..9c6c9d284 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -8,12 +8,16 @@ __all__ = ["AcLineSegment"] from typing import Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase from zepben.ewb.model.cim.iec61970.base.wires.conductor import Conductor from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo @@ -24,6 +28,18 @@ from zepben.ewb.model.cim.iec61970.base.wires.per_length_sequence_impedance import PerLengthSequenceImpedance +class AcLineSegmentPhaseList(LazyMridList): + def get_by_phase(self, phase: SinglePhaseKind): + """ + The individual phase models for an AcLineSegment. + `phase` the phase of the required [AcLineSegmentPhase] + """ + res = next((it for it in self if it.phase == phase), None) + if res is None: + raise KeyError(phase) + return res + + @zb_dataclass class AcLineSegment(Conductor): """ @@ -43,9 +59,9 @@ class AcLineSegment(Conductor): per_length_impedance: 'PerLengthImpedance | None' = None """A `zepben.ewb.model.cim.iec61970.base.wires.PerLengthImpedance` describing this AcLineSegment""" - _cuts: list['Cut'] | None = None - _clamps: list['Clamp'] | None = None - _phases: list['AcLineSegmentPhase'] | None = None + _cuts: list['Cut'] | None = field(default=None) + _clamps: list['Clamp'] | None = field(default=None) + _phases: list['AcLineSegmentPhase'] | None = field(default=None) @property def per_length_sequence_impedance(self) -> Optional['PerLengthSequenceImpedance']: @@ -79,27 +95,20 @@ def per_length_phase_impedance(self) -> Optional['PerLengthPhaseImpedance']: def per_length_phase_impedance(self, value: Optional['PerLengthPhaseImpedance']): self.per_length_impedance = value - @property - def cuts(self) -> Generator['Cut', None, None]: - """The `Cut`s for this `AcLineSegment`.""" - return ngen(self._cuts) - @property - def clamps(self) -> Generator['Clamp', None, None]: - """The `Clamp`s for this `AcLineSegment`.""" - return ngen(self._clamps) + cuts: MridCollection['Cut'] = LazyMridList( + _cuts, + "A Cut", + validate=lambda self, it: self._validate_cut(it) + ) - def _validate_cut(self, cut: 'Cut') -> bool: + def _validate_cut(self, cut: 'Cut') -> None: """ Validate a cut against this `AcLineSegment`'s `Cut`s. :param cut: The `Cut` to validate. - :return: True if `cut` is already associated with this `AcLineSegment`, otherwise False. - :raise ValueError: If `cut.ac_line_segment` is not this `AcLineSegment`, or if this `AcLineSegment` has a different `Cut` with the same mRID. + :raise ValueError: If `cut.ac_line_segment` is not this `AcLineSegment` """ - if self._validate_reference(cut, self.get_cut, "A Cut"): - return True - if not cut.ac_line_segment: cut.ac_line_segment = self @@ -107,19 +116,21 @@ def _validate_cut(self, cut: 'Cut') -> bool: cut.ac_line_segment is self, lambda: f"{cut} `ac_line_segment` property references {cut.ac_line_segment}, expected {str(self)}.", ) - return False - def _validate_clamp(self, clamp: 'Clamp') -> bool: + + clamps: MridCollection['Clamp'] = LazyMridList( + _clamps, + "A Clamp", + validate=lambda self, it: self._validate_clamp(it) + ) + + def _validate_clamp(self, clamp: 'Clamp') -> None: """ Validate a clamp against this `AcLineSegment`'s `Clamp`s. :param clamp: The `Clamp` to validate. - :return: True if `clamp` is already associated with this `AcLineSegment`, otherwise False. - :raise ValueError: If `clamp.ac_line_segment` is not this `AcLineSegment`, or if this `AcLineSegment` has a different `Clamp` with the same mRID. + :raise ValueError: If `clamp.ac_line_segment` is not this `AcLineSegment` """ - if self._validate_reference(clamp, self.get_clamp, "A Clamp"): - return True - if not clamp.ac_line_segment: clamp.ac_line_segment = self @@ -127,14 +138,27 @@ def _validate_clamp(self, clamp: 'Clamp') -> bool: clamp.ac_line_segment is self, lambda: f"{clamp} `ac_line_segment` property references {clamp.ac_line_segment}, expected {str(self)}.", ) - return False - @property - def phases(self) -> Generator['AcLineSegmentPhase', None, None]: + + phases: AcLineSegmentPhaseList = AcLineSegmentPhaseList( + _phases, + "An AcLineSegmentPhase", + validate=lambda self, it: self._validate_phase(it), + sort_by=lambda it: it.sequence_number or 0 + ) + + def _validate_phase(self, phase: AcLineSegmentPhase) -> None: """ - The individual phase models for this AcLineSegment. The returned collection is read only. + Validate a phase against this `AcLineSegment`'s `Phase`s. + + :param phase: The `Phase` to validate. + :raise ValueError: If `phase.ac_line_segment` is not this `AcLineSegment` """ - return ngen(self._phases) + if phase.ac_line_segment is None: + phase.ac_line_segment = self + + require(phase.ac_line_segment is self, lambda: f"{phase} `ac_line_segment` property references {phase.ac_line_segment}, expected {self}.") + def wire_info_for_phase(self, phase: SinglePhaseKind) -> 'WireInfo | None': """ @@ -150,162 +174,90 @@ def wire_info_for_phase(self, phase: SinglePhaseKind) -> 'WireInfo | None': else: return self.asset_info + # region deprecated list boilerplate + # region cuts boilerplate + + @deprecated("Use len(obj.cuts) instead.") def num_cuts(self): - """ - Get the number of `Cut`s for this `AcLineSegment`. - """ - return nlen(self._cuts) + return len(self.cuts) + @deprecated("Use obj.cuts.get_by_mrid(mrid) instead.") def get_cut(self, mrid: str) -> 'Cut': - """ - Get the `Cut` for this `AcLineSegment` identified by `mrid` - - :param mrid: The mRID of the required `Cut` - :return: The `Cut` with the specified `mrid` if it exists - :raise KeyError: If the `mrid` wasn't present. - """ - return get_by_mrid(self._cuts, mrid) + return self.cuts.get_by_mrid(mrid) + @deprecated("Use obj.cuts.append(cut) instead.") def add_cut(self, cut: 'Cut') -> 'AcLineSegment': - """ - Associate a `Cut` with this `AcLineSegment`. - - :param cut: the `Cut` to associate with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - :raise ValueError: If another `Cut` with the same `mrid` already exists for this `AcLineSegment`. - """ - if self._validate_cut(cut): - return self - - self._cuts = list() if self._cuts is None else self._cuts - self._cuts.append(cut) + self.cuts.append(cut) return self + @deprecated("Use obj.cuts.remove(cut) instead.") def remove_cut(self, cut: 'Cut') -> 'AcLineSegment': - """ - :param cut: The `Cut` to disassociate from this `AcLineSegment`. - :raise ValueError: If `cut` was not associated with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._cuts = safe_remove(self._cuts, cut) + self.cuts.remove(cut) return self + @deprecated("Use obj.cuts.clear() instead.") def clear_cuts(self) -> 'AcLineSegment': - """ - Clear all `Cut`s. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._cuts.clear() + self.cuts.clear() return self + # endregion cuts boilerplate + + # region clamps boilerplate + + @deprecated("Use len(obj.clamps) instead.") def num_clamps(self): - """ - Get the number of `Clamp`s for this `AcLineSegment`. - """ - return nlen(self._clamps) + return len(self.clamps) + @deprecated("Use obj.clamps.get_by_mrid(mrid) instead.") def get_clamp(self, mrid: str) -> 'Clamp': - """ - Get the `Clamp` for this `AcLineSegment` identified by `mrid` - - :param mrid: The mRID of the required `Clamp` - :return: The `Clamp` with the specified `mrid` if it exists - :raise KeyError: If the `mrid` wasn't present. - """ - return get_by_mrid(self._clamps, mrid) + return self.clamps.get_by_mrid(mrid) + @deprecated("Use obj.clamps.append(clamp) instead.") def add_clamp(self, clamp: 'Clamp') -> 'AcLineSegment': - """ - Associate a `Clamp` with this `AcLineSegment`. - - :param clamp: the `Clamp` to associate with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - :raise ValueError: If another `Clamp` with the same `mrid` already exists for this `AcLineSegment`. - """ - if self._validate_clamp(clamp): - return self - - self._clamps = list() if self._clamps is None else self._clamps - self._clamps.append(clamp) + self.clamps.append(clamp) return self + @deprecated("Use obj.clamps.remove(clamp) instead.") def remove_clamp(self, clamp: 'Clamp') -> 'AcLineSegment': - """ - :param clamp: The `Clamp` to disassociate from this `AcLineSegment`. - :raise ValueError: If `clamp` was not associated with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._clamps = safe_remove(self._clamps, clamp) + self.clamps.remove(clamp) return self + @deprecated("Use obj.clamps.clear() instead.") def clear_clamps(self) -> 'AcLineSegment': - """ - Clear all `Clamp`s. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._clamps.clear() + self.clamps.clear() return self + # endregion clamps boilerplate + + # region phases boilerplate + + @deprecated("Use len(obj.phases) instead.") def num_phases(self) -> int: - """ - Get the number of entries in the [AcLineSegmentPhase] collection. - """ - return nlen(self._phases) + return len(self.phases) + @deprecated("Use obj.phases.get_by_mrid(identifier) or obj.phases.get_by_phase(identifier) instead.") def get_phase(self, identifier: 'str | SinglePhaseKind') -> 'AcLineSegmentPhase | None': - """ - The individual phase models for this AcLineSegment. - - :param identifier: the mRID or ``SinglePhaseKind`` of the required [AcLineSegmentPhase] - :returns: The [AcLineSegmentPhase] with the specified [mRID] if it exists, otherwise null - """ if isinstance(identifier, str): - if self._phases is not None: - return get_by_mrid(self._phases, identifier) - + return self.phases.get_by_mrid(identifier) elif isinstance(identifier, SinglePhaseKind): - for it in self._phases: - if it == identifier: - return it - - raise KeyError(identifier) + return self.phases.get_by_phase(identifier) + raise KeyError(identifier) # Wrong error, but consistent with previous functionality - deprecated regardless. + @deprecated("Use obj.phases.append(phase) instead.") def add_phase(self, phase: AcLineSegmentPhase) -> 'AcLineSegment': - """ - Add an [AcLineSegmentPhase] to this [AcLineSegment]. - - :param phase: The [AcLineSegmentPhase] to add. - :returns: This [AcLineSegment] for fluent use. - """ - if self._validate_reference(phase, self.get_phase, "An AcLineSegmentPhase"): - return self - - if phase.ac_line_segment is None: - phase.ac_line_segment = self - - require(phase.ac_line_segment is self, lambda: f"{phase} `ac_line_segment` property references {phase.ac_line_segment}, expected {self}.") - - if self._phases is None: - self._phases = list() - self._phases.append(phase) - self._phases.sort(key=lambda it: it.sequence_number or 0) + self.phases.append(phase) return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: AcLineSegmentPhase) -> 'AcLineSegment': - """ - Remove an [AcLineSegmentPhase] from this [AcLineSegment]. - - :param phase: The [AcLineSegmentPhase] to remove. - :returns: true if [phase] is removed from the collection. - """ - self._phases = safe_remove(self._phases, phase) + self.phases.remove(phase) return self + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> 'AcLineSegment': - """ - Clear all [AcLineSegmentPhase]'s from this [AcLineSegment]. - - :returns: This [AcLineSegment] for fluent use. - """ - self._phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py index 4ca992a95..a018da065 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py @@ -37,11 +37,6 @@ class AcLineSegmentPhase(PowerSystemResource): sequence_number: int | None = None _ac_line_segment: AcLineSegment | None = None - def __init__(self, *args, ac_line_segment: AcLineSegment = None, **kwargs): - super(AcLineSegmentPhase, self).__init__(*args, **kwargs) - if ac_line_segment is not None: - self.ac_line_segment = ac_line_segment - @property def ac_line_segment(self) -> 'AcLineSegment | None': return self._ac_line_segment diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index b016c64ad..f89de25b1 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -7,8 +7,14 @@ __all__ = ["EnergyConsumer"] -from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing import Optional, List, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.phase_shunt_connection_kind import PhaseShuntConnectionKind from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require @@ -22,7 +28,7 @@ class EnergyConsumer(EnergyConnection): """Generic user of energy - a point of consumption on the power system phases. May also represent a pro-sumer with negative p/q values. """ - _energy_consumer_phases: Optional[List[EnergyConsumerPhase]] = None + _energy_consumer_phases: Optional[List[EnergyConsumerPhase]] = field(default=None) """The individual phase models for this energy consumer.""" customer_count: Optional[int] = None @@ -49,30 +55,25 @@ class EnergyConsumer(EnergyConnection): q_fixed: Optional[float] = None """Power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" - def __init__(self, *args, energy_consumer_phases: List[EnergyConsumerPhase] = None, **kwargs): - super(EnergyConsumer, self).__init__(*args, **kwargs) - if energy_consumer_phases: - for phase in energy_consumer_phases: - self.add_phase(phase) + phases: MridCollection[EnergyConsumerPhase] = LazyMridList( + _energy_consumer_phases, + "An EnergyConsumerPhase", + ) + # TODO: Remove hack + # Old inits used the wrong name. This allows backwards compatibility + energy_consumer_phases: None = Alias(phases) - @property - def phases(self) -> Generator[EnergyConsumerPhase, None, None]: - """The individual phase models for this energy consumer.""" - return ngen(self._energy_consumer_phases) + # region deprecated list boilerplate + # region phases boilerplate + + @deprecated("Use len(obj.phases) instead.") def num_phases(self): - """Get the number of `EnergySourcePhase`s for this `EnergyConsumer`.""" - return nlen(self._energy_consumer_phases) + return len(self.phases) + @deprecated("Use obj.phases.get_by_mrid(mrid) instead.") def get_phase(self, mrid: str) -> EnergyConsumerPhase: - """ - Get the `EnergyConsumerPhase` for this `EnergyConsumer` identified by `mrid` - - `mrid` The mRID of the required `EnergyConsumerPhase` - Returns The `EnergyConsumerPhase` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energy_consumer_phases, mrid) + return self.phases.get_by_mrid(mrid) def add_phase(self, phase: EnergyConsumerPhase) -> EnergyConsumer: """ @@ -95,22 +96,16 @@ def add_phase(self, phase: EnergyConsumerPhase) -> EnergyConsumer: self._energy_consumer_phases.append(phase) return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: EnergyConsumerPhase) -> EnergyConsumer: - """ - Disassociate `phase` from this `OperationalRestriction`. - - `phase` the `EnergyConsumerPhase` to disassociate with this `EnergyConsumer`. - Raises `KeyError` if `phase` was not associated with this `EnergyConsumer`. - Returns A reference to this `EnergyConsumer` to allow fluent use. - Raises `ValueError` if `phase` was not associated with this `EnergyConsumer`. - """ - self._energy_consumer_phases = safe_remove(self._energy_consumer_phases, phase) + self.phases.remove(phase) return self + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> EnergyConsumer: - """ - Clear all phases. - Returns A reference to this `EnergyConsumer` to allow fluent use. - """ - self._energy_consumer_phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py index fa7f3e222..18d565550 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py @@ -42,11 +42,6 @@ class EnergyConsumerPhase(PowerSystemResource): q_fixed: Optional[float] = None """Reactive power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" - def __init__(self, *args, energy_consumer: 'EnergyConsumer' = None, **kwargs): - super(EnergyConsumerPhase, self).__init__(*args, **kwargs) - if energy_consumer: - self.energy_consumer = energy_consumer - @property def energy_consumer(self): """The `EnergyConsumer` that has this phase.""" diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index 5f8fdf13a..48d1d0fc5 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -8,10 +8,14 @@ __all__ = ["EnergySource"] from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase @@ -23,7 +27,7 @@ class EnergySource(EnergyConnection): A generic equivalent for an energy supplier on a transmission or distribution voltage level. """ - _energy_source_phases: Optional[List[EnergySourcePhase]] = None + _energy_source_phases: Optional[List[EnergySourcePhase]] = field(default=None) active_power: Optional[float] = None """ @@ -113,32 +117,23 @@ class EnergySource(EnergyConnection): x0_max: Optional[float] = None """Maximum zero sequence Thevenin reactance.""" - def __init__(self, *args, energy_source_phases: List[EnergySourcePhase] = None, **kwargs): - super(EnergySource, self).__init__(*args, **kwargs) - if energy_source_phases: - for phase in energy_source_phases: - self.add_phase(phase) + phases: MridCollection[EnergySourcePhase] = LazyMridList( + _energy_source_phases, + "An EnergySourcePhase", + ) + energy_source_phases = Alias(phases) - @property - def phases(self) -> Generator[EnergySourcePhase, None, None]: - """ - The `EnergySourcePhase`s for this `EnergySource`. - """ - return ngen(self._energy_source_phases) + # region deprecated list boilerplate + # region phases boilerplate + + @deprecated("Use len(obj.phases) instead.") def num_phases(self): - """Return the number of `EnergySourcePhase`s associated with this `EnergySource`""" - return nlen(self._energy_source_phases) + return len(self.phases) + @deprecated("Use obj.phases.get_by_mrid(mrid) instead.") def get_phase(self, mrid: str) -> EnergySourcePhase: - """ - Get the `EnergySourcePhase` for this `EnergySource` identified by `mrid` - - `mrid` the mRID of the required `EnergySourcePhase` - Returns The `EnergySourcePhase` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energy_source_phases, mrid) + return self.phases.get_by_mrid(mrid) def add_phase(self, phase: EnergySourcePhase) -> EnergySource: """ @@ -161,21 +156,16 @@ def add_phase(self, phase: EnergySourcePhase) -> EnergySource: self._energy_source_phases.append(phase) return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: EnergySourcePhase) -> EnergySource: - """ - Disassociate an `phase` from this `EnergySource` - - `phase` the `EnergySourcePhase` to disassociate from this `EnergySource`. - Returns A reference to this `EnergySource` to allow fluent use. - Raises `ValueError` if `phase` was not associated with this `EnergySource`. - """ - self._energy_source_phases = safe_remove(self._energy_source_phases, phase) + self.phases.remove(phase) return self + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> EnergySource: - """ - Clear all phases. - Returns A reference to this `EnergySource` to allow fluent use. - """ - self._energy_source_phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py index 1824b259e..ccbf4094a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py @@ -31,11 +31,6 @@ class EnergySourcePhase(PowerSystemResource): the connection is from the indicated phase to the central ground or neutral point. If the energy source is delta connected, the phase indicates an energy source connected from the indicated phase to the next logical non-neutral phase.""" - def __init__(self, *args, energy_source: 'EnergySource' = None, **kwargs): - super(EnergySourcePhase, self).__init__(*args, **kwargs) - if energy_source: - self.energy_source = energy_source - @property def energy_source(self): """The `EnergySource` with this `EnergySourcePhase`""" diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py index 838c5aa83..7b7709f66 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py @@ -3,53 +3,25 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from __future__ import annotations + __all__ = ["PerLengthPhaseImpedance"] -from typing import Optional, List, Generator +from dataclasses import field +from typing import List, Generator + +from typing_extensions import deprecated +from zepben.ewb import remove_descriptor_annotations, zb_dataclass +from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.phase_impedance_data import PhaseImpedanceData from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.util import require, ngen, nlen, safe_remove, none -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass - - -@zb_dataclass -class PerLengthPhaseImpedance(PerLengthImpedance): - """ - Impedance and admittance parameters per unit length for n-wire unbalanced lines, in matrix form. - """ - _data: Optional[List[PhaseImpedanceData]] = None - def __init__(self, *args, data: List[PhaseImpedanceData] = None, **kwargs): - """ - `data` A list of `PhaseImpedanceData`s to associate with this `PerLengthPhaseImpedance`. - """ - super(PerLengthPhaseImpedance, self).__init__(*args, **kwargs) - if data: - for phase_data in data: - self.add_data(phase_data) - - @property - def data(self) -> Generator[PhaseImpedanceData, None, None]: - """ - The point data values that define this phase_impedance, sorted by `x_value` in ascending order. - """ - return ngen(self._data) - - @property - def diagonal(self) -> Generator[PhaseImpedanceData, None, None]: - """ - Get only the diagonal elements of the matrix, i.e toPhase == fromPhase. - """ - return ngen(pid for pid in self._data if pid.from_phase == pid.to_phase) - - def num_data(self): - """Return the number of :class:`PhaseImpedanceData` associated with this :class:`PerLengthPhaseImpedance`.""" - return nlen(self._data) - - def get_data(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: +class PhaseImpedanceDataList(LazyValidatedList): + def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: """ Get the matrix entry for the corresponding to and from phases. @@ -58,44 +30,69 @@ def get_data(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> Ph :returns: The :class:`PhaseImpedanceData` with the specified `from_phase` and `to_phase` if it exists. :raises KeyError: When no `PhaseImpedanceData` was found with a matching `from_phase` and `to_phase`. """ - if self._data: - phase_impedance_data = next((it for it in self._data if it.from_phase == from_phase and it.to_phase == to_phase), None) - if phase_impedance_data: - return phase_impedance_data + phase_impedance_data = next((it for it in self if it.from_phase == from_phase and it.to_phase == to_phase), None) + if phase_impedance_data: + return phase_impedance_data + raise KeyError((from_phase, to_phase)) - def add_data(self, phase_impedance_data: PhaseImpedanceData) -> 'PerLengthPhaseImpedance': + @property + def diagonal(self): """ - Add a :class:`PhaseImpedanceData` to this :class:`PerLengthPhaseImpedance`. - - :param phase_impedance_data: The :class:`PhaseImpedanceData` to add. - :returns: A reference to this :class:`PerLengthPhaseImpedance` to allow fluent use. - :raises ValueError: If another :class:`PhaseImpedanceData` with the same `from_phase` and `to_phase` already exists for this :class:`PerLengthPhaseImpedance`. + Get only the diagonal elements of the matrix, i.e toPhase == fromPhase. """ + return (pid for pid in self if pid.from_phase == pid.to_phase) + +@zb_dataclass +class PerLengthPhaseImpedance(PerLengthImpedance): + """ + Impedance and admittance parameters per unit length for n-wire unbalanced lines, in matrix form. + """ + + _data: List[PhaseImpedanceData] | None = field(default=None) + + data: PhaseImpedanceDataList[PhaseImpedanceData] = PhaseImpedanceDataList( + _data, + validate=lambda self, it: self._validate_data(it) + ) + + def _validate_data(self, phase_impedance_data: PhaseImpedanceData): require(none([it.from_phase == phase_impedance_data.from_phase and it.to_phase == phase_impedance_data.to_phase for it in self.data]), lambda: f"""Unable to add PhaseImpedanceData to {self}. A PhaseImpedanceData with from_phase {phase_impedance_data.from_phase} and to_phase {phase_impedance_data.to_phase} already exists in this PerLengthPhaseImpedance.""") - self._data = self._data or [] - self._data.append(phase_impedance_data) - return self + # region deprecated list boilerplate + # region data boilerplate - def remove_data(self, phase_impedance_data: PhaseImpedanceData) -> 'PerLengthPhaseImpedance': - """ - Remove a :class:`PhaseImpedanceData` from this :class:`PerLengthPhaseImpedance`. + @property + @deprecated("Use data.diagonal instead") + def diagonal(self) -> Generator[PhaseImpedanceData, None, None]: + return self.data.diagonal - :param phase_impedance_data: The :class:`PhaseImpedanceData` to remove from this :class:`PerLengthPhaseImpedance`. - :returns: A reference to this :class:`PerLengthPhaseImpedance` to allow fluent use. - :raises ValueError: If `phase_impedance_data` was not associated with this :class:`PerLengthPhaseImpedance`. - """ - self._data = safe_remove(self._data, phase_impedance_data) + @deprecated("Use len(data) instead.") + def num_data(self): + return len(self.data) + + @deprecated("Use data.get(from_phase, to_phase) instead.") + def get_data(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: + return self.data.get(from_phase, to_phase) + + @deprecated("Use data.append(phase_impedance_data) instead.") + def add_data(self, phase_impedance_data: 'PhaseImpedanceData') -> PerLengthPhaseImpedance: + self.data.append(phase_impedance_data) return self - def clear_data(self) -> 'PerLengthPhaseImpedance': - """ - Clear all :class:`PhaseImpedanceData` associated with this :class:`PerLengthPhaseImpedance`. - :returns: A reference to this :class:`PerLengthPhaseImpedance` to allow fluent use. - """ - self._data = None + @deprecated("Use data.remove(phase_impedance_data) instead.") + def remove_data(self, phase_impedance_data: 'PhaseImpedanceData') -> PerLengthPhaseImpedance: + self.data.remove(phase_impedance_data) + return self + + @deprecated("Use data.clear() instead.") + def clear_data(self) -> PerLengthPhaseImpedance: + self.data.clear() return self + + # endregion data boilerplate + + # endregion deprecated list boilerplate \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 0bd33000b..5649efe89 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -8,10 +8,13 @@ __all__ = ["PowerElectronicsConnection"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove, require +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit @@ -136,35 +139,33 @@ class PowerElectronicsConnection(RegulatingCondEq): Permitted range is between -1.0 and 1.0 (inclusive), with a negative sign referring to “sink”. """ - _power_electronics_units: Optional[List[PowerElectronicsUnit]] = None + _power_electronics_units: Optional[List[PowerElectronicsUnit]] = field(default=None) """An AC network connection may have several power electronics units connecting through it.""" - _power_electronics_connection_phases: Optional[List[PowerElectronicsConnectionPhase]] = None + _power_electronics_connection_phases: Optional[List[PowerElectronicsConnectionPhase]] = field(default=None) """The individual units models for the power electronics connection.""" - def __init__( - self, - *args, - power_electronics_units: List[PowerElectronicsUnit] = None, - power_electronics_connection_phases: List[PowerElectronicsConnectionPhase] = None, - inv_watt_resp_v1=None, - inv_watt_resp_v2=None, - inv_watt_resp_v3=None, - inv_watt_resp_v4=None, - inv_watt_resp_p_at_v1=None, - inv_watt_resp_p_at_v2=None, - inv_watt_resp_p_at_v3=None, - inv_watt_resp_p_at_v4=None, - inv_var_resp_v1=None, - inv_var_resp_v2=None, - inv_var_resp_v3=None, - inv_var_resp_v4=None, - inv_var_resp_q_at_v1=None, - inv_var_resp_q_at_v2=None, - inv_var_resp_q_at_v3=None, - inv_var_resp_q_at_v4=None, - **kwargs, - ): + def __init__(self, + *args, + power_electronics_units: List[PowerElectronicsUnit] = None, + power_electronics_connection_phases: List[PowerElectronicsConnectionPhase] = None, + inv_watt_resp_v1=None, + inv_watt_resp_v2=None, + inv_watt_resp_v3=None, + inv_watt_resp_v4=None, + inv_watt_resp_p_at_v1=None, + inv_watt_resp_p_at_v2=None, + inv_watt_resp_p_at_v3=None, + inv_watt_resp_p_at_v4=None, + inv_var_resp_v1=None, + inv_var_resp_v2=None, + inv_var_resp_v3=None, + inv_var_resp_v4=None, + inv_var_resp_q_at_v1=None, + inv_var_resp_q_at_v2=None, + inv_var_resp_q_at_v3=None, + inv_var_resp_q_at_v4=None, + **kwargs): super(PowerElectronicsConnection, self).__init__(*args, **kwargs) if power_electronics_units: for unit in power_electronics_units: @@ -417,84 +418,22 @@ def inv_var_resp_q_at_v4(self, value): require(value is None or -0.6 <= value <= 0.0, lambda: f"inv_var_resp_q_at_v4 [{value}] must be between -0.6 and 0.0.") self._inv_var_resp_q_at_v4 = value - @property - def units(self) -> Generator[PowerElectronicsUnit, None, None]: - """ - The `PowerElectronicsUnit`s for this `PowerElectronicsConnection`. - """ - return ngen(self._power_electronics_units) + units: MridCollection[PowerElectronicsUnit] = LazyMridList( + _power_electronics_units, + "A PowerElectronicsUnit", + ) - @property - def phases(self) -> Generator[PowerElectronicsConnectionPhase, None, None]: - """ - The `PowerElectronicsConnectionPhase`s for this `PowerElectronicsConnection`. - """ - return ngen(self._power_electronics_connection_phases) + phases: MridCollection[PowerElectronicsConnectionPhase] = LazyMridList( + _power_electronics_connection_phases, + "A PowerElectronicsConnectionPhase", + ) - def num_units(self): - """Return the number of `PowerElectronicsUnit`s associated with this `PowerElectronicsConnection`""" - return nlen(self._power_electronics_units) - def get_unit(self, mrid: str) -> PowerElectronicsUnit: - """ - Get the `PowerElectronicsUnit` for this - `PowerElectronicsConnection` identified by `mrid` - - `mrid` the mRID of the required `PowerElectronicsUnit` - Returns The `PowerElectronicsUnit` with the specified `mrid` - if it exists - - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_electronics_units, mrid) - - def add_unit(self, unit: PowerElectronicsUnit) -> PowerElectronicsConnection: - """ - Associate an `PowerElectronicsUnit` with this - `PowerElectronicsConnection` - `unit` the `PowerElectronicsUnit` to associate with this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if another `PowerElectronicsUnit` with the same `mrid` already exists for this `PowerElectronicsConnection`. - """ - if self._validate_reference(unit, self.get_unit, "A PowerElectronicsUnit"): - return self - self._power_electronics_units = list() if self._power_electronics_units is None else self._power_electronics_units - self._power_electronics_units.append(unit) - return self - def remove_unit(self, unit: PowerElectronicsUnit) -> PowerElectronicsConnection: - """ - Disassociate `unit` from this `PowerElectronicsConnection` - `unit` the `PowerElectronicsUnit` to disassociate from this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if `unit` was not associated with this `PowerElectronicsConnection`. - """ - self._power_electronics_units = safe_remove(self._power_electronics_units, unit) - return self - def clear_units(self) -> PowerElectronicsConnection: - """ - Clear all units. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - """ - self._power_electronics_units = None - return self - def num_phases(self): - """Return the number of `PowerElectronicsConnectionPhase`s associated with this `PowerElectronicsConnection`""" - return nlen(self._power_electronics_connection_phases) - - def get_phase(self, mrid: str) -> PowerElectronicsConnectionPhase: - """ - Get the `PowerElectronicsConnectionPhase` for this `PowerElectronicsConnection` identified by `mrid` - - `mrid` the mRID of the required `PowerElectronicsConnectionPhase` - Returns The `PowerElectronicsConnectionPhase` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_electronics_connection_phases, mrid) def add_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: """ @@ -517,21 +456,56 @@ def add_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsC self._power_electronics_connection_phases.append(phase) return self - def remove_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: - """ - Disassociate `phase` from this `PowerElectronicsConnection` - `phase` the `PowerElectronicsConnectionPhase` to disassociate from this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if `phase` was not associated with this `PowerElectronicsConnection`. - """ - self._power_electronics_connection_phases = safe_remove(self._power_electronics_connection_phases, phase) + + # region deprecated list boilerplate + # region units boilerplate + + @deprecated("Use len(obj.units) instead.") + def num_units(self): + return len(self.units) + + @deprecated("Use obj.units.get_by_mrid(mrid) instead.") + def get_unit(self, mrid: str) -> PowerElectronicsUnit: + return self.units.get_by_mrid(mrid) + + @deprecated("Use obj.units.append(unit) instead.") + def add_unit(self, unit: PowerElectronicsUnit) -> PowerElectronicsConnection: + self.units.append(unit) + return self + + @deprecated("Use obj.units.remove(unit) instead.") + def remove_unit(self, unit: PowerElectronicsUnit) -> PowerElectronicsConnection: + self.units.remove(unit) return self + @deprecated("Use obj.units.clear() instead.") + def clear_units(self) -> PowerElectronicsConnection: + self.units.clear() + return self + + # endregion units boilerplate + + # region phases boilerplate + + @deprecated("Use len(obj.phases) instead.") + def num_phases(self): + return len(self.phases) + + @deprecated("Use obj.phases.get_by_mrid(mrid) instead.") + def get_phase(self, mrid: str) -> PowerElectronicsConnectionPhase: + return self.phases.get_by_mrid(mrid) + + @deprecated("Use obj.phases.remove(phase) instead.") + def remove_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: + self.phases.remove(phase) + return self + + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> PowerElectronicsConnection: - """ - Clear all phases. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - """ - self._power_electronics_connection_phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index 1226c6bcb..4955f6f13 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -8,7 +8,12 @@ __all__ = ["PowerTransformer"] import sys -from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing import List, Optional, TYPE_CHECKING + +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList + if sys.version_info >= (3, 13): from warnings import deprecated else: @@ -19,7 +24,7 @@ from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_construction_kind import TransformerConstructionKind from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_function_kind import TransformerFunctionKind from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.util import require, nlen, get_by_mrid, ngen, safe_remove +from zepben.ewb.util import require if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo @@ -27,6 +32,36 @@ from zepben.ewb.model.cim.iec61970.base.wires.power_transformer_end import PowerTransformerEnd +class PowerTransformerEndList(LazyMridList['PowerTransformerEnd']): + + def get_by_num(self, end_number: int) -> PowerTransformerEnd: + """ + Get the `PowerTransformerEnd` on this `PowerTransformer` by its `end_number`. + + `end_number` The `end_number` of the `PowerTransformerEnd` in relation to this `PowerTransformer`s VectorGroup. + Returns The `PowerTransformerEnd` referred to by `end_number` + Raises IndexError if no `PowerTransformerEnd` was found with end_number `end_number`. + """ + end = next((it for it in self if it.end_number == end_number), None) + if end: + return end + raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self.instance)}") + + def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: + """ + Get the `PowerTransformerEnd` on this `PowerTransformer` by its `terminal`. + + `terminal` The `terminal` to find a `PowerTransformerEnd` for. + Returns The `PowerTransformerEnd` connected to the specified `terminal` + Raises IndexError if no `PowerTransformerEnd` connected to `terminal` was found on this `PowerTransformer`. + """ + + end = next((it for it in self if it.terminal == terminal), None) + if end: + return end + raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self.instance)}") + + @zb_dataclass class PowerTransformer(ConductingEquipment): """ @@ -68,7 +103,7 @@ class PowerTransformer(ConductingEquipment): numerical sequence if they are numbered: the phasors are assumed to rotate in a counter-clockwise sense. """ - _power_transformer_ends: Optional[List[PowerTransformerEnd]] = None + _power_transformer_ends: List[PowerTransformerEnd] | None = field(default=None) transformer_utilisation: Optional[float] = None """ @@ -94,10 +129,17 @@ def __init__(self, *args, power_transformer_ends: List[PowerTransformerEnd] = No end.power_transformer = self self.add_end(end) - @property - def ends(self) -> Generator[PowerTransformerEnd, None, None]: - """The `PowerTransformerEnd`s for this `PowerTransformer`.""" - return ngen(self._power_transformer_ends) + ends: PowerTransformerEndList = PowerTransformerEndList( + _power_transformer_ends, + "A PowerTransformerEnd", + validate=lambda self, it: self._validate_end(it), + sort_by=lambda it: it.end_number + ) + + @deprecated("...") + def num_ends(self): + return len(self.ends) + @property @deprecated("use asset_info instead.") @@ -123,74 +165,19 @@ def get_base_voltage(self, terminal: Terminal = None): else: return None - def _validate_end(self, end: PowerTransformerEnd) -> bool: - """ - Validate an end against this `PowerTransformer`'s `PowerTransformerEnd`s. - - `end` The `PowerTransformerEnd` to validate. - Returns True if `end` is already associated with this `PowerTransformer`, otherwise False. - Raises `ValueError` if `end.power_transformer` is not this `PowerTransformer`, or if this `PowerTransformer` has a different `PowerTransformerEnd` - with the same mRID. - """ - if self._validate_reference(end, self.get_end_by_mrid, "A PowerTransformerEnd"): - return True - - if self._validate_reference_by_field(end, end.end_number, self.get_end_by_num, "end_number"): - return True - - if not end.power_transformer: - end.power_transformer = self - - require( - end.power_transformer is self, - lambda: f"{end} `power_transformer` property references {end.power_transformer}, expected {str(self)}.", - ) - return False - - def num_ends(self): - """ - Get the number of `PowerTransformerEnd`s for this `PowerTransformer`. - """ - return nlen(self._power_transformer_ends) - + @deprecated("...") def get_end_by_mrid(self, mrid: str) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` for this `PowerTransformer` identified by `mrid` - - `mrid` the mRID of the required `PowerTransformerEnd` - Returns The `PowerTransformerEnd` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_transformer_ends, mrid) + return self.ends.get_by_mrid(mrid) + @deprecated("...") def get_end_by_num(self, end_number: int) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `end_number`. - - `end_number` The `end_number` of the `PowerTransformerEnd` in relation to this `PowerTransformer`s VectorGroup. - Returns The `PowerTransformerEnd` referred to by `end_number` - Raises IndexError if no `PowerTransformerEnd` was found with end_number `end_number`. - """ - if self._power_transformer_ends: - for end in self._power_transformer_ends: - if end.end_number == end_number: - return end - raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self)}") + return self.ends.get_by_num(end_number) + @deprecated("...") def get_end_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `terminal`. - - `terminal` The `terminal` to find a `PowerTransformerEnd` for. - Returns The `PowerTransformerEnd` connected to the specified `terminal` - Raises IndexError if no `PowerTransformerEnd` connected to `terminal` was found on this `PowerTransformer`. - """ - if self._power_transformer_ends: - for end in self._power_transformer_ends: - if end.terminal is terminal: - return end - raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self)}") + return self.ends.get_by_terminal(terminal) + @deprecated("...") def add_end(self, end: PowerTransformerEnd) -> PowerTransformer: """ Associate a `PowerTransformerEnd` with this `PowerTransformer`. If `end.end_number` == 0, the end will be assigned an end_number of @@ -200,30 +187,37 @@ def add_end(self, end: PowerTransformerEnd) -> PowerTransformer: Returns A reference to this `PowerTransformer` to allow fluent use. Raises `ValueError` if another `PowerTransformerEnd` with the same `mrid` already exists for this `PowerTransformer`. """ - if self._validate_end(end): - return self - - if end.end_number == 0: - end.end_number = self.num_ends() + 1 - - self._power_transformer_ends = list() if self._power_transformer_ends is None else self._power_transformer_ends - self._power_transformer_ends.append(end) - self._power_transformer_ends.sort(key=lambda t: t.end_number) + self.ends.append(end) return self + @deprecated("...") def remove_end(self, end: PowerTransformerEnd) -> PowerTransformer: - """ - `end` the `PowerTransformerEnd` to disassociate from this `PowerTransformer`. - Raises `ValueError` if `end` was not associated with this `PowerTransformer`. - Returns A reference to this `PowerTransformer` to allow fluent use. - """ - self._power_transformer_ends = safe_remove(self._power_transformer_ends, end) + self.ends.remove(end) return self + @deprecated("...") def clear_ends(self) -> PowerTransformer: + self.ends.clear() + return self + + def _validate_end(self, end: PowerTransformerEnd): """ - Clear all `PowerTransformerEnd`s. - Returns A reference to this `PowerTransformer` to allow fluent use. + Validate an end against this `PowerTransformer`'s `PowerTransformerEnd`s. + + `end` The `PowerTransformerEnd` to validate. + Returns True if `end` is already associated with this `PowerTransformer`, otherwise False. + Raises `ValueError` if `end.power_transformer` is not this `PowerTransformer`, or if this `PowerTransformer` has a different `PowerTransformerEnd` + with the same mRID. """ - self._power_transformer_ends.clear() - return self + self._validate_reference_by_field(end, end.end_number, self.get_end_by_num, "end_number") + + if not end.power_transformer: + end.power_transformer = self + + require( + end.power_transformer is self, + lambda: f"{end} `power_transformer` property references {end.power_transformer}, expected {str(self)}.", + ) + + if end.end_number == 0: + end.end_number = self.num_ends() + 1 diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py index 48510103f..f1f0c046b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py @@ -9,10 +9,14 @@ from typing import Optional, List, Generator, TYPE_CHECKING, Iterable from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.util import get_by_mrid, ngen, nlen, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction @@ -27,74 +31,40 @@ class ProtectedSwitch(Switch, metaclass=ABCMeta): breaking_capacity: Optional[int] = None """The maximum fault current in amps a breaking device can break safely under prescribed conditions of use.""" - _relay_functions: Optional[List[ProtectionRelayFunction]] = None + _relay_functions: Optional[List[ProtectionRelayFunction]] = field(default=None) - def __init__( - self, - *args, - relay_functions: Iterable[ProtectionRelayFunction] = None, - **kwargs - ): - super(ProtectedSwitch, self).__init__(*args, **kwargs) + relay_functions: MridCollection[ProtectionRelayFunction] = LazyMridList( + _relay_functions, + "A ProtectionRelayFunction", + ) - # breaking_capacity is handled via dataclassy. - if relay_functions is not None: - for relay_function in relay_functions: - self.add_relay_function(relay_function) - @property - def relay_functions(self) -> Generator[ProtectionRelayFunction, None, None]: - """ - Yields all :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - - :return: A generator that iterates over all :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - """ - return ngen(self._relay_functions) + # region deprecated list boilerplate + # region relay_functions boilerplate + @deprecated("Use len(obj.relay_functions) instead.") def num_relay_functions(self) -> int: - """ - Get the number of :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - - :return: The number of :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - """ - return nlen(self._relay_functions) + return len(self.relay_functions) + @deprecated("Use obj.relay_functions.get_by_mrid(mrid) instead.") def get_relay_function(self, mrid: str) -> ProtectionRelayFunction: - """ - Get a :class:`ProtectionRelayFunction` operating this :class:`ProtectedSwitch` with the specified `mrid`. - - :param mrid: The mRID of the desired :class:`ProtectionRelayFunction` - :return: The :class:`ProtectionRelayFunction` with the specified mRID if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._relay_functions, mrid) + return self.relay_functions.get_by_mrid(mrid) + @deprecated("Use obj.relay_functions.append(relay_function) instead.") def add_relay_function(self, relay_function: ProtectionRelayFunction) -> ProtectedSwitch: - """ - Associate this :class:`ProtectedSwitch` with a :class:`ProtectionRelayFunction` operating it. - :param relay_function: The :class:`ProtectionRelayFunction` to associate with this :class:`ProtectedSwitch`. - :return: A reference to this :class:`ProtectedSwitch` for fluent use. - """ - if self._validate_reference(relay_function, self.get_relay_function, "A ProtectionRelayFunction"): - return self - - self._relay_functions = list() if self._relay_functions is None else self._relay_functions - self._relay_functions.append(relay_function) + self.relay_functions.append(relay_function) return self + @deprecated("Use obj.relay_functions.remove(relay_function) instead.") def remove_relay_function(self, relay_function: Optional[ProtectionRelayFunction]) -> ProtectedSwitch: - """ - Disassociate this :class:`ProtectedSwitch` from a :class:`ProtectionRelayFunction`. - :param relay_function: The :class:`ProtectionRelayFunction` to disassociate from this :class:`ProtectedSwitch`. - :return: A reference to this :class:`ProtectedSwitch` for fluent use. - """ - self._relay_functions = safe_remove(self._relay_functions, relay_function) + self.relay_functions.remove(relay_function) return self + @deprecated("Use obj.relay_functions.clear() instead.") def clear_relay_functions(self) -> ProtectedSwitch: - """ - Disassociate all :class:`ProtectionRelayFunction` from this :class:`ProtectedSwitch`. - :return: A reference to this :class:`ProtectedSwitch` for fluent use. - """ - self._relay_functions = None + self.relay_functions.clear() return self + + # endregion relay_functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py index 4eaa8faf5..c2e40b9eb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py @@ -29,10 +29,6 @@ class RegulatingCondEq(EnergyConnection, metaclass=ABCMeta): _regulating_control: Optional[RegulatingControl] = None - def __init__(self, *args, regulating_control: Optional[RegulatingControl] = None, **kwargs): - super(RegulatingCondEq, self).__init__(*args, **kwargs) - if regulating_control: - self.regulating_control = regulating_control @property def regulating_control(self): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py index 875e18fcc..4a11127a4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py @@ -9,12 +9,16 @@ from typing import Optional, List, Generator, Iterable, TYPE_CHECKING from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.regulating_control_mode_kind import RegulatingControlModeKind from zepben.ewb.util import nlen, get_by_mrid, safe_remove, ngen from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal @@ -107,70 +111,41 @@ class RegulatingControl(PowerSystemResource, metaclass=ABCMeta): regulators, shunt compensators, or battery units. """ - _regulating_cond_eq: Optional[List[RegulatingCondEq]] = None + _regulating_cond_eq: Optional[List[RegulatingCondEq]] = field(default=None) """The [RegulatingCondEq] that are controlled by this regulating control scheme.""" - def __init__(self, *args, regulating_conducting_equipment: Optional[Iterable[RegulatingCondEq]] = None, **kwargs): - super(RegulatingControl, self).__init__(*args, **kwargs) - if regulating_conducting_equipment is not None: - for eq in regulating_conducting_equipment: - self.add_regulating_cond_eq(eq) + regulating_conducting_equipment: MridCollection[RegulatingCondEq] = LazyMridList( + _regulating_cond_eq, + "A RegulatingCondEq", + ) - @property - def regulating_conducting_equipment(self) -> Generator[RegulatingCondEq, None, None]: - """ - Yields all the :class:`RegulatingCondEq` that are controlled by this :class:`RegulatingControl`. - :return: A generator that iterates over all RegulatingCondEq controlled by this RegulatingControl. - """ - return ngen(self._regulating_cond_eq) + # region deprecated list boilerplate + # region regulating_conducting_equipment boilerplate + @deprecated("Use len(obj.regulating_conducting_equipment) instead.") def num_regulating_cond_eq(self) -> int: - """ - Get the number of :class:`RegulatingCondEq` that are controlled by this :class:`RegulatingControl`. - - :return: The number of RegulatingCondEq that are controlled by this RegulatingControl. - """ - return nlen(self._regulating_cond_eq) + return len(self.regulating_conducting_equipment) + @deprecated("Use obj.regulating_conducting_equipment.get_by_mrid(mrid) instead.") def get_regulating_cond_eq(self, mrid: str) -> RegulatingCondEq: - """ - Get a :class:`RegulatingCondEq` controlled by this :class:`RegulatingControl`. - - :param mrid: The mRID of the desired RegulatingCondEq - :return: The RegulatingCondEq with the specified mRID if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._regulating_cond_eq, mrid) + return self.regulating_conducting_equipment.get_by_mrid(mrid) + @deprecated("Use obj.regulating_conducting_equipment.append(regulating_cond_eq) instead.") def add_regulating_cond_eq(self, regulating_cond_eq: RegulatingCondEq) -> RegulatingControl: - """ - Associate this :class:`RegulatingControl` with a :class:`RegulatingCondEq` it is controlling. - - :param regulating_cond_eq: The RegulatingCondEq to associate with this RegulatingControl. - :return: A reference to this RegulatingControl for fluent use. - """ - if self._validate_reference(regulating_cond_eq, self.get_regulating_cond_eq, "A RegulatingCondEq"): - return self - - self._regulating_cond_eq = list() if self._regulating_cond_eq is None else self._regulating_cond_eq - self._regulating_cond_eq.append(regulating_cond_eq) + self.regulating_conducting_equipment.append(regulating_cond_eq) return self + @deprecated("Use obj.regulating_conducting_equipment.remove(regulating_cond_eq) instead.") def remove_regulating_cond_eq(self, regulating_cond_eq: Optional[RegulatingCondEq]) -> RegulatingControl: - """ - Disassociate this :class:`RegulatingControl` from a :class:`RegulatingCondEq`. - - :param regulating_cond_eq: The RegulatingCondEq to disassociate from this RegulatingControl. - :return: A reference to this RegulatingControl for fluent use. - """ - self._regulating_cond_eq = safe_remove(self._regulating_cond_eq, regulating_cond_eq) + self.regulating_conducting_equipment.remove(regulating_cond_eq) return self + @deprecated("Use obj.regulating_conducting_equipment.clear() instead.") def clear_regulating_cond_eq(self) -> RegulatingControl: - """ - Disassociate all :class:`RegulatingCondEq` from this :class:`RegulatingControl`. - :return: A reference to this RegulatingControl for fluent use. - """ - self._regulating_cond_eq = None + self.regulating_conducting_equipment.clear() return self + + # endregion regulating_conducting_equipment boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py index a2970cb74..602212359 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py @@ -6,11 +6,15 @@ __all__ = ["SynchronousMachine"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.rotating_machine import RotatingMachine from zepben.ewb.model.cim.iec61970.base.wires.synchronous_machine_kind import SynchronousMachineKind from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.reactive_capability_curve import ReactiveCapabilityCurve @@ -23,7 +27,7 @@ class SynchronousMachine(RotatingMachine): synchronous condenser or pump. """ - _reactive_capability_curves: Optional[List['ReactiveCapabilityCurve']] = None + _reactive_capability_curves: Optional[List['ReactiveCapabilityCurve']] = field(default=None) base_q: Optional[float] = None """Default base reactive power value in VAr. This value represents the initial reactive power that can be used by any application function.""" @@ -106,65 +110,42 @@ class SynchronousMachine(RotatingMachine): operating_mode: SynchronousMachineKind = SynchronousMachineKind.UNKNOWN """Current mode of operation.""" - def __init__(self, *args, curves: List['ReactiveCapabilityCurve'] = None, **kwargs): - """ - `reactive_capability_curves` A list of `ReactiveCapabilityCurve`s to associate with this `SynchronousMachine`. - """ - super(SynchronousMachine, self).__init__(*args, **kwargs) - if curves: - for rcc in curves: - self.add_curve(rcc) - - @property - def curves(self) -> Generator['ReactiveCapabilityCurve', None, None]: - """ - The available reactive capability curves for this synchronous machine. The first shall be the default for this :class:`SynchronousMachine`. - """ - return ngen(self._reactive_capability_curves) + curves: MridCollection['ReactiveCapabilityCurve'] = LazyMridList( + _reactive_capability_curves, + "A ReactiveCapabilityCurve", + ) + + + + + + # region deprecated list boilerplate + # region curves boilerplate + + @deprecated("Use len(obj.curves) instead.") def num_curves(self): - """Return the number of :class:`ReactiveCapabilityCurve`s associated with this :class:`SynchronousMachine`.""" - return nlen(self._reactive_capability_curves) + return len(self.curves) + @deprecated("Use obj.curves.get_by_mrid(mrid) instead.") def get_curve(self, mrid: str) -> 'ReactiveCapabilityCurve': - """ - Get the :class:`ReactiveCapabilityCurve` for this :class:`SynchronousMachine` identified by `mrid` - - :param mrid: The mRID of the required :class:`ReactiveCapabilityCurve`. - :returns: The :class:`ReactiveCapabilityCurve` with the specified `mrid` if it exists. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._reactive_capability_curves, mrid) + return self.curves.get_by_mrid(mrid) + @deprecated("Use obj.curves.append(curve) instead.") def add_curve(self, curve: 'ReactiveCapabilityCurve') -> 'SynchronousMachine': - """ - Associate a :class:`ReactiveCapabilityCurve` with this :class:`SynchronousMachine`. - - :param curve: The :class:`ReactiveCapabilityCurve` to associate with this :class:`SynchronousMachine`. - :returns: A reference to this :class:`SynchronousMachine` to allow fluent use. - :raises ValueError: If another :class:`ReactiveCapabilityCurve` with the same `mrid` already exists for this :class:`SynchronousMachine`. - """ - if self._validate_reference(curve, self.get_curve, "A ReactiveCapabilityCurve"): - return self - self._reactive_capability_curves = self._reactive_capability_curves or [] - self._reactive_capability_curves.append(curve) + self.curves.append(curve) return self + @deprecated("Use obj.curves.remove(curve) instead.") def remove_curve(self, curve: 'ReactiveCapabilityCurve') -> 'SynchronousMachine': - """ - Disassociate a :class:`ReactiveCapabilityCurve` from this :class:`SynchronousMachine`. - - :param curve: The :class:`ReactiveCapabilityCurve` to disassociate from this :class:`SynchronousMachine`. - :returns: A reference to this :class:`SynchronousMachine` to allow fluent use. - :raises ValueError: If `curve` was not associated with this :class:`SynchronousMachine`. - """ - self._reactive_capability_curves = safe_remove(self._reactive_capability_curves, curve) + self.curves.remove(curve) return self + @deprecated("Use obj.curves.clear() instead.") def clear_curves(self) -> 'SynchronousMachine': - """ - Clear all :class:`ReactiveCapabilityCurve` associated with this :class:`SynchronousMachine`. - :returns: A reference to this :class:`SynchronousMachine` to allow fluent use. - """ - self._reactive_capability_curves = None + self.curves.clear() return self + + # endregion curves boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py index b6ea34497..0f994bfce 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py @@ -53,11 +53,6 @@ class TransformerEnd(IdentifiedObject, metaclass=ABCMeta): """(accurate for 2- or 3-winding transformers only) Pi-model impedances of this transformer end. By convention, for a two winding transformer, the full values of the transformer should be entered on the high voltage end (endNumber=1).""" - def __init__(self, *args, terminal: Optional['Terminal'] = None, **kwargs): - super(TransformerEnd, self).__init__(*args, **kwargs) - if terminal is not None: - self.terminal = terminal - @property def terminal(self) -> Optional['Terminal']: """ diff --git a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py index 26d713af6..c2ae3ff63 100644 --- a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py +++ b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py @@ -8,10 +8,14 @@ __all__ = ["Circuit"] from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.line import Line from zepben.ewb.util import ngen, get_by_mrid, safe_remove, nlen from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop @@ -24,123 +28,73 @@ class Circuit(Line): """Missing description""" loop: Optional[Loop] = None - _end_terminals: Optional[List[Terminal]] = None - _end_substations: Optional[List[Substation]] = None - - def __init__(self, *args, end_terminals: List[Terminal] = None, end_substations: List[Substation] = None, **kwargs): - super(Circuit, self).__init__(*args, **kwargs) - if end_terminals: - for term in end_terminals: - self.add_end_terminal(term) - - if end_substations: - for sub in end_substations: - self.add_end_substation(sub) - - @property - def end_terminals(self) -> Generator[Terminal, None, None]: - """ - The `Terminal`s representing the ends for this `Circuit`. - """ - return ngen(self._end_terminals) - - @property - def end_substations(self) -> Generator[Substation, None, None]: - """ - The `Substations`s representing the ends for this `Circuit`. - """ - return ngen(self._end_substations) + _end_terminals: Optional[List[Terminal]] = field(default=None) + _end_substations: Optional[List[Substation]] = field(default=None) + end_terminals: MridCollection[Terminal] = LazyMridList( + _end_terminals, + "An Terminal", + ) + + end_substations: MridCollection[Substation] = LazyMridList( + _end_substations, + "An Substation", + ) + + + # region deprecated list boilerplate + # region end_terminals boilerplate + + @deprecated("Use len(obj.end_terminals) instead.") def num_end_terminals(self): - """Return the number of end `Terminal`s associated with this `Circuit`""" - return nlen(self._end_terminals) + return len(self.end_terminals) + @deprecated("Use obj.end_terminals.get_by_mrid(mrid) instead.") def get_end_terminal(self, mrid: str) -> Terminal: - """ - Get the `Terminal` for this `Circuit` identified by `mrid` - - `mrid` the mRID of the required `Terminal` - Returns The `Terminal` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._end_terminals, mrid) + return self.end_terminals.get_by_mrid(mrid) + @deprecated("Use obj.end_terminals.append(terminal) instead.") def add_end_terminal(self, terminal: Terminal) -> Circuit: - """ - Associate an `Terminal` with this `Circuit` - - `terminal` the `Terminal` to associate with this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if another `Terminal` with the same `mrid` already exists for this `Circuit`. - """ - if self._validate_reference(terminal, self.get_end_terminal, "An Terminal"): - return self - self._end_terminals = list() if self._end_terminals is None else self._end_terminals - self._end_terminals.append(terminal) + self.end_terminals.append(terminal) return self + @deprecated("Use obj.end_terminals.remove(terminal) instead.") def remove_end_terminal(self, terminal: Terminal) -> Circuit: - """ - Disassociate `terminal` from this `Circuit` - - `terminal` the `Terminal` to disassociate from this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if `terminal` was not associated with this `Circuit`. - """ - self._end_terminals = safe_remove(self._end_terminals, terminal) + self.end_terminals.remove(terminal) return self + @deprecated("Use obj.end_terminals.clear() instead.") def clear_end_terminals(self) -> Circuit: - """ - Clear all end terminals. - Returns A reference to this `Circuit` to allow fluent use. - """ - self._end_terminals = None + self.end_terminals.clear() return self + # endregion end_terminals boilerplate + + # region end_substations boilerplate + + @deprecated("Use len(obj.end_substations) instead.") def num_end_substations(self): - """Return the number of end `Substation`s associated with this `Circuit`""" - return nlen(self._end_substations) + return len(self.end_substations) + @deprecated("Use obj.end_substations.get_by_mrid(mrid) instead.") def get_end_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `Circuit` identified by `mrid` - - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._end_substations, mrid) + return self.end_substations.get_by_mrid(mrid) + @deprecated("Use obj.end_substations.append(substation) instead.") def add_end_substation(self, substation: Substation) -> Circuit: - """ - Associate an `Substation` with this `Circuit` - - `substation` the `Substation` to associate with this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `Circuit`. - """ - if self._validate_reference(substation, self.get_end_substation, "An Substation"): - return self - self._end_substations = list() if self._end_substations is None else self._end_substations - self._end_substations.append(substation) + self.end_substations.append(substation) return self + @deprecated("Use obj.end_substations.remove(substation) instead.") def remove_end_substation(self, substation: Substation) -> Circuit: - """ - Disassociate `substation` from this `Circuit` - - `substation` the `Substation` to disassociate from this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `Circuit`. - """ - self._end_substations = safe_remove(self._end_substations, substation) + self.end_substations.remove(substation) return self + @deprecated("Use obj.end_substations.clear() instead.") def clear_end_substations(self) -> Circuit: - """ - Clear all end substations. - Returns A reference to this `Circuit` to allow fluent use. - """ - self._end_substations = None + self.end_substations.clear() return self + + # endregion end_substations boilerplate + + # endregion deprecated list boilerplate diff --git a/test/dataclass_descriptors/test_basic_descriptor.py b/test/dataclass_descriptors/test_basic_descriptor.py index 701393446..6941d9682 100644 --- a/test/dataclass_descriptors/test_basic_descriptor.py +++ b/test/dataclass_descriptors/test_basic_descriptor.py @@ -4,14 +4,14 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import field -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass, DataclassBase -from zepben.ewb.dataclass_descriptors.descriptor_fix import remove_descriptor_annotations, BackedDescriptor +from zepben.ewb.dataclass_descriptors.dataclass_base import DataclassBase +from zepben.ewb import zb_dataclass, remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.descriptor_fix import BackedDescriptor @zb_dataclass -@remove_descriptor_annotations class DescriptorTest(DataclassBase): - _x: int = field() + _x: int = field(default=0) x: int = BackedDescriptor(_x) diff --git a/test/dataclass_descriptors/test_dataclass_base.py b/test/dataclass_descriptors/test_dataclass_base.py index 2db4ff624..f35c26ee5 100644 --- a/test/dataclass_descriptors/test_dataclass_base.py +++ b/test/dataclass_descriptors/test_dataclass_base.py @@ -7,13 +7,10 @@ import pytest -from zepben.ewb import Cut from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass, DataclassBase -from zepben.ewb.dataclass_descriptors.descriptor_fix import remove_descriptor_annotations @zb_dataclass -@remove_descriptor_annotations class Root(DataclassBase): mrid: str y: int @@ -26,15 +23,13 @@ def __init__(self, mrid, *_, **kwargs): self.mrid = mrid super(Root, self).__init__(**kwargs) - @zb_dataclass -@remove_descriptor_annotations class Child(Root): x: float = 42.0 z: str = "abc" dc_default: int = field(default=99) - dc_default_factory: List[int] = field(default_factory=lambda: [33]) + dc_default_factory: List[int] = field(default_factory=lambda : [33]) def test_dataclass_base(): @@ -48,8 +43,8 @@ def test_dataclass_base(): obj = Child(mrid, y=33, z="Hello there") # Memory layout correct # noinspection PyUnresolvedReferences - all_slots = set(obj.__slots__).union(set(Root.__slots__)) # Python 3.11+ stores parent slots only in parent - assert all_slots == {'mrid', 'y', 'x', 'z', 'dc_default', 'dc_default_factory'} + all_slots = set(obj.__slots__).union(set(Root.__slots__)) # Python 3.11+ stores parent slots only in parent + assert all_slots == {'mrid', 'y', 'x', 'z', 'dc_default', 'dc_default_factory'} # positional arg assert obj.mrid == mrid @@ -77,14 +72,3 @@ def test_dataclass_base(): other = Child("mrid2", y=42) other.dc_default_factory.append(24) assert obj.dc_default_factory == [33] - - -def test_identifiable_mrid(): - obj = Cut("it") - assert obj.mrid == "it" - - obj = Cut(mrid="it") - assert obj.mrid == "it" - - with pytest.raises(TypeError): - Cut("it", mrid="it") diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index 128f2c957..95ad5b51e 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -12,6 +12,7 @@ from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.services.network.network_service import NetworkService +from zepben.ewb import NetworkStateOperators, TestNetworkBuilder, NetworkTraceStep, Terminal, NominalPhasePath, Breaker, AcLineSegment, Clamp, Cut from zepben.ewb.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider From 52b77f9d62fc6736aff1f01bcc6d284041f5ae5a Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 20 Jul 2026 17:05:19 +1000 Subject: [PATCH 02/54] Backfill Signed-off-by: Boris Filin --- .../ewb/dataclass_descriptors/mrid_list.py | 38 +++++--- .../ewb/dataclass_descriptors/mrid_map.py | 9 +- .../model/cim/iec61970/base/core/feeder.py | 2 +- .../iec61970/base/core/geographical_region.py | 39 +++------ .../iec61970/base/core/identified_object.py | 2 +- .../base/core/sub_geographical_region.py | 37 ++------ .../cim/iec61970/base/core/substation.py | 30 +++---- .../iec61970/base/diagramlayout/diagram.py | 21 +---- .../iec61970/base/wires/ac_line_segment.py | 86 +++++-------------- .../model/cim/iec61970/base/wires/clamp.py | 6 +- .../ewb/model/cim/iec61970/base/wires/cut.py | 5 +- .../iec61970/base/wires/energy_consumer.py | 33 ++----- .../cim/iec61970/base/wires/energy_source.py | 35 ++------ .../wires/power_electronics_connection.py | 43 +++------- .../power_electronics_connection_phase.py | 6 +- .../iec61970/base/wires/power_transformer.py | 71 +++++++-------- 16 files changed, 156 insertions(+), 307 deletions(-) diff --git a/src/zepben/ewb/dataclass_descriptors/mrid_list.py b/src/zepben/ewb/dataclass_descriptors/mrid_list.py index 2774ce025..178b19a6b 100644 --- a/src/zepben/ewb/dataclass_descriptors/mrid_list.py +++ b/src/zepben/ewb/dataclass_descriptors/mrid_list.py @@ -4,12 +4,13 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import abstractmethod, ABC from dataclasses import dataclass, field, Field +from types import MemberDescriptorType from typing import Any, TypeVar, Protocol import pytest from typing_extensions import Self -from zepben.ewb import remove_descriptor_annotations, BackedDescriptor +from zepben.ewb import remove_descriptor_annotations, BackedDescriptor, require from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList, MutableCollection, IndexedMutableCollection, _IterableWrapper @@ -47,14 +48,36 @@ def _can_add_by_mrid(self, element: S) -> bool: return False +class Backfill: + def __init__(self, backfill_prop: Any): + if not any(isinstance(backfill_prop, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): + raise TypeError(f"backfill_prop parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {backfill_prop}") + self.backfill_prop = backfill_prop + + def apply(self, element: S, owner: Any): + if isinstance(self.backfill_prop, property): + name = self.backfill_prop.fget.__name__ + else: + name = self.backfill_prop.__name__ + + if getattr(element, name) is None: + setattr(element, name, owner) + + ref = getattr(element, name) + if ref is not owner: + raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") + + class LazyMridList(LazyValidatedList, MridCollection[S]): def __init__(self, private_field, element_description: str, + backfill: Backfill = None, validate=None, sort_by=None): super().__init__(private_field, validate, sort_by) self.element_description = element_description + self.backfill = backfill def _safe_get_by_mrid(self, mrid: str) -> S | None: existing = self._get() @@ -67,6 +90,9 @@ def append(self, item: T): if not self._can_add_by_mrid(item): return + if self.backfill is not None: + self.backfill.apply(item, self.instance) + super().append(item) @@ -133,16 +159,6 @@ def __getitem__(self, item): return self.backing_list[item] -# -# class Backfill(Protocol[S]): -# def __init__(self, backref_field: Any): -# if not isinstance(backref_field, Field) and not isinstance(backref_field, BackedDescriptor): -# raise TypeError(f"backref_field parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {backref_field}") -# self.backref_field = backref_field -# -# def apply(self, element: S, owner: Any): -# setattr(element, self.backref_field.__name__, ) - diff --git a/src/zepben/ewb/dataclass_descriptors/mrid_map.py b/src/zepben/ewb/dataclass_descriptors/mrid_map.py index c74c40fa6..0f71c474c 100644 --- a/src/zepben/ewb/dataclass_descriptors/mrid_map.py +++ b/src/zepben/ewb/dataclass_descriptors/mrid_map.py @@ -9,7 +9,7 @@ from zepben.ewb import remove_descriptor_annotations from zepben.ewb.dataclass_descriptors.lazy_list import _IterableWrapper -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, Backfill class HasMrid(Protocol): @@ -24,10 +24,12 @@ def __init__( self, private_field, element_description: str, + backfill: Backfill = None, validate=None): super().__init__(private_field) - self.validate = validate self.element_description = element_description + self.backfill = backfill + self.validate = validate def _get(self): return getattr(self.instance, self.backing_name) @@ -59,6 +61,9 @@ def append(self, element: S): if not self._can_add_by_mrid(element): return + if self.backfill is not None: + self.backfill.apply(element, self.instance) + if self.validate is not None: self.validate(self.instance, element) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index bcc00115e..0c82a5f4a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -38,7 +38,7 @@ class Feeder(EquipmentContainer): _normal_head_terminal: Terminal | None = None """The normal head terminal or terminals of the feeder.""" - normal_energizing_substation: Substation | None = None + normal_energizing_substation: Substation | None = field(default=None) """The substation that normally energizes the feeder. Also used for naming purposes.""" _current_equipment_by_id: Dict[str, Equipment] | None = field(default=None) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index ac5448eb2..923ca240e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -7,16 +7,15 @@ __all__ = ["GeographicalRegion"] -from typing import Optional, List, Generator from dataclasses import field +from typing import Optional, List + from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList @zb_dataclass @@ -36,33 +35,10 @@ def __init__(self, *args, sub_geographical_regions: List[SubGeographicalRegion] sub_geographical_regions: MridCollection[SubGeographicalRegion] = LazyMridList( _sub_geographical_regions, "A SubGeographicalRegion", + backfill=Backfill(SubGeographicalRegion.geographical_region) ) - def add_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: - """ - Associate a `SubGeographicalRegion` with this `GeographicalRegion` - - `sub_geographical_region` The `SubGeographicalRegion` to associate with this `GeographicalRegion`. - Returns A reference to this `GeographicalRegion` to allow fluent use. - Raises `ValueError` if another `SubGeographicalRegion` with the same `mrid` already exists for this `GeographicalRegion`, or if - `sub_geographical_region.geographical_region` is not this `GeographicalRegion`. - """ - if self._validate_reference(sub_geographical_region, self.get_sub_geographical_region, "A SubGeographicalRegion"): - return self - - if sub_geographical_region.geographical_region is None: - sub_geographical_region.geographical_region = self - - require(sub_geographical_region.geographical_region is self, lambda: f"{sub_geographical_region} `geographical_region` property references " + - f"{sub_geographical_region.geographical_region}, expected {self}.") - - self._sub_geographical_regions = list() if self._sub_geographical_regions is None else self._sub_geographical_regions - self._sub_geographical_regions.append(sub_geographical_region) - return self - - - # region deprecated list boilerplate # region sub_geographical_regions boilerplate @@ -74,6 +50,11 @@ def num_sub_geographical_regions(self) -> int: def get_sub_geographical_region(self, mrid: str) -> SubGeographicalRegion: return self.sub_geographical_regions.get_by_mrid(mrid) + @deprecated("Use obj.sub_geographical_regions.append(sub_geographical_region) instead.") + def add_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: + self.sub_geographical_regions.append(sub_geographical_region) + return self + @deprecated("Use obj.sub_geographical_regions.remove(sub_geographical_region) instead.") def remove_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: self.sub_geographical_regions.remove(sub_geographical_region) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py b/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py index b36c786a9..e6c750d04 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py @@ -62,7 +62,7 @@ def __init__(self, mrid: str, *args, names: Optional[List[Name]] = None, **kwarg def __str__(self): class_name = f'{self.__class__.__name__}' - if self.name: + if getattr(self, "name", None): return f'{class_name}{{{self.mrid}|{self.name}}}' return f'{class_name}{{{self.mrid}}}' diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index a1b7fd736..e401267ce 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -15,11 +15,11 @@ from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.model.cim.iec61970.base.core.substation import Substation if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.geographical_region import GeographicalRegion - from zepben.ewb.model.cim.iec61970.base.core.substation import Substation @zb_dataclass @@ -28,7 +28,7 @@ class SubGeographicalRegion(IdentifiedObject): A subset of a geographical region of a power system network model. """ - geographical_region: Optional[GeographicalRegion] = None + geographical_region: Optional[GeographicalRegion] = field(default=None) """The geographical region to which this sub-geographical region is within.""" _substations: Optional[List[Substation]] = field(default=None) @@ -59,34 +59,10 @@ def num_substations(self) -> int: substations: MridCollection[Substation] = LazyMridList( _substations, "A Substation", + backfill=Backfill(Substation.sub_geographical_region) ) - def add_substation(self, substation: Substation) -> SubGeographicalRegion: - """ - Associate a `Substation` with this `GeographicalRegion` - - `substation` the `Substation` to associate with this `SubGeographicalRegion`. - - Returns A reference to this `SubGeographicalRegion` to allow fluent use. - - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `SubGeographicalRegion`, or if - `substation.sub_geographical_region` is not this `SubGeographicalRegion`. - """ - if self._validate_reference(substation, self.get_substation, "A Substation"): - return self - - if substation.sub_geographical_region is None: - substation.sub_geographical_region = self - - require(substation.sub_geographical_region is self, lambda: f"{substation} `sub_geographical_region` property references {substation.sub_geographical_region}, expected {self}.") - - self._substations = list() if self._substations is None else self._substations - self._substations.append(substation) - return self - - - # region deprecated list boilerplate # region substations boilerplate @@ -98,6 +74,11 @@ def num_substations(self) -> int: def get_substation(self, mrid: str) -> Substation: return self.substations.get_by_mrid(mrid) + @deprecated("Use obj.substations.append(substation) instead.") + def add_substation(self, substation: Substation) -> SubGeographicalRegion: + self.substations.append(substation) + return self + @deprecated("Use obj.substations.remove(substation) instead.") def remove_substation(self, substation: Substation) -> SubGeographicalRegion: self.substations.remove(substation) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index 41fead9ba..d34be25fb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -15,11 +15,11 @@ from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop - from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion from zepben.ewb.model.cim.iec61970.infiec61970.feeder.circuit import Circuit @@ -31,7 +31,8 @@ class Substation(EquipmentContainer): is passed for the purposes of switching or modifying its characteristics. """ - _sub_geographical_region: Optional[SubGeographicalRegion] = None + sub_geographical_region: Optional[SubGeographicalRegion] = field(default=None) + """The SubGeographicalRegion containing the substation.""" _normal_energized_feeders: Optional[List[Feeder]] = field(default=None) @@ -76,27 +77,11 @@ def circuits(self) -> Generator[Circuit, None, None]: feeders: MridCollection[Feeder] = LazyMridList( _normal_energized_feeders, "A Feeder", + backfill=Backfill(Feeder.normal_energizing_substation) ) normal_energized_feeders = Alias(feeders) - - def add_circuit(self, circuit: Circuit) -> Substation: - """ - Associate a `Circuit` with this `Substation` - - `circuit` The `Circuit` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Circuit` with the same `mrid` already exists for this `Substation`. - """ - if self._validate_reference(circuit, self.get_circuit, "A Circuit"): - return self - self._circuits = list() if self._circuits is None else self._circuits - self._circuits.append(circuit) - return self - - - # region deprecated list boilerplate # region circuits boilerplate @@ -189,6 +174,11 @@ def num_feeders(self): def get_feeder(self, mrid: str) -> Feeder: return self.feeders.get_by_mrid(mrid) + @deprecated("Use obj.feeders.append(feeder) instead.") + def add_feeder(self, feeder: Feeder) -> Substation: + self.feeders.append(feeder) + return self + @deprecated("Use obj.feeders.remove(feeder) instead.") def remove_feeder(self, feeder: Feeder) -> Substation: self.feeders.remove(feeder) diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index 8eba60fbe..0767d2244 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -8,21 +8,17 @@ __all__ = ["Diagram"] from dataclasses import field -from typing import Optional, Dict, List, TYPE_CHECKING +from typing import Dict from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, Backfill from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject +from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_style import DiagramStyle from zepben.ewb.model.cim.iec61970.base.diagramlayout.orientation_kind import OrientationKind -from zepben.ewb.util import require - -if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject @zb_dataclass @@ -43,19 +39,10 @@ class Diagram(IdentifiedObject): diagram_objects: MridCollection[DiagramObject] = LazyMridMap( _diagram_objects, "A DiagramObject", - validate=lambda self, it: self._validate_diagram_object(it) + backfill=Backfill(DiagramObject.diagram) ) """The diagram objects belonging to this diagram.""" - def _validate_diagram_object(self, diagram_object: DiagramObject) -> None: - """ - Fill and validate the `diagram` backref for the `diagram_object` - """ - if not diagram_object.diagram: - diagram_object.diagram = self - require(diagram_object.diagram is self, lambda: f"{str(diagram_object)} `diagram` property references " - f"{str(diagram_object.diagram)}, expected {str(self)}.") - # region deprecated list boilerplate # region cuts boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index 9c6c9d284..b0a5eae4a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -7,23 +7,20 @@ __all__ = ["AcLineSegment"] -from typing import Optional, Generator, TYPE_CHECKING from dataclasses import field +from typing import Optional, TYPE_CHECKING + from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase +from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp from zepben.ewb.model.cim.iec61970.base.wires.conductor import Conductor +from zepben.ewb.model.cim.iec61970.base.wires.cut import Cut from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo - from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp - from zepben.ewb.model.cim.iec61970.base.wires.cut import Cut - from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.per_length_phase_impedance import PerLengthPhaseImpedance from zepben.ewb.model.cim.iec61970.base.wires.per_length_sequence_impedance import PerLengthSequenceImpedance @@ -59,9 +56,9 @@ class AcLineSegment(Conductor): per_length_impedance: 'PerLengthImpedance | None' = None """A `zepben.ewb.model.cim.iec61970.base.wires.PerLengthImpedance` describing this AcLineSegment""" - _cuts: list['Cut'] | None = field(default=None) - _clamps: list['Clamp'] | None = field(default=None) - _phases: list['AcLineSegmentPhase'] | None = field(default=None) + _cuts: list[Cut] | None = field(default=None) + _clamps: list[Clamp] | None = field(default=None) + _phases: list[AcLineSegmentPhase] | None = field(default=None) @property def per_length_sequence_impedance(self) -> Optional['PerLengthSequenceImpedance']: @@ -96,69 +93,26 @@ def per_length_phase_impedance(self, value: Optional['PerLengthPhaseImpedance']) self.per_length_impedance = value - cuts: MridCollection['Cut'] = LazyMridList( + cuts: MridCollection[Cut] = LazyMridList( _cuts, "A Cut", - validate=lambda self, it: self._validate_cut(it) + backfill=Backfill(Cut.ac_line_segment) ) - def _validate_cut(self, cut: 'Cut') -> None: - """ - Validate a cut against this `AcLineSegment`'s `Cut`s. - - :param cut: The `Cut` to validate. - :raise ValueError: If `cut.ac_line_segment` is not this `AcLineSegment` - """ - if not cut.ac_line_segment: - cut.ac_line_segment = self - - require( - cut.ac_line_segment is self, - lambda: f"{cut} `ac_line_segment` property references {cut.ac_line_segment}, expected {str(self)}.", - ) - - clamps: MridCollection['Clamp'] = LazyMridList( + clamps: MridCollection[Clamp] = LazyMridList( _clamps, "A Clamp", - validate=lambda self, it: self._validate_clamp(it) + backfill=Backfill(Clamp.ac_line_segment) ) - def _validate_clamp(self, clamp: 'Clamp') -> None: - """ - Validate a clamp against this `AcLineSegment`'s `Clamp`s. - - :param clamp: The `Clamp` to validate. - :raise ValueError: If `clamp.ac_line_segment` is not this `AcLineSegment` - """ - if not clamp.ac_line_segment: - clamp.ac_line_segment = self - - require( - clamp.ac_line_segment is self, - lambda: f"{clamp} `ac_line_segment` property references {clamp.ac_line_segment}, expected {str(self)}.", - ) - - phases: AcLineSegmentPhaseList = AcLineSegmentPhaseList( _phases, "An AcLineSegmentPhase", - validate=lambda self, it: self._validate_phase(it), + backfill=Backfill(AcLineSegmentPhase.ac_line_segment), sort_by=lambda it: it.sequence_number or 0 ) - def _validate_phase(self, phase: AcLineSegmentPhase) -> None: - """ - Validate a phase against this `AcLineSegment`'s `Phase`s. - - :param phase: The `Phase` to validate. - :raise ValueError: If `phase.ac_line_segment` is not this `AcLineSegment` - """ - if phase.ac_line_segment is None: - phase.ac_line_segment = self - - require(phase.ac_line_segment is self, lambda: f"{phase} `ac_line_segment` property references {phase.ac_line_segment}, expected {self}.") - def wire_info_for_phase(self, phase: SinglePhaseKind) -> 'WireInfo | None': """ @@ -182,16 +136,16 @@ def num_cuts(self): return len(self.cuts) @deprecated("Use obj.cuts.get_by_mrid(mrid) instead.") - def get_cut(self, mrid: str) -> 'Cut': + def get_cut(self, mrid: str) -> Cut: return self.cuts.get_by_mrid(mrid) @deprecated("Use obj.cuts.append(cut) instead.") - def add_cut(self, cut: 'Cut') -> 'AcLineSegment': + def add_cut(self, cut: Cut) -> 'AcLineSegment': self.cuts.append(cut) return self @deprecated("Use obj.cuts.remove(cut) instead.") - def remove_cut(self, cut: 'Cut') -> 'AcLineSegment': + def remove_cut(self, cut: Cut) -> 'AcLineSegment': self.cuts.remove(cut) return self @@ -209,16 +163,16 @@ def num_clamps(self): return len(self.clamps) @deprecated("Use obj.clamps.get_by_mrid(mrid) instead.") - def get_clamp(self, mrid: str) -> 'Clamp': + def get_clamp(self, mrid: str) -> Clamp: return self.clamps.get_by_mrid(mrid) @deprecated("Use obj.clamps.append(clamp) instead.") - def add_clamp(self, clamp: 'Clamp') -> 'AcLineSegment': + def add_clamp(self, clamp: Clamp) -> 'AcLineSegment': self.clamps.append(clamp) return self @deprecated("Use obj.clamps.remove(clamp) instead.") - def remove_clamp(self, clamp: 'Clamp') -> 'AcLineSegment': + def remove_clamp(self, clamp: Clamp) -> 'AcLineSegment': self.clamps.remove(clamp) return self diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index f845a3c86..2007debce 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -5,10 +5,9 @@ __all__ = ["Clamp"] +from dataclasses import field from typing import Optional, TYPE_CHECKING -from typing_extensions import deprecated - from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @@ -26,7 +25,8 @@ class Clamp(ConductingEquipment): length_from_terminal_1: Optional[float] = None """The length to the place where the clamp is located starting from side one of the line segment, i.e. the line segment terminal with sequence number equal to 1.""" - _ac_line_segment: Optional['AcLineSegment'] = None + ac_line_segment: Optional['AcLineSegment'] = field(default=None) + """The line segment to which the clamp is connected.""" max_terminals = 1 diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index 2a9cd9e11..e8ae1309d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -5,10 +5,9 @@ __all__ = ["Cut"] +from dataclasses import field from typing import Optional, TYPE_CHECKING -from typing_extensions import deprecated - from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @@ -32,6 +31,8 @@ class Cut(Switch): length_from_terminal_1: Optional[float] = None """The length to the place where the cut is located starting from side one of the cut line segment, i.e. the line segment Terminal with sequenceNumber equal to 1.""" + ac_line_segment: Optional['AcLineSegment'] = field(default=None) + """The line segment to which the cut is applied.""" _ac_line_segment: Optional['AcLineSegment'] = None @property diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index f89de25b1..6b99da983 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -8,20 +8,16 @@ __all__ = ["EnergyConsumer"] from dataclasses import field -from typing import Optional, List, TYPE_CHECKING +from typing import Optional, List from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb import Alias from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection +from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase from zepben.ewb.model.cim.iec61970.base.wires.phase_shunt_connection_kind import PhaseShuntConnectionKind -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass - -if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase @zb_dataclass @@ -58,6 +54,7 @@ class EnergyConsumer(EnergyConnection): phases: MridCollection[EnergyConsumerPhase] = LazyMridList( _energy_consumer_phases, "An EnergyConsumerPhase", + backfill=Backfill(EnergyConsumerPhase.energy_consumer) ) # TODO: Remove hack # Old inits used the wrong name. This allows backwards compatibility @@ -75,25 +72,9 @@ def num_phases(self): def get_phase(self, mrid: str) -> EnergyConsumerPhase: return self.phases.get_by_mrid(mrid) + @deprecated("Use obj.phases.append(phase) instead.") def add_phase(self, phase: EnergyConsumerPhase) -> EnergyConsumer: - """ - Associate an `EnergyConsumerPhase` with this `EnergyConsumer` - - `phase` the `EnergyConsumerPhase` to associate with this `EnergyConsumer`. - Returns A reference to this `EnergyConsumer` to allow fluent use. - Raises `ValueError` if another `EnergyConsumerPhase` with the same `mrid` already exists for this `EnergyConsumer`, or if `phase.energy_consumer` is not - this `EnergyConsumer`. - """ - if self._validate_reference(phase, self.get_phase, "An EnergyConsumerPhase"): - return self - - if phase.energy_consumer is None: - phase.energy_consumer = self - - require(phase.energy_consumer is self, lambda: f"{phase} `energy_consumer` property references {phase.energy_consumer}, expected {self}.") - - self._energy_consumer_phases = list() if self._energy_consumer_phases is None else self._energy_consumer_phases - self._energy_consumer_phases.append(phase) + self.phases.append(phase) return self @deprecated("Use obj.phases.remove(phase) instead.") diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index 48d1d0fc5..47c5befbb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -7,18 +7,16 @@ __all__ = ["EnergySource"] -from typing import List, Optional, Generator, TYPE_CHECKING from dataclasses import field +from typing import List, Optional + from typing_extensions import deprecated -from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require +from zepben.ewb import Alias from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList - -if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection +from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase @zb_dataclass @@ -120,6 +118,7 @@ class EnergySource(EnergyConnection): phases: MridCollection[EnergySourcePhase] = LazyMridList( _energy_source_phases, "An EnergySourcePhase", + backfill=Backfill(EnergySourcePhase.energy_source) ) energy_source_phases = Alias(phases) @@ -135,25 +134,9 @@ def num_phases(self): def get_phase(self, mrid: str) -> EnergySourcePhase: return self.phases.get_by_mrid(mrid) + @deprecated("Use obj.phases.append(phase) instead.") def add_phase(self, phase: EnergySourcePhase) -> EnergySource: - """ - Associate an `EnergySourcePhase` with this `EnergySource` - - `phase` the `EnergySourcePhase` to associate with this `EnergySource`. - Returns A reference to this `EnergySource` to allow fluent use. - Raises `ValueError` if another `EnergySourcePhase` with the same `mrid` already exists for this `EnergySource`, or if `phase.energy_source` is not - this `EnergySource`. - """ - if self._validate_reference(phase, self.get_phase, "An EnergySourcePhase"): - return self - - if phase.energy_source is None: - phase.energy_source = self - - require(phase.energy_source is self, lambda: f"{phase} `energy_source` property references {phase.energy_source}, expected {self}.") - - self._energy_source_phases = list() if self._energy_source_phases is None else self._energy_source_phases - self._energy_source_phases.append(phase) + self.phases.append(phase) return self @deprecated("Use obj.phases.remove(phase) instead.") diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 5649efe89..5e7cf25b2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -7,18 +7,19 @@ __all__ = ["PowerElectronicsConnection"] -from typing import Optional, List, Generator, TYPE_CHECKING from dataclasses import field +from typing import Optional, List, TYPE_CHECKING + from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList -from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq -from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase +from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq +from zepben.ewb.util import require if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit - from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase @zb_dataclass @@ -426,38 +427,13 @@ def inv_var_resp_q_at_v4(self, value): phases: MridCollection[PowerElectronicsConnectionPhase] = LazyMridList( _power_electronics_connection_phases, "A PowerElectronicsConnectionPhase", + backfill=Backfill(PowerElectronicsConnectionPhase.power_electronics_connection) ) - - - - def add_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: - """ - Associate a `PowerElectronicsConnectionPhase` with this `PowerElectronicsConnection` - - `phase` the `PowerElectronicsConnectionPhase` to associate with this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if another `PowerElectronicsConnectionPhase` with the same `mrid` already exists for this `PowerElectronicsConnection`, or if - `phase.power_electronics_connection` is not this `PowerElectronicsConnection`. - """ - if self._validate_reference(phase, self.get_phase, "A PowerElectronicsConnectionPhase"): - return self - - if phase.power_electronics_connection is None: - phase.power_electronics_connection = self - - require(phase.power_electronics_connection is self, lambda: f"{phase} `power_electronics_connection` property references {phase.power_electronics_connection}, expected {self}.") - - self._power_electronics_connection_phases = list() if self._power_electronics_connection_phases is None else self._power_electronics_connection_phases - self._power_electronics_connection_phases.append(phase) - return self - - - # region deprecated list boilerplate # region units boilerplate @@ -496,6 +472,11 @@ def num_phases(self): def get_phase(self, mrid: str) -> PowerElectronicsConnectionPhase: return self.phases.get_by_mrid(mrid) + @deprecated("Use obj.phases.append(phase) instead.") + def add_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: + self.phases.append(phase) + return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: self.phases.remove(phase) diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py index 3e3ab216b..71762ab69 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py @@ -5,10 +5,9 @@ __all__ = ["PowerElectronicsConnectionPhase"] +from dataclasses import field from typing import Optional, TYPE_CHECKING -from typing_extensions import deprecated - from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @@ -21,7 +20,8 @@ class PowerElectronicsConnectionPhase(PowerSystemResource): """A single phase of a power electronics connection.""" - _power_electronics_connection: Optional['PowerElectronicsConnection'] = None + power_electronics_connection: Optional['PowerElectronicsConnection'] = None + """The power electronics connection to which the phase belongs.""" p: Optional[float] = None """Active power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index 4955f6f13..80005abbf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -11,8 +11,7 @@ from dataclasses import field from typing import List, Optional, TYPE_CHECKING -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList +from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill if sys.version_info >= (3, 13): from warnings import deprecated @@ -24,12 +23,11 @@ from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_construction_kind import TransformerConstructionKind from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_function_kind import TransformerFunctionKind from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.util import require +from zepben.ewb.model.cim.iec61970.base.wires.power_transformer_end import PowerTransformerEnd if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal - from zepben.ewb.model.cim.iec61970.base.wires.power_transformer_end import PowerTransformerEnd class PowerTransformerEndList(LazyMridList['PowerTransformerEnd']): @@ -132,13 +130,16 @@ def __init__(self, *args, power_transformer_ends: List[PowerTransformerEnd] = No ends: PowerTransformerEndList = PowerTransformerEndList( _power_transformer_ends, "A PowerTransformerEnd", + backfill=Backfill(PowerTransformerEnd.power_transformer), validate=lambda self, it: self._validate_end(it), sort_by=lambda it: it.end_number ) - @deprecated("...") - def num_ends(self): - return len(self.ends) + def _validate_end(self, end: PowerTransformerEnd): + self._validate_reference_by_field(end, end.end_number, self.ends.get_by_num, "end_number") + + if end.end_number == 0: + end.end_number = self.num_ends() + 1 @property @@ -165,59 +166,47 @@ def get_base_voltage(self, terminal: Terminal = None): else: return None - @deprecated("...") + + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. + + # ends boilerplate + + @deprecated("Use `len(power_transformer.ends)` instead.") + def num_ends(self): + return len(self.ends) + + @deprecated("Use `power_transformer.ends.get_by_mrid(mrid)` instead.") def get_end_by_mrid(self, mrid: str) -> PowerTransformerEnd: return self.ends.get_by_mrid(mrid) - @deprecated("...") + @deprecated("Use `power_transformer.ends.get_by_num(end_number)` instead.") def get_end_by_num(self, end_number: int) -> PowerTransformerEnd: return self.ends.get_by_num(end_number) - @deprecated("...") + @deprecated("Use `power_transformer.ends.get_by_terminal(terminal)` instead.") def get_end_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: return self.ends.get_by_terminal(terminal) - @deprecated("...") + @deprecated("Use `power_transformer.ends.append(end)` instead.") def add_end(self, end: PowerTransformerEnd) -> PowerTransformer: - """ - Associate a `PowerTransformerEnd` with this `PowerTransformer`. If `end.end_number` == 0, the end will be assigned an end_number of - `self.num_ends() + 1`. - - `end` the `PowerTransformerEnd` to associate with this `PowerTransformer`. - Returns A reference to this `PowerTransformer` to allow fluent use. - Raises `ValueError` if another `PowerTransformerEnd` with the same `mrid` already exists for this `PowerTransformer`. - """ self.ends.append(end) return self - @deprecated("...") + @deprecated("Use `power_transformer.ends.remove(end)` instead.") def remove_end(self, end: PowerTransformerEnd) -> PowerTransformer: self.ends.remove(end) return self - @deprecated("...") + @deprecated("Use `power_transformer.ends.clear()` instead.") def clear_ends(self) -> PowerTransformer: self.ends.clear() return self - def _validate_end(self, end: PowerTransformerEnd): - """ - Validate an end against this `PowerTransformer`'s `PowerTransformerEnd`s. + # endregion - `end` The `PowerTransformerEnd` to validate. - Returns True if `end` is already associated with this `PowerTransformer`, otherwise False. - Raises `ValueError` if `end.power_transformer` is not this `PowerTransformer`, or if this `PowerTransformer` has a different `PowerTransformerEnd` - with the same mRID. - """ - self._validate_reference_by_field(end, end.end_number, self.get_end_by_num, "end_number") - - if not end.power_transformer: - end.power_transformer = self - - require( - end.power_transformer is self, - lambda: f"{end} `power_transformer` property references {end.power_transformer}, expected {str(self)}.", - ) - - if end.end_number == 0: - end.end_number = self.num_ends() + 1 + # endregion From 5f416ad867b160473780335980bc147cb98f7cc5 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Tue, 21 Jul 2026 14:04:02 +1000 Subject: [PATCH 03/54] Backfill progress Signed-off-by: Boris Filin --- .../ewb/dataclass_descriptors/lazy_list.py | 1 + .../ewb/dataclass_descriptors/mrid_list.py | 23 ++++++++++++---- src/zepben/ewb/dataclass_descriptors/tst.py | 5 ++++ .../base/core/sub_geographical_region.py | 26 ++++++++++++++----- .../iec61970/base/wires/power_transformer.py | 1 + 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/zepben/ewb/dataclass_descriptors/lazy_list.py b/src/zepben/ewb/dataclass_descriptors/lazy_list.py index c4980e993..43b19ca5e 100644 --- a/src/zepben/ewb/dataclass_descriptors/lazy_list.py +++ b/src/zepben/ewb/dataclass_descriptors/lazy_list.py @@ -173,6 +173,7 @@ def __getitem__(self, item): return (self._get_or_empty())[item] + if __name__ == '__main__': @zb_dataclass class C(DataclassBase): diff --git a/src/zepben/ewb/dataclass_descriptors/mrid_list.py b/src/zepben/ewb/dataclass_descriptors/mrid_list.py index 178b19a6b..3b31802c0 100644 --- a/src/zepben/ewb/dataclass_descriptors/mrid_list.py +++ b/src/zepben/ewb/dataclass_descriptors/mrid_list.py @@ -5,7 +5,7 @@ from abc import abstractmethod, ABC from dataclasses import dataclass, field, Field from types import MemberDescriptorType -from typing import Any, TypeVar, Protocol +from typing import Any, TypeVar, Protocol, Callable import pytest from typing_extensions import Self @@ -59,14 +59,27 @@ def apply(self, element: S, owner: Any): name = self.backfill_prop.fget.__name__ else: name = self.backfill_prop.__name__ - - if getattr(element, name) is None: - setattr(element, name, owner) - ref = getattr(element, name) + if hasattr(self.backfill_prop, "__target"): + backing_name = self.backfill_prop.__target.__name__ + else: + backing_name = name + + if getattr(element, backing_name) is None: + setattr(element, backing_name, owner) + + ref = getattr(element, backing_name) if ref is not owner: raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") +def targets(target: Any): + if not any(isinstance(target, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): + raise TypeError(f"target parameter of the target decorator has to be an instance of dataclass Field, instead is {target}") + + def dec(func: Callable): + setattr(func, "__target", target) + return func + return dec class LazyMridList(LazyValidatedList, MridCollection[S]): def __init__(self, diff --git a/src/zepben/ewb/dataclass_descriptors/tst.py b/src/zepben/ewb/dataclass_descriptors/tst.py index 55ed4ce86..b0e8fa5e9 100644 --- a/src/zepben/ewb/dataclass_descriptors/tst.py +++ b/src/zepben/ewb/dataclass_descriptors/tst.py @@ -130,6 +130,9 @@ class C: _x: list[int] | None = field(default=None) x: ListWrapper = ListWrapper(_x, sort_by=lambda it: -it) + @property + def thing(self): + return 42 if __name__ == '__main__': c = C([1, 2, 3]) @@ -141,6 +144,8 @@ class C: print(c) print(c.x) + c.thing = 24 + # class C: # def __init__(self, x, f1, f2): # self.x = x diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index e401267ce..6b1783a9c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -7,15 +7,14 @@ __all__ = ["SubGeographicalRegion"] -from typing import Optional, List, Generator, TYPE_CHECKING from dataclasses import field +from typing import Optional, List, TYPE_CHECKING + from typing_extensions import deprecated -from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, targets +from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.substation import Substation if TYPE_CHECKING: @@ -28,8 +27,7 @@ class SubGeographicalRegion(IdentifiedObject): A subset of a geographical region of a power system network model. """ - geographical_region: Optional[GeographicalRegion] = field(default=None) - """The geographical region to which this sub-geographical region is within.""" + _geographical_region: Optional[GeographicalRegion] = field(default=None) _substations: Optional[List[Substation]] = field(default=None) @@ -39,6 +37,16 @@ def __init__(self, *args, substations: List[Substation] = None, **kwargs): for sub in substations: self.add_substation(sub) + @property + @targets(_geographical_region) + def geographical_region(self): + """The geographical region to which this sub-geographical region is within.""" + return self._geographical_region + + @geographical_region.setter + @deprecated("Geographical region is a backfill property - it should only be set by adding the sub region to the sub regions list") + def geographical_region(self, value): + self._geographical_region = value @property def geographical_region(self): @@ -92,3 +100,7 @@ def clear_substations(self) -> SubGeographicalRegion: # endregion substations boilerplate # endregion deprecated list boilerplate + +if __name__ == '__main__': + s = SubGeographicalRegion("") + s.geographical_region = GeographicalRegion("") \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index 80005abbf..857a45288 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -11,6 +11,7 @@ from dataclasses import field from typing import List, Optional, TYPE_CHECKING +from zepben.ewb import remove_descriptor_annotations from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill if sys.version_info >= (3, 13): From 6df08e99b070936401f0ee0ffd3668b2264bf4f5 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Tue, 21 Jul 2026 16:15:26 +1000 Subject: [PATCH 04/54] Indexed lists migration Signed-off-by: Boris Filin --- .../ewb/dataclass_descriptors/lazy_list.py | 160 ++++++++- .../iec61968/assetinfo/relay_info.py | 124 +++---- .../protection/protection_relay_function.py | 336 +++++------------- .../ewb/model/cim/iec61968/common/location.py | 161 ++++----- .../base/diagramlayout/diagram_object.py | 154 ++++---- 5 files changed, 418 insertions(+), 517 deletions(-) diff --git a/src/zepben/ewb/dataclass_descriptors/lazy_list.py b/src/zepben/ewb/dataclass_descriptors/lazy_list.py index 43b19ca5e..7969c2c1f 100644 --- a/src/zepben/ewb/dataclass_descriptors/lazy_list.py +++ b/src/zepben/ewb/dataclass_descriptors/lazy_list.py @@ -2,10 +2,10 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from abc import ABC +from abc import ABC, abstractmethod from collections.abc import Collection from dataclasses import field -from typing import TypeVar, Protocol, Any, Iterable +from typing import TypeVar, Protocol, Any, Iterable, Generic, Iterator, Sequence, overload, Callable from typing_extensions import deprecated, Self @@ -112,7 +112,74 @@ def __set__(self, instance, value): else: self.__get__(instance).clear() -class LazyValidatedList(_IterableWrapper, IndexedMutableCollection[T]): + +class AbstractBackedCollection(Collection[T], Generic[T], ABC): + + @abstractmethod + def _get_collection(self) -> Collection[T]: + ... + + @abstractmethod + def append(self, element: T) -> None: + ... + + def extend(self, elements: Iterable[T]) -> None: + for element in elements: + self.append(element) + + @abstractmethod + def remove(self, element: T) -> None: + ... + + @abstractmethod + def clear(self) -> None: + ... + + def __len__(self) -> int: + return len(self._get_collection()) + + def __iter__(self) -> Iterator[T]: + return iter(self._get_collection()) + + def __contains__(self, element: object) -> bool: + return element in self._get_collection() + + def for_each_indexed(self, action: Callable[[int, T], Any]): + """ + Call the `action` on each item in the list + + :param action: An action to apply to each :class:`RelaySetting` in the `thresholds` collection, taking the index of the threshold, and the threshold itself. + """ + for index, item in enumerate(self._get_collection()): + action(index, item) + + + +class AbstractBackedList( + AbstractBackedCollection[T], + Sequence[T], + Generic[T], + ABC, +): + + @abstractmethod + def _get_collection(self) -> Sequence[T]: + ... + + @overload + def __getitem__(self, index: int) -> T: + ... + + @overload + def __getitem__(self, index: slice) -> Sequence[T]: + ... + + def __getitem__(self, index: int | slice) -> T | Sequence[T]: + return self._get_collection()[index] + + +class LazyValidatedList(_IterableWrapper, AbstractBackedList[T]): + def __init__(self, private_field, validate=None, @@ -124,7 +191,7 @@ def __init__(self, def _get(self): return getattr(self.instance, self.backing_name) - def _get_or_empty(self) -> list[T]: + def _get_collection(self) -> list[T]: return getattr(self.instance, self.backing_name) or [] # clearIfEmpty is inlined for performance @@ -144,16 +211,16 @@ def append(self, item): existing.sort(key=self.sort_by) def __len__(self): - return len(self._get_or_empty()) + return len(self._get_collection()) def __iter__(self): - return iter(self._get_or_empty()) + return iter(self._get_collection()) def __contains__(self, item): - return item in (self._get_or_empty()) + return item in (self._get_collection()) def remove(self, item): - existing = self._get_or_empty() + existing = self._get_collection() existing.remove(item) if not existing: self.clear() @@ -164,31 +231,92 @@ def clear(self): def __repr__(self): if self.instance is None: return self.__class__.__repr__() - return str(self._get_or_empty()) + return str(self._get_collection()) def __str__(self): return self.__repr__() def __getitem__(self, item): - return (self._get_or_empty())[item] + return (self._get_collection())[item] +class LazyIndexedList(LazyValidatedList[T]): + + def __init__( + self, + private_field, + element_description: str + ): + super().__init__(private_field) + self.element_description = element_description + + def insert(self, index: int, item: T) -> None: + """ + Insert ``item`` at ``index``. + """ + size = len(self) + + if not 0 <= index <= size: + raise ValueError( + f"Unable to add {self.element_description} to " + f"{self.instance}. " + f"Sequence number {index} is invalid. " + f"Expected a value between 0 and {size}. " + "Make sure you are adding the items in order and there are " + "no gaps in the numbering." + ) + + existing = getattr(self.instance, self.backing_name) + + if existing is None: + existing = [item] + setattr(self.instance, self.backing_name, existing) + else: + existing.insert(index, item) + + if self.sort_by is not None: + existing.sort(key=self.sort_by) + + def append(self, item: T) -> None: + self.insert(len(self), item) + + def pop(self, index: int = -1) -> T: + """ + Remove and return the item at ``index``. + + Uses normal Python list semantics, including support for negative + indexes and raising ``IndexError`` when the index is invalid. + """ + existing = getattr(self.instance, self.backing_name) + + if existing is None: + raise IndexError("pop from empty list") + + item = existing.pop(index) + + if not existing: + self.clear() + + return item if __name__ == '__main__': @zb_dataclass class C(DataclassBase): _xs: list[int] | None = field(default=None) - xs: IndexedMutableCollection[int] = LazyValidatedList( + xs: LazyIndexedList[int] = LazyIndexedList( _xs, - validate=lambda self, it: print("validate", it), - sort_by=lambda it: -it + "Thing" ) c = C(xs=[24, 42]) - print(c._xs) + print(c._xs, c.xs) c.xs.clear() - print(c._xs) + print(c._xs, c.xs) c.xs.append(1) c.xs.append(2) c.xs.append(3) - print(c._xs) + print(c._xs, c.xs) + c.xs.insert(2, 42) + print(c._xs, c.xs) + c.xs.pop(2) + print(c._xs, c.xs) diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index 1422e7455..ab18d871e 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -7,8 +7,12 @@ __all__ = ["RelayInfo"] +from dataclasses import field from typing import Optional, List, Generator, Callable, Any +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.util import ngen, nlen, safe_remove, require @@ -26,20 +30,12 @@ class RelayInfo(AssetInfo): reclose_fast: Optional[bool] = None """True if reclose_delays are associated with a fast Curve, false otherwise.""" - _reclose_delays: Optional[List[float]] = None + _reclose_delays: Optional[List[float]] = field(default=None) - def __init__(self, *args, reclose_delays: Optional[List[float]] = None, **kwargs): - super(RelayInfo, self).__init__(*args, **kwargs) - if reclose_delays: - for index, delay in enumerate(reclose_delays): - self.add_delay(delay, index) - - @property - def reclose_delays(self) -> Generator[float, None, None]: - """ - The reclose delays for this curve and relay type. The index of the list is the reclose step, and the value is the overall delay time. - """ - return ngen(self._reclose_delays) + reclose_delays: LazyIndexedList[float] = LazyIndexedList( + _reclose_delays, + "float" + ) def set_delays(self, delays: List[float]) -> RelayInfo: """ @@ -48,83 +44,53 @@ def set_delays(self, delays: List[float]) -> RelayInfo: :param delays: The delays to set. The provided list will be copied. :return: A reference to this :class:`RelayInfo` to allow fluent use. """ - self._reclose_delays = delays.copy() + self.reclose_delays.clear() + self.reclose_delays.extend(delays) return self - def num_delays(self) -> int: - """ - Get the number of reclose delays for this :class:`RelayInfo` - """ - return nlen(self._reclose_delays) + # region deprecated methods - def get_delay(self, index: int) -> float: - """ - Get the reclose delay at the specified index, if it exists. Otherwise, this returns + # region reclose_delays boilerplate - :param index: The index of the delay to retrieve. - :return: The reclose delay at `index` if it exists, otherwise None. - """ - if self._reclose_delays: - return self._reclose_delays[index] - else: - raise IndexError(index) - - def for_each_delay(self, action: Callable[[int, float], Any]): - """ - Call the `action` on each delay in the `reclose_delays` collection - - :param action: An action to apply to each delay in the `reclose_delays` collection, taking the index of the delay, and the delay itself. - """ - for index, point in enumerate(self._reclose_delays): - action(index, point) - - def add_delay(self, delay: float, index: int = None) -> RelayInfo: - """ - Add a reclose delay. + @deprecated("Use len(reclose_delays) instead.") + def num_delays(self) -> int: + return len(self.reclose_delays) - :param delay: The delay in seconds to add. - :param index: The index into the list to add the delay at. Defaults to the end of the list. - :return: A reference to this :class:`RelayInfo` to allow fluent use. - """ - if index is None: - index = self.num_delays() - require(0 <= index <= self.num_delays(), - lambda: f"Unable to add float to {str(self)}. Index number {index} " - f"is invalid. Expected a value between 0 and {self.num_delays()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._reclose_delays = list() if self._reclose_delays is None else self._reclose_delays - self._reclose_delays.insert(index, delay) + @deprecated("Use reclose_delays[index] instead.") + def get_delay(self, index: int) -> float: + return self.reclose_delays[index] + + @deprecated("Use reclose_delays.for_each_indexed(action) instead.") + def for_each_delay( + self, + action: Callable[[int, float], Any], + ) -> None: + self.reclose_delays.for_each_indexed(action) + + @deprecated("Use reclose_delays.append(delay)") + def add_delay( + self, + delay: float, + index: int | None = None, + ) -> RelayInfo: + if index is None: index = len(self.reclose_delays) + self.reclose_delays.insert(index, delay) return self + @deprecated("Use reclose_delays.remove(delay) instead.") def remove_delay(self, delay: float) -> RelayInfo: - """ - Remove a delay from the list. - - :param delay: The delay to remove. - :return: A reference to this :class:`RelayInfo` to allow fluent use. - """ - self._reclose_delays = safe_remove(self._reclose_delays, delay) + self.reclose_delays.remove(delay) return self + @deprecated("Use reclose_delays.pop(index) instead.") def remove_delay_at(self, index: int) -> float: - """ - Remove a delay from the list. - - :param index: The index of the delay to remove. - :return: The delay that was removed, or `None` if no delay was present at `index`. - :raises IndexError: If `sequence_number` is out of range. - """ - if self._reclose_delays: - delay = self._reclose_delays.pop(index) - self._reclose_delays = self._reclose_delays if self._reclose_delays else None - return delay - raise IndexError(index) + return self.reclose_delays.pop(index) + @deprecated("Use reclose_delays.clear() instead.") def clear_delays(self) -> RelayInfo: - """ - Clear all reclose delays. - - :return: A reference to this :class:`RelayInfo` to allow fluent use. - """ - self._reclose_delays = None + self.reclose_delays.clear() return self + + # endregion + + # endregion \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 66cea87a5..50e72b4de 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -11,6 +11,8 @@ import warnings from typing import Optional, List, Generator, Iterable, Callable, TYPE_CHECKING, Any from abc import ABCMeta + +from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList if sys.version_info >= (3, 13): from warnings import deprecated else: @@ -68,41 +70,10 @@ class ProtectionRelayFunction(PowerSystemResource, metaclass=ABCMeta): _schemes: Optional[List[ProtectionRelayScheme]] = field(default=None) - _time_limits: Optional[List[float]] = None + _time_limits: Optional[List[float]] = field(default=None) - _thresholds: Optional[List[RelaySetting]] = None + _thresholds: Optional[List[RelaySetting]] = field(default=None) - def __init__( - self, - *args, - sensors: Iterable[Sensor] = None, - protected_switches: Iterable[ProtectedSwitch] = None, - schemes: Iterable[ProtectionRelayScheme] = None, - time_limits: Iterable[float] = None, - thresholds: Iterable[RelaySetting] = None, - relay_info: RelayInfo | None = None, - **kwargs - ): - super(ProtectionRelayFunction, self).__init__(*args, **kwargs) - - if sensors is not None: - for sensor in sensors: - self.add_sensor(sensor) - if protected_switches is not None: - for protected_switch in protected_switches: - self.add_protected_switch(protected_switch) - if schemes is not None: - for scheme in schemes: - self.add_scheme(scheme) - if time_limits is not None: - for time_limit in time_limits: - self.add_time_limit(time_limit) - if thresholds is not None: - for threshold in thresholds: - self.add_threshold(threshold) - if relay_info is not None: - warnings.warn("relay_info is deprecated, use asset_info instead.") - self.asset_info = relay_info @property @deprecated("use asset_info instead.") @@ -115,247 +86,123 @@ def relay_info(self): def relay_info(self, relay_info: Optional[RelayInfo]): self.asset_info = relay_info - @property - def thresholds(self) -> Generator[RelaySetting, None, None]: - """ - [ZBEX] Yields all the thresholds[:class:`RelaySettings`] for this :class:`ProtectionRelayFunction`. The order of thresholds corresponds to the order of time limits. - - :return: A generator that iterates over all thresholds[:class:`RelaySettings`] for this relay function. - """ - return ngen(self._thresholds) - - @property - def time_limits(self) -> Generator[float, None, None]: - """ - [ZBEX] Yields all the time limits (in seconds) for this relay function. Order of entries corresponds to the order of entries in thresholds. + time_limits: LazyIndexedList[float] = LazyIndexedList( + _time_limits, + "float" + ) - :return: A generator that iterates over all time limits for this relay function. - """ - return ngen(self._time_limits) + thresholds: LazyIndexedList[RelaySetting] = LazyIndexedList( + _thresholds, + "RelaySetting" + ) - @property - def sensors(self) -> Generator[Sensor, None, None]: - """ - [ZBEX] Yields all the :class:`Sensors` for this relay function. + sensors: MridCollection[Sensor] = LazyMridList( + _sensors, + "A Sensor", + ) - :return: A generator that iterates over all :class:`Sensors` for this relay function. - """ - return ngen(self._sensors) + protected_switches: MridCollection[ProtectedSwitch] = LazyMridList( + _protected_switches, + "A ProtectedSwitch", + ) - @property - def protected_switches(self) -> Generator[ProtectedSwitch, None, None]: - """ - [ZBEX] Yields the :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. + schemes: MridCollection[ProtectionRelayScheme] = LazyMridList( + _schemes, + "A ProtectionRelayScheme", + ) - :return: A generator that iterates over all :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. - """ - return ngen(self._protected_switches) - @property - def schemes(self) -> Generator[ProtectionRelayScheme, None, None]: - """ - [ZBEX] Yields the :class:`ProtectionRelaySchemes` this :class:`ProtectionRelayFunction` operates under. + # region deprecated list boilerplate - :return: A generator that iterates over all :class:`ProtectionRelaySchemes` this :class:`ProtectionRelayFunction` operates under. - """ - return ngen(self._schemes) + # region thresholds boilerplate + @deprecated("Use thresholds.for_each_indexed(action)") def for_each_threshold(self, action: Callable[[int, RelaySetting], Any]): - """ - Call the `action` on each :class:`RelaySetting` in the `thresholds` collection - - :param action: An action to apply to each :class:`RelaySetting` in the `thresholds` collection, taking the index of the threshold, and the threshold itself. - """ - for index, point in enumerate(self.thresholds): - action(index, point) - - def add_threshold(self, threshold: RelaySetting, sequence_number: int = None) -> ProtectionRelayFunction: - """ - Add a threshold[:class:`RelaySetting`] to this :class:`ProtectionRelayFunction`'s list of thresholds. - - :param threshold: The threshold[:class:`RelaySetting`] to add to this :class:`ProtectionRelayFunction`. - :param sequence_number: The sequence number of the `threshold` being added. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if sequence_number is None: - sequence_number = self.num_thresholds() - require(0 <= sequence_number <= self.num_thresholds(), - lambda: f"Unable to add RelaySetting to {str(self)}. Sequence number {sequence_number} " - f"is invalid. Expected a value between 0 and {self.num_thresholds()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._thresholds = list() if self._thresholds is None else self._thresholds - self._thresholds.insert(sequence_number, threshold) + self.thresholds.for_each_indexed(action) + + @deprecated("Use thresholds.append(threshold)") + def add_threshold( + self, + threshold: RelaySetting, + sequence_number: int = None, + ) -> ProtectionRelayFunction: + if sequence_number is None: sequence_number = len(self.thresholds) + self.thresholds.insert(sequence_number, threshold) return self + @deprecated("Use len(thresholds) instead.") def num_thresholds(self) -> int: - """ - Get the number of thresholds for this :class:`ProtectionRelayFunction`. - - :return: The number of thresholds for this `ProtectionRelayFunction`. - """ - return nlen(self._thresholds) + return len(self.thresholds) + @deprecated("Use thresholds[sequence_number] instead.") def get_threshold(self, sequence_number: int) -> RelaySetting: - """ - Get the threshold[:class:`RelaySetting`] for this :class:`ProtectionRelayFunction` by its `sequence_number`. - - :param sequence_number: The sequence_number of the threshold :class:`RelaySetting` for this :class:`ProtectionRelayFunction`. - :returns: The threshold[:class:`RelaySetting`] for this :class:`ProtectionRelayFunction` with sequence number `sequence_number` - :raises IndexError: if no :class:`RelaySetting` was found with sequence_number `sequence_number`. - """ - if self._thresholds is not None: - return self._thresholds[sequence_number] - else: - raise IndexError(sequence_number) - - def remove_threshold(self, threshold: RelaySetting) -> ProtectionRelayFunction: - """ - Removes a threshold[:class:`RelaySetting`] from this :class:`ProtectionRelayFunction`. - - :param threshold: The threshold[:class:`RelaySetting`] to disassociate from this :class:`ProtectionRelayFunction`. - :returns: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._thresholds = safe_remove(self._thresholds, threshold) - return self + return self.thresholds[sequence_number] - def remove_threshold_at(self, sequence_number: int) -> RelaySetting: - """ - Removes a threshold[:class:`RelaySetting`] from this :class:`ProtectionRelayFunction`. + @deprecated("Use thresholds.remove(threshold) instead.") + def remove_threshold( + self, + threshold: RelaySetting, + ) -> ProtectionRelayFunction: + self.thresholds.remove(threshold) + return self - :param sequence_number: The sequence_number of the threshold[:class:`RelaySetting`] to disassociate from this :class:`ProtectionRelayFunction`. - :returns: A reference to removed threshold[:class:`RelaySetting`]. - :raises IndexError: If `sequence_number` is out of range. - """ - threshold = self.get_threshold(sequence_number) - self._thresholds = safe_remove(self._thresholds, threshold) - return threshold + @deprecated("Use thresholds.pop(sequence_number) instead.") + def remove_threshold_at( + self, + sequence_number: int, + ) -> RelaySetting: + return self.thresholds.pop(sequence_number) + @deprecated("Use thresholds.clear() instead.") def clear_thresholds(self) -> ProtectionRelayFunction: - """ - Removes all thresholds from this :class:`ProtectionRelayFunction`. - - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._thresholds = None + self.thresholds.clear() return self + # endregion + + # region time_limits boilerplate + @deprecated("Use time_limits.for_each_indexed(action)") def for_each_time_limit(self, action: Callable[[int, float], Any]): - """ - Call the `action` on each time limit in the `time_limits` collection - - :param action: An action to apply to each time limit in the `time_limits` collection, taking the index of the limit, and the limit itself. - """ - for index, limit in enumerate(self.time_limits): - action(index, limit) - - def add_time_limit(self, time_limit: float, index: int = None) -> ProtectionRelayFunction: - """ - Add a time limit. - - :param time_limit: The time limit in seconds to add to this :class:`ProtectionRelayFunction`. - :param index: The index into the list to add the time limit at. Defaults to the end of the list. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if index is None: - index = self.num_time_limits() - require(0 <= index <= self.num_time_limits(), - lambda: f"Unable to add float to {str(self)}. Sequence number {index} " - f"is invalid. Expected a value between 0 and {self.num_time_limits()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._time_limits = list() if self._time_limits is None else self._time_limits - self._time_limits.insert(index, time_limit) - return self + self.time_limits.for_each_indexed(action) - def num_time_limits(self) -> int: - return nlen(self._time_limits) - - def get_time_limit(self, index: int): - """ - Get the time limit for this :class:`ProtectionRelayFunction` by its `index`. - - :param index: The index of the desired time limit. - :returns: The time limit with the specified `index` if it exists. - :raises IndexError: if no time limit was found with provided index. - """ - if self._time_limits is not None: - return self._time_limits[index] - else: - raise IndexError(index) - - def remove_time_limit(self, time_limit: float) -> ProtectionRelayFunction: - """ - Remove a time limit from the list. - - :param time_limit: The time limit to remove. - :returns: A reference to this `ProtectionRelayFunction` to allow fluent use. - """ - self._time_limits = safe_remove(self._time_limits, time_limit) + @deprecated( + "Use time_limits.append(time_limit)") + def add_time_limit( + self, + time_limit: float, + index: int = None, + ) -> ProtectionRelayFunction: + if index is None: index = len(self.time_limits) + self.time_limits.insert(index, time_limit) return self - def remove_time_limit_at(self, index: int) -> float: - """ - Remove a time limit from the list. - - :param index: The time limit to remove. - :returns: The time limit that was removed, or `None` if no time limit was present at `index`. - :raises IndexError: If `sequence_number` is out of range. - """ - if self._time_limits: - limit = self._time_limits.pop(index) - self._time_limits = self._time_limits if self._time_limits else None - return limit - raise IndexError(index) + @deprecated("Use len(time_limits) instead.") + def num_time_limits(self) -> int: + return len(self.time_limits) - def clear_time_limits(self) -> ProtectionRelayFunction: - """ - Removes all time limits from this :class:`ProtectionRelayFunction`. + @deprecated("Use time_limits[index] instead.") + def get_time_limit(self, index: int) -> float: + return self.time_limits[index] - :returns: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._time_limits = None + @deprecated("Use time_limits.remove(time_limit) instead.") + def remove_time_limit( + self, + time_limit: float, + ) -> ProtectionRelayFunction: + self.time_limits.remove(time_limit) return self - sensors: MridCollection[Sensor] = LazyMridList( - _sensors, - "A Sensor", - ) - - - - - - - protected_switches: MridCollection[ProtectedSwitch] = LazyMridList( - _protected_switches, - "A ProtectedSwitch", - ) - - - + @deprecated("Use time_limits.pop(index) instead.") + def remove_time_limit_at(self, index: int) -> float: + return self.time_limits.pop(index) - def remove_protected_switch(self, protected_switch: Optional[ProtectedSwitch]) -> ProtectionRelayFunction: - """ - Disassociate this :class:`ProtectionRelayFunction` from a :class:`ProtectedSwitch`. - - :param protected_switch: The :class:`ProtectedSwitch` to disassociate from this :class:`ProtectionRelayFunction`. - :raises ValueError: If protected_switch was not associated with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._sensors = safe_remove(self._protected_switches, protected_switch) + @deprecated("Use time_limits.clear() instead.") + def clear_time_limits(self) -> ProtectionRelayFunction: + self.time_limits.clear() return self + # endregion - schemes: MridCollection[ProtectionRelayScheme] = LazyMridList( - _schemes, - "A ProtectionRelayScheme", - ) - - - - - - - # region deprecated list boilerplate # region sensors boilerplate @deprecated("Use len(obj.sensors) instead.") @@ -398,6 +245,11 @@ def add_protected_switch(self, protected_switch: ProtectedSwitch) -> ProtectionR self.protected_switches.append(protected_switch) return self + @deprecated("Use obj.protected_switches.remove(protected_switch) instead.") + def remove_protected_switch(self, protected_switch: Optional[ProtectedSwitch]) -> ProtectionRelayFunction: + self.protected_switches.remove(protected_switch) + return self + @deprecated("Use obj.protected_switches.clear() instead.") def clear_protected_switches(self) -> ProtectionRelayFunction: self.protected_switches.clear() diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index 361d964a7..3cc31a458 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -7,13 +7,17 @@ __all__ = ["Location"] -from typing import List, Optional, Generator, Callable, Any +from dataclasses import field +from typing import List, Optional, Callable, Any +from typing_extensions import deprecated + +from zepben.ewb import Alias +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import require, nlen, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @zb_dataclass @@ -25,107 +29,84 @@ class Location(IdentifiedObject): main_address: Optional[StreetAddress] = None """Main address of the location.""" - _position_points: Optional[List[PositionPoint]] = None - - def __init__(self, *args, position_points: List[PositionPoint] = None, **kwargs): - """ - `position_points` A list of `PositionPoint`s to associate with this `Location`. - """ - super(Location, self).__init__(*args, **kwargs) - if position_points: - for point in position_points: - self.add_point(point) - - @property - def points(self) -> Generator[PositionPoint, None, None]: - """ - Returns Generator over the `PositionPoint`s of this `Location`. - """ - for point in ngen(self._position_points): - yield point - - def for_each_point(self, action: Callable[[int, PositionPoint], Any]): - """ - Call the `action` on each :class:`PositionPoint` in the `points` collection - - :param action: An action to apply to each :class:`PositionPoint` in the `points` collection, taking the index of the point, and the point itself. - """ - for index, point in enumerate(self.points): - action(index, point) + _position_points: Optional[List[PositionPoint]] = field(default=None) + + points: LazyIndexedList[PositionPoint] = LazyIndexedList( + _position_points, + "PositionPoint", + ) + position_points = Alias(points) + + + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. + + # region points boilerplate + @deprecated("Use points.for_each_indexed(action) instead.") + def for_each_point( + self, + action: Callable[[int, PositionPoint], Any], + ): + self.points.for_each_indexed(action) + + @deprecated("Use len(points) instead.") def num_points(self): - """ - Returns The number of `PositionPoint`s in this `Location` - """ - return nlen(self._position_points) + return len(self.points) + @deprecated("Use points[sequence_number] instead.") def get_point(self, sequence_number: int) -> PositionPoint: - """ - Get the `sequence_number` `PositionPoint` for this `Location`. - - `sequence_number` The sequence number of the `PositionPoint` to get. - Returns The `PositionPoint` identified by `sequence_number` - Raises IndexError if this `Location` didn't contain `sequence_number` points. - """ - return self._position_points[sequence_number] + return self.points[sequence_number] - def __getitem__(self, item): - return self.get_point(item) + @deprecated("Use points[item] instead.") + def __getitem__(self, item: int) -> PositionPoint: + return self.points[item] + @deprecated("Use points.append(point) instead.") def add_point(self, point: PositionPoint) -> Location: - """ - Associate a `PositionPoint` with this `Location`, assigning it a sequence_number of `num_points`. - `point` The `PositionPoint` to associate with this `Location`. - Returns A reference to this `Location` to allow fluent use. - """ - return self.insert_point(point) - - def insert_point(self, point: PositionPoint, sequence_number: int = None) -> Location: - """ - Associate a `PositionPoint` with this `Location` - - `point` The `PositionPoint` to associate with this `Location`. - `sequence_number` The sequence number of the `PositionPoint`. - Returns A reference to this `Location` to allow fluent use. - Raises `ValueError` if `sequence_number` < 0 or > `num_points()`. - """ - if sequence_number is None: - sequence_number = self.num_points() - require(0 <= sequence_number <= self.num_points(), - lambda: f"Unable to add PositionPoint to {str(self)}. Sequence number {sequence_number} " - f"is invalid. Expected a value between 0 and {self.num_points()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._position_points = list() if self._position_points is None else self._position_points - self._position_points.insert(sequence_number, point) + self.points.append(point) return self - def __setitem__(self, key, value): - return self.insert_point(value, key) + @deprecated("Use points.insert(sequence_number, point)") + def insert_point( + self, + point: PositionPoint, + sequence_number: int | None = None, + ) -> Location: + if sequence_number is None: sequence_number = len(self.points) + self.points.insert(sequence_number, point) - def remove_point(self, point: PositionPoint) -> Location: - """ - Remove a `PositionPoint` from this `Location` - `point` The `PositionPoint` to remove. - Raises `ValueError` if `point` was not part of this `Location` - Returns A reference to this `Location` to allow fluent use. - """ - self._position_points = safe_remove(self._position_points, point) return self - def remove_point_by_sequence_number(self, sequence_number: int) -> PositionPoint: - """ - Remove a :class:`PositionPoint` from this :class:`Location` by its sequence number. + @deprecated("Use points.insert(key, value) instead.") + def __setitem__( + self, + key: int, + value: PositionPoint, + ) -> None: + self.points.insert(key, value) - NOTE: This will update the sequence numbers of all items located after the removed sequence number. + @deprecated("Use points.remove(point) instead.") + def remove_point(self, point: PositionPoint) -> Location: + self.points.remove(point) + return self - :param sequence_number: The sequence number of the `PositionPoint` to remove. - :return: The :class:`PositionPoint` that was removed, or null if there was no :class:`PositionPoint` for the given `sequenceNumber`. - :raises IndexError: If no :class:`PositionPoint` with the specified `sequence_number` was not associated with this :class:`Location`. - """ - point = self.get_point(sequence_number) - self._position_points = safe_remove(self._position_points, point) - return point + @deprecated("Use points.pop(sequence_number) instead.") + def remove_point_by_sequence_number( + self, + sequence_number: int, + ) -> PositionPoint: + return self.points.pop(sequence_number) + @deprecated("Use points.clear() instead.") def clear_points(self) -> Location: - self._position_points = None + self.points.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 55f4e0bba..04ff014db 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -7,10 +7,13 @@ __all__ = ["DiagramObject"] +from dataclasses import field from typing import Optional, List, Generator, Callable, TYPE_CHECKING, Any from typing_extensions import deprecated +from zepben.ewb import Alias +from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint from zepben.ewb.util import nlen, ngen, require, safe_remove @@ -38,15 +41,8 @@ class DiagramObject(IdentifiedObject): rotation: float = 0.0 """Sets the angle of rotation of the diagram object. Zero degrees is pointing to the top of the diagram. Rotation is clockwise.""" - _diagram_object_points: Optional[List[DiagramObjectPoint]] = None + _diagram_object_points: Optional[List[DiagramObjectPoint]] = field(default=None) - def __init__(self, *args, diagram: Diagram = None, diagram_object_points: List[DiagramObjectPoint] = None, **kwargs): - super(DiagramObject, self).__init__(*args, **kwargs) - if diagram: - self.diagram = diagram - if diagram_object_points: - for point in diagram_object_points: - self.add_point(point) @property def diagram(self): @@ -61,105 +57,83 @@ def diagram(self, diag): else: raise ValueError(f"diagram for {str(self)} has already been set to {self._diagram}, cannot reset this field to {diag}") - @property - def points(self) -> Generator[DiagramObjectPoint, None, None]: - """ - The `DiagramObjectPoint`s for this `DiagramObject`. - """ - return ngen(self._diagram_object_points) + points: LazyIndexedList[DiagramObjectPoint] = LazyIndexedList( + _diagram_object_points, + "DiagramObjectPoint", + ) + diagram_object_points = Alias(points) + + + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. - def for_each_point(self, action: Callable[[int, DiagramObjectPoint], Any]): - """ - Call the `action` on each :class:`DiagramObjectPoint` in the `points` collection + # region points boilerplate - :param action: An action to apply to each :class:`DiagramObjectPoint` in the `points` collection, taking the index of the point, and the point itself. - """ - for index, point in enumerate(self.points): - action(index, point) + @deprecated("Use points.for_each_indexed(action) instead.") + def for_each_point( + self, + action: Callable[[int, DiagramObjectPoint], Any], + ): + self.points.for_each_indexed(action) + @deprecated("Use len(points) instead.") def num_points(self): - """ - Returns the number of `DiagramObjectPoint`s associated with this `DiagramObject` - """ - return nlen(self._diagram_object_points) + return len(self.points) + @deprecated("Use points[sequence_number] instead.") def get_point(self, sequence_number: int) -> DiagramObjectPoint: - """ - Get the `DiagramObjectPoint` for this `DiagramObject` represented by `sequence_number` . - A diagram object can have 0 or more points to reflect its layout position, routing (for polylines) or boundary (for polygons). - Index in the underlying points collection corresponds to the sequence number - - `sequence_number` The sequence number of the `DiagramObjectPoint` to get. - Returns The `DiagramObjectPoint` identified by `sequence_number` - Raises IndexError if this `DiagramObject` didn't contain `sequence_number` points. - """ - if self._diagram_object_points is not None: - return self._diagram_object_points[sequence_number] - else: - raise IndexError(sequence_number) + return self.points[sequence_number] + @deprecated("Use points[item] instead.") def __getitem__(self, item: int) -> DiagramObjectPoint: - return self.get_point(item) + return self.points[item] + @deprecated("Use points.append(point) instead.") def add_point(self, point: DiagramObjectPoint) -> DiagramObject: - """ - Associate a `DiagramObjectPoint` with this `DiagramObject`, assigning it a sequence_number of `num_points`. - `point` The `DiagramObjectPoint` to associate with this `DiagramObject`. - Returns A reference to this `DiagramObject` to allow fluent use. - """ - return self.insert_point(point) - - def insert_point(self, point: DiagramObjectPoint, sequence_number: int = None) -> DiagramObject: - """ - Associate a `DiagramObjectPoint` with this `DiagramObject` - - `point` The `DiagramObjectPoint` to associate with this `DiagramObject`. - `sequence_number` The sequence number of the `DiagramObjectPoint`. - Returns A reference to this `DiagramObject` to allow fluent use. - Raises `ValueError` if `sequence_number` < 0 or > `num_points()`. - """ - if sequence_number is None: - sequence_number = self.num_points() - require(0 <= sequence_number <= self.num_points(), - lambda: f"Unable to add DiagramObjectPoint to {str(self)}. Sequence number {sequence_number}" - f" is invalid. Expected a value between 0 and {self.num_points()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._diagram_object_points = list() if self._diagram_object_points is None else self._diagram_object_points - self._diagram_object_points.insert(sequence_number, point) + self.points.append(point) return self - def __setitem__(self, key, value): - self.insert_point(value, key) + @deprecated("Use points.insert(sequence_number, point)") + def insert_point( + self, + point: DiagramObjectPoint, + sequence_number: int | None = None, + ) -> DiagramObject: + if sequence_number is None: sequence_number = len(self.points) + self.points.insert(sequence_number, point) - def remove_point(self, point: DiagramObjectPoint) -> DiagramObject: - """ - Disassociate `point` from this `DiagramObject` - - `point` The `DiagramObjectPoint` to disassociate from this `DiagramObject`. - Returns A reference to this `DiagramObject` to allow fluent use. - Raises `ValueError` if `point` was not associated with this `DiagramObject`. - """ - self._diagram_object_points = safe_remove(self._diagram_object_points, point) return self - def remove_point_by_sequence_number(self, sequence_number: int) -> DiagramObjectPoint: - """ - Remove a :class:`DiagramObjectPoint` from this :class:`DiagramObject` by its sequence number. + @deprecated("Use points.insert(key, value) instead.") + def __setitem__( + self, + key: int, + value: DiagramObjectPoint, + ) -> None: + self.points.insert(key, value) - NOTE: This will update the sequence numbers of all items located after the removed sequence number. + @deprecated("Use points.remove(point) instead.") + def remove_point(self, point: DiagramObjectPoint) -> DiagramObject: + self.points.remove(point) + return self - :param sequence_number: The sequence number of the `DiagramObjectPoint` to remove. - :return: The :class:`DiagramObjectPoint` that was removed, or null if there was no :class:`DiagramObjectPoint` for the given `sequenceNumber`. - :raises IndexError: If no :class:`DiagramObjectPoint` with the specified `sequence_number` was not associated with this :class:`DiagramObject`. - """ - point = self.get_point(sequence_number) - self._diagram_object_points = safe_remove(self._diagram_object_points, point) - return point + @deprecated("Use points.pop(sequence_number) instead.") + def remove_point_by_sequence_number( + self, + sequence_number: int, + ) -> DiagramObjectPoint: + return self.points.pop(sequence_number) + @deprecated("Use points.clear() instead.") def clear_points(self) -> DiagramObject: - """ - Clear all points. - Returns A reference to this `DiagramObject` to allow fluent use. - """ - self._diagram_object_points = None + self.points.clear() return self + + # endregion + + # endregion + From 2e325d291c64a3b5621fe53f0b2c0284f7c77773 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Tue, 21 Jul 2026 17:26:27 +1000 Subject: [PATCH 05/54] Remove inits; migrate contactdetails, usagepoint, connectivitynode, conductingequipment Signed-off-by: Boris Filin --- src/zepben/ewb/__init__.py | 2 +- .../iec61968/assetinfo/relay_info.py | 5 +- .../iec61968/common/contact_details.py | 140 +++++++-------- .../cim/iec61968/metering/usage_point.py | 66 +++---- .../base/core/conducting_equipment.py | 162 +++++++----------- .../iec61970/base/core/connectivity_node.py | 75 +++----- .../ewb/model/cim/iec61970/base/core/curve.py | 9 - .../iec61970/base/core/geographical_region.py | 7 - .../base/core/sub_geographical_region.py | 5 - .../model/cim/iec61970/base/core/terminal.py | 15 +- .../wires/power_electronics_connection.py | 81 +-------- .../iec61970/base/wires/power_transformer.py | 10 +- .../base/wires/power_transformer_end.py | 131 +++++++++----- .../iec61970/base/wires/shunt_compensator.py | 5 - 14 files changed, 270 insertions(+), 443 deletions(-) diff --git a/src/zepben/ewb/__init__.py b/src/zepben/ewb/__init__.py index 3db0b2d5d..2defac2d2 100644 --- a/src/zepben/ewb/__init__.py +++ b/src/zepben/ewb/__init__.py @@ -127,6 +127,7 @@ from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import * from zepben.ewb.model.cim.iec61970.base.core.base_voltage import * +from zepben.ewb.model.cim.iec61970.base.core.terminal import * from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import * from zepben.ewb.model.cim.iec61970.base.core.connectivity_node import * from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import * @@ -144,7 +145,6 @@ from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import * from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import * from zepben.ewb.model.cim.iec61970.base.core.substation import * -from zepben.ewb.model.cim.iec61970.base.core.terminal import * from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram import * from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import * diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index ab18d871e..7c02259f2 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -8,15 +8,14 @@ __all__ = ["RelayInfo"] from dataclasses import field -from typing import Optional, List, Generator, Callable, Any +from typing import Optional, List, Callable, Any from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo -from zepben.ewb.util import ngen, nlen, safe_remove, require -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py index c7cbe845b..b0fe95a38 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -5,11 +5,13 @@ __all__ = ["ContactDetails"] +from dataclasses import field from typing import Generator, Any from typing_extensions import deprecated from zepben.ewb import zb_dataclass +from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress @@ -63,41 +65,31 @@ def id(self) -> str: business_name: str | None = None """[ZBEX] The business name of this contact.""" - _phone_numbers: list[TelephoneNumber] | None = None + _phone_numbers: list[TelephoneNumber] | None = field(default=None) - _electronic_addresses: list[ElectronicAddress] | None = None + phone_numbers: LazyValidatedList[TelephoneNumber] = LazyValidatedList(_phone_numbers) + """[ZBEX] Phone numbers.""" - def __init__(self, id: str|None=None, *args, phone_numbers: list[TelephoneNumber] = None, electronic_addresses: list[ElectronicAddress] = None, **kwargs): + _electronic_addresses: list[ElectronicAddress] | None = field(default=None) + + electronic_addresses: LazyValidatedList[ElectronicAddress] = LazyValidatedList(_electronic_addresses) + """[ZBEX] Electronic addresses.""" + + def __init__(self, id: str|None=None, *args, **kwargs): if id is not None: if "mrid" in kwargs: raise TypeError("ContactDetails.id is an alias for mrid. Do not pass both to the constructor!") kwargs["mrid"] = id super(ContactDetails, self).__init__(*args, **kwargs) - for number in phone_numbers or []: - self.add_phone_number(number) - - for email in electronic_addresses or []: - self.add_electronic_address(email) def __str__(self): - return f"ContactDetails({self.id})" + return f"ContactDetails({self.mrid})" def __hash__(self): # noinspection PyUnresolvedReferences return hash((type(self), *(getattr(self, s) for s in self.__slots__))) - @zbex - @property - def phone_numbers(self) -> Generator[TelephoneNumber, None, None]: - """[ZBEX] Phone numbers.""" - return ngen(self._phone_numbers) - - @zbex - @property - def electronic_addresses(self) -> Generator[ElectronicAddress, None, None]: - """[ZBEX] Electronic addresses.""" - return ngen(self._electronic_addresses) def __eq__(self, other: Any) -> bool: # @@ -118,79 +110,65 @@ def __eq__(self, other: Any) -> bool: self._electronic_addresses == other._electronic_addresses, )) + + # region deprecated list boilerplate + + # region phone_numbers boilerplate + + @deprecated("Use len(phone_numbers) instead.") def num_phone_numbers(self) -> int: - """Get the number of entries in the ``TelephoneNumber`` collection.""" - return nlen(self._phone_numbers) - - def add_phone_number(self, phone_number: TelephoneNumber) -> "ContactDetails": - """ - Add an ``TelephoneNumber`` to this ``ContactDetails``. - - :param phone_number: The ``TelephoneNumber`` to add. - :return: This ``ContactDetails`` for fluent use. - """ - if self._phone_numbers is None: - self._phone_numbers = [] - self._phone_numbers.append(phone_number) + return len(self.phone_numbers) + + @deprecated("Use phone_numbers.append(phone_number) instead.") + def add_phone_number( + self, + phone_number: TelephoneNumber, + ) -> "ContactDetails": + self.phone_numbers.append(phone_number) return self - def remove_phone_number(self, phone_number: TelephoneNumber) -> bool: - """ - Remove an ``TelephoneNumber`` from this ``ContactDetails``. - - :param phone_number: The ``TelephoneNumber`` to remove. - :return: True if the ``TelephoneNumber`` was removed. - """ - if not self._phone_numbers: - raise KeyError(phone_number) - self._phone_numbers.remove(phone_number) - if self.num_phone_numbers == 0: - self._phone_numbers = None + @deprecated("Use phone_numbers.remove(phone_number) instead.") + def remove_phone_number( + self, + phone_number: TelephoneNumber, + ) -> bool: + self.phone_numbers.remove(phone_number) return True + @deprecated("Use phone_numbers.clear() instead.") def clear_phone_numbers(self) -> "ContactDetails": - """ - Clear all ``TelephoneNumber``'s from this ``ContactDetails``. - :return: this ``ContactDetails`` for fluent use. - """ - self._phone_numbers = None + self.phone_numbers.clear() return self + # endregion + + # region electronic_addresses boilerplate + + @deprecated("Use len(electronic_addresses) instead.") def num_electronic_addresses(self) -> int: - """Get the number of entries in the [ElectronicAddress] collection.""" - return nlen(self._electronic_addresses) - - def add_electronic_address(self, electronic_address: ElectronicAddress) -> "ContactDetails": - """ - Add an ``ElectronicAddress`` to this ``ContactDetails``. - - :param electronic_address: The ``ElectronicAddress`` to add. - :return: this ``ContactDetails`` for fluent use. - """ - if self._electronic_addresses is None: - self._electronic_addresses = [] - self._electronic_addresses.append(electronic_address) + return len(self.electronic_addresses) + + @deprecated("Use electronic_addresses.append(electronic_address) instead.") + def add_electronic_address( + self, + electronic_address: ElectronicAddress, + ) -> "ContactDetails": + self.electronic_addresses.append(electronic_address) return self - def remove_electronic_address(self, electronic_address: ElectronicAddress) -> bool: - """ - Remove an ``ElectronicAddress`` from this ``ContactDetails``. - - :param electronic_address: The ``ElectronicAddress`` to remove. - :return: True if the ``ElectronicAddress`` was removed. - """ - if not self._electronic_addresses: - raise KeyError(electronic_address) - self._electronic_addresses.remove(electronic_address) - if self.num_electronic_addresses == 0: - self._electronic_addresses = None + @deprecated("Use electronic_addresses.remove(electronic_address) instead.") + def remove_electronic_address( + self, + electronic_address: ElectronicAddress, + ) -> bool: + self.electronic_addresses.remove(electronic_address) return True + @deprecated("Use electronic_addresses.clear() instead.") def clear_electronic_addresses(self) -> "ContactDetails": - """ - Clear all ``ElectronicAddress``'s from this ``ContactDetails``. - - :return: this ``ContactDetails`` for fluent use. - """ - self._electronic_addresses = None + self.electronic_addresses.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index dc8235b9b..16fba5a6e 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -62,13 +62,7 @@ class UsagePoint(IdentifiedObject): _equipment: list[Equipment] | None = field(default=None) _end_devices: list[EndDevice] | None = field(default=None) - _contacts: list[ContactDetails] | None = None - - def __init__(self, *args, contacts: List[ContactDetails] = None, **kwargs): - super(UsagePoint, self).__init__(*args, **kwargs) - if contacts: - for c in contacts: - self.add_contact(c) + _contacts: list[ContactDetails] | None = field(default=None) end_devices: MridCollection[EndDevice] = LazyMridList( @@ -81,6 +75,10 @@ def __init__(self, *args, contacts: List[ContactDetails] = None, **kwargs): "An Equipment", ) + contacts: MridCollection[ContactDetails] = LazyMridList( + _contacts, + "A ContactDetails" + ) def num_equipment(self): """ @@ -88,51 +86,35 @@ def num_equipment(self): """ return nlen(self._equipment) - def get_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `UsagePoint` identified by `mrid` + # region deprecated list boilerplate - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._equipment, mrid) + # region contacts boilerplate - def add_equipment(self, equipment: Equipment) -> UsagePoint: - """ - Associate an `Equipment` with this `UsagePoint` + @deprecated("Use len(contacts) instead.") + def num_contacts(self) -> int: + return len(self.contacts) - `equipment` The `Equipment` to associate with this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `UsagePoint`. - """ - if self._validate_reference(equipment, self.get_equipment, "An Equipment"): - return self + @deprecated("Use contacts.get_by_mrid(mrid) instead.") + def get_contact(self, mrid: str) -> ContactDetails: + return self.contacts.get_by_mrid(mrid) - self._equipment = list() if self._equipment is None else self._equipment - self._equipment.append(equipment) + @deprecated("Use contacts.append(contact) instead.") + def add_contact(self, contact: ContactDetails) -> UsagePoint: + self.contacts.append(contact) return self - def remove_equipment(self, equipment: Equipment) -> UsagePoint: - """ - Disassociate an `Equipment` from this `UsagePoint` - - `equipment` The `Equipment` to disassociate with this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if `equipment` was not associated with this `UsagePoint`. - """ - self._equipment = safe_remove(self._equipment, equipment) + @deprecated("Use contacts.remove(contact) instead.") + def remove_contact(self, contact: ContactDetails) -> UsagePoint: + self.contacts.remove(contact) return self - def clear_equipment(self) -> UsagePoint: - """ - Clear all equipment. - Returns A reference to this `UsagePoint` to allow fluent use. - """ - self._equipment = None + @deprecated("Use contacts.clear() instead.") + def clear_contacts(self) -> UsagePoint: + self.contacts.clear() return self - # region deprecated list boilerplate + # endregion + # region end_devices boilerplate @deprecated("Use len(obj.end_devices) instead.") diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py index f93a0bf30..ab802ccff 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py @@ -12,15 +12,26 @@ from abc import ABCMeta from dataclasses import field +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill +from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.util import get_by_mrid, require, ngen from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.base_voltage import BaseVoltage - from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal +class TerminalsList(LazyMridList[Terminal]): + + def get_by_sequence_number(self, sequence_number: int) -> Terminal: + term = next((it for it in self if it.sequence_number == sequence_number), None) + if term is None: + raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self.instance)}") + return term + @zb_dataclass class ConductingEquipment(Equipment, metaclass=ABCMeta): """ @@ -42,14 +53,6 @@ class ConductingEquipment(Equipment, metaclass=ABCMeta): _terminals: List[Terminal] = field(default_factory=list) max_terminals = int(sys.maxsize) - def __init__(self, *args, terminals: List[Terminal] = None, **kwargs): - super(ConductingEquipment, self).__init__(*args, **kwargs) - if terminals: - for term in terminals: - if term.conducting_equipment is None: - term.conducting_equipment = self - self.add_terminal(term) - # pylint: disable=unused-argument def get_base_voltage(self, terminal: Terminal = None): """ @@ -70,13 +73,13 @@ def base_voltage_value(self) -> int: """ return self.base_voltage.nominal_voltage if self.base_voltage and self.base_voltage.nominal_voltage else 0 - @property - def terminals(self) -> Generator[Terminal, None, None]: - """ - `ConductingEquipment` have `Terminal`s that may be connected to other `ConductingEquipment` - `Terminal`s via `ConnectivityNode`s. - """ - return ngen(self._terminals) + terminals: TerminalsList = TerminalsList( + _terminals, + "A Terminal", + backfill=Backfill(Terminal.conducting_equipment), + validate=lambda self, it: self._validate_terminal(it), + sort_by=lambda it: it.sequence_number + ) def __repr__(self): return (f"{super(ConductingEquipment, self).__repr__()}, in_service={self.in_service}, " @@ -92,111 +95,70 @@ def _validate_terminal(self, terminal: Terminal) -> bool: Raises `ValueError` if `Terminal`s `conducting_equipment` is not this `ConductingEquipment`, or if this `ConductingEquipment` has a different `Terminal` with the same mRID. """ - if self._validate_reference(terminal, self.get_terminal_by_mrid, "A Terminal"): - return True - if self._validate_reference_by_field(terminal, terminal.sequence_number, self.get_terminal_by_sn, "sequence_number"): return True - if not terminal.conducting_equipment: - terminal.conducting_equipment = self - - require(terminal.conducting_equipment is self, - lambda: f"Terminal {terminal} references another piece of conducting equipment {terminal.conducting_equipment}, expected {str(self)}.",) - return False - - def num_terminals(self): - """ - Get the number of `Terminal`s for this `ConductingEquipment`. - """ - return len(self._terminals) + require(self.num_terminals() < self.max_terminals, + lambda: f"Unable to add {terminal} to {str(self)}. This conducting equipment already has the maximum number of terminals ({self.max_terminals}).") - def get_terminal(self, identifier: Union[int, str]): - """ - Get the `Terminal` for this `ConductingEquipment` identified by `mrid` or `sequence_number` + if terminal.sequence_number == 0: + terminal.sequence_number = self.num_terminals() + 1 - :param identifier: the mRID of the required `Terminal`, or the `sequence_number` of the terminal in relation - to this `ConductingEquipment` - :return: The `Terminal` with the specified `mrid` if it exists + return False - Raises `KeyError` if `mrid` wasn't present. - Raises `TypeError` if the identifier wasn't a recognised type - """ - if isinstance(identifier, int): - return self.get_terminal_by_sn(identifier) - elif isinstance(identifier, str): - return self.get_terminal_by_mrid(identifier) - raise TypeError(f'`identifier` parameter not a recognised type: {type(identifier)}') - def get_terminal_by_mrid(self, mrid: str) -> Terminal: - """ - Get the `Terminal` for this `ConductingEquipment` identified by `mrid` + # region deprecated list boilerplate - :param mrid: the mRID of the required `Terminal` + # region terminals boilerplate - :return: The `Terminal` with the specified `mrid` if it exists + @deprecated("Use len(terminals) instead.") + def num_terminals(self) -> int: + return len(self.terminals) - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._terminals, mrid) + @deprecated( + "Use terminals.get_by_sequence_number(identifier) for integer identifiers " + + "or terminals.get_by_mrid(identifier) for string identifiers instead." + ) + def get_terminal(self, identifier: Union[int, str]) -> Terminal: + if isinstance(identifier, int): + return self.terminals.get_by_sequence_number(identifier) - def get_terminal_by_sn(self, sequence_number: int): - """ - Get the `Terminal` on this `ConductingEquipment` by its `sequence_number`. + if isinstance(identifier, str): + return self.terminals.get_by_mrid(identifier) - :param sequence_number: The `sequence_number` of the `Terminal` in relation to this `ConductingEquipment`. + raise TypeError( + f"`identifier` parameter not a recognised type: {type(identifier)}" + ) - :return: The `Terminal` on this `ConductingEquipment` with sequence number `sequence_number` + @deprecated("Use terminals.get_by_mrid(mrid) instead.") + def get_terminal_by_mrid(self, mrid: str) -> Terminal: + return self.terminals.get_by_mrid(mrid) - Raises IndexError if no `Terminal` was found with sequence_number `sequence_number`. - """ - for term in self._terminals: - if term.sequence_number == sequence_number: - return term - raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self)}") + @deprecated( + "Use terminals.get_by_sequence_number(sequence_number) instead." + ) + def get_terminal_by_sn(self, sequence_number: int) -> Terminal: + return self.terminals.get_by_sequence_number(sequence_number) - def __getitem__(self, item: int): - return self.get_terminal_by_sn(item) + @deprecated("Use terminals.get_by_sequence_number(item) instead.") + def __getitem__(self, item: int) -> Terminal: + return self.terminals.get_by_sequence_number(item) + @deprecated("Use terminals.append(terminal) instead.") def add_terminal(self, terminal: Terminal) -> ConductingEquipment: - """ - Associate `terminal` with this `ConductingEquipment`. If `terminal.sequence_number` == 0, the terminal will be assigned a sequence_number of - `self.num_terminals() + 1`. - - `terminal` The `Terminal` to associate with this `ConductingEquipment`. - Returns A reference to this `ConductingEquipment` to allow fluent use. - Raises `ValueError` if another `Terminal` with the same `mrid` already exists for this `ConductingEquipment`. - Raises `ValueError` if `max_terminals` has already been reached. - """ - if self._validate_terminal(terminal): - return self - - require(self.num_terminals() < self.max_terminals, - lambda: f"Unable to add {terminal} to {str(self)}. This conducting equipment already has the maximum number of terminals ({self.max_terminals}).") - - if terminal.sequence_number == 0: - terminal.sequence_number = self.num_terminals() + 1 - - self._terminals.append(terminal) - self._terminals.sort(key=lambda t: t.sequence_number) - + self.terminals.append(terminal) return self + @deprecated("Use terminals.remove(terminal) instead.") def remove_terminal(self, terminal: Terminal) -> ConductingEquipment: - """ - Disassociate `terminal` from this `ConductingEquipment` - - `terminal` the `Terminal` to disassociate from this `ConductingEquipment`. - Returns A reference to this `ConductingEquipment` to allow fluent use. - Raises `ValueError` if `terminal` was not associated with this `ConductingEquipment`. - """ - self._terminals.remove(terminal) + self.terminals.remove(terminal) return self + @deprecated("Use terminals.clear() instead.") def clear_terminals(self) -> ConductingEquipment: - """ - Clear all terminals. - Returns A reference to this `ConductingEquipment` to allow fluent use. - """ - self._terminals.clear() + self.terminals.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index 1251778e6..ab037ac63 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -10,6 +10,9 @@ from typing import Generator, List, TYPE_CHECKING from dataclasses import field +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import get_by_mrid, ngen from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @@ -30,21 +33,15 @@ class ConnectivityNode(IdentifiedObject, WeakrefSlot): """ _terminals: List[Terminal] = field(default_factory=list) - def __init__(self, *args, terminals: List[Terminal] = None, **kwargs): - super(ConnectivityNode, self).__init__(*args, **kwargs) - if terminals: - for term in terminals: - self.add_terminal(term) + terminals: MridCollection[Terminal] = LazyMridList( + _terminals, + "A Terminal" + ) + """The `Terminal`s attached to this `ConnectivityNode`""" def __iter__(self): return iter(self._terminals) - @property - def terminals(self) -> Generator[Terminal, None, None]: - """ - The `Terminal`s attached to this `ConnectivityNode` - """ - return ngen(self._terminals) def is_switched(self): return self.get_switch() is not None @@ -59,51 +56,33 @@ def get_switch(self): pass return None - def num_terminals(self): - """ - Get the number of `Terminal`s for this `ConnectivityNode`. - """ - return len(self._terminals) - - def get_terminal(self, mrid: str) -> Terminal: - """ - Get the `Terminal` for this `ConnectivityNode` identified by `mrid` + # region deprecated list boilerplate - `mrid` The mRID of the required `Terminal` - Returns The `Terminal` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._terminals, mrid) + # region terminals boilerplate - def add_terminal(self, terminal: Terminal) -> ConnectivityNode: - """ - Associate a `terminal.Terminal` with this `ConnectivityNode` + @deprecated("Use len(terminals) instead.") + def num_terminals(self) -> int: + return len(self.terminals) - `terminal` The `Terminal` to add. Will only add to this object if it is not already associated. - Returns A reference to this `ConnectivityNode` to allow fluent use. - Raises `ValueError` if another `Terminal` with the same `mrid` already exists for this `ConnectivityNode`. - """ - if self._validate_reference(terminal, self.get_terminal, "A Terminal"): - return self + @deprecated("Use terminals.get_by_mrid(mrid) instead.") + def get_terminal(self, mrid: str) -> Terminal: + return self.terminals.get_by_mrid(mrid) - self._terminals.append(terminal) + @deprecated("Use terminals.append(terminal) instead.") + def add_terminal(self, terminal: Terminal) -> ConnectivityNode: + self.terminals.append(terminal) return self + @deprecated("Use terminals.remove(terminal) instead.") def remove_terminal(self, terminal: Terminal) -> ConnectivityNode: - """ - Disassociate `terminal` from this `ConnectivityNode`. - - `terminal` The `Terminal` to disassociate from this `ConnectivityNode`. - Returns A reference to this `ConnectivityNode` to allow fluent use. - Raises `ValueError` if `terminal` was not associated with this `ConnectivityNode`. - """ - self._terminals.remove(terminal) + self.terminals.remove(terminal) return self + @deprecated("Use terminals.clear() instead.") def clear_terminals(self) -> ConnectivityNode: - """ - Clear all terminals. - Returns A reference to this `ConnectivityNode` to allow fluent use. - """ - self._terminals.clear() + self.terminals.clear() return self + + # endregion + + # endregion \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index 821e87af8..be90f3852 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -53,15 +53,6 @@ class Curve(IdentifiedObject, metaclass=ABCMeta): _data: list[CurveData] | None = field(default=None) - def __init__(self, *args, data: List[CurveData] = None, **kwargs): - """ - `data` A list of `CurveData`s to associate with this `Curve`. - """ - super(Curve, self).__init__(*args, **kwargs) - if data: - for curve_data in data: - self.add_curve_data(curve_data) - data: CurveDataList[CurveData] = CurveDataList( _data, validate=lambda self, it: self._validate_data(it), diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index 923ca240e..b5efdd541 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -25,13 +25,6 @@ class GeographicalRegion(IdentifiedObject): """ _sub_geographical_regions: Optional[List[SubGeographicalRegion]] = field(default=None) - def __init__(self, *args, sub_geographical_regions: List[SubGeographicalRegion] = None, **kwargs): - super(GeographicalRegion, self).__init__(*args, **kwargs) - if sub_geographical_regions: - for sgr in sub_geographical_regions: - self.add_sub_geographical_region(sgr) - - sub_geographical_regions: MridCollection[SubGeographicalRegion] = LazyMridList( _sub_geographical_regions, "A SubGeographicalRegion", diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index 6b1783a9c..b90c911cf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -31,11 +31,6 @@ class SubGeographicalRegion(IdentifiedObject): _substations: Optional[List[Substation]] = field(default=None) - def __init__(self, *args, substations: List[Substation] = None, **kwargs): - super(SubGeographicalRegion, self).__init__(*args, **kwargs) - if substations: - for sub in substations: - self.add_substation(sub) @property @targets(_geographical_region) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py index a7d43f2df..339178aa9 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py @@ -16,7 +16,6 @@ from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import AcDcTerminal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode -from zepben.ewb.model.cim.iec61970.base.wires.busbar_section import BusbarSection from zepben.ewb.services.network.tracing.feeder.feeder_direction import FeederDirection from zepben.ewb.services.network.tracing.phases.phase_status import PhaseStatus from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @@ -32,7 +31,7 @@ class Terminal(AcDcTerminal): An AC electrical connection point to a piece of conducting equipment. Terminals are connected at physical connection points called connectivity nodes. """ - _conducting_equipment: Optional[ConductingEquipment] = None + _conducting_equipment: Optional['ConductingEquipment'] = None """The conducting equipment of the terminal. Conducting equipment have terminals that may be connected to other conducting equipment terminals via connectivity nodes.""" @@ -58,21 +57,12 @@ class Terminal(AcDcTerminal): _normal_phases: PhaseStatus = None _current_phases: PhaseStatus = None - def __init__(self, *args, conducting_equipment: ConductingEquipment = None, connectivity_node: ConnectivityNode = None, **kwargs): + def __init__(self, *args, **kwargs): super(Terminal, self).__init__(*args, **kwargs) self._normal_phases = PhaseStatus(self) - self._current_phases = PhaseStatus(self) - if conducting_equipment: - self.conducting_equipment = conducting_equipment - - # We set the connectivity node to itself if the name parameter is not used to make sure the positional argument is wrapped in a reference. - if connectivity_node: - self.connectivity_node = connectivity_node - else: - self.connectivity_node = self._cn @property def normal_phases(self) -> PhaseStatus: @@ -174,6 +164,7 @@ def is_feeder_head_terminal(self): return True def has_connected_busbars(self): + from zepben.ewb.model.cim.iec61970.base.wires.busbar_section import BusbarSection try: return any(it != self and isinstance(it.conducting_equipment, BusbarSection) for it in self.connectivity_node.terminals) except AttributeError: diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 5e7cf25b2..006c5aaaf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -12,6 +12,7 @@ from typing_extensions import deprecated +from zepben.ewb import Alias from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase @@ -146,83 +147,6 @@ class PowerElectronicsConnection(RegulatingCondEq): _power_electronics_connection_phases: Optional[List[PowerElectronicsConnectionPhase]] = field(default=None) """The individual units models for the power electronics connection.""" - def __init__(self, - *args, - power_electronics_units: List[PowerElectronicsUnit] = None, - power_electronics_connection_phases: List[PowerElectronicsConnectionPhase] = None, - inv_watt_resp_v1=None, - inv_watt_resp_v2=None, - inv_watt_resp_v3=None, - inv_watt_resp_v4=None, - inv_watt_resp_p_at_v1=None, - inv_watt_resp_p_at_v2=None, - inv_watt_resp_p_at_v3=None, - inv_watt_resp_p_at_v4=None, - inv_var_resp_v1=None, - inv_var_resp_v2=None, - inv_var_resp_v3=None, - inv_var_resp_v4=None, - inv_var_resp_q_at_v1=None, - inv_var_resp_q_at_v2=None, - inv_var_resp_q_at_v3=None, - inv_var_resp_q_at_v4=None, - **kwargs): - super(PowerElectronicsConnection, self).__init__(*args, **kwargs) - if power_electronics_units: - for unit in power_electronics_units: - self.add_unit(unit) - - if power_electronics_connection_phases: - for phase in power_electronics_connection_phases: - self.add_phase(phase) - - if inv_watt_resp_v1 is not None: - self.inv_watt_resp_v1 = inv_watt_resp_v1 - - if inv_watt_resp_v2 is not None: - self.inv_watt_resp_v2 = inv_watt_resp_v2 - - if inv_watt_resp_v3 is not None: - self.inv_watt_resp_v3 = inv_watt_resp_v3 - - if inv_watt_resp_v4 is not None: - self.inv_watt_resp_v4 = inv_watt_resp_v4 - - if inv_watt_resp_p_at_v1 is not None: - self.inv_watt_resp_p_at_v1 = inv_watt_resp_p_at_v1 - - if inv_watt_resp_p_at_v2 is not None: - self.inv_watt_resp_p_at_v2 = inv_watt_resp_p_at_v2 - - if inv_watt_resp_p_at_v3 is not None: - self.inv_watt_resp_p_at_v3 = inv_watt_resp_p_at_v3 - - if inv_watt_resp_p_at_v4 is not None: - self.inv_watt_resp_p_at_v4 = inv_watt_resp_p_at_v4 - - if inv_var_resp_v1 is not None: - self.inv_var_resp_v1 = inv_var_resp_v1 - - if inv_var_resp_v2 is not None: - self.inv_var_resp_v2 = inv_var_resp_v2 - - if inv_var_resp_v3 is not None: - self.inv_var_resp_v3 = inv_var_resp_v3 - - if inv_var_resp_v4 is not None: - self.inv_var_resp_v4 = inv_var_resp_v4 - - if inv_var_resp_q_at_v1 is not None: - self.inv_var_resp_q_at_v1 = inv_var_resp_q_at_v1 - - if inv_var_resp_q_at_v2 is not None: - self.inv_var_resp_q_at_v2 = inv_var_resp_q_at_v2 - - if inv_var_resp_q_at_v3 is not None: - self.inv_var_resp_q_at_v3 = inv_var_resp_q_at_v3 - - if inv_var_resp_q_at_v4 is not None: - self.inv_var_resp_q_at_v4 = inv_var_resp_q_at_v4 @property def inv_watt_resp_v1(self): @@ -423,13 +347,14 @@ def inv_var_resp_q_at_v4(self, value): _power_electronics_units, "A PowerElectronicsUnit", ) + power_electronics_units = Alias(units) phases: MridCollection[PowerElectronicsConnectionPhase] = LazyMridList( _power_electronics_connection_phases, "A PowerElectronicsConnectionPhase", backfill=Backfill(PowerElectronicsConnectionPhase.power_electronics_connection) ) - + power_electronics_connection_phases = Alias(phases) diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index 857a45288..24e124de9 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -11,7 +11,7 @@ from dataclasses import field from typing import List, Optional, TYPE_CHECKING -from zepben.ewb import remove_descriptor_annotations +from zepben.ewb import remove_descriptor_annotations, Alias from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill if sys.version_info >= (3, 13): @@ -120,13 +120,6 @@ class PowerTransformer(ConductingEquipment): The function of this transformer. """ - def __init__(self, *args, power_transformer_ends: List[PowerTransformerEnd] = None, **kwargs): - super(PowerTransformer, self).__init__(*args, **kwargs) - if power_transformer_ends: - for end in power_transformer_ends: - if end.power_transformer is None: - end.power_transformer = self - self.add_end(end) ends: PowerTransformerEndList = PowerTransformerEndList( _power_transformer_ends, @@ -135,6 +128,7 @@ def __init__(self, *args, power_transformer_ends: List[PowerTransformerEnd] = No validate=lambda self, it: self._validate_end(it), sort_by=lambda it: it.end_number ) + power_transformer_ends = Alias(ends) def _validate_end(self, end: PowerTransformerEnd): self._validate_reference_by_field(end, end.end_number, self.ends.get_by_num, "end_number") diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index 4fa6b2a23..caaf788fb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -8,8 +8,12 @@ __all__ = ["PowerTransformerEnd"] import warnings +from dataclasses import field from typing import Optional, List, Generator, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd @@ -22,6 +26,24 @@ from zepben.ewb.model.cim.iec61970.base.wires.power_transformer import PowerTransformer +class TransformerEndRatedSList(LazyValidatedList[TransformerEndRatedS]): + + def get_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS | None: + return next((rating for rating in self if rating.cooling_type == cooling_type), None) + + def remove_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS | None: + rating = self.get_by_cooling_type(cooling_type) + if rating is not None: + self.remove(rating) + return rating + + @zb_dataclass class PowerTransformerEnd(TransformerEnd): """ @@ -80,16 +102,14 @@ class PowerTransformerEnd(TransformerEnd): secondary side end of a transformer with vector group code of 'Dyn11', specify the connection kind as wye with neutral and specify the phase angle of the clock as 11. The clock value of the transformer end number specified as 1, is assumed to be zero.""" - _s_ratings: Optional[List[TransformerEndRatedS]] = None + _s_ratings: Optional[List[TransformerEndRatedS]] = field(default=None) """ Backing list for storing transformer ratings. Placed here to not mess with __init__ param order. Must always be placed at the end. Should not be used directly, instead use add_rating and get_rating functions. """ - def __init__(self, *args, power_transformer: PowerTransformer = None, rated_s: int = None, ratings: list[TransformerEndRatedS] = None, **kwargs): + def __init__(self, *args, rated_s: int = None, **kwargs): super(PowerTransformerEnd, self).__init__(*args, **kwargs) - if power_transformer: - self.power_transformer = power_transformer if self._s_ratings: raise ValueError("Do not directly set s_ratings through the constructor. You have one more constructor parameter than expected.") if rated_s and self._rated_s: @@ -104,9 +124,6 @@ def __init__(self, *args, power_transformer: PowerTransformer = None, rated_s: i if self._rated_s is not None: self.rated_s = self._rated_s self._rated_s = None - if ratings: - for rating in ratings: - self.add_rating(rating.rated_s, rating.cooling_type) @property def power_transformer(self): @@ -145,9 +162,15 @@ def rated_s(self, rated_s: Optional[int]): if rated_s is not None: self.add_transformer_end_rated_s(TransformerEndRatedS(TransformerCoolingType.UNKNOWN, rated_s)) - @property - def s_ratings(self) -> Generator[TransformerEndRatedS, None, None]: - return ngen(self._s_ratings) + s_ratings: TransformerEndRatedSList = TransformerEndRatedSList( + _s_ratings, + validate=lambda self, it: self._validate_rating(it), + sort_by=lambda it: -it.rated_s + ) + + def _validate_rating(self, rating: TransformerEndRatedS): + if any(it.cooling_type == rating.cooling_type for it in self.s_ratings): + raise ValueError(f"A rating for coolingType {rating.cooling_type.name} already exists, please remove it first.") def resistance_reactance(self): """ @@ -166,48 +189,68 @@ def resistance_reactance(self): else None ) - def num_ratings(self) -> int: - return nlen(self._s_ratings) - - def get_rating(self, cooling_type: TransformerCoolingType) -> TransformerEndRatedS: - if self._s_ratings: - for s_rating in self._s_ratings: - if s_rating.cooling_type == cooling_type: - return s_rating - raise KeyError(cooling_type) + # region deprecated list methods - def add_rating(self, rated_s: int, cooling_type: TransformerCoolingType = TransformerCoolingType.UNKNOWN) -> PowerTransformerEnd: - self._s_ratings = self._s_ratings if self._s_ratings else list() + # region s_ratings boilerplate - for s_rating in self._s_ratings: - if s_rating.cooling_type == cooling_type: - raise ValueError(f"A rating for coolingType {cooling_type.name} already exists, please remove it first.") - - self._s_ratings.append(TransformerEndRatedS(cooling_type, rated_s)) - - def sort_by_rated_s(t: TransformerEndRatedS) -> int: - return t.rated_s + @deprecated("Use len(s_ratings) instead.") + def num_ratings(self) -> int: + return len(self.s_ratings) + + @deprecated("Use s_ratings.get_by_cooling_type(cooling_type) instead.") + def get_rating( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS: + rating = self.s_ratings.get_by_cooling_type(cooling_type) + + if rating is None: + raise KeyError(cooling_type) + + return rating + + @deprecated("Use s_ratings.append(TransformerEndRatedS(cooling_type, rated_s)) instead.") + def add_rating( + self, + rated_s: int, + cooling_type: TransformerCoolingType = TransformerCoolingType.UNKNOWN, + ) -> PowerTransformerEnd: + self.s_ratings.append(TransformerEndRatedS(cooling_type, rated_s)) + return self - self._s_ratings.sort(key=sort_by_rated_s, reverse=True) + @deprecated("Use s_ratings.append(transformer_end_rated_s) instead.") + def add_transformer_end_rated_s( + self, + transformer_end_rated_s: TransformerEndRatedS, + ) -> PowerTransformerEnd: + self.s_ratings.append(transformer_end_rated_s) + return self + @deprecated("Use s_ratings.remove(transformer_end_rated_s) instead.") + def remove_rating( + self, + transformer_end_rated_s: TransformerEndRatedS, + ) -> PowerTransformerEnd: + self.s_ratings.remove(transformer_end_rated_s) return self - def add_transformer_end_rated_s(self, transformer_end_rated_s: TransformerEndRatedS) -> PowerTransformerEnd: - return self.add_rating(transformer_end_rated_s.rated_s, transformer_end_rated_s.cooling_type) + @deprecated("Use s_ratings.remove_by_cooling_type(cooling_type) instead.") + def remove_rating_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS: + rating = self.s_ratings.remove_by_cooling_type(cooling_type) - def remove_rating(self, transformer_end_rated_s: TransformerEndRatedS) -> PowerTransformerEnd: - self._s_ratings = safe_remove(self._s_ratings, transformer_end_rated_s) - return self + if rating is None: + raise IndexError(cooling_type) - def remove_rating_by_cooling_type(self, cooling_type: TransformerCoolingType) -> TransformerEndRatedS: - if self._s_ratings: - for transformer_end_rated_s in self._s_ratings: - if transformer_end_rated_s.cooling_type == cooling_type: - self._s_ratings.remove(transformer_end_rated_s) - self._s_ratings = self._s_ratings if self._s_ratings else None - return transformer_end_rated_s - raise IndexError(cooling_type) + return rating + @deprecated("Use s_ratings.clear() instead.") def clear_ratings(self) -> PowerTransformerEnd: - self._s_ratings = None + self.s_ratings.clear() return self + + # endregion + + # endregion \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py b/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py index 7e8a542b9..fe5a8d868 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py @@ -57,11 +57,6 @@ class ShuntCompensator(RegulatingCondEq, metaclass=ABCMeta): _grounding_terminal: 'Terminal | None' = None sections: Optional[float] = None - def __init__(self, *args, grounding_terminal = None, **kwargs): - super(ShuntCompensator, self).__init__(*args, **kwargs) - if grounding_terminal is not None: - self.grounding_terminal = grounding_terminal - @property @deprecated("use asset_info instead.") def shunt_compensator_info(self) -> Optional['ShuntCompensatorInfo']: From e1b102defd503c7e3c47e308b20fcf4ef073e5b0 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 27 Jul 2026 12:13:06 +1000 Subject: [PATCH 06/54] Backfill finished Signed-off-by: Boris Filin --- .../ewb/dataclass_descriptors/mrid_list.py | 11 ++-- .../model/cim/iec61970/base/core/feeder.py | 13 ++--- .../base/core/sub_geographical_region.py | 34 +++---------- .../cim/iec61970/base/core/substation.py | 50 ++++++++----------- .../model/cim/iec61970/base/core/terminal.py | 6 ++- .../base/diagramlayout/diagram_object.py | 25 +++++----- .../base/wires/ac_line_segment_phase.py | 10 ++-- .../model/cim/iec61970/base/wires/clamp.py | 18 ++++--- .../ewb/model/cim/iec61970/base/wires/cut.py | 16 +++--- .../base/wires/energy_consumer_phase.py | 15 +++--- .../base/wires/energy_source_phase.py | 18 ++++--- .../power_electronics_connection_phase.py | 16 +++--- .../base/wires/power_transformer_end.py | 8 ++- 13 files changed, 113 insertions(+), 127 deletions(-) diff --git a/src/zepben/ewb/dataclass_descriptors/mrid_list.py b/src/zepben/ewb/dataclass_descriptors/mrid_list.py index 3b31802c0..195254139 100644 --- a/src/zepben/ewb/dataclass_descriptors/mrid_list.py +++ b/src/zepben/ewb/dataclass_descriptors/mrid_list.py @@ -49,16 +49,11 @@ def _can_add_by_mrid(self, element: S) -> bool: class Backfill: - def __init__(self, backfill_prop: Any): - if not any(isinstance(backfill_prop, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): - raise TypeError(f"backfill_prop parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {backfill_prop}") + def __init__(self, backfill_prop: property): self.backfill_prop = backfill_prop def apply(self, element: S, owner: Any): - if isinstance(self.backfill_prop, property): - name = self.backfill_prop.fget.__name__ - else: - name = self.backfill_prop.__name__ + name = self.backfill_prop.fget.__name__ if hasattr(self.backfill_prop, "__target"): backing_name = self.backfill_prop.__target.__name__ @@ -72,7 +67,7 @@ def apply(self, element: S, owner: Any): if ref is not owner: raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") -def targets(target: Any): +def internal(target: Any): if not any(isinstance(target, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): raise TypeError(f"target parameter of the target decorator has to be an instance of dataclass Field, instead is {target}") diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 0c82a5f4a..ca1958378 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb import get_by_mrid, remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, internal from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer @@ -38,8 +38,7 @@ class Feeder(EquipmentContainer): _normal_head_terminal: Terminal | None = None """The normal head terminal or terminals of the feeder.""" - normal_energizing_substation: Substation | None = field(default=None) - """The substation that normally energizes the feeder. Also used for naming purposes.""" + _normal_energizing_substation: Substation | None = field(default=None) _current_equipment_by_id: Dict[str, Equipment] | None = field(default=None) """The equipment contained in this feeder in the current state of the network.""" @@ -96,6 +95,7 @@ def normal_head_terminal(self, term: Optional[Terminal]): ) """[ZBEX]""" @property + @internal(_normal_energizing_substation) def normal_energizing_substation(self): """The substation that normally energizes the feeder. Also used for naming purposes.""" return self._normal_energizing_substation @@ -105,13 +105,6 @@ def normal_energizing_substation(self): def normal_energizing_substation(self, value): self._normal_energizing_substation = value - @property - def current_equipment(self) -> Generator[Equipment, None, None]: - """ - Contained `Equipment` using the current state of the network. - """ - return ngen(self._current_equipment) - # region deprecated list boilerplate # region current_equipment boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index b90c911cf..448a12ba2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, targets +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.substation import Substation @@ -31,40 +31,22 @@ class SubGeographicalRegion(IdentifiedObject): _substations: Optional[List[Substation]] = field(default=None) - - @property - @targets(_geographical_region) - def geographical_region(self): - """The geographical region to which this sub-geographical region is within.""" - return self._geographical_region - - @geographical_region.setter - @deprecated("Geographical region is a backfill property - it should only be set by adding the sub region to the sub regions list") - def geographical_region(self, value): - self._geographical_region = value - @property + @internal(_geographical_region) def geographical_region(self): """The geographical region to which this sub-geographical region is within.""" return self._geographical_region - @geographical_region.setter - @deprecated("geographical_region should never be set directly - it is automatically set when adding it to the `sub_geographical_regions` list") - def geographical_region(self, value): - self._geographical_region = value - - def num_substations(self) -> int: - """ - Returns The number of `Substation`s associated with this `SubGeographicalRegion` - """ - return nlen(self._substations) - substations: MridCollection[Substation] = LazyMridList( _substations, "A Substation", backfill=Backfill(Substation.sub_geographical_region) ) + @geographical_region.setter + @deprecated("Geographical region is a backfill property - it should only be set by adding the sub region to the sub regions list") + def geographical_region(self, value): + self._geographical_region = value # region deprecated list boilerplate # region substations boilerplate @@ -95,7 +77,3 @@ def clear_substations(self) -> SubGeographicalRegion: # endregion substations boilerplate # endregion deprecated list boilerplate - -if __name__ == '__main__': - s = SubGeographicalRegion("") - s.geographical_region = GeographicalRegion("") \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index d34be25fb..3904988b4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -15,7 +15,7 @@ from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder if TYPE_CHECKING: @@ -31,15 +31,31 @@ class Substation(EquipmentContainer): is passed for the purposes of switching or modifying its characteristics. """ - sub_geographical_region: Optional[SubGeographicalRegion] = field(default=None) - """The SubGeographicalRegion containing the substation.""" + _sub_geographical_region: Optional[SubGeographicalRegion] = field(default=None) _normal_energized_feeders: Optional[List[Feeder]] = field(default=None) + feeders: MridCollection[Feeder] = LazyMridList( + _normal_energized_feeders, + "A Feeder", + backfill=Backfill(Feeder.normal_energizing_substation) + ) + normal_energized_feeders = Alias(feeders) + _loops: Optional[List[Loop]] = field(default=None) + loops: MridCollection[Loop] = LazyMridList( + _loops, + "A Loop", + ) + _energized_loops: Optional[List[Loop]] = field(default=None) + energized_loops: MridCollection[Loop] = LazyMridList( + _energized_loops, + "A Loop", + ) + _circuits: Optional[List[Circuit]] = field(default=None) circuits: MridCollection[Circuit] = LazyMridList( @@ -48,40 +64,18 @@ class Substation(EquipmentContainer): ) @property + @internal(_sub_geographical_region) def sub_geographical_region(self): """The SubGeographicalRegion containing the substation.""" return self._sub_geographical_region + + @sub_geographical_region.setter @deprecated("sub_geographical_region should never be set directly - it is automatically set when adding it to the `substations` list") def sub_geographical_region(self, value): self._sub_geographical_region = value - @property - def circuits(self) -> Generator[Circuit, None, None]: - """ - The `Circuit`s originating from this substation. - """ - return ngen(self._circuits) - - loops: MridCollection[Loop] = LazyMridList( - _loops, - "A Loop", - ) - - energized_loops: MridCollection[Loop] = LazyMridList( - _energized_loops, - "A Loop", - ) - - feeders: MridCollection[Feeder] = LazyMridList( - _normal_energized_feeders, - "A Feeder", - backfill=Backfill(Feeder.normal_energizing_substation) - ) - normal_energized_feeders = Alias(feeders) - - # region deprecated list boilerplate # region circuits boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py index 339178aa9..d8c5965f2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py @@ -7,12 +7,14 @@ __all__ = ["Terminal"] +from dataclasses import field from typing import Optional, Generator from typing import TYPE_CHECKING from weakref import ref, ReferenceType from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import AcDcTerminal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode @@ -31,7 +33,7 @@ class Terminal(AcDcTerminal): An AC electrical connection point to a piece of conducting equipment. Terminals are connected at physical connection points called connectivity nodes. """ - _conducting_equipment: Optional['ConductingEquipment'] = None + _conducting_equipment: Optional['ConductingEquipment'] = field(default=None) """The conducting equipment of the terminal. Conducting equipment have terminals that may be connected to other conducting equipment terminals via connectivity nodes.""" @@ -75,6 +77,7 @@ def current_phases(self) -> PhaseStatus: return self._current_phases @property + @internal(_conducting_equipment) def conducting_equipment(self): """ The conducting equipment of the terminal. Conducting equipment have terminals that may be connected to other conducting equipment terminals via @@ -162,6 +165,7 @@ def is_feeder_head_terminal(self): for feeder in filter(lambda c: isinstance(c, Feeder), self.conducting_equipment.containers): if feeder.normal_head_terminal == self: return True + return False def has_connected_busbars(self): from zepben.ewb.model.cim.iec61970.base.wires.busbar_section import BusbarSection diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 04ff014db..25dfe983f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -14,6 +14,7 @@ from zepben.ewb import Alias from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint from zepben.ewb.util import nlen, ngen, require, safe_remove @@ -30,7 +31,13 @@ class DiagramObject(IdentifiedObject): analog values, breakers, disconnectors, power transformers, and transmission lines. """ - _diagram: Optional[Diagram] = None + _diagram: Optional[Diagram] = field(default=None) + + @property + @internal(_diagram) + def diagram(self): + """A diagram object is part of a diagram.""" + return self._diagram identified_object_mrid: Optional[str] = None """The domain object to which this diagram object is associated.""" @@ -43,11 +50,12 @@ class DiagramObject(IdentifiedObject): _diagram_object_points: Optional[List[DiagramObjectPoint]] = field(default=None) + points: LazyIndexedList[DiagramObjectPoint] = LazyIndexedList( + _diagram_object_points, + "DiagramObjectPoint", + ) + diagram_object_points = Alias(points) - @property - def diagram(self): - """A diagram object is part of a diagram.""" - return self._diagram @diagram.setter @deprecated("diagram should never be set directly - it is automatically set when adding it to the `diagram_objects` list") @@ -57,13 +65,6 @@ def diagram(self, diag): else: raise ValueError(f"diagram for {str(self)} has already been set to {self._diagram}, cannot reset this field to {diag}") - points: LazyIndexedList[DiagramObjectPoint] = LazyIndexedList( - _diagram_object_points, - "DiagramObjectPoint", - ) - diagram_object_points = Alias(points) - - # region deprecated list boilerplate # # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py index a018da065..beabfa53d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py @@ -7,10 +7,12 @@ __all__ = ['AcLineSegmentPhase'] +from dataclasses import field from typing import TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo @@ -35,12 +37,15 @@ class AcLineSegmentPhase(PowerSystemResource): phase: SinglePhaseKind = SinglePhaseKind.X sequence_number: int | None = None - _ac_line_segment: AcLineSegment | None = None + _ac_line_segment: AcLineSegment | None = field(default=None) @property + @internal(_ac_line_segment) def ac_line_segment(self) -> 'AcLineSegment | None': return self._ac_line_segment + asset_info: WireInfo | None = None + @ac_line_segment.setter @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `phases` list") def ac_line_segment(self, ac_line_segment: 'AcLineSegment') -> None: @@ -48,6 +53,3 @@ def ac_line_segment(self, ac_line_segment: 'AcLineSegment') -> None: self._ac_line_segment = ac_line_segment else: raise ValueError(f"ac_line_segment has already been set to {self._ac_line_segment}. Cannot set this field again") - - - asset_info: WireInfo | None = None diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index 2007debce..08192bfce 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -8,6 +8,9 @@ from dataclasses import field from typing import Optional, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @@ -25,21 +28,22 @@ class Clamp(ConductingEquipment): length_from_terminal_1: Optional[float] = None """The length to the place where the clamp is located starting from side one of the line segment, i.e. the line segment terminal with sequence number equal to 1.""" - ac_line_segment: Optional['AcLineSegment'] = field(default=None) + _ac_line_segment: Optional['AcLineSegment'] = field(default=None) """The line segment to which the clamp is connected.""" - max_terminals = 1 - @property + @internal(_ac_line_segment) def ac_line_segment(self) -> Optional['AcLineSegment']: """The line segment to which the clamp is connected.""" return self._ac_line_segment - @ac_line_segment.setter - @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `clamps` list") - def ac_line_segment(self, value): - self._ac_line_segment = value + max_terminals = 1 @property def length_from_t1_or_0(self) -> float: return self.length_from_terminal_1 or 0.0 + + @ac_line_segment.setter + @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `clamps` list") + def ac_line_segment(self, value): + self._ac_line_segment = value diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index e8ae1309d..21cb43c05 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -8,6 +8,9 @@ from dataclasses import field from typing import Optional, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @@ -31,20 +34,19 @@ class Cut(Switch): length_from_terminal_1: Optional[float] = None """The length to the place where the cut is located starting from side one of the cut line segment, i.e. the line segment Terminal with sequenceNumber equal to 1.""" - ac_line_segment: Optional['AcLineSegment'] = field(default=None) - """The line segment to which the cut is applied.""" - _ac_line_segment: Optional['AcLineSegment'] = None + _ac_line_segment: Optional['AcLineSegment'] = field(default=None) @property + @internal(_ac_line_segment) def ac_line_segment(self) -> Optional['AcLineSegment']: """The line segment to which the cut is applied.""" return self._ac_line_segment + @property + def length_from_t1_or_0(self) -> float: + return self.length_from_terminal_1 or 0.0 + @ac_line_segment.setter @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `cuts` list") def ac_line_segment(self, value): self._ac_line_segment = value - - @property - def length_from_t1_or_0(self) -> float: - return self.length_from_terminal_1 or 0.0 diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py index 18d565550..09f8de947 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py @@ -5,10 +5,12 @@ __all__ = ["EnergyConsumerPhase"] +from dataclasses import field from typing import Optional, TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @@ -21,7 +23,13 @@ class EnergyConsumerPhase(PowerSystemResource): """A single phase of an energy consumer.""" - _energy_consumer: Optional['EnergyConsumer'] = None + _energy_consumer: Optional['EnergyConsumer'] = field(default=None) + + @property + @internal(_energy_consumer) + def energy_consumer(self): + """The `EnergyConsumer` that has this phase.""" + return self._energy_consumer phase: SinglePhaseKind = SinglePhaseKind.X """Phase of this energy consumer component. If the energy consumer is wye connected, the connection is from the indicated phase to the central ground or @@ -42,11 +50,6 @@ class EnergyConsumerPhase(PowerSystemResource): q_fixed: Optional[float] = None """Reactive power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" - @property - def energy_consumer(self): - """The `EnergyConsumer` that has this phase.""" - return self._energy_consumer - @energy_consumer.setter @deprecated("energy_consumer should never be set directly - it is automatically set when adding it to the `phases` list") def energy_consumer(self, ec): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py index ccbf4094a..4ebbb62d6 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py @@ -5,10 +5,12 @@ __all__ = ["EnergySourcePhase"] +from dataclasses import field from typing import Optional, TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @@ -23,21 +25,21 @@ class EnergySourcePhase(PowerSystemResource): A single phase of an energy source. """ - _energy_source: Optional['EnergySource'] = None - """The `zepben.ewb.model.cim.iec61970.wires.EnergySource` with this `EnergySourcePhase`""" - - phase: SinglePhaseKind = SinglePhaseKind.NONE - """A `zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind.SinglePhaseKind` Phase of this energy source component. If the energy source is wye connected, - the connection is from the indicated phase to the central ground or neutral point. If the energy source is delta connected, the phase indicates an energy - source connected from the indicated phase to the next logical non-neutral phase.""" + _energy_source: Optional['EnergySource'] = field(default=None) @property + @internal(_energy_source) def energy_source(self): """The `EnergySource` with this `EnergySourcePhase`""" return self._energy_source + phase: SinglePhaseKind = SinglePhaseKind.NONE + """A `zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind.SinglePhaseKind` Phase of this energy source component. If the energy source is wye connected, + the connection is from the indicated phase to the central ground or neutral point. If the energy source is delta connected, the phase indicates an energy + source connected from the indicated phase to the next logical non-neutral phase.""" + @energy_source.setter - @deprecated("energy_sounrce should never be set directly - it is automatically set when adding it to the `phases` list") + @deprecated("energy_source should never be set directly - it is automatically set when adding it to the `phases` list") def energy_source(self, es): if self._energy_source is None or self._energy_source is es: self._energy_source = es diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py index 71762ab69..8778ddea0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py @@ -8,6 +8,9 @@ from dataclasses import field from typing import Optional, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass @@ -20,9 +23,15 @@ class PowerElectronicsConnectionPhase(PowerSystemResource): """A single phase of a power electronics connection.""" - power_electronics_connection: Optional['PowerElectronicsConnection'] = None + _power_electronics_connection: Optional['PowerElectronicsConnection'] = field(default=None) """The power electronics connection to which the phase belongs.""" + @property + @internal(_power_electronics_connection) + def power_electronics_connection(self): + """The power electronics connection to which the phase belongs.""" + return self._power_electronics_connection + p: Optional[float] = None """Active power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" @@ -36,11 +45,6 @@ class PowerElectronicsConnectionPhase(PowerSystemResource): q: Optional[float] = None """Reactive power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" - @property - def power_electronics_connection(self): - """The power electronics connection to which the phase belongs.""" - return self._power_electronics_connection - @power_electronics_connection.setter @deprecated("power_electronics_connection should never be set directly - it is automatically set when adding it to the `phases` list") def power_electronics_connection(self, value): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index caaf788fb..983217170 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -13,7 +13,9 @@ from typing_extensions import deprecated +from zepben.ewb import Alias from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd @@ -62,7 +64,7 @@ class PowerTransformerEnd(TransformerEnd): Instead use the TransformerMeshImpedance or split the transformer into multiple PowerTransformers. """ - _power_transformer: Optional[PowerTransformer] = None + _power_transformer: Optional[PowerTransformer] = field(default=None) """The power transformer of this power transformer end.""" _rated_s: Optional[int] = None @@ -110,7 +112,7 @@ class PowerTransformerEnd(TransformerEnd): def __init__(self, *args, rated_s: int = None, **kwargs): super(PowerTransformerEnd, self).__init__(*args, **kwargs) - if self._s_ratings: + if "_s_ratings" in kwargs: raise ValueError("Do not directly set s_ratings through the constructor. You have one more constructor parameter than expected.") if rated_s and self._rated_s: raise ValueError(f"Cannot specify both rated_s and _rated_s properties when constructing {self}. Check your constructor parameters.") @@ -126,6 +128,7 @@ def __init__(self, *args, rated_s: int = None, **kwargs): self._rated_s = None @property + @internal(_power_transformer) def power_transformer(self): """The power transformer of this power transformer end.""" return self._power_transformer @@ -167,6 +170,7 @@ def rated_s(self, rated_s: Optional[int]): validate=lambda self, it: self._validate_rating(it), sort_by=lambda it: -it.rated_s ) + ratings = Alias(s_ratings) def _validate_rating(self, rating: TransformerEndRatedS): if any(it.cooling_type == rating.cooling_type for it in self.s_ratings): From 3703e9efc1b992956cb432de0628044a52de757b Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 27 Jul 2026 14:37:06 +1000 Subject: [PATCH 07/54] Rearrange boilerplate; Fix tests to assume list-like object instead of a generator Signed-off-by: Boris Filin --- src/zepben/ewb/__init__.py | 4 +- .../MANIFESTO.md | 0 .../__init__.py | 0 src/zepben/ewb/boilerplate/backfill.py | 4 + .../ewb/boilerplate/collections/__init__.py | 4 + .../ewb/boilerplate/collections/base.py | 73 ++++ .../ewb/boilerplate/collections/lazy_list.py | 164 +++++++++ .../collections}/mrid_list.py | 15 +- .../collections}/mrid_map.py | 10 +- .../ewb/boilerplate/collections/wrapper.py | 80 +++++ .../dataclass_base.py | 0 .../descriptor_fix.py | 0 .../ewb/dataclass_descriptors/lazy_list.py | 322 ------------------ src/zepben/ewb/dataclass_descriptors/tst.py | 167 --------- .../iec61968/assetinfo/relay_info.py | 4 +- .../iec61968/common/contact_details.py | 5 +- .../metering/pan_demand_reponse_function.py | 2 +- .../iec61970/base/core/hv_customer.py | 2 +- .../cim/extensions/iec61970/base/core/site.py | 2 +- .../extensions/iec61970/base/feeder/loop.py | 8 +- .../iec61970/base/feeder/lv_feeder.py | 12 +- .../iec61970/base/feeder/lv_substation.py | 7 +- .../generation/production/ev_charging_unit.py | 2 +- .../protection/directional_current_relay.py | 2 +- .../base/protection/distance_relay.py | 2 +- .../protection/protection_relay_function.py | 11 +- .../protection/protection_relay_scheme.py | 8 +- .../protection/protection_relay_system.py | 8 +- .../iec61970/base/protection/voltage_relay.py | 2 +- .../iec61970/base/wires/battery_control.py | 2 +- .../cim/iec61968/assetinfo/cable_info.py | 2 +- .../cim/iec61968/assetinfo/no_load_test.py | 2 +- .../iec61968/assetinfo/open_circuit_test.py | 2 +- .../iec61968/assetinfo/overhead_wire_info.py | 2 +- .../assetinfo/power_transformer_info.py | 8 +- .../iec61968/assetinfo/short_circuit_test.py | 2 +- .../assetinfo/shunt_compensator_info.py | 2 +- .../cim/iec61968/assetinfo/switch_info.py | 2 +- .../assetinfo/transformer_end_info.py | 2 +- .../assetinfo/transformer_tank_info.py | 8 +- .../iec61968/assetinfo/transformer_test.py | 2 +- .../model/cim/iec61968/assetinfo/wire_info.py | 2 +- .../ewb/model/cim/iec61968/assets/asset.py | 8 +- .../cim/iec61968/assets/asset_container.py | 2 +- .../cim/iec61968/assets/asset_function.py | 2 +- .../model/cim/iec61968/assets/asset_info.py | 2 +- .../assets/asset_organisation_role.py | 2 +- .../model/cim/iec61968/assets/asset_owner.py | 2 +- .../model/cim/iec61968/assets/streetlight.py | 2 +- .../model/cim/iec61968/assets/structure.py | 2 +- .../model/cim/iec61968/common/agreement.py | 2 +- .../ewb/model/cim/iec61968/common/document.py | 2 +- .../ewb/model/cim/iec61968/common/location.py | 4 +- .../model/cim/iec61968/common/organisation.py | 2 +- .../cim/iec61968/common/organisation_role.py | 2 +- .../model/cim/iec61968/customers/customer.py | 9 +- .../iec61968/customers/customer_agreement.py | 8 +- .../iec61968/customers/pricing_structure.py | 8 +- .../model/cim/iec61968/customers/tariff.py | 2 +- .../infassetinfo/current_transformer_info.py | 2 +- .../potential_transformer_info.py | 2 +- .../iec61968/infiec61968/infassets/pole.py | 8 +- .../model/cim/iec61968/metering/end_device.py | 8 +- .../iec61968/metering/end_device_function.py | 2 +- .../ewb/model/cim/iec61968/metering/meter.py | 2 +- .../cim/iec61968/metering/usage_point.py | 9 +- .../operations/operational_restriction.py | 8 +- .../auxiliaryequipment/auxiliary_equipment.py | 2 +- .../auxiliaryequipment/current_transformer.py | 2 +- .../auxiliaryequipment/fault_indicator.py | 2 +- .../potential_transformer.py | 2 +- .../base/auxiliaryequipment/sensor.py | 8 +- .../cim/iec61970/base/core/ac_dc_terminal.py | 2 +- .../cim/iec61970/base/core/base_voltage.py | 2 +- .../base/core/conducting_equipment.py | 8 +- .../iec61970/base/core/connectivity_node.py | 7 +- .../base/core/connectivity_node_container.py | 2 +- .../ewb/model/cim/iec61970/base/core/curve.py | 8 +- .../model/cim/iec61970/base/core/equipment.py | 8 +- .../iec61970/base/core/equipment_container.py | 11 +- .../model/cim/iec61970/base/core/feeder.py | 11 +- .../iec61970/base/core/geographical_region.py | 4 +- .../cim/iec61970/base/core/identifiable.py | 2 +- .../iec61970/base/core/identified_object.py | 2 +- .../ewb/model/cim/iec61970/base/core/name.py | 2 +- .../model/cim/iec61970/base/core/name_type.py | 4 +- .../base/core/power_system_resource.py | 9 +- .../base/core/sub_geographical_region.py | 4 +- .../cim/iec61970/base/core/substation.py | 9 +- .../model/cim/iec61970/base/core/terminal.py | 4 +- .../iec61970/base/diagramlayout/diagram.py | 6 +- .../base/diagramlayout/diagram_object.py | 9 +- .../base/equivalents/equivalent_branch.py | 2 +- .../base/equivalents/equivalent_equipment.py | 2 +- .../generation/production/battery_unit.py | 8 +- .../production/photo_voltaic_unit.py | 2 +- .../production/power_electronics_unit.py | 2 +- .../production/power_electronics_wind_unit.py | 2 +- .../cim/iec61970/base/meas/accumulator.py | 2 +- .../model/cim/iec61970/base/meas/analog.py | 2 +- .../model/cim/iec61970/base/meas/control.py | 2 +- .../model/cim/iec61970/base/meas/discrete.py | 2 +- .../model/cim/iec61970/base/meas/iopoint.py | 2 +- .../cim/iec61970/base/meas/measurement.py | 2 +- .../iec61970/base/protection/current_relay.py | 2 +- .../cim/iec61970/base/scada/remote_control.py | 2 +- .../cim/iec61970/base/scada/remote_point.py | 2 +- .../cim/iec61970/base/scada/remote_source.py | 2 +- .../iec61970/base/wires/ac_line_segment.py | 4 +- .../base/wires/ac_line_segment_phase.py | 4 +- .../model/cim/iec61970/base/wires/breaker.py | 2 +- .../cim/iec61970/base/wires/busbar_section.py | 2 +- .../model/cim/iec61970/base/wires/clamp.py | 4 +- .../cim/iec61970/base/wires/conductor.py | 2 +- .../cim/iec61970/base/wires/connector.py | 2 +- .../ewb/model/cim/iec61970/base/wires/cut.py | 4 +- .../cim/iec61970/base/wires/disconnector.py | 2 +- .../base/wires/earth_fault_compensator.py | 2 +- .../iec61970/base/wires/energy_connection.py | 2 +- .../iec61970/base/wires/energy_consumer.py | 4 +- .../base/wires/energy_consumer_phase.py | 4 +- .../cim/iec61970/base/wires/energy_source.py | 4 +- .../base/wires/energy_source_phase.py | 4 +- .../ewb/model/cim/iec61970/base/wires/fuse.py | 2 +- .../model/cim/iec61970/base/wires/ground.py | 2 +- .../base/wires/ground_disconnector.py | 2 +- .../base/wires/grounding_impedance.py | 2 +- .../model/cim/iec61970/base/wires/jumper.py | 2 +- .../model/cim/iec61970/base/wires/junction.py | 2 +- .../ewb/model/cim/iec61970/base/wires/line.py | 2 +- .../base/wires/linear_shunt_compensator.py | 2 +- .../iec61970/base/wires/load_break_switch.py | 2 +- .../base/wires/per_length_impedance.py | 2 +- .../base/wires/per_length_line_parameter.py | 2 +- .../base/wires/per_length_phase_impedance.py | 6 +- .../wires/per_length_sequence_impedance.py | 2 +- .../cim/iec61970/base/wires/petersen_coil.py | 2 +- .../wires/power_electronics_connection.py | 4 +- .../power_electronics_connection_phase.py | 4 +- .../iec61970/base/wires/power_transformer.py | 6 +- .../base/wires/power_transformer_end.py | 9 +- .../iec61970/base/wires/protected_switch.py | 8 +- .../iec61970/base/wires/ratio_tap_changer.py | 2 +- .../base/wires/reactive_capability_curve.py | 2 +- .../model/cim/iec61970/base/wires/recloser.py | 2 +- .../iec61970/base/wires/regulating_cond_eq.py | 2 +- .../iec61970/base/wires/regulating_control.py | 8 +- .../iec61970/base/wires/rotating_machine.py | 2 +- .../iec61970/base/wires/series_compensator.py | 2 +- .../iec61970/base/wires/shunt_compensator.py | 2 +- .../base/wires/static_var_compensator.py | 2 +- .../model/cim/iec61970/base/wires/switch.py | 2 +- .../base/wires/synchronous_machine.py | 8 +- .../cim/iec61970/base/wires/tap_changer.py | 2 +- .../base/wires/tap_changer_control.py | 2 +- .../iec61970/base/wires/transformer_end.py | 2 +- .../base/wires/transformer_star_impedance.py | 2 +- .../iec61970/infiec61970/feeder/circuit.py | 8 +- .../connectivity/connectivity_result.py | 2 +- src/zepben/ewb/streaming/grpc/grpc.py | 2 +- test/busbranch/test_bus_branch.py | 8 +- .../test_basic_descriptor.py | 4 +- .../test_dataclass_base.py | 2 +- .../common/service_comparator_validator.py | 18 +- .../test_network_trace_step_path_provider.py | 2 +- .../tracing/test_assign_to_lv_feeders.py | 2 +- 166 files changed, 608 insertions(+), 819 deletions(-) rename src/zepben/ewb/{dataclass_descriptors => boilerplate}/MANIFESTO.md (100%) rename src/zepben/ewb/{dataclass_descriptors => boilerplate}/__init__.py (100%) create mode 100644 src/zepben/ewb/boilerplate/backfill.py create mode 100644 src/zepben/ewb/boilerplate/collections/__init__.py create mode 100644 src/zepben/ewb/boilerplate/collections/base.py create mode 100644 src/zepben/ewb/boilerplate/collections/lazy_list.py rename src/zepben/ewb/{dataclass_descriptors => boilerplate/collections}/mrid_list.py (91%) rename src/zepben/ewb/{dataclass_descriptors => boilerplate/collections}/mrid_map.py (93%) create mode 100644 src/zepben/ewb/boilerplate/collections/wrapper.py rename src/zepben/ewb/{dataclass_descriptors => boilerplate}/dataclass_base.py (100%) rename src/zepben/ewb/{dataclass_descriptors => boilerplate}/descriptor_fix.py (100%) delete mode 100644 src/zepben/ewb/dataclass_descriptors/lazy_list.py delete mode 100644 src/zepben/ewb/dataclass_descriptors/tst.py diff --git a/src/zepben/ewb/__init__.py b/src/zepben/ewb/__init__.py index 2defac2d2..c3ea06945 100644 --- a/src/zepben/ewb/__init__.py +++ b/src/zepben/ewb/__init__.py @@ -13,8 +13,8 @@ from zepben.ewb.auth.client.zepben_token_fetcher import * from zepben.ewb.auth.common.auth_exception import * from zepben.ewb.auth.common.auth_method import * -from zepben.ewb.dataclass_descriptors.dataclass_base import * -from zepben.ewb.dataclass_descriptors.descriptor_fix import * +from zepben.ewb.boilerplate.dataclass_base import * +from zepben.ewb.boilerplate.descriptor_fix import * from zepben.ewb.util import * ############# diff --git a/src/zepben/ewb/dataclass_descriptors/MANIFESTO.md b/src/zepben/ewb/boilerplate/MANIFESTO.md similarity index 100% rename from src/zepben/ewb/dataclass_descriptors/MANIFESTO.md rename to src/zepben/ewb/boilerplate/MANIFESTO.md diff --git a/src/zepben/ewb/dataclass_descriptors/__init__.py b/src/zepben/ewb/boilerplate/__init__.py similarity index 100% rename from src/zepben/ewb/dataclass_descriptors/__init__.py rename to src/zepben/ewb/boilerplate/__init__.py diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py new file mode 100644 index 000000000..35e3122c5 --- /dev/null +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -0,0 +1,4 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/src/zepben/ewb/boilerplate/collections/__init__.py b/src/zepben/ewb/boilerplate/collections/__init__.py new file mode 100644 index 000000000..35e3122c5 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/src/zepben/ewb/boilerplate/collections/base.py b/src/zepben/ewb/boilerplate/collections/base.py new file mode 100644 index 000000000..fcd9c9524 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/base.py @@ -0,0 +1,73 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC, abstractmethod +from typing import Collection, Protocol, Iterable, Generic, Iterator, Callable, Any, Sequence, overload, TypeVar + + +T = TypeVar("T") + + +class AbstractBackedCollection(Collection[T], Generic[T], ABC): + + @abstractmethod + def _get_collection(self) -> Collection[T]: + ... + + @abstractmethod + def append(self, element: T) -> None: + ... + + def extend(self, elements: Iterable[T]) -> None: + for element in elements: + self.append(element) + + @abstractmethod + def remove(self, element: T) -> None: + ... + + @abstractmethod + def clear(self) -> None: + ... + + def __len__(self) -> int: + return len(self._get_collection()) + + def __iter__(self) -> Iterator[T]: + return iter(self._get_collection()) + + def __contains__(self, element: object) -> bool: + return element in self._get_collection() + + def for_each_indexed(self, action: Callable[[int, T], Any]): + """ + Call the `action` on each item in the list + + :param action: An action to apply to each :class:`RelaySetting` in the `thresholds` collection, taking the index of the threshold, and the threshold itself. + """ + for index, item in enumerate(self._get_collection()): + action(index, item) + + +class AbstractBackedList( + AbstractBackedCollection[T], + Sequence[T], + Generic[T], + ABC, +): + + @abstractmethod + def _get_collection(self) -> Sequence[T]: + ... + + @overload + def __getitem__(self, index: int) -> T: + ... + + @overload + def __getitem__(self, index: slice) -> Sequence[T]: + ... + + def __getitem__(self, index: int | slice) -> T | Sequence[T]: + return self._get_collection()[index] diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py new file mode 100644 index 000000000..b7a28dcb5 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -0,0 +1,164 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from dataclasses import field +from typing import TypeVar, Protocol + +from zepben.ewb import DataclassBase, zb_dataclass +from zepben.ewb.boilerplate.collections.base import AbstractBackedList +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper + + +# from descriptor_fix import BackedDescriptor, remove_descriptor_annotations + + +class HasMrid(Protocol): + mrid: str + +T = TypeVar("T") +S = TypeVar("S", bound=HasMrid) + + +class LazyValidatedList(_IterableWrapper, AbstractBackedList[T]): + + def __init__(self, + private_field, + validate=None, + sort_by=None): + super().__init__(private_field) + self.validate = validate + self.sort_by = sort_by + + def _get(self): + return getattr(self.instance, self.backing_name) + + def _get_collection(self) -> list[T]: + return getattr(self.instance, self.backing_name) or [] + + # clearIfEmpty is inlined for performance + + def append(self, item): + if self.validate is not None: + self.validate(self.instance, item) + + existing = getattr(self.instance, self.backing_name) + if existing is None: + existing = [item] + setattr(self.instance, self.backing_name, existing) + else: + existing.append(item) + + if self.sort_by is not None: + existing.sort(key=self.sort_by) + + def __len__(self): + return len(self._get_collection()) + + def __iter__(self): + return iter(self._get_collection()) + + def __contains__(self, item): + return item in (self._get_collection()) + + def remove(self, item): + existing = self._get_collection() + existing.remove(item) + if not existing: + self.clear() + + def clear(self): + setattr(self.instance, self.backing_name, None) + + def __repr__(self): + if self.instance is None: + return self.__class__.__repr__() + return str(self._get_collection()) + + def __str__(self): + return self.__repr__() + + def __getitem__(self, item): + return (self._get_collection())[item] + + +class LazyIndexedList(LazyValidatedList[T]): + + def __init__( + self, + private_field, + element_description: str + ): + super().__init__(private_field) + self.element_description = element_description + + def insert(self, index: int, item: T) -> None: + """ + Insert ``item`` at ``index``. + """ + size = len(self) + + if not 0 <= index <= size: + raise ValueError( + f"Unable to add {self.element_description} to " + f"{self.instance}. " + f"Sequence number {index} is invalid. " + f"Expected a value between 0 and {size}. " + "Make sure you are adding the items in order and there are " + "no gaps in the numbering." + ) + + existing = getattr(self.instance, self.backing_name) + + if existing is None: + existing = [item] + setattr(self.instance, self.backing_name, existing) + else: + existing.insert(index, item) + + if self.sort_by is not None: + existing.sort(key=self.sort_by) + + def append(self, item: T) -> None: + self.insert(len(self), item) + + def pop(self, index: int = -1) -> T: + """ + Remove and return the item at ``index``. + + Uses normal Python list semantics, including support for negative + indexes and raising ``IndexError`` when the index is invalid. + """ + existing = getattr(self.instance, self.backing_name) + + if existing is None: + raise IndexError("pop from empty list") + + item = existing.pop(index) + + if not existing: + self.clear() + + return item + +if __name__ == '__main__': + @zb_dataclass + class C(DataclassBase): + _xs: list[int] | None = field(default=None) + xs: LazyIndexedList[int] = LazyIndexedList( + _xs, + "Thing" + ) + + c = C(xs=[24, 42]) + print(c._xs, c.xs) + c.xs.clear() + print(c._xs, c.xs) + c.xs.append(1) + c.xs.append(2) + c.xs.append(3) + print(c._xs, c.xs) + c.xs.insert(2, 42) + print(c._xs, c.xs) + c.xs.pop(2) + print(c._xs, c.xs) diff --git a/src/zepben/ewb/dataclass_descriptors/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py similarity index 91% rename from src/zepben/ewb/dataclass_descriptors/mrid_list.py rename to src/zepben/ewb/boilerplate/collections/mrid_list.py index 195254139..29b2f691e 100644 --- a/src/zepben/ewb/dataclass_descriptors/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -5,13 +5,15 @@ from abc import abstractmethod, ABC from dataclasses import dataclass, field, Field from types import MemberDescriptorType -from typing import Any, TypeVar, Protocol, Callable +from typing import Any, TypeVar, Protocol, Callable, Sequence import pytest from typing_extensions import Self -from zepben.ewb import remove_descriptor_annotations, BackedDescriptor, require -from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList, MutableCollection, IndexedMutableCollection, _IterableWrapper +from zepben.ewb import BackedDescriptor +from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper +from zepben.ewb.boilerplate.collections.base import AbstractBackedCollection, AbstractBackedList class HasMrid(Protocol): @@ -20,7 +22,7 @@ class HasMrid(Protocol): T = TypeVar("T") S = TypeVar("S", bound=HasMrid) -class MridCollection(MutableCollection[S], ABC): +class MridCollection(AbstractBackedCollection[S], ABC): instance: Any element_description: str @@ -104,7 +106,7 @@ def append(self, item: T): super().append(item) -class MridList(_IterableWrapper, IndexedMutableCollection[S], MridCollection[S]): +class MridList(_IterableWrapper, AbstractBackedList[S], MridCollection[S]): def __init__(self, private_field, element_description: str, @@ -126,6 +128,9 @@ def __get__(self, instance, owner=None) -> Self: def __post_init__(self): self.backing_list = getattr(self.instance, self.backing_name) + def _get_collection(self) -> Sequence[T]: + return self.backing_list + def _safe_get_by_mrid(self, mrid: str) -> S | None: found = next((element for element in self.backing_list if element.mrid == mrid), None) return found diff --git a/src/zepben/ewb/dataclass_descriptors/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py similarity index 93% rename from src/zepben/ewb/dataclass_descriptors/mrid_map.py rename to src/zepben/ewb/boilerplate/collections/mrid_map.py index 0f71c474c..ac5af32aa 100644 --- a/src/zepben/ewb/dataclass_descriptors/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -3,13 +3,12 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import dataclass, field -from typing import Protocol, TypeVar +from typing import Protocol, TypeVar, Collection import pytest -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.lazy_list import _IterableWrapper -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, Backfill +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, Backfill class HasMrid(Protocol): @@ -20,6 +19,9 @@ class HasMrid(Protocol): class LazyMridMap(_IterableWrapper[S], MridCollection[S]): + def _get_collection(self) -> Collection[T]: + pass + def __init__( self, private_field, diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py new file mode 100644 index 000000000..9913f8df8 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -0,0 +1,80 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC +from typing import Any +from typing_extensions import Self, deprecated + +from zepben.ewb import BackedDescriptor, resolve_default +from zepben.ewb.boilerplate.collections.base import AbstractBackedList + + +class _Wrapper(BackedDescriptor): + + instance: Any + backing_name: Any + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + """ + This __new__ stores the init arguments, and otherwise acts as default. + We need this to efficiently and truly re-create the wrapper and bind it to an instance upon a get call. + """ + obj = super().__new__(cls) + obj._init_args = args + obj._init_kwargs = kwargs.copy() + return obj + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.instance = None + self.backing_name = None + + def __get__(self, instance, _=None) -> Self: + """ + This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. + From there on, the wrapper functions as a lazy list. + """ + if instance is None: + return self + obj = type(self)(*self._init_args, **self._init_kwargs) + obj.instance = instance + obj.backing_name = self.private_field.name + return obj + + +class _WrapperFgetFix(_Wrapper): + """ + This class exists to fix the tests that rely on the old lists being @property. + TODO: Remove in a separate PR fixing tests + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + def fget(instance): + return self.__get__(instance) + self.fget = fget + + def __set_name__(self, owner, name): + super().__set_name__(owner, name) + self.fget.__name__ = name + self.fget.__qualname__ = f"{owner.__qualname__}.{name}" + + @deprecated("Lists are no longer a property") + def fget(self, instance): ... + + +class _IterableWrapper(_WrapperFgetFix, AbstractBackedList, ABC): + """ + This class allows us to assign lists at init time to avoid special-case handling. + """ + def __set__(self, instance, value): + if instance is None: + return + if not hasattr(instance, self.backing_name): + resolve_default(instance, self.private_field) + elif getattr(instance, self.backing_name): + raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") + if value is not None: + self.__get__(instance).extend(value) + else: + self.__get__(instance).clear() diff --git a/src/zepben/ewb/dataclass_descriptors/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py similarity index 100% rename from src/zepben/ewb/dataclass_descriptors/dataclass_base.py rename to src/zepben/ewb/boilerplate/dataclass_base.py diff --git a/src/zepben/ewb/dataclass_descriptors/descriptor_fix.py b/src/zepben/ewb/boilerplate/descriptor_fix.py similarity index 100% rename from src/zepben/ewb/dataclass_descriptors/descriptor_fix.py rename to src/zepben/ewb/boilerplate/descriptor_fix.py diff --git a/src/zepben/ewb/dataclass_descriptors/lazy_list.py b/src/zepben/ewb/dataclass_descriptors/lazy_list.py deleted file mode 100644 index 7969c2c1f..000000000 --- a/src/zepben/ewb/dataclass_descriptors/lazy_list.py +++ /dev/null @@ -1,322 +0,0 @@ -# Copyright 2026 Zeppelin Bend Pty Ltd -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -from abc import ABC, abstractmethod -from collections.abc import Collection -from dataclasses import field -from typing import TypeVar, Protocol, Any, Iterable, Generic, Iterator, Sequence, overload, Callable - -from typing_extensions import deprecated, Self - -from zepben.ewb import BackedDescriptor, remove_descriptor_annotations, resolve_default, DataclassBase, zb_dataclass - - -# from descriptor_fix import BackedDescriptor, remove_descriptor_annotations - - -class HasMrid(Protocol): - mrid: str - -T = TypeVar("T") -S = TypeVar("S", bound=HasMrid) - -class MutableCollection(Collection[T], Protocol[T]): - def append(self, item: T) -> None: ... - - def remove(self, item: T) -> None: ... - - def clear(self) -> None: ... - - def extend(self, items: Collection[T]) -> None: - for item in items: - self.append(item) - - def __next__(self): - # TODO: Remove test hack in separate PR - return next(iter(self)) - - def __eq__(self, other): - return isinstance(other, Iterable) and all(a == b for a, b in zip(self, other)) - - -class IndexedMutableCollection(MutableCollection, ABC): - def __getitem__(self, idx: int) -> T: ... - - - -class _Wrapper(BackedDescriptor): - - instance: Any - backing_name: Any - - def __new__(cls, *args: Any, **kwargs: Any) -> Self: - """ - This __new__ stores the init arguments, and otherwise acts as default. - We need this to efficiently and truly re-create the wrapper and bind it to an instance upon a get call. - """ - obj = super().__new__(cls) - obj._init_args = args - obj._init_kwargs = kwargs.copy() - return obj - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.instance = None - self.backing_name = None - - def __get__(self, instance, _=None) -> Self: - """ - This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. - From there on, the wrapper functions as a lazy list. - """ - if instance is None: - return self - obj = type(self)(*self._init_args, **self._init_kwargs) - obj.instance = instance - obj.backing_name = self.private_field.name - return obj - -class _WrapperFgetFix(_Wrapper): - """ - This class exists to fix the tests that rely on the old lists being @property. - TODO: Remove in a separate PR fixing tests - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - def fget(instance): - return self.__get__(instance) - self.fget = fget - - def __set_name__(self, owner, name): - super().__set_name__(owner, name) - self.fget.__name__ = name - self.fget.__qualname__ = f"{owner.__qualname__}.{name}" - - @deprecated("Lists are no longer a property") - def fget(self, instance): ... - -class _IterableWrapper(_WrapperFgetFix, IndexedMutableCollection, ABC): - """ - This class allows us to assign lists at init time to avoid special-case handling. - """ - def __set__(self, instance, value): - if instance is None: - return - if not hasattr(instance, self.backing_name): - resolve_default(instance, self.private_field) - elif getattr(instance, self.backing_name): - raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") - if value is not None: - self.__get__(instance).extend(value) - else: - self.__get__(instance).clear() - - -class AbstractBackedCollection(Collection[T], Generic[T], ABC): - - @abstractmethod - def _get_collection(self) -> Collection[T]: - ... - - @abstractmethod - def append(self, element: T) -> None: - ... - - def extend(self, elements: Iterable[T]) -> None: - for element in elements: - self.append(element) - - @abstractmethod - def remove(self, element: T) -> None: - ... - - @abstractmethod - def clear(self) -> None: - ... - - def __len__(self) -> int: - return len(self._get_collection()) - - def __iter__(self) -> Iterator[T]: - return iter(self._get_collection()) - - def __contains__(self, element: object) -> bool: - return element in self._get_collection() - - def for_each_indexed(self, action: Callable[[int, T], Any]): - """ - Call the `action` on each item in the list - - :param action: An action to apply to each :class:`RelaySetting` in the `thresholds` collection, taking the index of the threshold, and the threshold itself. - """ - for index, item in enumerate(self._get_collection()): - action(index, item) - - - -class AbstractBackedList( - AbstractBackedCollection[T], - Sequence[T], - Generic[T], - ABC, -): - - @abstractmethod - def _get_collection(self) -> Sequence[T]: - ... - - @overload - def __getitem__(self, index: int) -> T: - ... - - @overload - def __getitem__(self, index: slice) -> Sequence[T]: - ... - - def __getitem__(self, index: int | slice) -> T | Sequence[T]: - return self._get_collection()[index] - - -class LazyValidatedList(_IterableWrapper, AbstractBackedList[T]): - - def __init__(self, - private_field, - validate=None, - sort_by=None): - super().__init__(private_field) - self.validate = validate - self.sort_by = sort_by - - def _get(self): - return getattr(self.instance, self.backing_name) - - def _get_collection(self) -> list[T]: - return getattr(self.instance, self.backing_name) or [] - - # clearIfEmpty is inlined for performance - - def append(self, item): - if self.validate is not None: - self.validate(self.instance, item) - - existing = getattr(self.instance, self.backing_name) - if existing is None: - existing = [item] - setattr(self.instance, self.backing_name, existing) - else: - existing.append(item) - - if self.sort_by is not None: - existing.sort(key=self.sort_by) - - def __len__(self): - return len(self._get_collection()) - - def __iter__(self): - return iter(self._get_collection()) - - def __contains__(self, item): - return item in (self._get_collection()) - - def remove(self, item): - existing = self._get_collection() - existing.remove(item) - if not existing: - self.clear() - - def clear(self): - setattr(self.instance, self.backing_name, None) - - def __repr__(self): - if self.instance is None: - return self.__class__.__repr__() - return str(self._get_collection()) - - def __str__(self): - return self.__repr__() - - def __getitem__(self, item): - return (self._get_collection())[item] - - -class LazyIndexedList(LazyValidatedList[T]): - - def __init__( - self, - private_field, - element_description: str - ): - super().__init__(private_field) - self.element_description = element_description - - def insert(self, index: int, item: T) -> None: - """ - Insert ``item`` at ``index``. - """ - size = len(self) - - if not 0 <= index <= size: - raise ValueError( - f"Unable to add {self.element_description} to " - f"{self.instance}. " - f"Sequence number {index} is invalid. " - f"Expected a value between 0 and {size}. " - "Make sure you are adding the items in order and there are " - "no gaps in the numbering." - ) - - existing = getattr(self.instance, self.backing_name) - - if existing is None: - existing = [item] - setattr(self.instance, self.backing_name, existing) - else: - existing.insert(index, item) - - if self.sort_by is not None: - existing.sort(key=self.sort_by) - - def append(self, item: T) -> None: - self.insert(len(self), item) - - def pop(self, index: int = -1) -> T: - """ - Remove and return the item at ``index``. - - Uses normal Python list semantics, including support for negative - indexes and raising ``IndexError`` when the index is invalid. - """ - existing = getattr(self.instance, self.backing_name) - - if existing is None: - raise IndexError("pop from empty list") - - item = existing.pop(index) - - if not existing: - self.clear() - - return item - -if __name__ == '__main__': - @zb_dataclass - class C(DataclassBase): - _xs: list[int] | None = field(default=None) - xs: LazyIndexedList[int] = LazyIndexedList( - _xs, - "Thing" - ) - - c = C(xs=[24, 42]) - print(c._xs, c.xs) - c.xs.clear() - print(c._xs, c.xs) - c.xs.append(1) - c.xs.append(2) - c.xs.append(3) - print(c._xs, c.xs) - c.xs.insert(2, 42) - print(c._xs, c.xs) - c.xs.pop(2) - print(c._xs, c.xs) diff --git a/src/zepben/ewb/dataclass_descriptors/tst.py b/src/zepben/ewb/dataclass_descriptors/tst.py deleted file mode 100644 index b0e8fa5e9..000000000 --- a/src/zepben/ewb/dataclass_descriptors/tst.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright 2026 Zeppelin Bend Pty Ltd -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -from __future__ import annotations - -import copy -import time -from abc import abstractmethod, ABC -from dataclasses import dataclass, field -from typing import Any -from typing_extensions import Self, deprecated -from zepben.ewb import remove_descriptor_annotations, BackedDescriptor, resolve_default - - -class _Wrapper(BackedDescriptor): - - instance: Any - backing_name: Any - - def __new__(cls, *args: Any, **kwargs: Any) -> Self: - """ - This __new__ stores the init arguments, and otherwise acts as default. - We need this to efficiently and truly re-create the wrapper and bind it to an instance upon a get call. - """ - obj = super().__new__(cls) - obj._init_args = args - obj._init_kwargs = kwargs.copy() - return obj - - def __get__(self, instance, _=None) -> Self: - """ - This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. - From there on, the wrapper functions as a lazy list. - """ - obj = type(self)(*self._init_args, **self._init_kwargs) - obj.instance = instance - obj.backing_name = self.private_field.name - return obj - -class _WrapperFgetFix(_Wrapper): - """ - This class exists to fix the tests that rely on the old lists being @property. - TODO: Remove in a separate PR fixing tests - """ - def __set_name__(self, owner, name): - super().__set_name__(owner, name) - - def fget(instance): - return self.__get__(instance) - - fget.__name__ = name - fget.__qualname__ = f"{owner.__qualname__}.{name}" - - self.fget = fget - - @deprecated("Lists are no longer a property") - def fget(self, instance): - # TODO: Remove test hack in separate PR - raise NotImplementedError() - -class _IterableWrapper(_WrapperFgetFix, ABC): - """ - This class allows us to assign lists at init time to avoid special-case handling. - """ - def __set__(self, instance, value): - if instance is None: - return - if not hasattr(instance, self.backing_name): - resolve_default(instance, self.private_field) - elif getattr(instance, self.backing_name): - raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") - if value is not None: - self.__get__(instance).extend(value) - else: - self.__get__(instance).clear() - - @abstractmethod - def extend(self, items): ... - - @abstractmethod - def clear(self): ... - - -class ListWrapper(_IterableWrapper): - - def clear(self): - self._set(None) - - def extend(self, items): - for item in items: - self.append(item) - - def __init__( - self, - private_field: Any, - validate = None, - sort_by = None - ): - super().__init__(private_field) - self.validate = validate - self.sort_by = sort_by - - def _get(self): - if self.instance is None: - return self - return getattr(self.instance, self.backing_name) - - def _set(self, value): - if self.instance is None: - return - setattr(self.instance, self.backing_name, value) - - def __str__(self): - return str(self._get()) - - def append(self, item): - l = self._get() - if l is None: - self._set([item]) - else: - l.append(item) - - - - -@dataclass -class C: - _x: list[int] | None = field(default=None) - x: ListWrapper = ListWrapper(_x, sort_by=lambda it: -it) - - @property - def thing(self): - return 42 - -if __name__ == '__main__': - c = C([1, 2, 3]) - print(c) - print(c.x) - print(c.x.sort_by) - c.x.clear() - c.x.append(42) - print(c) - print(c.x) - - c.thing = 24 - - # class C: - # def __init__(self, x, f1, f2): - # self.x = x - # self.y = x + x - # self.f1 = f1 - # self.f2 = f2 - # - # N = 1_000_000 - # t = time.time() - # for _ in range(N): - # tmp = C("blahblahblah", lambda : 42, lambda : 24) - # print(time.time() - t) - # - # c = C("blahblahblah", lambda : 42, lambda : 24) - # t = time.time() - # for _ in range(N): - # tmp = copy.copy(c) - # print(time.time() - t) - ... \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index 7c02259f2..b107ac078 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -12,8 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py index b0fe95a38..31e5336ad 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -6,19 +6,18 @@ __all__ = ["ContactDetails"] from dataclasses import field -from typing import Generator, Any +from typing import Any from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61968.common.telephone_number import TelephoneNumber from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable -from zepben.ewb.util import ngen, nlen @zbex diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py b/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py index 934f21fed..29355bff7 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py @@ -12,7 +12,7 @@ from zepben.ewb.model.cim.iec61968.metering.end_device_function import EndDeviceFunction from zepben.ewb.model.cim.iec61968.metering.end_device_function_kind import EndDeviceFunctionKind from zepben.ewb.util import require -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/core/hv_customer.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/core/hv_customer.py index 1e6114845..becd2615c 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/core/hv_customer.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/core/hv_customer.py @@ -7,7 +7,7 @@ from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.model.cim.extensions.zbex import zbex -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/core/site.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/core/site.py index b7148b6c1..7723ee17a 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/core/site.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/core/site.py @@ -7,7 +7,7 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py index 79f3e77fc..d9356a515 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py @@ -7,16 +7,14 @@ __all__ = ["Loop"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import safe_remove, ngen, nlen, get_by_mrid -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.substation import Substation diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py index 1ea96f249..679214809 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py @@ -9,24 +9,20 @@ import typing from dataclasses import field -from typing import Generator, Optional, Dict, List +from typing import Optional, Dict -from pygments.lexer import default from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection -from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer -from zepben.ewb.util import safe_remove_by_id, nlen, ngen -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if typing.TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal - from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_substation import LvSubstation @zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index bf51f54d4..f4502f637 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -12,13 +12,12 @@ from typing_extensions import deprecated -from zepben.ewb import ngen, nlen, safe_remove_by_id, get_by_mrid, remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection -from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/generation/production/ev_charging_unit.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/generation/production/ev_charging_unit.py index 622acbf1c..f2c8fc7ff 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/generation/production/ev_charging_unit.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/generation/production/ev_charging_unit.py @@ -7,7 +7,7 @@ from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit from zepben.ewb.model.cim.extensions.zbex import zbex -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/directional_current_relay.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/directional_current_relay.py index 69cbfe732..0deac4622 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/directional_current_relay.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/directional_current_relay.py @@ -9,7 +9,7 @@ from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.extensions.iec61970.base.protection.polarizing_quantity_type import PolarizingQuantityType from zepben.ewb.model.cim.extensions.zbex import zbex -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/distance_relay.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/distance_relay.py index ca63148d8..33462a8fe 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/distance_relay.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/distance_relay.py @@ -9,7 +9,7 @@ from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction from zepben.ewb.model.cim.extensions.zbex import zbex -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 50e72b4de..7bce7fcd8 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -8,25 +8,22 @@ __all__ = ["ProtectionRelayFunction"] import sys -import warnings -from typing import Optional, List, Generator, Iterable, Callable, TYPE_CHECKING, Any +from typing import Optional, List, Callable, TYPE_CHECKING, Any from abc import ABCMeta -from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList if sys.version_info >= (3, 13): from warnings import deprecated else: from typing_extensions import deprecated from dataclasses import field -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.model.cim.extensions.iec61970.base.protection.power_direction_kind import PowerDirectionKind from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource -from zepben.ewb.util import require, nlen, ngen, safe_remove, get_by_mrid -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61968.assetinfo.relay_info import RelayInfo diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py index dbeb9cf07..d6a4737d6 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py @@ -7,16 +7,14 @@ __all__ = ["ProtectionRelayScheme"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import ngen, get_by_mrid, nlen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_system import ProtectionRelaySystem diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py index 49276fd6d..c3a48512f 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py @@ -7,17 +7,15 @@ __all__ = ["ProtectionRelaySystem"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment -from zepben.ewb.util import ngen, get_by_mrid, nlen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_scheme import ProtectionRelayScheme diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/voltage_relay.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/voltage_relay.py index cf119993e..aca21b9da 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/voltage_relay.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/voltage_relay.py @@ -8,7 +8,7 @@ from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction from zepben.ewb.model.cim.extensions.zbex import zbex -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/wires/battery_control.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/wires/battery_control.py index 47fe8fe45..243603e8d 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/wires/battery_control.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/wires/battery_control.py @@ -10,7 +10,7 @@ from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control_mode import BatteryControlMode from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.wires.regulating_control import RegulatingControl -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/cable_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/cable_info.py index daa52d17f..a8bf9abe8 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/cable_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/cable_info.py @@ -6,7 +6,7 @@ __all__ = ["CableInfo"] from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/no_load_test.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/no_load_test.py index 0d57e928f..be6a5b632 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/no_load_test.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/no_load_test.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61968.assetinfo.transformer_test import TransformerTest -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/open_circuit_test.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/open_circuit_test.py index 09a0ff2fc..90874bb4c 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/open_circuit_test.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/open_circuit_test.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61968.assetinfo.transformer_test import TransformerTest -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/overhead_wire_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/overhead_wire_info.py index be3ccf9f9..587c99011 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/overhead_wire_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/overhead_wire_info.py @@ -6,7 +6,7 @@ __all__ = ["OverheadWireInfo"] from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py index e966a3e14..b894424cf 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py @@ -7,16 +7,14 @@ __all__ = ["PowerTransformerInfo"] -from typing import List, Optional, Generator, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.transformer_tank_info import TransformerTankInfo diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/short_circuit_test.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/short_circuit_test.py index cf347b3c1..0704648de 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/short_circuit_test.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/short_circuit_test.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61968.assetinfo.transformer_test import TransformerTest -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/shunt_compensator_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/shunt_compensator_info.py index 4033d3a00..273e3f65d 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/shunt_compensator_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/shunt_compensator_info.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/switch_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/switch_info.py index de848c078..68c0ae33c 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/switch_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/switch_info.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_end_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_end_info.py index 3d572fd2d..15b95921f 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_end_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_end_info.py @@ -11,7 +11,7 @@ from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.cim.iec61970.base.wires.winding_connection import WindingConnection from zepben.ewb.model.resistance_reactance import ResistanceReactance -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.no_load_test import NoLoadTest diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py index a0e0c406f..be905cde1 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py @@ -7,16 +7,14 @@ __all__ = ["TransformerTankInfo"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance -from zepben.ewb.util import nlen, ngen, safe_remove, get_by_mrid -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_test.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_test.py index 0bac019b3..7909ba2b1 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_test.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_test.py @@ -9,7 +9,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/wire_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/wire_info.py index cce3ec5e2..2e4e26c5e 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/wire_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/wire_info.py @@ -10,7 +10,7 @@ from zepben.ewb.model.cim.iec61968.assetinfo.wire_insulation_kind import WireInsulationKind from zepben.ewb.model.cim.iec61968.assetinfo.wire_material_kind import WireMaterialKind from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset.py b/src/zepben/ewb/model/cim/iec61968/assets/asset.py index afb538c2c..f6b3880e9 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset.py @@ -7,16 +7,14 @@ __all__ = ["Asset"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset_organisation_role import AssetOrganisationRole diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset_container.py b/src/zepben/ewb/model/cim/iec61968/assets/asset_container.py index 72b494166..dda814866 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset_container.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset_container.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61968.assets.asset import Asset -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset_function.py b/src/zepben/ewb/model/cim/iec61968/assets/asset_function.py index 989bce7f6..a02073b7d 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset_function.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset_function.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset_info.py b/src/zepben/ewb/model/cim/iec61968/assets/asset_info.py index 49712ba99..0e9a1e69b 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset_info.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset_organisation_role.py b/src/zepben/ewb/model/cim/iec61968/assets/asset_organisation_role.py index 22d5370f1..59d882d97 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset_organisation_role.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset_organisation_role.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61968.common.organisation_role import OrganisationRole -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset_owner.py b/src/zepben/ewb/model/cim/iec61968/assets/asset_owner.py index f65cdd3df..6898e3ff7 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset_owner.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset_owner.py @@ -6,7 +6,7 @@ __all__ = ["AssetOwner"] from zepben.ewb.model.cim.iec61968.assets.asset_organisation_role import AssetOrganisationRole -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/assets/streetlight.py b/src/zepben/ewb/model/cim/iec61968/assets/streetlight.py index cd797c94f..dd4b280c1 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/streetlight.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/streetlight.py @@ -9,7 +9,7 @@ from zepben.ewb.model.cim.iec61968.assets.asset import Asset from zepben.ewb.model.cim.iec61968.infiec61968.infassets.streetlight_lamp_kind import StreetlightLampKind -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.infiec61968.infassets.pole import Pole diff --git a/src/zepben/ewb/model/cim/iec61968/assets/structure.py b/src/zepben/ewb/model/cim/iec61968/assets/structure.py index 1cc64eabd..5e68bdb8d 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/structure.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/structure.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61968.assets.asset_container import AssetContainer -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/common/agreement.py b/src/zepben/ewb/model/cim/iec61968/common/agreement.py index 01454a575..8234053c8 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/agreement.py +++ b/src/zepben/ewb/model/cim/iec61968/common/agreement.py @@ -9,7 +9,7 @@ from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.model.cim.iec61970.base.domain.date_time_interval import DateTimeInterval -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/common/document.py b/src/zepben/ewb/model/cim/iec61968/common/document.py index be8a37f93..e0a0cad5e 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/document.py +++ b/src/zepben/ewb/model/cim/iec61968/common/document.py @@ -10,7 +10,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index 3cc31a458..e6d22987d 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -13,8 +13,8 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61968/common/organisation.py b/src/zepben/ewb/model/cim/iec61968/common/organisation.py index d639ccc8b..8c9af02f4 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/organisation.py +++ b/src/zepben/ewb/model/cim/iec61968/common/organisation.py @@ -6,7 +6,7 @@ __all__ = ["Organisation"] from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/common/organisation_role.py b/src/zepben/ewb/model/cim/iec61968/common/organisation_role.py index cb6f981b4..2f81ea37f 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/organisation_role.py +++ b/src/zepben/ewb/model/cim/iec61968/common/organisation_role.py @@ -9,7 +9,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.organisation import Organisation diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index 9787f551d..ed32f5557 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -7,16 +7,15 @@ __all__ = ["Customer"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.organisation_role import OrganisationRole from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb import Alias +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer_agreement import CustomerAgreement diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py index 9d6b0a4e0..83e7a12d2 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py @@ -7,15 +7,13 @@ __all__ = ["CustomerAgreement"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.agreement import Agreement -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer import Customer diff --git a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py index be237a622..dfe79e4d2 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py @@ -7,15 +7,13 @@ __all__ = ["PricingStructure"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.document import Document -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.tariff import Tariff diff --git a/src/zepben/ewb/model/cim/iec61968/customers/tariff.py b/src/zepben/ewb/model/cim/iec61968/customers/tariff.py index 15d68f4e7..48cbcdd5f 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/tariff.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/tariff.py @@ -6,7 +6,7 @@ __all__ = ["Tariff"] from zepben.ewb.model.cim.iec61968.common.document import Document -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassetinfo/current_transformer_info.py b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassetinfo/current_transformer_info.py index 03eda1131..9487750bd 100644 --- a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassetinfo/current_transformer_info.py +++ b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassetinfo/current_transformer_info.py @@ -9,7 +9,7 @@ from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.cim.iec61968.infiec61968.infcommon.ratio import Ratio -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassetinfo/potential_transformer_info.py b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassetinfo/potential_transformer_info.py index d6cbed26e..1dc3f4939 100644 --- a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassetinfo/potential_transformer_info.py +++ b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassetinfo/potential_transformer_info.py @@ -9,7 +9,7 @@ from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.cim.iec61968.infiec61968.infcommon.ratio import Ratio -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py index a3ad34d3a..92da48fd8 100644 --- a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py +++ b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py @@ -7,15 +7,13 @@ __all__ = ["Pole"] -from typing import List, Optional, Generator, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.structure import Structure -from zepben.ewb.util import get_by_mrid, ngen, nlen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.streetlight import Streetlight diff --git a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py index bc4e3daff..c0c3158e4 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py @@ -7,16 +7,14 @@ __all__ = ["EndDevice"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_container import AssetContainer -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location diff --git a/src/zepben/ewb/model/cim/iec61968/metering/end_device_function.py b/src/zepben/ewb/model/cim/iec61968/metering/end_device_function.py index 1a23fad6d..75a768f1e 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/end_device_function.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/end_device_function.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61968.assets.asset_function import AssetFunction -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/metering/meter.py b/src/zepben/ewb/model/cim/iec61968/metering/meter.py index ad972e4b5..562e92440 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/meter.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/meter.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61968.metering.end_device import EndDevice -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index 16fba5a6e..594a5613e 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -7,17 +7,16 @@ __all__ = ["UsagePoint"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.util import nlen +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location diff --git a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py index 5b79d57ee..481c6af89 100644 --- a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py +++ b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py @@ -7,15 +7,13 @@ __all__ = ["OperationalRestriction"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.document import Document -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/auxiliary_equipment.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/auxiliary_equipment.py index beedf8a74..c013c5a49 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/auxiliary_equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/auxiliary_equipment.py @@ -9,7 +9,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/current_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/current_transformer.py index b5bee6f7d..e9e15461d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/current_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/current_transformer.py @@ -12,7 +12,7 @@ else: from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.sensor import Sensor if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/fault_indicator.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/fault_indicator.py index b7474b8ab..fa3ed533e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/fault_indicator.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/fault_indicator.py @@ -6,7 +6,7 @@ __all__ = ["FaultIndicator"] from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/potential_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/potential_transformer.py index 6392d4a29..72f1a0bb1 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/potential_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/potential_transformer.py @@ -12,7 +12,7 @@ else: from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.potential_transformer_kind import PotentialTransformerKind from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.sensor import Sensor diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py index e8370543a..8ba98f005 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py @@ -7,16 +7,14 @@ __all__ = ["Sensor"] -from typing import Generator, Optional, List, TYPE_CHECKING, Iterable +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment -from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/ac_dc_terminal.py b/src/zepben/ewb/model/cim/iec61970/base/core/ac_dc_terminal.py index 50fdae902..1918844c2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/ac_dc_terminal.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/ac_dc_terminal.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/base_voltage.py b/src/zepben/ewb/model/cim/iec61970/base/core/base_voltage.py index bad3800f0..b900bd8a3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/base_voltage.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/base_voltage.py @@ -6,7 +6,7 @@ __all__ = ["BaseVoltage"] from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py index ab802ccff..32b6422a4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py @@ -8,17 +8,17 @@ __all__ = ['ConductingEquipment'] import sys -from typing import List, Optional, Generator, TYPE_CHECKING, Union +from typing import List, Optional, TYPE_CHECKING, Union from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment -from zepben.ewb.util import get_by_mrid, require, ngen -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.util import require +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.base_voltage import BaseVoltage diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index ab037ac63..a41e2d4b6 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -7,15 +7,14 @@ __all__ = ["ConnectivityNode"] -from typing import Generator, List, TYPE_CHECKING +from typing import List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import get_by_mrid, ngen -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node_container.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node_container.py index 8837a546f..689a0e9a4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node_container.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node_container.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index be90f3852..0e1fa4039 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -6,16 +6,16 @@ __all__ = ["Curve"] from dataclasses import field -from typing import Optional, List, Generator +from typing import Optional from abc import ABCMeta from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations, zb_dataclass -from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList +from zepben.ewb import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList from zepben.ewb.model.cim.iec61970.base.core.curve_data import CurveData from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import require, ngen, nlen, safe_remove +from zepben.ewb.util import require class CurveDataList(LazyValidatedList[CurveData]): diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index fd41ad360..604f838ec 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -19,10 +19,10 @@ from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.core.substation import Substation from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.util import ngen +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb import Alias +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.metering.usage_point import UsagePoint diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py index 481f2c70e..bad482aaa 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py @@ -8,17 +8,16 @@ __all__ = ['EquipmentContainer'] from dataclasses import field -from typing import Optional, Dict, Generator, List, TYPE_CHECKING, TypeVar, Iterable, Type +from typing import Dict, Generator, TYPE_CHECKING, TypeVar, Iterable, Type from abc import ABCMeta from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection -from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap +from zepben.ewb import Alias +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import ConnectivityNodeContainer -from zepben.ewb.util import nlen, ngen, safe_remove_by_id -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.services.network.tracing.networktrace.operators.network_state_operators import NetworkStateOperators diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index ca1958378..30c9c0e3f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -8,17 +8,14 @@ __all__ = ["Feeder"] from dataclasses import field -from typing import Optional, Dict, List, Generator, TYPE_CHECKING +from typing import Optional, Dict, TYPE_CHECKING from typing_extensions import deprecated -from zepben.ewb import get_by_mrid, remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, internal -from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap -from zepben.ewb.model.cim.extensions.zbex import zbex +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, internal +from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer -from zepben.ewb.util import ngen, nlen, safe_remove_by_id -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_substation import LvSubstation diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index b5efdd541..ecbbcf664 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -12,8 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/identifiable.py b/src/zepben/ewb/model/cim/iec61970/base/core/identifiable.py index 57a6bfb07..1b5f09a51 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/identifiable.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/identifiable.py @@ -9,7 +9,7 @@ from typing import TypeVar, overload, Callable, Any from zepben.ewb import require -from zepben.ewb.dataclass_descriptors.dataclass_base import DataclassBase, zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import DataclassBase, zb_dataclass TIdentifiable = TypeVar('TIdentifiable') diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py b/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py index e6c750d04..ef9b28821 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py @@ -15,7 +15,7 @@ from zepben.ewb.model.cim.iec61970.base.core.name import Name from zepben.ewb.model.cim.iec61970.base.core.name_type import NameType from zepben.ewb.util import require, nlen, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass logger = logging.getLogger(__name__) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name.py b/src/zepben/ewb/model/cim/iec61970/base/core/name.py index d457580f6..123b7cffc 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Optional -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py index a15d9a1b0..447c1f2c4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py @@ -12,8 +12,8 @@ from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable from zepben.ewb.model.cim.iec61970.base.core.name import Name -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.descriptor_fix import BackedDescriptor +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.descriptor_fix import BackedDescriptor if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py index 50331de93..3c04f3f14 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py @@ -7,16 +7,15 @@ __all__ = ['PowerSystemResource'] -from typing import Optional, TYPE_CHECKING, List, Generator, Iterable +from typing import Optional, TYPE_CHECKING, List from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.util import nlen +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset import Asset diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index 448a12ba2..da176f013 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -12,8 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, internal +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.substation import Substation diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index 3904988b4..c57f8efa0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -7,15 +7,14 @@ __all__ = ["Substation"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, internal +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb import Alias +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py index d8c5965f2..768bec06a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py @@ -14,13 +14,13 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import AcDcTerminal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.services.network.tracing.feeder.feeder_direction import FeederDirection from zepben.ewb.services.network.tracing.phases.phase_status import PhaseStatus -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index 0767d2244..f89d525cb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -12,9 +12,9 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, Backfill -from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, Backfill +from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_style import DiagramStyle diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 25dfe983f..2edd8537d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -8,17 +8,16 @@ __all__ = ["DiagramObject"] from dataclasses import field -from typing import Optional, List, Generator, Callable, TYPE_CHECKING, Any +from typing import Optional, List, Callable, TYPE_CHECKING, Any from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint -from zepben.ewb.util import nlen, ngen, require, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram import Diagram diff --git a/src/zepben/ewb/model/cim/iec61970/base/equivalents/equivalent_branch.py b/src/zepben/ewb/model/cim/iec61970/base/equivalents/equivalent_branch.py index ca457d53a..16926c532 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/equivalents/equivalent_branch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/equivalents/equivalent_branch.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61970.base.equivalents.equivalent_equipment import EquivalentEquipment -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/equivalents/equivalent_equipment.py b/src/zepben/ewb/model/cim/iec61970/base/equivalents/equivalent_equipment.py index e4056d927..62f233d20 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/equivalents/equivalent_equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/equivalents/equivalent_equipment.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py index 1dd0e62ed..060279fe3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py +++ b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py @@ -7,17 +7,15 @@ __all__ = ["BatteryUnit"] -from typing import List, Optional, Generator, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control_mode import BatteryControlMode from zepben.ewb.model.cim.iec61970.base.generation.production.battery_state_kind import BatteryStateKind from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control import BatteryControl diff --git a/src/zepben/ewb/model/cim/iec61970/base/generation/production/photo_voltaic_unit.py b/src/zepben/ewb/model/cim/iec61970/base/generation/production/photo_voltaic_unit.py index 2d75d6e78..0a1789a03 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/generation/production/photo_voltaic_unit.py +++ b/src/zepben/ewb/model/cim/iec61970/base/generation/production/photo_voltaic_unit.py @@ -6,7 +6,7 @@ __all__ = ["PhotoVoltaicUnit"] from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/generation/production/power_electronics_unit.py b/src/zepben/ewb/model/cim/iec61970/base/generation/production/power_electronics_unit.py index 9154b4bce..7a9b7a0cf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/generation/production/power_electronics_unit.py +++ b/src/zepben/ewb/model/cim/iec61970/base/generation/production/power_electronics_unit.py @@ -9,7 +9,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection import PowerElectronicsConnection diff --git a/src/zepben/ewb/model/cim/iec61970/base/generation/production/power_electronics_wind_unit.py b/src/zepben/ewb/model/cim/iec61970/base/generation/production/power_electronics_wind_unit.py index e7b6f2dc2..682a468d1 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/generation/production/power_electronics_wind_unit.py +++ b/src/zepben/ewb/model/cim/iec61970/base/generation/production/power_electronics_wind_unit.py @@ -6,7 +6,7 @@ __all__ = ["PowerElectronicsWindUnit"] from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/meas/accumulator.py b/src/zepben/ewb/model/cim/iec61970/base/meas/accumulator.py index d42d01505..4c60f7dc7 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/meas/accumulator.py +++ b/src/zepben/ewb/model/cim/iec61970/base/meas/accumulator.py @@ -6,7 +6,7 @@ __all__ = ["Accumulator"] from zepben.ewb.model.cim.iec61970.base.meas.measurement import Measurement -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/meas/analog.py b/src/zepben/ewb/model/cim/iec61970/base/meas/analog.py index 6b0c7abcb..9e347c2ae 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/meas/analog.py +++ b/src/zepben/ewb/model/cim/iec61970/base/meas/analog.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61970.base.meas.measurement import Measurement -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/meas/control.py b/src/zepben/ewb/model/cim/iec61970/base/meas/control.py index 5898d4412..5dbc78e1f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/meas/control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/meas/control.py @@ -8,7 +8,7 @@ from typing import Optional, TYPE_CHECKING from zepben.ewb.model.cim.iec61970.base.meas.iopoint import IoPoint -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.scada.remote_control import RemoteControl diff --git a/src/zepben/ewb/model/cim/iec61970/base/meas/discrete.py b/src/zepben/ewb/model/cim/iec61970/base/meas/discrete.py index c9eafc92c..9d379be70 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/meas/discrete.py +++ b/src/zepben/ewb/model/cim/iec61970/base/meas/discrete.py @@ -6,7 +6,7 @@ __all__ = ["Discrete"] from zepben.ewb.model.cim.iec61970.base.meas.measurement import Measurement -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/meas/iopoint.py b/src/zepben/ewb/model/cim/iec61970/base/meas/iopoint.py index 920044b1c..43a7d00e7 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/meas/iopoint.py +++ b/src/zepben/ewb/model/cim/iec61970/base/meas/iopoint.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/meas/measurement.py b/src/zepben/ewb/model/cim/iec61970/base/meas/measurement.py index a012eef2e..174ee4888 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/meas/measurement.py +++ b/src/zepben/ewb/model/cim/iec61970/base/meas/measurement.py @@ -11,7 +11,7 @@ from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.domain.unit_symbol import UnitSymbol -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.scada.remote_source import RemoteSource diff --git a/src/zepben/ewb/model/cim/iec61970/base/protection/current_relay.py b/src/zepben/ewb/model/cim/iec61970/base/protection/current_relay.py index b4482b6b1..4ac41a425 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/protection/current_relay.py +++ b/src/zepben/ewb/model/cim/iec61970/base/protection/current_relay.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/scada/remote_control.py b/src/zepben/ewb/model/cim/iec61970/base/scada/remote_control.py index 1b2ccdcf2..f9192f500 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/scada/remote_control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/scada/remote_control.py @@ -8,7 +8,7 @@ from typing import Optional, TYPE_CHECKING from zepben.ewb.model.cim.iec61970.base.scada.remote_point import RemotePoint -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.meas.control import Control diff --git a/src/zepben/ewb/model/cim/iec61970/base/scada/remote_point.py b/src/zepben/ewb/model/cim/iec61970/base/scada/remote_point.py index 0c84afd1a..31c222c7b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/scada/remote_point.py +++ b/src/zepben/ewb/model/cim/iec61970/base/scada/remote_point.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/scada/remote_source.py b/src/zepben/ewb/model/cim/iec61970/base/scada/remote_source.py index fcdc4c81e..1b840b89e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/scada/remote_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/scada/remote_source.py @@ -8,7 +8,7 @@ from typing import Optional, TYPE_CHECKING from zepben.ewb.model.cim.iec61970.base.scada.remote_point import RemotePoint -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.meas.measurement import Measurement diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index b0a5eae4a..e3b871db7 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -12,8 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp from zepben.ewb.model.cim.iec61970.base.wires.conductor import Conductor diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py index beabfa53d..16ac46c33 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py @@ -12,11 +12,11 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment import AcLineSegment diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/breaker.py b/src/zepben/ewb/model/cim/iec61970/base/wires/breaker.py index f448347b7..78adfe90b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/breaker.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/breaker.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61970.base.wires.protected_switch import ProtectedSwitch -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/busbar_section.py b/src/zepben/ewb/model/cim/iec61970/base/wires/busbar_section.py index 276d0d215..578685c71 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/busbar_section.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/busbar_section.py @@ -6,7 +6,7 @@ __all__ = ["BusbarSection"] from zepben.ewb.model.cim.iec61970.base.wires.connector import Connector -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index 08192bfce..404993bce 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -10,9 +10,9 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment import AcLineSegment diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/conductor.py b/src/zepben/ewb/model/cim/iec61970/base/wires/conductor.py index 73a50dd1f..5bb75c75d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/conductor.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/conductor.py @@ -9,7 +9,7 @@ from typing import Optional, TYPE_CHECKING from abc import ABCMeta -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if sys.version_info >= (3, 13): from warnings import deprecated else: diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/connector.py b/src/zepben/ewb/model/cim/iec61970/base/wires/connector.py index 36d8ec9b8..8d6d7051e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/connector.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/connector.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index 21cb43c05..fb9912df1 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -10,9 +10,9 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment import AcLineSegment diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/disconnector.py b/src/zepben/ewb/model/cim/iec61970/base/wires/disconnector.py index 9c77e0f2e..1c70d9e67 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/disconnector.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/disconnector.py @@ -6,7 +6,7 @@ __all__ = ["Disconnector"] from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/earth_fault_compensator.py b/src/zepben/ewb/model/cim/iec61970/base/wires/earth_fault_compensator.py index f8a161f7f..67034cde2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/earth_fault_compensator.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/earth_fault_compensator.py @@ -9,7 +9,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_connection.py index ec73aaa6f..5097a9250 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_connection.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 6b99da983..0c95462fb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -13,8 +13,8 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase from zepben.ewb.model.cim.iec61970.base.wires.phase_shunt_connection_kind import PhaseShuntConnectionKind diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py index 09f8de947..cee56b5a2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py @@ -10,10 +10,10 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer import EnergyConsumer diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index 47c5befbb..abb919e4e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -13,8 +13,8 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py index 4ebbb62d6..46358a03b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py @@ -10,10 +10,10 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.energy_source import EnergySource diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/fuse.py b/src/zepben/ewb/model/cim/iec61970/base/wires/fuse.py index 3f3a591a2..7bd4c3a17 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/fuse.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/fuse.py @@ -8,7 +8,7 @@ from typing import Optional, TYPE_CHECKING from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ground.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ground.py index 17aa87ef0..e7f40eabe 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ground.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ground.py @@ -6,7 +6,7 @@ __all__ = ["Ground"] from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ground_disconnector.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ground_disconnector.py index dc08f3498..af35df889 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ground_disconnector.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ground_disconnector.py @@ -6,7 +6,7 @@ __all__ = ["GroundDisconnector"] from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/grounding_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/grounding_impedance.py index 1f4587c53..e307f683a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/grounding_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/grounding_impedance.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61970.base.wires.earth_fault_compensator import EarthFaultCompensator -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/jumper.py b/src/zepben/ewb/model/cim/iec61970/base/wires/jumper.py index fb97c8cbc..642360974 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/jumper.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/jumper.py @@ -6,7 +6,7 @@ __all__ = ["Jumper"] from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/junction.py b/src/zepben/ewb/model/cim/iec61970/base/wires/junction.py index 540911f46..63f04fe4f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/junction.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/junction.py @@ -6,7 +6,7 @@ __all__ = ["Junction"] from zepben.ewb.model.cim.iec61970.base.wires.connector import Connector -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/line.py b/src/zepben/ewb/model/cim/iec61970/base/wires/line.py index d11578f45..cf1113f2b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/line.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/line.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/linear_shunt_compensator.py b/src/zepben/ewb/model/cim/iec61970/base/wires/linear_shunt_compensator.py index 5ef1cc366..1443ab703 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/linear_shunt_compensator.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/linear_shunt_compensator.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61970.base.wires.shunt_compensator import ShuntCompensator -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/load_break_switch.py b/src/zepben/ewb/model/cim/iec61970/base/wires/load_break_switch.py index 4f7a6d760..57f2b8dce 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/load_break_switch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/load_break_switch.py @@ -6,7 +6,7 @@ __all__ = ["LoadBreakSwitch"] from zepben.ewb.model.cim.iec61970.base.wires.protected_switch import ProtectedSwitch -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_impedance.py index b1ceca26a..cb612afcd 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_impedance.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.wires.per_length_line_parameter import PerLengthLineParameter -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_line_parameter.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_line_parameter.py index 7a8821fb9..2acbdf96c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_line_parameter.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_line_parameter.py @@ -8,7 +8,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py index 7b7709f66..9bae87c2d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py @@ -12,12 +12,12 @@ from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations, zb_dataclass -from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList +from zepben.ewb import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.phase_impedance_data import PhaseImpedanceData from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind -from zepben.ewb.util import require, ngen, nlen, safe_remove, none +from zepben.ewb.util import require, none class PhaseImpedanceDataList(LazyValidatedList): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_sequence_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_sequence_impedance.py index d940ba5fd..7c23e1293 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_sequence_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_sequence_impedance.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/petersen_coil.py b/src/zepben/ewb/model/cim/iec61970/base/wires/petersen_coil.py index c0fb12c4d..b435ba397 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/petersen_coil.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/petersen_coil.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61970.base.wires.earth_fault_compensator import EarthFaultCompensator -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 006c5aaaf..0eed0a1e2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -13,8 +13,8 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq from zepben.ewb.util import require diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py index 8778ddea0..87eed05ef 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py @@ -10,10 +10,10 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection import PowerElectronicsConnection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index 24e124de9..fddaa03d4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -11,15 +11,15 @@ from dataclasses import field from typing import List, Optional, TYPE_CHECKING -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill +from zepben.ewb import Alias +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList, Backfill if sys.version_info >= (3, 13): from warnings import deprecated else: from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.model.cim.extensions.iec61970.base.wires.vector_group import VectorGroup from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_construction_kind import TransformerConstructionKind from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_function_kind import TransformerFunctionKind diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index 983217170..4786d7c3a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -9,20 +9,19 @@ import warnings from dataclasses import field -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd from zepben.ewb.model.cim.iec61970.base.wires.winding_connection import WindingConnection from zepben.ewb.model.resistance_reactance import ResistanceReactance -from zepben.ewb.util import ngen, nlen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.power_transformer import PowerTransformer diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py index f1f0c046b..65b11195c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py @@ -7,16 +7,14 @@ __all__ = ["ProtectedSwitch"] -from typing import Optional, List, Generator, TYPE_CHECKING, Iterable +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch -from zepben.ewb.util import get_by_mrid, ngen, nlen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ratio_tap_changer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ratio_tap_changer.py index e7293ecb3..20f47620f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ratio_tap_changer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ratio_tap_changer.py @@ -10,7 +10,7 @@ from typing import Optional, TYPE_CHECKING from zepben.ewb.model.cim.iec61970.base.wires.tap_changer import TapChanger -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/reactive_capability_curve.py b/src/zepben/ewb/model/cim/iec61970/base/wires/reactive_capability_curve.py index a955cbd73..871c84fc4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/reactive_capability_curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/reactive_capability_curve.py @@ -6,7 +6,7 @@ __all__ = ["ReactiveCapabilityCurve"] from zepben.ewb.model.cim.iec61970.base.core.curve import Curve -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/recloser.py b/src/zepben/ewb/model/cim/iec61970/base/wires/recloser.py index b6a16c5db..cb79229b0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/recloser.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/recloser.py @@ -6,7 +6,7 @@ __all__ = ["Recloser"] from zepben.ewb.model.cim.iec61970.base.wires.protected_switch import ProtectedSwitch -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py index c2e40b9eb..8554de6a6 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py @@ -11,7 +11,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.regulating_control import RegulatingControl diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py index 4a11127a4..76ca8cf20 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py @@ -7,7 +7,7 @@ __all__ = ["RegulatingControl"] -from typing import Optional, List, Generator, Iterable, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated @@ -15,10 +15,8 @@ from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.regulating_control_mode_kind import RegulatingControlModeKind -from zepben.ewb.util import nlen, get_by_mrid, safe_remove, ngen -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/rotating_machine.py b/src/zepben/ewb/model/cim/iec61970/base/wires/rotating_machine.py index a930c2209..91a9def59 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/rotating_machine.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/rotating_machine.py @@ -9,7 +9,7 @@ from abc import ABCMeta from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/series_compensator.py b/src/zepben/ewb/model/cim/iec61970/base/wires/series_compensator.py index 17dae934f..051610c23 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/series_compensator.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/series_compensator.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py b/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py index fe5a8d868..12e93c6c8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py @@ -15,7 +15,7 @@ else: from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.wires.phase_shunt_connection_kind import PhaseShuntConnectionKind from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/static_var_compensator.py b/src/zepben/ewb/model/cim/iec61970/base/wires/static_var_compensator.py index 35751a8af..0dfa736bb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/static_var_compensator.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/static_var_compensator.py @@ -9,7 +9,7 @@ from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq from zepben.ewb.model.cim.iec61970.base.wires.svc_control_mode import SVCControlMode -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/switch.py b/src/zepben/ewb/model/cim/iec61970/base/wires/switch.py index 9f948b7f9..e03d5ab54 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/switch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/switch.py @@ -11,7 +11,7 @@ from typing import Optional, TYPE_CHECKING from abc import ABCMeta -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if sys.version_info >= (3, 13): from warnings import deprecated else: diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py index 602212359..eeaf4a685 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py @@ -5,16 +5,14 @@ __all__ = ["SynchronousMachine"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.rotating_machine import RotatingMachine from zepben.ewb.model.cim.iec61970.base.wires.synchronous_machine_kind import SynchronousMachineKind -from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.reactive_capability_curve import ReactiveCapabilityCurve diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/tap_changer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/tap_changer.py index 619c14522..42f7de330 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/tap_changer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/tap_changer.py @@ -10,7 +10,7 @@ from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.util import require -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.tap_changer_control import TapChangerControl diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/tap_changer_control.py b/src/zepben/ewb/model/cim/iec61970/base/wires/tap_changer_control.py index c32a35081..692a06292 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/tap_changer_control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/tap_changer_control.py @@ -8,7 +8,7 @@ from typing import Optional from zepben.ewb.model.cim.iec61970.base.wires.regulating_control import RegulatingControl -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py index 0f994bfce..e0ebe95bc 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py @@ -10,7 +10,7 @@ from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import require -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.base_voltage import BaseVoltage diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_star_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_star_impedance.py index 5f097aeac..b786f1b67 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_star_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_star_impedance.py @@ -9,7 +9,7 @@ from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.resistance_reactance import ResistanceReactance -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.transformer_end_info import TransformerEndInfo diff --git a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py index c2ae3ff63..2481e8117 100644 --- a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py +++ b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py @@ -7,15 +7,13 @@ __all__ = ["Circuit"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.line import Line -from zepben.ewb.util import ngen, get_by_mrid, safe_remove, nlen -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop diff --git a/src/zepben/ewb/services/network/tracing/connectivity/connectivity_result.py b/src/zepben/ewb/services/network/tracing/connectivity/connectivity_result.py index 410b507ae..6245145f4 100644 --- a/src/zepben/ewb/services/network/tracing/connectivity/connectivity_result.py +++ b/src/zepben/ewb/services/network/tracing/connectivity/connectivity_result.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb import NominalPhasePath diff --git a/src/zepben/ewb/streaming/grpc/grpc.py b/src/zepben/ewb/streaming/grpc/grpc.py index 66c22058a..6c63c4b76 100644 --- a/src/zepben/ewb/streaming/grpc/grpc.py +++ b/src/zepben/ewb/streaming/grpc/grpc.py @@ -10,7 +10,7 @@ from typing import TypeVar, Generic, Callable, List, Union, Coroutine, Any from dataclasses import dataclass, field -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass T = TypeVar("T") diff --git a/test/busbranch/test_bus_branch.py b/test/busbranch/test_bus_branch.py index cffa0cf6c..3e84e30c9 100644 --- a/test/busbranch/test_bus_branch.py +++ b/test/busbranch/test_bus_branch.py @@ -309,15 +309,15 @@ async def test_switches_excluded_when_getting_voltage(): def _get_expected(nb_network, line, pt, es, ec, pec, eb, ec_eb1, ec_eb2): # -- Bus - exp_bb0 = (create_terminal_based_id({next(es.terminals), get_term(pt, 1)}), + exp_bb0 = (create_terminal_based_id({next(iter(es.terminals)), get_term(pt, 1)}), (20000, frozenset(), frozenset({get_term(es, 1), get_term(pt, 1)}), frozenset(), nb_network)) exp_bb1 = (create_terminal_based_id({get_term(pt, 2), get_term(line, 1)}), (400, frozenset(), frozenset({get_term(line, 1), get_term(pt, 2)}), frozenset(), nb_network)) - exp_bb2 = (create_terminal_based_id({get_term(line, 2), next(ec.terminals), next(pec.terminals), get_term(eb, 1)}), + exp_bb2 = (create_terminal_based_id({get_term(line, 2), next(iter(ec.terminals)), next(iter(pec.terminals)), get_term(eb, 1)}), (400, frozenset(), frozenset({get_term(ec, 1), get_term(line, 2), get_term(pec, 1), get_term(eb, 1)}), frozenset(), nb_network)) - exp_bb3 = (create_terminal_based_id({get_term(eb, 2), next(ec_eb1.terminals), next(ec_eb2.terminals)}), - (400, frozenset(), frozenset({get_term(eb, 2), next(ec_eb1.terminals), next(ec_eb2.terminals)}), frozenset(), nb_network)) + exp_bb3 = (create_terminal_based_id({get_term(eb, 2), next(iter(ec_eb1.terminals)), next(iter(ec_eb2.terminals))}), + (400, frozenset(), frozenset({get_term(eb, 2), next(iter(ec_eb1.terminals)), next(iter(ec_eb2.terminals))}), frozenset(), nb_network)) # -- Branch exp_branch = (f"tb_{line.mrid}", ((exp_bb1[1], exp_bb2[1]), 100, frozenset({line}), frozenset({*line.terminals}), frozenset(), nb_network)) diff --git a/test/dataclass_descriptors/test_basic_descriptor.py b/test/dataclass_descriptors/test_basic_descriptor.py index 6941d9682..f0b9b1caa 100644 --- a/test/dataclass_descriptors/test_basic_descriptor.py +++ b/test/dataclass_descriptors/test_basic_descriptor.py @@ -4,9 +4,9 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import field -from zepben.ewb.dataclass_descriptors.dataclass_base import DataclassBase +from zepben.ewb.boilerplate.dataclass_base import DataclassBase from zepben.ewb import zb_dataclass, remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.descriptor_fix import BackedDescriptor +from zepben.ewb.boilerplate.descriptor_fix import BackedDescriptor @zb_dataclass diff --git a/test/dataclass_descriptors/test_dataclass_base.py b/test/dataclass_descriptors/test_dataclass_base.py index f35c26ee5..ebc954a1b 100644 --- a/test/dataclass_descriptors/test_dataclass_base.py +++ b/test/dataclass_descriptors/test_dataclass_base.py @@ -7,7 +7,7 @@ import pytest -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass, DataclassBase +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass, DataclassBase @zb_dataclass diff --git a/test/services/common/service_comparator_validator.py b/test/services/common/service_comparator_validator.py index b0530a169..c9fc79d0a 100644 --- a/test/services/common/service_comparator_validator.py +++ b/test/services/common/service_comparator_validator.py @@ -182,7 +182,7 @@ def validate_collection( in_source, target_empty, { - _prop_name(prop): CollectionDifference(missing_from_target=[next(_get_prop(in_source, prop))]) + _prop_name(prop): CollectionDifference(missing_from_target=[next(iter(_get_prop(in_source, prop)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -191,7 +191,7 @@ def validate_collection( source_empty, in_target, { - _prop_name(prop): CollectionDifference(missing_from_source=[next(_get_prop(in_target, prop))]) + _prop_name(prop): CollectionDifference(missing_from_source=[next(iter(_get_prop(in_target, prop)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -201,8 +201,8 @@ def validate_collection( in_target_difference, { _prop_name(prop): CollectionDifference( - missing_from_source=[next(_get_prop(in_target_difference, prop))], - missing_from_target=[next(_get_prop(in_source, prop))], + missing_from_source=[next(iter(_get_prop(in_target_difference, prop)))], + missing_from_target=[next(iter(_get_prop(in_source, prop)))], ) }, ) @@ -235,7 +235,7 @@ def validate_name_collection( in_source, target_empty, { - _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_target=[next(_get_prop(in_source, IdentifiedObject.names))]) + _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_target=[next(iter(_get_prop(in_source, IdentifiedObject.names)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -244,7 +244,7 @@ def validate_name_collection( source_empty, in_target, { - _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_source=[next(_get_prop(in_target, IdentifiedObject.names))]) + _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_source=[next(iter(_get_prop(in_target, IdentifiedObject.names)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -254,8 +254,8 @@ def validate_name_collection( in_target_difference, { _prop_name(IdentifiedObject.names): CollectionDifference( - missing_from_source=[next(_get_prop(in_target_difference, IdentifiedObject.names))], - missing_from_target=[next(_get_prop(in_source, IdentifiedObject.names))], + missing_from_source=[next(iter(_get_prop(in_target_difference, IdentifiedObject.names)))], + missing_from_target=[next(iter(_get_prop(in_source, IdentifiedObject.names)))], ) }, ) @@ -286,7 +286,7 @@ def validate_indexed_collection( self.validate_compare(in_source, in_target, options=options, options_stop_compare=options_stop_compare) def get_item(obj) -> Optional[R]: - return next(_get_prop(obj, prop), None) + return next(iter(_get_prop(obj, prop)), None) diff = ObjectDifference( in_source, diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index 95ad5b51e..8c943ad74 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -8,7 +8,7 @@ from services.network.test_data.cuts_and_clamps_network import CutsAndClampsNetwork from zepben.ewb import NetworkStateOperators, TestNetworkBuilder, NetworkTraceStep, Terminal, NominalPhasePath, Breaker, AcLineSegment, Clamp, Cut -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.services.network.network_service import NetworkService diff --git a/test/services/network/tracing/test_assign_to_lv_feeders.py b/test/services/network/tracing/test_assign_to_lv_feeders.py index 94bcd234d..9ee758731 100644 --- a/test/services/network/tracing/test_assign_to_lv_feeders.py +++ b/test/services/network/tracing/test_assign_to_lv_feeders.py @@ -391,7 +391,7 @@ async def run_with_operators(operators: Type[NetworkStateOperators]): operators.associate_energizing_feeder(back_feed, lv_feeder) await Tracing.assign_equipment_to_lv_feeders().run( - next(b7.terminals), + next(iter(b7.terminals)), network.lv_feeder_start_points, terminal_to_aux_equipment = dict(), lv_feeders_to_assign=[lv_feeder], From 3f1e39ad2876866690bcf4375a7163aca80115d2 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 27 Jul 2026 15:19:34 +1000 Subject: [PATCH 08/54] Move backfill; Fix base to use zb dataclass; Fix iterable wrapper to not rely on abstractbackedlist Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/backfill.py | 42 ++++++++ ...base.py => abstract_backed_collections.py} | 2 +- .../ewb/boilerplate/collections/lazy_list.py | 37 +------ .../ewb/boilerplate/collections/mrid_list.py | 101 +++--------------- .../ewb/boilerplate/collections/mrid_map.py | 53 +-------- .../ewb/boilerplate/collections/wrapper.py | 4 +- src/zepben/ewb/boilerplate/dataclass_base.py | 101 +++++++++--------- src/zepben/ewb/boilerplate/descriptor_fix.py | 3 - .../base/core/conducting_equipment.py | 3 +- .../model/cim/iec61970/base/core/feeder.py | 3 +- .../iec61970/base/core/geographical_region.py | 3 +- .../base/core/sub_geographical_region.py | 3 +- .../cim/iec61970/base/core/substation.py | 3 +- .../model/cim/iec61970/base/core/terminal.py | 2 +- .../iec61970/base/diagramlayout/diagram.py | 3 +- .../base/diagramlayout/diagram_object.py | 2 +- .../iec61970/base/wires/ac_line_segment.py | 3 +- .../base/wires/ac_line_segment_phase.py | 2 +- .../model/cim/iec61970/base/wires/clamp.py | 2 +- .../ewb/model/cim/iec61970/base/wires/cut.py | 2 +- .../iec61970/base/wires/energy_consumer.py | 3 +- .../base/wires/energy_consumer_phase.py | 2 +- .../cim/iec61970/base/wires/energy_source.py | 3 +- .../base/wires/energy_source_phase.py | 2 +- .../wires/power_electronics_connection.py | 3 +- .../power_electronics_connection_phase.py | 2 +- .../iec61970/base/wires/power_transformer.py | 3 +- .../base/wires/power_transformer_end.py | 2 +- 28 files changed, 143 insertions(+), 251 deletions(-) rename src/zepben/ewb/boilerplate/collections/{base.py => abstract_backed_collections.py} (94%) diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py index 35e3122c5..ec5599d1a 100644 --- a/src/zepben/ewb/boilerplate/backfill.py +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -2,3 +2,45 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from dataclasses import Field +from types import MemberDescriptorType +from typing import Any, Callable, TypeVar, Protocol + +from zepben.ewb import BackedDescriptor + + +class HasMrid(Protocol): + mrid: str + + +S = TypeVar("S", bound=HasMrid) + + +class Backfill: + def __init__(self, backfill_prop: property): + self.backfill_prop = backfill_prop + + def apply(self, element: S, owner: Any): + name = self.backfill_prop.fget.__name__ + + if hasattr(self.backfill_prop, "__target"): + backing_name = self.backfill_prop.__target.__name__ + else: + backing_name = name + + if getattr(element, backing_name) is None: + setattr(element, backing_name, owner) + + ref = getattr(element, backing_name) + if ref is not owner: + raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") + + +def internal(target: Any): + if not any(isinstance(target, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): + raise TypeError(f"target parameter of the target decorator has to be an instance of dataclass Field, instead is {target}") + + def dec(func: Callable): + setattr(func, "__target", target) + return func + return dec diff --git a/src/zepben/ewb/boilerplate/collections/base.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py similarity index 94% rename from src/zepben/ewb/boilerplate/collections/base.py rename to src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py index fcd9c9524..f8c1c8862 100644 --- a/src/zepben/ewb/boilerplate/collections/base.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py @@ -3,7 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import ABC, abstractmethod -from typing import Collection, Protocol, Iterable, Generic, Iterator, Callable, Any, Sequence, overload, TypeVar +from typing import Collection, Iterable, Generic, Iterator, Callable, Any, Sequence, overload, TypeVar T = TypeVar("T") diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index b7a28dcb5..ed5df35ec 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -2,22 +2,13 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from dataclasses import field -from typing import TypeVar, Protocol +from typing import TypeVar -from zepben.ewb import DataclassBase, zb_dataclass -from zepben.ewb.boilerplate.collections.base import AbstractBackedList +from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -# from descriptor_fix import BackedDescriptor, remove_descriptor_annotations - - -class HasMrid(Protocol): - mrid: str - T = TypeVar("T") -S = TypeVar("S", bound=HasMrid) class LazyValidatedList(_IterableWrapper, AbstractBackedList[T]): @@ -36,8 +27,6 @@ def _get(self): def _get_collection(self) -> list[T]: return getattr(self.instance, self.backing_name) or [] - # clearIfEmpty is inlined for performance - def append(self, item): if self.validate is not None: self.validate(self.instance, item) @@ -140,25 +129,3 @@ def pop(self, index: int = -1) -> T: self.clear() return item - -if __name__ == '__main__': - @zb_dataclass - class C(DataclassBase): - _xs: list[int] | None = field(default=None) - xs: LazyIndexedList[int] = LazyIndexedList( - _xs, - "Thing" - ) - - c = C(xs=[24, 42]) - print(c._xs, c.xs) - c.xs.clear() - print(c._xs, c.xs) - c.xs.append(1) - c.xs.append(2) - c.xs.append(3) - print(c._xs, c.xs) - c.xs.insert(2, 42) - print(c._xs, c.xs) - c.xs.pop(2) - print(c._xs, c.xs) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 29b2f691e..d5f6f5040 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -3,25 +3,23 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import abstractmethod, ABC -from dataclasses import dataclass, field, Field -from types import MemberDescriptorType -from typing import Any, TypeVar, Protocol, Callable, Sequence +from typing import Any, TypeVar, Protocol, Sequence -import pytest from typing_extensions import Self -from zepben.ewb import BackedDescriptor +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedCollection, AbstractBackedList from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -from zepben.ewb.boilerplate.collections.base import AbstractBackedCollection, AbstractBackedList class HasMrid(Protocol): mrid: str -T = TypeVar("T") + S = TypeVar("S", bound=HasMrid) + class MridCollection(AbstractBackedCollection[S], ABC): instance: Any element_description: str @@ -30,6 +28,11 @@ class MridCollection(AbstractBackedCollection[S], ABC): def _safe_get_by_mrid(self, mrid: str) -> S | None: ... def get_by_mrid(self, mrid: str) -> S: + """ + Get an element matching given ``mrid`` + + raises KeyError if one is not present + """ res = self._safe_get_by_mrid(mrid) if res is None: raise KeyError @@ -50,34 +53,6 @@ def _can_add_by_mrid(self, element: S) -> bool: return False -class Backfill: - def __init__(self, backfill_prop: property): - self.backfill_prop = backfill_prop - - def apply(self, element: S, owner: Any): - name = self.backfill_prop.fget.__name__ - - if hasattr(self.backfill_prop, "__target"): - backing_name = self.backfill_prop.__target.__name__ - else: - backing_name = name - - if getattr(element, backing_name) is None: - setattr(element, backing_name, owner) - - ref = getattr(element, backing_name) - if ref is not owner: - raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") - -def internal(target: Any): - if not any(isinstance(target, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): - raise TypeError(f"target parameter of the target decorator has to be an instance of dataclass Field, instead is {target}") - - def dec(func: Callable): - setattr(func, "__target", target) - return func - return dec - class LazyMridList(LazyValidatedList, MridCollection[S]): def __init__(self, private_field, @@ -96,7 +71,7 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: found = next((element for element in existing if element.mrid == mrid), None) return found - def append(self, item: T): + def append(self, item: S): if not self._can_add_by_mrid(item): return @@ -128,7 +103,7 @@ def __get__(self, instance, owner=None) -> Self: def __post_init__(self): self.backing_list = getattr(self.instance, self.backing_name) - def _get_collection(self) -> Sequence[T]: + def _get_collection(self) -> Sequence[S]: return self.backing_list def _safe_get_by_mrid(self, mrid: str) -> S | None: @@ -170,55 +145,3 @@ def __str__(self): def __getitem__(self, item): return self.backing_list[item] - - - - - - -if __name__ == '__main__': - @dataclass(slots=True) - class Ido: - mrid: str - n: int = 42 - - @dataclass(slots=True) - class A: - # _x: list[Ido] = field(default_factory=list) - _x: list[Ido] = field(default=None) - # x: MridCollection[Ido] = MridList( - x: MridCollection[Ido] = LazyMridList( - _x, - "A Thingo", - validate=(lambda self, it: self.validate_x(it)), - sort_by=(lambda it: it.n), - ) - - def validate_x(self, item): - print(f"validating {item}") - - def __hash__(self): - return object.__hash__(self) - - o1 = Ido("1") - o2 = Ido("2", 24) - oX = Ido("2", 32) - - a = A() - print(a, a.x) - a.x.append(o1) - print(a, a.x) - a.x.append(o2) - print(a, a.x) - for t in a.x: - print(t) - a.x.remove(o1) - print(a, a.x) - a.x.remove(o2) - print(a, a.x) - - a.x.append(o2) - print(a, a.x) - with pytest.raises(ValueError): - a.x.append(oX) - print(a, a.x) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py index ac5af32aa..f4fadd1d9 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -2,13 +2,11 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from dataclasses import dataclass, field from typing import Protocol, TypeVar, Collection -import pytest - +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, Backfill class HasMrid(Protocol): @@ -103,50 +101,3 @@ def __str__(self): def __getitem__(self, item): return (self._get_or_empty())[item] - - -if __name__ == '__main__': - @dataclass(slots=True) - class Ido: - mrid: str - n: int = 42 - - @dataclass(slots=True) - class A: - _x: list[Ido] | None = field(default=None) - x: MridCollection[Ido] = LazyMridMap( - _x, - "A Thingo", - validate=(lambda self, it: self.validate_x(it)), - # cached=False - ) - - def validate_x(self, item): - print(f"{self} validating {item}") - - - def __hash__(self): - return object.__hash__(self) - - o1 = Ido("1") - o2 = Ido("2", 24) - oX = Ido("2", 32) - - a = A() - print(a, a.x) - a.x.append(o1) - print(a, a.x) - a.x.append(o2) - print(a, a.x) - for t in a.x: - print(t) - a.x.remove(o1) - print(a, a.x) - a.x.remove(o2) - print(a, a.x) - - a.x.append(o2) - print(a, a.x) - with pytest.raises(ValueError): - a.x.append(oX) - print(a, a.x) diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py index 9913f8df8..786141d14 100644 --- a/src/zepben/ewb/boilerplate/collections/wrapper.py +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -7,7 +7,7 @@ from typing_extensions import Self, deprecated from zepben.ewb import BackedDescriptor, resolve_default -from zepben.ewb.boilerplate.collections.base import AbstractBackedList +from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList, AbstractBackedCollection class _Wrapper(BackedDescriptor): @@ -63,7 +63,7 @@ def __set_name__(self, owner, name): def fget(self, instance): ... -class _IterableWrapper(_WrapperFgetFix, AbstractBackedList, ABC): +class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection, ABC): """ This class allows us to assign lists at init time to avoid special-case handling. """ diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index ceed79f33..7f49170ef 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -13,56 +13,6 @@ def _is_set(obj: object, name: str) -> bool: return False return True -@dataclass(slots=True) -class DataclassBase: - """ - Instantiate the default fields and interpret the kwargs like ``@dataclass`` does - - This class serves as a base class for mostly init-less dataclasses used in CIM, - allowing custom inits to not break the entire inheritance tree. - It fills fields with default values, and treats kwargs the same way @dataclass does. - - For more motivation, refer to `MANIFESTO.md` - """ - def __init__(self, **kwargs) -> None: - - # Manually assign defaults in dataclass fields - for f in fields(type(self)): - # We cannot just check kwargs because fields could be set in subclass __init__'s - if _is_set(self, f.name) or f.name in kwargs: - continue - - resolve_default(self, f) - - # Assign all of the kwargs manually. - # str-based setattr triggers descriptors, allowing us to intercept __set__. - for attr, value in kwargs.items(): - setattr(self, attr, value) - - - # Currently post init implementation requires a lot of extra logic, - # which would mess with readability. If you require it - add it. - if callable(getattr(self, "__post_init__", None)): - raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") - -def resolve_default(instance, field: Field): - # Set field defaults - if field.default is not MISSING: - setattr(instance, field.name, field.default) - elif field.default_factory is not MISSING: - setattr(instance, field.name, field.default_factory()) - - # Ignore custom descriptors - elif hasattr(field, '__get__'): - return - - # Mimic default Python missing arg error - else: - raise TypeError( - f"Missing required field {field.name!r} " - f"for {type(instance).__name__}" - ) - T = TypeVar("T", bound=type) def remove_descriptor_annotations(cls: T) -> T: @@ -143,3 +93,54 @@ def zb_dataclass(cls: type[object]): """ cls = remove_descriptor_annotations(cls) return dataclass(init=False, eq=False, slots=True, repr=False)(cls) + + +@zb_dataclass +class DataclassBase: + """ + Instantiate the default fields and interpret the kwargs like ``@dataclass`` does + + This class serves as a base class for mostly init-less dataclasses used in CIM, + allowing custom inits to not break the entire inheritance tree. + It fills fields with default values, and treats kwargs the same way @dataclass does. + + For more motivation, refer to `MANIFESTO.md` + """ + def __init__(self, **kwargs) -> None: + + # Manually assign defaults in dataclass fields + for f in fields(type(self)): + # We cannot just check kwargs because fields could be set in subclass __init__'s + if _is_set(self, f.name) or f.name in kwargs: + continue + + resolve_default(self, f) + + # Assign all of the kwargs manually. + # str-based setattr triggers descriptors, allowing us to intercept __set__. + for attr, value in kwargs.items(): + setattr(self, attr, value) + + + # Currently post init implementation requires a lot of extra logic, + # which would mess with readability. If you require it - add it. + if callable(getattr(self, "__post_init__", None)): + raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") + +def resolve_default(instance, field: Field): + # Set field defaults + if field.default is not MISSING: + setattr(instance, field.name, field.default) + elif field.default_factory is not MISSING: + setattr(instance, field.name, field.default_factory()) + + # Ignore custom descriptors + elif hasattr(field, '__get__'): + return + + # Mimic default Python missing arg error + else: + raise TypeError( + f"Missing required field {field.name!r} " + f"for {type(instance).__name__}" + ) diff --git a/src/zepben/ewb/boilerplate/descriptor_fix.py b/src/zepben/ewb/boilerplate/descriptor_fix.py index 6b4afbb91..ae8c2cc3d 100644 --- a/src/zepben/ewb/boilerplate/descriptor_fix.py +++ b/src/zepben/ewb/boilerplate/descriptor_fix.py @@ -10,9 +10,6 @@ from __future__ import annotations from dataclasses import Field -from typing import TypeVar - - class BackedDescriptor: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py index 32b6422a4..8ed0a6b7c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py @@ -14,7 +14,8 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.util import require diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 30c9c0e3f..01ce4ed0b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -12,7 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, internal +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index ecbbcf664..7400e2db9 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index da176f013..ed7a7b3ff 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill, internal +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.substation import Substation diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index c57f8efa0..1d63b2e95 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill, internal +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py index 768bec06a..ea1d8392a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py @@ -14,7 +14,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import AcDcTerminal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index f89d525cb..cf0a02a99 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 2edd8537d..4ea2e9683 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -14,7 +14,7 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index e3b871db7..6fa190c15 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp from zepben.ewb.model.cim.iec61970.base.wires.conductor import Conductor diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py index 16ac46c33..61fb22ee1 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py @@ -12,7 +12,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index 404993bce..6c07993e3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index fb9912df1..7836d745e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 0c95462fb..45bd5933c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase from zepben.ewb.model.cim.iec61970.base.wires.phase_shunt_connection_kind import PhaseShuntConnectionKind diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py index cee56b5a2..fa1748731 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index abb919e4e..77bcbd1f5 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py index 46358a03b..149166903 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 0eed0a1e2..9adcfb80e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq from zepben.ewb.util import require diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py index 87eed05ef..6bdd66733 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index fddaa03d4..3a1ff4b1d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -12,7 +12,8 @@ from typing import List, Optional, TYPE_CHECKING from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill if sys.version_info >= (3, 13): from warnings import deprecated diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index 4786d7c3a..c64d2fb03 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -15,7 +15,7 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd From 464abc48744a04934d459f73dd483be74b5596dc Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 27 Jul 2026 16:24:40 +1000 Subject: [PATCH 09/54] More cleanup Signed-off-by: Boris Filin --- src/zepben/ewb/__init__.py | 2 +- ...descriptor_fix.py => backed_descriptor.py} | 5 +- src/zepben/ewb/boilerplate/backfill.py | 35 +++++----- .../ewb/boilerplate/collections/lazy_list.py | 23 ++----- .../collections/mrid_collection.py | 48 ++++++++++++++ .../ewb/boilerplate/collections/mrid_list.py | 66 +++---------------- .../ewb/boilerplate/collections/mrid_map.py | 34 ++-------- src/zepben/ewb/boilerplate/dataclass_base.py | 49 +++++++------- .../extensions/iec61970/base/feeder/loop.py | 3 +- .../iec61970/base/feeder/lv_feeder.py | 2 +- .../iec61970/base/feeder/lv_substation.py | 2 +- .../protection/protection_relay_function.py | 3 +- .../protection/protection_relay_scheme.py | 3 +- .../protection/protection_relay_system.py | 3 +- .../assetinfo/power_transformer_info.py | 3 +- .../assetinfo/transformer_tank_info.py | 3 +- .../ewb/model/cim/iec61968/assets/asset.py | 3 +- .../model/cim/iec61968/customers/customer.py | 3 +- .../iec61968/customers/customer_agreement.py | 3 +- .../iec61968/customers/pricing_structure.py | 3 +- .../iec61968/infiec61968/infassets/pole.py | 3 +- .../model/cim/iec61968/metering/end_device.py | 3 +- .../cim/iec61968/metering/usage_point.py | 3 +- .../operations/operational_restriction.py | 3 +- .../base/auxiliaryequipment/sensor.py | 3 +- .../iec61970/base/core/connectivity_node.py | 3 +- .../model/cim/iec61970/base/core/equipment.py | 3 +- .../iec61970/base/core/equipment_container.py | 2 +- .../model/cim/iec61970/base/core/feeder.py | 2 +- .../iec61970/base/core/geographical_region.py | 3 +- .../model/cim/iec61970/base/core/name_type.py | 2 +- .../base/core/power_system_resource.py | 3 +- .../base/core/sub_geographical_region.py | 3 +- .../cim/iec61970/base/core/substation.py | 3 +- .../iec61970/base/diagramlayout/diagram.py | 2 +- .../iec61970/base/wires/ac_line_segment.py | 3 +- .../iec61970/base/wires/energy_consumer.py | 3 +- .../cim/iec61970/base/wires/energy_source.py | 3 +- .../wires/power_electronics_connection.py | 3 +- .../iec61970/base/wires/protected_switch.py | 3 +- .../iec61970/base/wires/regulating_control.py | 3 +- .../base/wires/synchronous_machine.py | 3 +- .../iec61970/infiec61970/feeder/circuit.py | 3 +- .../test_basic_descriptor.py | 2 +- 44 files changed, 178 insertions(+), 185 deletions(-) rename src/zepben/ewb/boilerplate/{descriptor_fix.py => backed_descriptor.py} (91%) create mode 100644 src/zepben/ewb/boilerplate/collections/mrid_collection.py diff --git a/src/zepben/ewb/__init__.py b/src/zepben/ewb/__init__.py index c3ea06945..114dcc8e3 100644 --- a/src/zepben/ewb/__init__.py +++ b/src/zepben/ewb/__init__.py @@ -14,7 +14,7 @@ from zepben.ewb.auth.common.auth_exception import * from zepben.ewb.auth.common.auth_method import * from zepben.ewb.boilerplate.dataclass_base import * -from zepben.ewb.boilerplate.descriptor_fix import * +from zepben.ewb.boilerplate.backed_descriptor import * from zepben.ewb.util import * ############# diff --git a/src/zepben/ewb/boilerplate/descriptor_fix.py b/src/zepben/ewb/boilerplate/backed_descriptor.py similarity index 91% rename from src/zepben/ewb/boilerplate/descriptor_fix.py rename to src/zepben/ewb/boilerplate/backed_descriptor.py index ae8c2cc3d..4aab5fd79 100644 --- a/src/zepben/ewb/boilerplate/descriptor_fix.py +++ b/src/zepben/ewb/boilerplate/backed_descriptor.py @@ -34,12 +34,11 @@ def __set_name__(self, owner, name): self.backing_name = self.private_field.name def __get__(self, instance, _): + if instance is None: + return self return getattr(instance, self.backing_name) def __set__(self, instance, value): return setattr(instance, self.backing_name, value) Alias = BackedDescriptor - -# Alias for dataclass with params. Makes it easier to edit params for all of CIM at once. -# zb_dataclass = dataclass(init=False, eq=False, slots=True, repr=False) diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py index ec5599d1a..7b063ea9b 100644 --- a/src/zepben/ewb/boilerplate/backfill.py +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -4,29 +4,31 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import Field from types import MemberDescriptorType -from typing import Any, Callable, TypeVar, Protocol +from typing import Any, Callable, TypeVar from zepben.ewb import BackedDescriptor +from zepben.ewb.boilerplate.collections.mrid_collection import S -class HasMrid(Protocol): - mrid: str - - -S = TypeVar("S", bound=HasMrid) +F = TypeVar("F", bound=Callable[..., Any]) class Backfill: - def __init__(self, backfill_prop: property): + def __init__(self, backfill_prop: property) -> None: self.backfill_prop = backfill_prop - def apply(self, element: S, owner: Any): + def apply(self, element: S, owner: Any) -> None: name = self.backfill_prop.fget.__name__ - if hasattr(self.backfill_prop, "__target"): - backing_name = self.backfill_prop.__target.__name__ - else: - backing_name = name + target = getattr(self.backfill_prop.fget, "_internal_target", None) + backing_name = name if target is None else ( + getattr(target, "name", None) + or getattr(target, "__name__", None) + or getattr(target.fget, "__name__", None) + ) + + if backing_name is None: + raise TypeError(f"Cannot determine backing name for {target!r}") if getattr(element, backing_name) is None: setattr(element, backing_name, owner) @@ -36,11 +38,14 @@ def apply(self, element: S, owner: Any): raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") -def internal(target: Any): +def internal(target: Any) -> Callable[[F], F]: if not any(isinstance(target, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): - raise TypeError(f"target parameter of the target decorator has to be an instance of dataclass Field, instead is {target}") + raise TypeError(f"target parameter of the target decorator has to be an instance of property or dataclass Field, instead is {target}") + + if isinstance(target, property) and target.fget is None: + raise TypeError(f"Cannot backfill a property without a getter: {target}") def dec(func: Callable): - setattr(func, "__target", target) + setattr(func, "_internal_target", target) return func return dec diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index ed5df35ec..15744b1ef 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -41,15 +41,6 @@ def append(self, item): if self.sort_by is not None: existing.sort(key=self.sort_by) - def __len__(self): - return len(self._get_collection()) - - def __iter__(self): - return iter(self._get_collection()) - - def __contains__(self, item): - return item in (self._get_collection()) - def remove(self, item): existing = self._get_collection() existing.remove(item) @@ -64,21 +55,16 @@ def __repr__(self): return self.__class__.__repr__() return str(self._get_collection()) - def __str__(self): - return self.__repr__() - - def __getitem__(self, item): - return (self._get_collection())[item] - class LazyIndexedList(LazyValidatedList[T]): def __init__( self, private_field, - element_description: str + element_description: str, + validate = None, ): - super().__init__(private_field) + super().__init__(private_field, validate=validate) self.element_description = element_description def insert(self, index: int, item: T) -> None: @@ -97,6 +83,9 @@ def insert(self, index: int, item: T) -> None: "no gaps in the numbering." ) + if self.validate is not None: + self.validate(self.instance, item) + existing = getattr(self.instance, self.backing_name) if existing is None: diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py new file mode 100644 index 000000000..f08f7b0df --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -0,0 +1,48 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC, abstractmethod +from typing import Any, Protocol, TypeVar + +from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedCollection + + +class HasMrid(Protocol): + mrid: str + + +S = TypeVar("S", bound=HasMrid) + + +class MridCollection(AbstractBackedCollection[S], ABC): + instance: Any + element_description: str + + @abstractmethod + def _safe_get_by_mrid(self, mrid: str) -> S | None: ... + + def get_by_mrid(self, mrid: str) -> S: + """ + Get an element matching given ``mrid`` + + raises KeyError if one is not present + """ + res = self._safe_get_by_mrid(mrid) + if res is None: + raise KeyError(mrid) + return res + + def _can_add_by_mrid(self, element: S) -> bool: + existing = self._safe_get_by_mrid(element.mrid) + + if existing is None: + return True + + if existing is not element: + raise ValueError( + f"{self.element_description} with mRID {element.mrid} " + f"already exists in {self.instance}." + ) + + return False diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index d5f6f5040..8409d2aac 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -2,57 +2,17 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from abc import abstractmethod, ABC -from typing import Any, TypeVar, Protocol, Sequence +from typing import TypeVar, Protocol, Sequence from typing_extensions import Self from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedCollection, AbstractBackedList +from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class HasMrid(Protocol): - mrid: str - - -S = TypeVar("S", bound=HasMrid) - - -class MridCollection(AbstractBackedCollection[S], ABC): - instance: Any - element_description: str - - @abstractmethod - def _safe_get_by_mrid(self, mrid: str) -> S | None: ... - - def get_by_mrid(self, mrid: str) -> S: - """ - Get an element matching given ``mrid`` - - raises KeyError if one is not present - """ - res = self._safe_get_by_mrid(mrid) - if res is None: - raise KeyError - return res - - def _can_add_by_mrid(self, element: S) -> bool: - existing = self._safe_get_by_mrid(element.mrid) - - if existing is None: - return True - - if existing is not element: - raise ValueError( - f"{self.element_description} with mRID {element.mrid} " - f"already exists in {self.instance}." - ) - - return False - - class LazyMridList(LazyValidatedList, MridCollection[S]): def __init__(self, private_field, @@ -85,10 +45,12 @@ class MridList(_IterableWrapper, AbstractBackedList[S], MridCollection[S]): def __init__(self, private_field, element_description: str, + backfill: Backfill = None, validate=None, sort_by=None): super().__init__(private_field) self.element_description = element_description + self.backfill = backfill self.validate = validate self.sort_by = sort_by self.backing_list = None @@ -114,6 +76,9 @@ def append(self, item: S): if not self._can_add_by_mrid(item): return + if self.backfill is not None: + self.backfill.apply(item, self.instance) + if self.validate is not None: self.validate(self.instance, item) @@ -122,15 +87,6 @@ def append(self, item: S): if self.sort_by is not None: self.backing_list.sort(key=self.sort_by) - def __len__(self): - return len(self.backing_list) - - def __iter__(self): - return iter(self.backing_list) - - def __contains__(self, item): - return item in self.backing_list - def remove(self, item): self.backing_list.remove(item) @@ -139,9 +95,3 @@ def clear(self): def __repr__(self): return str(self.backing_list) - - def __str__(self): - return self.__repr__() - - def __getitem__(self, item): - return self.backing_list[item] diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py index f4fadd1d9..26752d683 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -5,21 +5,14 @@ from typing import Protocol, TypeVar, Collection from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class HasMrid(Protocol): - mrid: str - T = TypeVar("T") -S = TypeVar("S", bound=HasMrid) class LazyMridMap(_IterableWrapper[S], MridCollection[S]): - def _get_collection(self) -> Collection[T]: - pass - def __init__( self, private_field, @@ -37,26 +30,15 @@ def _get(self): def _get_or_empty(self) -> dict[str, S]: return getattr(self.instance, self.backing_name) or {} + def _get_collection(self) -> list[S]: + return list(self._get_or_empty().values()) + def _safe_get_by_mrid(self, mrid: str) -> S | None: return self._get_or_empty().get(mrid, None) def get_by_mrid(self, mrid: str) -> S: return self._get_or_empty()[mrid] - def _can_add_by_mrid(self, element: S) -> bool: - existing = self._safe_get_by_mrid(element.mrid) - - if existing is None: - return True - - if existing is not element: - raise ValueError( - f"{self.element_description} with mRID {element.mrid} " - f"already exists in {self.instance}." - ) - - return False - def append(self, element: S): if not self._can_add_by_mrid(element): return @@ -77,11 +59,8 @@ def append(self, element: S): def __len__(self): return len(self._get_or_empty()) - def __iter__(self): - return iter(self._get_or_empty().values()) - def __contains__(self, item: S): - return self._get_or_empty().get(item.mrid, None) == item + return self._get_or_empty().get(getattr(item, "mrid", None), None) == item def remove(self, item): existing = self._get_or_empty() @@ -96,8 +75,5 @@ def clear(self): def __repr__(self): return str(self._get_or_empty()) - def __str__(self): - return self.__repr__() - def __getitem__(self, item): return (self._get_or_empty())[item] diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index 7f49170ef..020a5dc10 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -3,7 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import dataclass, fields, MISSING, Field -from typing import TypeVar +from typing import TypeVar, dataclass_transform def _is_set(obj: object, name: str) -> bool: @@ -85,7 +85,6 @@ class MyClass: return cls - def zb_dataclass(cls: type[object]): """ Shorthand alias for ``@dataclass(init=False,eq=False,slots=True,repr=False)`` @@ -94,6 +93,24 @@ def zb_dataclass(cls: type[object]): cls = remove_descriptor_annotations(cls) return dataclass(init=False, eq=False, slots=True, repr=False)(cls) +def resolve_default(instance, field: Field): + # Set field defaults + if field.default is not MISSING: + setattr(instance, field.name, field.default) + elif field.default_factory is not MISSING: + setattr(instance, field.name, field.default_factory()) + + # Ignore custom descriptors + elif hasattr(field, '__get__') and not isinstance(field, Field): + return + + # Mimic default Python missing arg error + else: + raise TypeError( + f"Missing required field {field.name!r} " + f"for {type(instance).__name__}" + ) + @zb_dataclass class DataclassBase: @@ -107,6 +124,10 @@ class DataclassBase: For more motivation, refer to `MANIFESTO.md` """ def __init__(self, **kwargs) -> None: + # Currently post init implementation requires a lot of extra logic, + # which would mess with readability. If you require it - add it. + if callable(getattr(self, "__post_init__", None)): + raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") # Manually assign defaults in dataclass fields for f in fields(type(self)): @@ -120,27 +141,3 @@ def __init__(self, **kwargs) -> None: # str-based setattr triggers descriptors, allowing us to intercept __set__. for attr, value in kwargs.items(): setattr(self, attr, value) - - - # Currently post init implementation requires a lot of extra logic, - # which would mess with readability. If you require it - add it. - if callable(getattr(self, "__post_init__", None)): - raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") - -def resolve_default(instance, field: Field): - # Set field defaults - if field.default is not MISSING: - setattr(instance, field.name, field.default) - elif field.default_factory is not MISSING: - setattr(instance, field.name, field.default_factory()) - - # Ignore custom descriptors - elif hasattr(field, '__get__'): - return - - # Mimic default Python missing arg error - else: - raise TypeError( - f"Missing required field {field.name!r} " - f"for {type(instance).__name__}" - ) diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py index d9356a515..2659d0d43 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.substation import Substation diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py index 679214809..4ac09a559 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index f4502f637..8d7ef7724 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -12,7 +12,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 7bce7fcd8..e099d658d 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -23,7 +23,8 @@ from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61968.assetinfo.relay_info import RelayInfo diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py index d6a4737d6..e99991643 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_system import ProtectionRelaySystem diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py index c3a48512f..c62658d45 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py @@ -15,7 +15,8 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_scheme import ProtectionRelayScheme diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py index b894424cf..3dfafdd20 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.transformer_tank_info import TransformerTankInfo diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py index be905cde1..a5653106e 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset.py b/src/zepben/ewb/model/cim/iec61968/assets/asset.py index f6b3880e9..b28e0a2eb 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset_organisation_role import AssetOrganisationRole diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index ed32f5557..1e66805ac 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -15,7 +15,8 @@ from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer_agreement import CustomerAgreement diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py index 83e7a12d2..1d2ae9700 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py @@ -13,7 +13,8 @@ from zepben.ewb.model.cim.iec61968.common.agreement import Agreement from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer import Customer diff --git a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py index dfe79e4d2..c8d60f991 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py @@ -13,7 +13,8 @@ from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.tariff import Tariff diff --git a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py index 92da48fd8..451f7f25c 100644 --- a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py +++ b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py @@ -13,7 +13,8 @@ from zepben.ewb.model.cim.iec61968.assets.structure import Structure from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.streetlight import Streetlight diff --git a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py index c0c3158e4..877a7ac53 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61968.assets.asset_container import AssetContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index 594a5613e..625c6a0d1 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -16,7 +16,8 @@ from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.util import nlen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location diff --git a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py index 481c6af89..e0f0a8145 100644 --- a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py +++ b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py @@ -13,7 +13,8 @@ from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py index 8ba98f005..99094aa19 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index a41e2d4b6..a103ed3f3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -12,7 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index 604f838ec..423cb884d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -22,7 +22,8 @@ from zepben.ewb.util import ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.metering.usage_point import UsagePoint diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py index bad482aaa..d4c2bcc53 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py @@ -14,7 +14,7 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import ConnectivityNodeContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 01ce4ed0b..7c5f87bcc 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -12,7 +12,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index 7400e2db9..9a44f3aed 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py index 447c1f2c4..84cdd64ef 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable from zepben.ewb.model.cim.iec61970.base.core.name import Name from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.descriptor_fix import BackedDescriptor +from zepben.ewb.boilerplate.backed_descriptor import BackedDescriptor if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py index 3c04f3f14..8482bba2c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py @@ -15,7 +15,8 @@ from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import nlen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset import Asset diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index ed7a7b3ff..258c08430 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.substation import Substation diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index 1d63b2e95..83d037ff8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index cf0a02a99..eb62a1e33 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index 6fa190c15..a19f232ab 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 45bd5933c..8bb47d7d8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index 77bcbd1f5..491371a53 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 9adcfb80e..00e52dcbf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py index 65b11195c..bb5091b87 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py index 76ca8cf20..5bfa3848c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py @@ -16,7 +16,8 @@ from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.regulating_control_mode_kind import RegulatingControlModeKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py index eeaf4a685..f98cfc0a7 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py @@ -12,7 +12,8 @@ from zepben.ewb.model.cim.iec61970.base.wires.rotating_machine import RotatingMachine from zepben.ewb.model.cim.iec61970.base.wires.synchronous_machine_kind import SynchronousMachineKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.reactive_capability_curve import ReactiveCapabilityCurve diff --git a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py index 2481e8117..f0f1fd83e 100644 --- a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py +++ b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py @@ -13,7 +13,8 @@ from zepben.ewb.model.cim.iec61970.base.wires.line import Line from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop diff --git a/test/dataclass_descriptors/test_basic_descriptor.py b/test/dataclass_descriptors/test_basic_descriptor.py index f0b9b1caa..8d6c756ed 100644 --- a/test/dataclass_descriptors/test_basic_descriptor.py +++ b/test/dataclass_descriptors/test_basic_descriptor.py @@ -6,7 +6,7 @@ from zepben.ewb.boilerplate.dataclass_base import DataclassBase from zepben.ewb import zb_dataclass, remove_descriptor_annotations -from zepben.ewb.boilerplate.descriptor_fix import BackedDescriptor +from zepben.ewb.boilerplate.backed_descriptor import BackedDescriptor @zb_dataclass From 1a6e2e2d3674ca7728b9bf56f357d56e83257e56 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 27 Jul 2026 17:07:02 +1000 Subject: [PATCH 10/54] Yet more cleanup Signed-off-by: Boris Filin --- .../ewb/boilerplate/backed_descriptor.py | 7 +-- src/zepben/ewb/boilerplate/backfill.py | 6 +- ...tions.py => abstract_backed_collection.py} | 25 +------- .../collections/abstract_backed_list.py | 31 +++++++++ .../collections/lazy_collection.py | 60 ++++++++++++++++++ .../ewb/boilerplate/collections/lazy_list.py | 63 ++----------------- .../collections/mrid_collection.py | 2 +- .../ewb/boilerplate/collections/mrid_list.py | 54 +++++++++------- .../ewb/boilerplate/collections/mrid_map.py | 34 +++++----- .../ewb/boilerplate/collections/wrapper.py | 15 +++-- src/zepben/ewb/boilerplate/dataclass_base.py | 32 +++++++--- .../iec61968/assetinfo/relay_info.py | 4 +- .../iec61968/common/contact_details.py | 7 ++- .../protection/protection_relay_function.py | 6 +- .../ewb/model/cim/iec61968/common/location.py | 4 +- .../ewb/model/cim/iec61970/base/core/curve.py | 4 +- .../base/diagramlayout/diagram_object.py | 4 +- .../base/wires/per_length_phase_impedance.py | 4 +- .../base/wires/power_transformer_end.py | 4 +- 19 files changed, 205 insertions(+), 161 deletions(-) rename src/zepben/ewb/boilerplate/collections/{abstract_backed_collections.py => abstract_backed_collection.py} (72%) create mode 100644 src/zepben/ewb/boilerplate/collections/abstract_backed_list.py create mode 100644 src/zepben/ewb/boilerplate/collections/lazy_collection.py diff --git a/src/zepben/ewb/boilerplate/backed_descriptor.py b/src/zepben/ewb/boilerplate/backed_descriptor.py index 4aab5fd79..4c63fe961 100644 --- a/src/zepben/ewb/boilerplate/backed_descriptor.py +++ b/src/zepben/ewb/boilerplate/backed_descriptor.py @@ -2,10 +2,6 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -# Copyright 2026 Zeppelin Bend Pty Ltd -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations @@ -39,6 +35,9 @@ def __get__(self, instance, _): return getattr(instance, self.backing_name) def __set__(self, instance, value): + if self.backing_name is None: + raise ValueError(f"Descriptor {self} is not yet aware of the supporting field - `__set__` cannot be called") return setattr(instance, self.backing_name, value) + Alias = BackedDescriptor diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py index 7b063ea9b..27df77c3f 100644 --- a/src/zepben/ewb/boilerplate/backfill.py +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -2,6 +2,7 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. + from dataclasses import Field from types import MemberDescriptorType from typing import Any, Callable, TypeVar @@ -15,6 +16,9 @@ class Backfill: def __init__(self, backfill_prop: property) -> None: + if backfill_prop.fget is None: + raise TypeError(f"Cannot backfill a property without a getter: {backfill_prop!r}") + self.backfill_prop = backfill_prop def apply(self, element: S, owner: Any) -> None: @@ -24,7 +28,7 @@ def apply(self, element: S, owner: Any) -> None: backing_name = name if target is None else ( getattr(target, "name", None) or getattr(target, "__name__", None) - or getattr(target.fget, "__name__", None) + or getattr(getattr(target, "fget", None), "__name__", None) ) if backing_name is None: diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py similarity index 72% rename from src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py rename to src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py index f8c1c8862..859216aa8 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -3,7 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import ABC, abstractmethod -from typing import Collection, Iterable, Generic, Iterator, Callable, Any, Sequence, overload, TypeVar +from typing import Collection, Iterable, Generic, Iterator, Callable, TypeVar T = TypeVar("T") @@ -40,7 +40,7 @@ def __iter__(self) -> Iterator[T]: def __contains__(self, element: object) -> bool: return element in self._get_collection() - def for_each_indexed(self, action: Callable[[int, T], Any]): + def for_each_indexed(self, action: Callable[[int, T], object]) -> None: """ Call the `action` on each item in the list @@ -50,24 +50,3 @@ def for_each_indexed(self, action: Callable[[int, T], Any]): action(index, item) -class AbstractBackedList( - AbstractBackedCollection[T], - Sequence[T], - Generic[T], - ABC, -): - - @abstractmethod - def _get_collection(self) -> Sequence[T]: - ... - - @overload - def __getitem__(self, index: int) -> T: - ... - - @overload - def __getitem__(self, index: slice) -> Sequence[T]: - ... - - def __getitem__(self, index: int | slice) -> T | Sequence[T]: - return self._get_collection()[index] diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py new file mode 100644 index 000000000..0e60de702 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py @@ -0,0 +1,31 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC, abstractmethod +from typing import Sequence, Generic, overload + +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection, T + + +class AbstractBackedList( + AbstractBackedCollection[T], + Sequence[T], + Generic[T], + ABC, +): + + @abstractmethod + def _get_collection(self) -> Sequence[T]: + ... + + @overload + def __getitem__(self, index: int) -> T: + ... + + @overload + def __getitem__(self, index: slice) -> Sequence[T]: + ... + + def __getitem__(self, index: int | slice) -> T | Sequence[T]: + return self._get_collection()[index] diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py new file mode 100644 index 000000000..b7fe49595 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_collection.py @@ -0,0 +1,60 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import TypeVar + +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper + + +T = TypeVar("T") + + +class LazyCollection(_IterableWrapper[T], AbstractBackedList[T]): + + def __init__( + self, + private_field, + validate=None, + sort_by=None + ) -> None: + super().__init__(private_field) + self.validate = validate + self.sort_by = sort_by + + def _get(self) -> list[T] | None: + return getattr(self.instance, self.backing_name) + + def _get_collection(self) -> list[T]: + return getattr(self.instance, self.backing_name) or [] + + def append(self, item: T) -> None: + if self.validate is not None: + self.validate(self.instance, item) + + existing = getattr(self.instance, self.backing_name) + if existing is None: + existing = [item] + setattr(self.instance, self.backing_name, existing) + else: + existing.append(item) + + if self.sort_by is not None: + existing.sort(key=self.sort_by) + + def remove(self, item: T) -> None: + existing = self._get_collection() + existing.remove(item) + if not existing: + self.clear() + + def clear(self) -> None: + setattr(self.instance, self.backing_name, None) + + def __repr__(self) -> str: + if self.instance is None: + return object.__repr__(self) + return repr(self._get_collection()) + + diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index 15744b1ef..14b54afcd 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -2,69 +2,19 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from typing import TypeVar -from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList -from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection, T -T = TypeVar("T") - - -class LazyValidatedList(_IterableWrapper, AbstractBackedList[T]): - - def __init__(self, - private_field, - validate=None, - sort_by=None): - super().__init__(private_field) - self.validate = validate - self.sort_by = sort_by - - def _get(self): - return getattr(self.instance, self.backing_name) - - def _get_collection(self) -> list[T]: - return getattr(self.instance, self.backing_name) or [] - - def append(self, item): - if self.validate is not None: - self.validate(self.instance, item) - - existing = getattr(self.instance, self.backing_name) - if existing is None: - existing = [item] - setattr(self.instance, self.backing_name, existing) - else: - existing.append(item) - - if self.sort_by is not None: - existing.sort(key=self.sort_by) - - def remove(self, item): - existing = self._get_collection() - existing.remove(item) - if not existing: - self.clear() - - def clear(self): - setattr(self.instance, self.backing_name, None) - - def __repr__(self): - if self.instance is None: - return self.__class__.__repr__() - return str(self._get_collection()) - - -class LazyIndexedList(LazyValidatedList[T]): +class LazyList(LazyCollection[T]): def __init__( self, private_field, element_description: str, - validate = None, - ): - super().__init__(private_field, validate=validate) + validate=None, + ) -> None: + super().__init__(private_field, validate=validate, sort_by=None) self.element_description = element_description def insert(self, index: int, item: T) -> None: @@ -94,9 +44,6 @@ def insert(self, index: int, item: T) -> None: else: existing.insert(index, item) - if self.sort_by is not None: - existing.sort(key=self.sort_by) - def append(self, item: T) -> None: self.insert(len(self), item) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py index f08f7b0df..a5f2d43de 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_collection.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from typing import Any, Protocol, TypeVar -from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedCollection +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection class HasMrid(Protocol): diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 8409d2aac..260e6e2ff 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -2,24 +2,26 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from typing import TypeVar, Protocol, Sequence +from typing import Sequence from typing_extensions import Self from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList -from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class LazyMridList(LazyValidatedList, MridCollection[S]): - def __init__(self, - private_field, - element_description: str, - backfill: Backfill = None, - validate=None, - sort_by=None): +class LazyMridList(LazyCollection[S], MridCollection[S]): + def __init__( + self, + private_field, + element_description: str, + backfill: Backfill = None, + validate=None, + sort_by=None + ) -> None: super().__init__(private_field, validate, sort_by) self.element_description = element_description self.backfill = backfill @@ -31,7 +33,7 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: found = next((element for element in existing if element.mrid == mrid), None) return found - def append(self, item: S): + def append(self, item: S) -> None: if not self._can_add_by_mrid(item): return @@ -41,13 +43,15 @@ def append(self, item: S): super().append(item) -class MridList(_IterableWrapper, AbstractBackedList[S], MridCollection[S]): - def __init__(self, - private_field, - element_description: str, - backfill: Backfill = None, - validate=None, - sort_by=None): +class MridList(_IterableWrapper[S], AbstractBackedList[S], MridCollection[S]): + def __init__( + self, + private_field, + element_description: str, + backfill: Backfill = None, + validate=None, + sort_by=None + ) -> None: super().__init__(private_field) self.element_description = element_description self.backfill = backfill @@ -62,7 +66,7 @@ def __get__(self, instance, owner=None) -> Self: obj.__post_init__() return obj - def __post_init__(self): + def __post_init__(self) -> None: self.backing_list = getattr(self.instance, self.backing_name) def _get_collection(self) -> Sequence[S]: @@ -72,7 +76,7 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: found = next((element for element in self.backing_list if element.mrid == mrid), None) return found - def append(self, item: S): + def append(self, item: S) -> None: if not self._can_add_by_mrid(item): return @@ -87,11 +91,13 @@ def append(self, item: S): if self.sort_by is not None: self.backing_list.sort(key=self.sort_by) - def remove(self, item): + def remove(self, item: S) -> None: self.backing_list.remove(item) - def clear(self): + def clear(self) -> None: self.backing_list.clear() - def __repr__(self): - return str(self.backing_list) + def __repr__(self) -> str: + if self.instance is None: + return object.__repr__(self) + return repr(self.backing_list) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py index 26752d683..9d1672c58 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -2,36 +2,36 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from typing import Protocol, TypeVar, Collection +from typing import TypeVar, ValuesView from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper - T = TypeVar("T") -class LazyMridMap(_IterableWrapper[S], MridCollection[S]): +class LazyMridMap(_IterableWrapper[T], MridCollection[S]): def __init__( self, private_field, element_description: str, backfill: Backfill = None, - validate=None): + validate=None + ) -> None: super().__init__(private_field) self.element_description = element_description self.backfill = backfill self.validate = validate - def _get(self): + def _get(self) -> dict[str, S] | None: return getattr(self.instance, self.backing_name) def _get_or_empty(self) -> dict[str, S]: return getattr(self.instance, self.backing_name) or {} - def _get_collection(self) -> list[S]: - return list(self._get_or_empty().values()) + def _get_collection(self) -> ValuesView[S]: + return self._get_or_empty().values() def _safe_get_by_mrid(self, mrid: str) -> S | None: return self._get_or_empty().get(mrid, None) @@ -39,7 +39,7 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: def get_by_mrid(self, mrid: str) -> S: return self._get_or_empty()[mrid] - def append(self, element: S): + def append(self, element: S) -> None: if not self._can_add_by_mrid(element): return @@ -56,24 +56,26 @@ def append(self, element: S): else: existing[element.mrid] = element - def __len__(self): + def __len__(self) -> int: return len(self._get_or_empty()) - def __contains__(self, item: S): - return self._get_or_empty().get(getattr(item, "mrid", None), None) == item + def __contains__(self, item: S) -> bool: + return self._get_or_empty().get(getattr(item, "mrid", None)) == item - def remove(self, item): + def remove(self, item) -> None: existing = self._get_or_empty() del existing[item.mrid] if not existing: self.clear() - def clear(self): + def clear(self) -> None: setattr(self.instance, self.backing_name, None) - def __repr__(self): - return str(self._get_or_empty()) + def __repr__(self) -> str: + if self.instance is None: + return object.__repr__(self) + return repr(self._get_or_empty()) - def __getitem__(self, item): + def __getitem__(self, item) -> S: return (self._get_or_empty())[item] diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py index 786141d14..d697e1a23 100644 --- a/src/zepben/ewb/boilerplate/collections/wrapper.py +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -2,12 +2,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. + from abc import ABC -from typing import Any +from typing import Any, TypeVar + from typing_extensions import Self, deprecated from zepben.ewb import BackedDescriptor, resolve_default -from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList, AbstractBackedCollection +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection class _Wrapper(BackedDescriptor): @@ -25,7 +27,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self: obj._init_kwargs = kwargs.copy() return obj - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.instance = None self.backing_name = None @@ -48,7 +50,7 @@ class _WrapperFgetFix(_Wrapper): This class exists to fix the tests that rely on the old lists being @property. TODO: Remove in a separate PR fixing tests """ - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) def fget(instance): return self.__get__(instance) @@ -62,12 +64,13 @@ def __set_name__(self, owner, name): @deprecated("Lists are no longer a property") def fget(self, instance): ... +T = TypeVar("T") -class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection, ABC): +class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection[T], ABC): """ This class allows us to assign lists at init time to avoid special-case handling. """ - def __set__(self, instance, value): + def __set__(self, instance, value) -> None: if instance is None: return if not hasattr(instance, self.backing_name): diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index 020a5dc10..ace55ca85 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -2,8 +2,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. + from dataclasses import dataclass, fields, MISSING, Field -from typing import TypeVar, dataclass_transform +from typing import TypeVar, cast + +from typing_extensions import dataclass_transform + + +T = TypeVar("T") def _is_set(obj: object, name: str) -> bool: @@ -13,9 +19,7 @@ def _is_set(obj: object, name: str) -> bool: return False return True -T = TypeVar("T", bound=type) - -def remove_descriptor_annotations(cls: T) -> T: +def remove_descriptor_annotations(cls: type[T]) -> T: """ Remove annotations for class attributes that are data descriptors. @@ -85,13 +89,23 @@ class MyClass: return cls -def zb_dataclass(cls: type[object]): +@dataclass_transform(eq_default=False, order_default=False) +def zb_dataclass(cls: type[T]) -> type[T]: """ - Shorthand alias for ``@dataclass(init=False,eq=False,slots=True,repr=False)`` + Shorthand alias for ``@dataclass(init=False, eq=False, slots=True, repr=False)`` Allows us to modify dataclass parameters for all of CIM from a single reference point """ cls = remove_descriptor_annotations(cls) - return dataclass(init=False, eq=False, slots=True, repr=False)(cls) + # The cast is purely for type checkers to be aware of the true class of cls + return cast( + type[T], + dataclass( + init=False, + eq=False, + slots=True, + repr=False, + )(cls), + ) def resolve_default(instance, field: Field): # Set field defaults @@ -100,9 +114,7 @@ def resolve_default(instance, field: Field): elif field.default_factory is not MISSING: setattr(instance, field.name, field.default_factory()) - # Ignore custom descriptors - elif hasattr(field, '__get__') and not isinstance(field, Field): - return + # Note: custom descriptors are not included in `fields(cls)`, so we don't need to handle them here # Mimic default Python missing arg error else: diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index b107ac078..c7441f355 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo @@ -31,7 +31,7 @@ class RelayInfo(AssetInfo): _reclose_delays: Optional[List[float]] = field(default=None) - reclose_delays: LazyIndexedList[float] = LazyIndexedList( + reclose_delays: LazyList[float] = LazyList( _reclose_delays, "float" ) diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py index 31e5336ad..4a08b7312 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -11,7 +11,8 @@ from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress @@ -66,12 +67,12 @@ def id(self) -> str: _phone_numbers: list[TelephoneNumber] | None = field(default=None) - phone_numbers: LazyValidatedList[TelephoneNumber] = LazyValidatedList(_phone_numbers) + phone_numbers: AbstractBackedList[TelephoneNumber] = LazyCollection(_phone_numbers) """[ZBEX] Phone numbers.""" _electronic_addresses: list[ElectronicAddress] | None = field(default=None) - electronic_addresses: LazyValidatedList[ElectronicAddress] = LazyValidatedList(_electronic_addresses) + electronic_addresses: AbstractBackedList[ElectronicAddress] = LazyCollection(_electronic_addresses) """[ZBEX] Electronic addresses.""" def __init__(self, id: str|None=None, *args, **kwargs): diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index e099d658d..580691d78 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -11,7 +11,7 @@ from typing import Optional, List, Callable, TYPE_CHECKING, Any from abc import ABCMeta -from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyList if sys.version_info >= (3, 13): from warnings import deprecated else: @@ -84,12 +84,12 @@ def relay_info(self): def relay_info(self, relay_info: Optional[RelayInfo]): self.asset_info = relay_info - time_limits: LazyIndexedList[float] = LazyIndexedList( + time_limits: LazyList[float] = LazyList( _time_limits, "float" ) - thresholds: LazyIndexedList[RelaySetting] = LazyIndexedList( + thresholds: LazyList[RelaySetting] = LazyList( _thresholds, "RelaySetting" ) diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index e6d22987d..cae9e9219 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -14,7 +14,7 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject @@ -31,7 +31,7 @@ class Location(IdentifiedObject): _position_points: Optional[List[PositionPoint]] = field(default=None) - points: LazyIndexedList[PositionPoint] = LazyIndexedList( + points: LazyList[PositionPoint] = LazyList( _position_points, "PositionPoint", ) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index 0e1fa4039..73b7ed83a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -12,13 +12,13 @@ from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.model.cim.iec61970.base.core.curve_data import CurveData from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import require -class CurveDataList(LazyValidatedList[CurveData]): +class CurveDataList(LazyCollection[CurveData]): def get(self, x: float) -> CurveData: """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 4ea2e9683..d4415c9c3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint @@ -49,7 +49,7 @@ def diagram(self): _diagram_object_points: Optional[List[DiagramObjectPoint]] = field(default=None) - points: LazyIndexedList[DiagramObjectPoint] = LazyIndexedList( + points: LazyList[DiagramObjectPoint] = LazyList( _diagram_object_points, "DiagramObjectPoint", ) diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py index 9bae87c2d..9290d87e9 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py @@ -13,14 +13,14 @@ from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.phase_impedance_data import PhaseImpedanceData from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.util import require, none -class PhaseImpedanceDataList(LazyValidatedList): +class PhaseImpedanceDataList(LazyCollection): def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: """ Get the matrix entry for the corresponding to and from phases. diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index c64d2fb03..7bbd6306f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -14,7 +14,7 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS @@ -27,7 +27,7 @@ from zepben.ewb.model.cim.iec61970.base.wires.power_transformer import PowerTransformer -class TransformerEndRatedSList(LazyValidatedList[TransformerEndRatedS]): +class TransformerEndRatedSList(LazyCollection[TransformerEndRatedS]): def get_by_cooling_type( self, From a2659597d18f68552b0b899f708fbd4333d4a352 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Wed, 29 Jul 2026 15:24:09 +1000 Subject: [PATCH 11/54] Move extension lists to individual files Signed-off-by: Boris Filin --- .../ewb/boilerplate/relations/__init__.py | 0 .../relations/ac_line_segment_phase_list.py | 21 ++++++++++ .../relations/battery_control_list.py | 24 +++++++++++ .../boilerplate/relations/curve_data_list.py | 38 +++++++++++++++++ .../relations/phase_impedance_data_list.py | 36 ++++++++++++++++ .../relations/power_transformer_end_list.py | 42 +++++++++++++++++++ .../boilerplate/relations/terminal_list.py | 18 ++++++++ .../relations/transformer_end_rated_s_list.py | 27 ++++++++++++ .../base/core/conducting_equipment.py | 10 +---- .../ewb/model/cim/iec61970/base/core/curve.py | 29 +------------ .../generation/production/battery_unit.py | 17 +------- .../iec61970/base/wires/ac_line_segment.py | 13 +----- .../base/wires/per_length_phase_impedance.py | 26 +----------- .../iec61970/base/wires/power_transformer.py | 32 +------------- .../base/wires/power_transformer_end.py | 20 +-------- 15 files changed, 213 insertions(+), 140 deletions(-) create mode 100644 src/zepben/ewb/boilerplate/relations/__init__.py create mode 100644 src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/battery_control_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/curve_data_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/terminal_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py diff --git a/src/zepben/ewb/boilerplate/relations/__init__.py b/src/zepben/ewb/boilerplate/relations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py new file mode 100644 index 000000000..a19a660aa --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py @@ -0,0 +1,21 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import SinglePhaseKind +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList + + +class AcLineSegmentPhaseList(LazyMridList): + def get_by_phase(self, phase: SinglePhaseKind): + """ + The individual phase models for an AcLineSegment. + `phase` the phase of the required [AcLineSegmentPhase] + """ + res = next((it for it in self if it.phase == phase), None) + if res is None: + raise KeyError(phase) + return res diff --git a/src/zepben/ewb/boilerplate/relations/battery_control_list.py b/src/zepben/ewb/boilerplate/relations/battery_control_list.py new file mode 100644 index 000000000..dcdeb7439 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/battery_control_list.py @@ -0,0 +1,24 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import BatteryControlMode +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList + + +class BatteryControlList(LazyMridList): + def get_by_mode(self, control_mode: BatteryControlMode): + """ + Get the `BatteryControl` identified by its `control_mode` + + `control_mode` the `BatteryControlMode` of the desired `BatteryControl` + Returns The `BatteryControl` with the specified `control_mode` if it exists + Raises `KeyError` if a `BatteryControl` with `control_mode` wasn't present. + """ + for control in self: + if control.control_mode == control_mode: + return control + raise IndexError(f"No BatteryControl with a control_mode of {control_mode} was found in BatteryUnit {str(self)}") diff --git a/src/zepben/ewb/boilerplate/relations/curve_data_list.py b/src/zepben/ewb/boilerplate/relations/curve_data_list.py new file mode 100644 index 000000000..3ee44553f --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/curve_data_list.py @@ -0,0 +1,38 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import TYPE_CHECKING + +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection + +if TYPE_CHECKING: + from zepben.ewb import CurveData + + +class CurveDataList(LazyCollection): + + def get(self, x: float) -> 'CurveData': + """ + Get the :class:`CurveData` identified by its `x_value`. + + :param x: The X value of the required :class:`CurveData`. + :returns: The :class:`CurveData` with the specified `x` if it exists. + :raises KeyError: When no `CurveData` was found with `x`. + """ + curve_data = next((it for it in self if it.x_value == x), None) + if curve_data: + return curve_data + raise KeyError(x) + + def remove_data_at(self, x: float) -> 'CurveData': + """ + Disassociate a :class:`CurveData` from this collection based on its `x_value`. + + :param x: The :class:`CurveData` to disassociate from this :class:`Curve`. + :returns: A reference to the removed :class:`CurveData`. + :raises IndexError: If no :class:`CurveData` with a value of `x` was not associated with this :class:`Curve`. + """ + data = self.get(x) + self.remove(data) + return data diff --git a/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py new file mode 100644 index 000000000..dae48773f --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py @@ -0,0 +1,36 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +if TYPE_CHECKING: + from zepben.ewb import SinglePhaseKind, PhaseImpedanceData + + +class PhaseImpedanceDataList(LazyCollection): + def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: + """ + Get the matrix entry for the corresponding to and from phases. + + :param from_phase: The from_phase to lookup. + :param to_phase: The to_phase to lookup. + :returns: The :class:`PhaseImpedanceData` with the specified `from_phase` and `to_phase` if it exists. + :raises KeyError: When no `PhaseImpedanceData` was found with a matching `from_phase` and `to_phase`. + """ + phase_impedance_data = next((it for it in self if it.from_phase == from_phase and it.to_phase == to_phase), None) + if phase_impedance_data: + return phase_impedance_data + + raise KeyError((from_phase, to_phase)) + + @property + def diagonal(self): + """ + Get only the diagonal elements of the matrix, i.e toPhase == fromPhase. + """ + return (pid for pid in self if pid.from_phase == pid.to_phase) diff --git a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py new file mode 100644 index 000000000..9c740cf8a --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py @@ -0,0 +1,42 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +if TYPE_CHECKING: + from zepben.ewb import PowerTransformerEnd, Terminal + + +class PowerTransformerEndList(LazyMridList['PowerTransformerEnd']): + + def get_by_num(self, end_number: int) -> PowerTransformerEnd: + """ + Get the `PowerTransformerEnd` on this `PowerTransformer` by its `end_number`. + + `end_number` The `end_number` of the `PowerTransformerEnd` in relation to this `PowerTransformer`s VectorGroup. + Returns The `PowerTransformerEnd` referred to by `end_number` + Raises IndexError if no `PowerTransformerEnd` was found with end_number `end_number`. + """ + end = next((it for it in self if it.end_number == end_number), None) + if end: + return end + raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self.instance)}") + + def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: + """ + Get the `PowerTransformerEnd` on this `PowerTransformer` by its `terminal`. + + `terminal` The `terminal` to find a `PowerTransformerEnd` for. + Returns The `PowerTransformerEnd` connected to the specified `terminal` + Raises IndexError if no `PowerTransformerEnd` connected to `terminal` was found on this `PowerTransformer`. + """ + + end = next((it for it in self if it.terminal == terminal), None) + if end: + return end + raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self.instance)}") diff --git a/src/zepben/ewb/boilerplate/relations/terminal_list.py b/src/zepben/ewb/boilerplate/relations/terminal_list.py new file mode 100644 index 000000000..b46ada487 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/terminal_list.py @@ -0,0 +1,18 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import Terminal +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList + + +class TerminalsList(LazyMridList[Terminal]): + + def get_by_sequence_number(self, sequence_number: int) -> Terminal: + term = next((it for it in self if it.sequence_number == sequence_number), None) + if term is None: + raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self.instance)}") + return term diff --git a/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py new file mode 100644 index 000000000..580015532 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py @@ -0,0 +1,27 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import TransformerEndRatedS, TransformerCoolingType +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection + + +class TransformerEndRatedSList(LazyCollection[TransformerEndRatedS]): + + def get_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS | None: + return next((rating for rating in self if rating.cooling_type == cooling_type), None) + + def remove_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS | None: + rating = self.get_by_cooling_type(cooling_type) + if rating is not None: + self.remove(rating) + return rating diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py index 8ed0a6b7c..0cb3b5d35 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py @@ -14,10 +14,10 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment +from zepben.ewb.boilerplate.relations.terminal_list import TerminalsList from zepben.ewb.util import require from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -25,14 +25,6 @@ from zepben.ewb.model.cim.iec61970.base.core.base_voltage import BaseVoltage -class TerminalsList(LazyMridList[Terminal]): - - def get_by_sequence_number(self, sequence_number: int) -> Terminal: - term = next((it for it in self if it.sequence_number == sequence_number), None) - if term is None: - raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self.instance)}") - return term - @zb_dataclass class ConductingEquipment(Equipment, metaclass=ABCMeta): """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index 73b7ed83a..d918383f5 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -12,39 +12,12 @@ from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.model.cim.iec61970.base.core.curve_data import CurveData from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject +from zepben.ewb.boilerplate.relations.curve_data_list import CurveDataList from zepben.ewb.util import require -class CurveDataList(LazyCollection[CurveData]): - - def get(self, x: float) -> CurveData: - """ - Get the :class:`CurveData` identified by its `x_value`. - - :param x: The X value of the required :class:`CurveData`. - :returns: The :class:`CurveData` with the specified `x` if it exists. - :raises KeyError: When no `CurveData` was found with `x`. - """ - curve_data = next((it for it in self if it.x_value == x), None) - if curve_data: - return curve_data - raise KeyError(x) - - def remove_data_at(self, x: float) -> CurveData: - """ - Disassociate a :class:`CurveData` from this collection based on its `x_value`. - - :param x: The :class:`CurveData` to disassociate from this :class:`Curve`. - :returns: A reference to the removed :class:`CurveData`. - :raises IndexError: If no :class:`CurveData` with a value of `x` was not associated with this :class:`Curve`. - """ - data = self.get(x) - self.remove(data) - return data - @zb_dataclass class Curve(IdentifiedObject, metaclass=ABCMeta): """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py index 060279fe3..8fde00398 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py +++ b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py @@ -15,27 +15,12 @@ from zepben.ewb.model.cim.iec61970.base.generation.production.battery_state_kind import BatteryStateKind from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.relations.battery_control_list import BatteryControlList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control import BatteryControl -class BatteryControlList(LazyMridList): - def get_by_mode(self, control_mode: BatteryControlMode): - """ - Get the `BatteryControl` identified by its `control_mode` - - `control_mode` the `BatteryControlMode` of the desired `BatteryControl` - Returns The `BatteryControl` with the specified `control_mode` if it exists - Raises `KeyError` if a `BatteryControl` with `control_mode` wasn't present. - """ - for control in self: - if control.control_mode == control_mode: - return control - raise IndexError(f"No BatteryControl with a control_mode of {control_mode} was found in BatteryUnit {str(self)}") - - @zb_dataclass class BatteryUnit(PowerElectronicsUnit): """An electrochemical energy storage device.""" diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index a19f232ab..4e4320a21 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -21,24 +21,13 @@ from zepben.ewb.model.cim.iec61970.base.wires.conductor import Conductor from zepben.ewb.model.cim.iec61970.base.wires.cut import Cut from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind +from zepben.ewb.boilerplate.relations.ac_line_segment_phase_list import AcLineSegmentPhaseList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.per_length_phase_impedance import PerLengthPhaseImpedance from zepben.ewb.model.cim.iec61970.base.wires.per_length_sequence_impedance import PerLengthSequenceImpedance -class AcLineSegmentPhaseList(LazyMridList): - def get_by_phase(self, phase: SinglePhaseKind): - """ - The individual phase models for an AcLineSegment. - `phase` the phase of the required [AcLineSegmentPhase] - """ - res = next((it for it in self if it.phase == phase), None) - if res is None: - raise KeyError(phase) - return res - - @zb_dataclass class AcLineSegment(Conductor): """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py index 9290d87e9..ddff061e4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py @@ -13,37 +13,13 @@ from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.phase_impedance_data import PhaseImpedanceData from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind +from zepben.ewb.boilerplate.relations.phase_impedance_data_list import PhaseImpedanceDataList from zepben.ewb.util import require, none -class PhaseImpedanceDataList(LazyCollection): - def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: - """ - Get the matrix entry for the corresponding to and from phases. - - :param from_phase: The from_phase to lookup. - :param to_phase: The to_phase to lookup. - :returns: The :class:`PhaseImpedanceData` with the specified `from_phase` and `to_phase` if it exists. - :raises KeyError: When no `PhaseImpedanceData` was found with a matching `from_phase` and `to_phase`. - """ - phase_impedance_data = next((it for it in self if it.from_phase == from_phase and it.to_phase == to_phase), None) - if phase_impedance_data: - return phase_impedance_data - - raise KeyError((from_phase, to_phase)) - - @property - def diagonal(self): - """ - Get only the diagonal elements of the matrix, i.e toPhase == fromPhase. - """ - return (pid for pid in self if pid.from_phase == pid.to_phase) - - @zb_dataclass class PerLengthPhaseImpedance(PerLengthImpedance): """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index 3a1ff4b1d..2473e4883 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -12,8 +12,8 @@ from typing import List, Optional, TYPE_CHECKING from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.relations.power_transformer_end_list import PowerTransformerEndList if sys.version_info >= (3, 13): from warnings import deprecated @@ -32,36 +32,6 @@ from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal -class PowerTransformerEndList(LazyMridList['PowerTransformerEnd']): - - def get_by_num(self, end_number: int) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `end_number`. - - `end_number` The `end_number` of the `PowerTransformerEnd` in relation to this `PowerTransformer`s VectorGroup. - Returns The `PowerTransformerEnd` referred to by `end_number` - Raises IndexError if no `PowerTransformerEnd` was found with end_number `end_number`. - """ - end = next((it for it in self if it.end_number == end_number), None) - if end: - return end - raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self.instance)}") - - def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `terminal`. - - `terminal` The `terminal` to find a `PowerTransformerEnd` for. - Returns The `PowerTransformerEnd` connected to the specified `terminal` - Raises IndexError if no `PowerTransformerEnd` connected to `terminal` was found on this `PowerTransformer`. - """ - - end = next((it for it in self if it.terminal == terminal), None) - if end: - return end - raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self.instance)}") - - @zb_dataclass class PowerTransformer(ConductingEquipment): """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index 7bbd6306f..a93cc67f7 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -14,12 +14,12 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd from zepben.ewb.model.cim.iec61970.base.wires.winding_connection import WindingConnection +from zepben.ewb.boilerplate.relations.transformer_end_rated_s_list import TransformerEndRatedSList from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -27,24 +27,6 @@ from zepben.ewb.model.cim.iec61970.base.wires.power_transformer import PowerTransformer -class TransformerEndRatedSList(LazyCollection[TransformerEndRatedS]): - - def get_by_cooling_type( - self, - cooling_type: TransformerCoolingType, - ) -> TransformerEndRatedS | None: - return next((rating for rating in self if rating.cooling_type == cooling_type), None) - - def remove_by_cooling_type( - self, - cooling_type: TransformerCoolingType, - ) -> TransformerEndRatedS | None: - rating = self.get_by_cooling_type(cooling_type) - if rating is not None: - self.remove(rating) - return rating - - @zb_dataclass class PowerTransformerEnd(TransformerEnd): """ From 6511d6c361ae47830b4261112a3747358e3956b5 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Wed, 29 Jul 2026 15:46:07 +1000 Subject: [PATCH 12/54] Move unnecessary aliases to custom inits Signed-off-by: Boris Filin --- .../collections/abstract_backed_collection.py | 4 ++-- .../ewb/model/cim/iec61968/common/location.py | 5 ++++- .../ewb/model/cim/iec61968/customers/customer.py | 5 ++++- .../ewb/model/cim/iec61970/base/core/equipment.py | 4 +++- .../ewb/model/cim/iec61970/base/core/substation.py | 5 ++++- .../iec61970/base/diagramlayout/diagram_object.py | 4 +++- .../cim/iec61970/base/wires/energy_consumer.py | 8 +++++--- .../model/cim/iec61970/base/wires/energy_source.py | 5 ++++- .../base/wires/power_electronics_connection.py | 13 ++++++++++--- .../cim/iec61970/base/wires/power_transformer.py | 5 ++++- .../iec61970/base/wires/power_transformer_end.py | 4 ++-- 11 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py index 859216aa8..8bbb19688 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -19,8 +19,8 @@ def _get_collection(self) -> Collection[T]: def append(self, element: T) -> None: ... - def extend(self, elements: Iterable[T]) -> None: - for element in elements: + def extend(self, elements: Iterable[T] | None) -> None: + for element in elements or []: self.append(element) @abstractmethod diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index cae9e9219..0e38da570 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -35,7 +35,10 @@ class Location(IdentifiedObject): _position_points, "PositionPoint", ) - position_points = Alias(points) + + def __init__(self, *args, position_points=None, **kwargs): + super(Location, self).__init__(*args, **kwargs) + self.points.extend(position_points) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index 1e66805ac..b4f7be250 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -40,7 +40,10 @@ class Customer(OrganisationRole): _customer_agreements, "A CustomerAgreement", ) - customer_agreements = Alias(agreements) + + def __init__(self, *args, customer_agreements=None, **kwargs): + super(Customer, self).__init__(*args, **kwargs) + self.agreements.extend(customer_agreements) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index 423cb884d..bff37c204 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -56,7 +56,6 @@ class Equipment(PowerSystemResource, metaclass=ABCMeta): _equipment_containers, "An EquipmentContainer", ) - equipment_containers = Alias(containers) current_containers: MridCollection[EquipmentContainer] = LazyMridList( _current_containers, @@ -73,6 +72,9 @@ class Equipment(PowerSystemResource, metaclass=ABCMeta): "An OperationalRestriction", ) + def __init__(self, *args, equipment_containers=None, **kwargs): + super(Equipment, self).__init__(*args, **kwargs) + self.containers.extend(equipment_containers) @property def sites(self) -> Generator['Site', None, None]: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index 83d037ff8..db3a11d05 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -41,7 +41,6 @@ class Substation(EquipmentContainer): "A Feeder", backfill=Backfill(Feeder.normal_energizing_substation) ) - normal_energized_feeders = Alias(feeders) _loops: Optional[List[Loop]] = field(default=None) @@ -64,6 +63,10 @@ class Substation(EquipmentContainer): "A Circuit", ) + def __init__(self, *args, normal_energized_feeders=None, **kwargs): + super(Substation, self).__init__(*args, **kwargs) + self.feeders.extend(normal_energized_feeders) + @property @internal(_sub_geographical_region) def sub_geographical_region(self): diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index d4415c9c3..6390f2e6f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -53,8 +53,10 @@ def diagram(self): _diagram_object_points, "DiagramObjectPoint", ) - diagram_object_points = Alias(points) + def __init__(self, *args, diagram_object_points=None, **kwargs): + super(DiagramObject, self).__init__(*args, **kwargs) + self.points.extend(diagram_object_points) @diagram.setter @deprecated("diagram should never be set directly - it is automatically set when adding it to the `diagram_objects` list") diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 8bb47d7d8..0377b2b02 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -58,9 +58,11 @@ class EnergyConsumer(EnergyConnection): "An EnergyConsumerPhase", backfill=Backfill(EnergyConsumerPhase.energy_consumer) ) - # TODO: Remove hack - # Old inits used the wrong name. This allows backwards compatibility - energy_consumer_phases: None = Alias(phases) + + + def __init__(self, *args, energy_consumer_phases=None, **kwargs): + super(EnergyConsumer, self).__init__(*args, **kwargs) + self.phases.extend(energy_consumer_phases) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index 491371a53..933408cd8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -122,7 +122,10 @@ class EnergySource(EnergyConnection): "An EnergySourcePhase", backfill=Backfill(EnergySourcePhase.energy_source) ) - energy_source_phases = Alias(phases) + + def __init__(self, *args, energy_source_phases=None, **kwargs): + super(EnergySource, self).__init__(*args, **kwargs) + self.phases.extend(energy_source_phases) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 00e52dcbf..9165bab93 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -349,16 +349,23 @@ def inv_var_resp_q_at_v4(self, value): _power_electronics_units, "A PowerElectronicsUnit", ) - power_electronics_units = Alias(units) phases: MridCollection[PowerElectronicsConnectionPhase] = LazyMridList( _power_electronics_connection_phases, "A PowerElectronicsConnectionPhase", backfill=Backfill(PowerElectronicsConnectionPhase.power_electronics_connection) ) - power_electronics_connection_phases = Alias(phases) - + def __init__( + self, + *args, + power_electronics_units=None, + power_electronics_connection_phases=None, + **kwargs + ): + super(PowerElectronicsConnection, self).__init__(*args, **kwargs) + self.units.extend(power_electronics_units) + self.phases.extend(power_electronics_connection_phases) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index 2473e4883..f05bde4a0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -99,7 +99,10 @@ class PowerTransformer(ConductingEquipment): validate=lambda self, it: self._validate_end(it), sort_by=lambda it: it.end_number ) - power_transformer_ends = Alias(ends) + + def __init__(self, *args, power_transformer_ends=None, **kwargs): + super(PowerTransformer, self).__init__(*args, **kwargs) + self.ends.extend(power_transformer_ends) def _validate_end(self, end: PowerTransformerEnd): self._validate_reference_by_field(end, end.end_number, self.ends.get_by_num, "end_number") diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index a93cc67f7..5bc3609a5 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -91,8 +91,9 @@ class PowerTransformerEnd(TransformerEnd): Should not be used directly, instead use add_rating and get_rating functions. """ - def __init__(self, *args, rated_s: int = None, **kwargs): + def __init__(self, *args, rated_s: int = None, ratings=None, **kwargs): super(PowerTransformerEnd, self).__init__(*args, **kwargs) + self.s_ratings.extend(ratings) if "_s_ratings" in kwargs: raise ValueError("Do not directly set s_ratings through the constructor. You have one more constructor parameter than expected.") if rated_s and self._rated_s: @@ -151,7 +152,6 @@ def rated_s(self, rated_s: Optional[int]): validate=lambda self, it: self._validate_rating(it), sort_by=lambda it: -it.rated_s ) - ratings = Alias(s_ratings) def _validate_rating(self, rating: TransformerEndRatedS): if any(it.cooling_type == rating.cooling_type for it in self.s_ratings): From df81be95c12281126ab28836e434f868bb17ea5c Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Wed, 29 Jul 2026 17:16:19 +1000 Subject: [PATCH 13/54] Make list fields private; add a test; supposedly fix license issue Signed-off-by: Boris Filin --- .../ewb/boilerplate/backed_descriptor.py | 12 +- .../collections/lazy_collection.py | 14 +- .../ewb/boilerplate/collections/lazy_list.py | 10 +- .../collections/mrid_collection.py | 2 +- .../ewb/boilerplate/collections/mrid_list.py | 26 +- .../ewb/boilerplate/collections/mrid_map.py | 16 +- .../ewb/boilerplate/collections/wrapper.py | 16 +- src/zepben/ewb/boilerplate/dataclass_base.py | 1 - .../ewb/boilerplate/relations/__init__.py | 4 + .../relations/power_transformer_end_list.py | 4 +- .../boilerplate/relations/terminal_list.py | 2 +- .../iec61970/base/feeder/lv_substation.py | 10 +- test/boilerplate/test_mrid_list.py | 339 ++++++++++++++++++ 13 files changed, 401 insertions(+), 55 deletions(-) create mode 100644 test/boilerplate/test_mrid_list.py diff --git a/src/zepben/ewb/boilerplate/backed_descriptor.py b/src/zepben/ewb/boilerplate/backed_descriptor.py index 4c63fe961..9a52eb6f3 100644 --- a/src/zepben/ewb/boilerplate/backed_descriptor.py +++ b/src/zepben/ewb/boilerplate/backed_descriptor.py @@ -19,25 +19,25 @@ def __init__(self, private_field): if not isinstance(private_field, Field) and not isinstance(private_field, BackedDescriptor): raise TypeError(f"private_field parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {private_field}") self.private_field: Field | 'BackedDescriptor' = private_field - self.backing_name: str | None = None + self._backing_name: str | None = None self.name = None def __set_name__(self, owner, name): if name is None: return self.__name__ = self.name = name - if not self.backing_name: - self.backing_name = self.private_field.name + if not self._backing_name: + self._backing_name = self.private_field.name def __get__(self, instance, _): if instance is None: return self - return getattr(instance, self.backing_name) + return getattr(instance, self._backing_name) def __set__(self, instance, value): - if self.backing_name is None: + if self._backing_name is None: raise ValueError(f"Descriptor {self} is not yet aware of the supporting field - `__set__` cannot be called") - return setattr(instance, self.backing_name, value) + return setattr(instance, self._backing_name, value) Alias = BackedDescriptor diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py index b7fe49595..860427abd 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_collection.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_collection.py @@ -24,19 +24,19 @@ def __init__( self.sort_by = sort_by def _get(self) -> list[T] | None: - return getattr(self.instance, self.backing_name) + return getattr(self._instance, self._backing_name) def _get_collection(self) -> list[T]: - return getattr(self.instance, self.backing_name) or [] + return getattr(self._instance, self._backing_name) or [] def append(self, item: T) -> None: if self.validate is not None: - self.validate(self.instance, item) + self.validate(self._instance, item) - existing = getattr(self.instance, self.backing_name) + existing = getattr(self._instance, self._backing_name) if existing is None: existing = [item] - setattr(self.instance, self.backing_name, existing) + setattr(self._instance, self._backing_name, existing) else: existing.append(item) @@ -50,10 +50,10 @@ def remove(self, item: T) -> None: self.clear() def clear(self) -> None: - setattr(self.instance, self.backing_name, None) + setattr(self._instance, self._backing_name, None) def __repr__(self) -> str: - if self.instance is None: + if self._instance is None: return object.__repr__(self) return repr(self._get_collection()) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index 14b54afcd..ef466361f 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -26,7 +26,7 @@ def insert(self, index: int, item: T) -> None: if not 0 <= index <= size: raise ValueError( f"Unable to add {self.element_description} to " - f"{self.instance}. " + f"{self._instance}. " f"Sequence number {index} is invalid. " f"Expected a value between 0 and {size}. " "Make sure you are adding the items in order and there are " @@ -34,13 +34,13 @@ def insert(self, index: int, item: T) -> None: ) if self.validate is not None: - self.validate(self.instance, item) + self.validate(self._instance, item) - existing = getattr(self.instance, self.backing_name) + existing = getattr(self._instance, self._backing_name) if existing is None: existing = [item] - setattr(self.instance, self.backing_name, existing) + setattr(self._instance, self._backing_name, existing) else: existing.insert(index, item) @@ -54,7 +54,7 @@ def pop(self, index: int = -1) -> T: Uses normal Python list semantics, including support for negative indexes and raising ``IndexError`` when the index is invalid. """ - existing = getattr(self.instance, self.backing_name) + existing = getattr(self._instance, self._backing_name) if existing is None: raise IndexError("pop from empty list") diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py index a5f2d43de..2b7524ce1 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_collection.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -42,7 +42,7 @@ def _can_add_by_mrid(self, element: S) -> bool: if existing is not element: raise ValueError( f"{self.element_description} with mRID {element.mrid} " - f"already exists in {self.instance}." + f"already exists in {self._instance}." ) return False diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 260e6e2ff..25efba636 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -38,7 +38,7 @@ def append(self, item: S) -> None: return if self.backfill is not None: - self.backfill.apply(item, self.instance) + self.backfill.apply(item, self._instance) super().append(item) @@ -57,7 +57,7 @@ def __init__( self.backfill = backfill self.validate = validate self.sort_by = sort_by - self.backing_list = None + self._backing_list = None def __get__(self, instance, owner=None) -> Self: obj = super().__get__(instance, owner) @@ -67,13 +67,13 @@ def __get__(self, instance, owner=None) -> Self: return obj def __post_init__(self) -> None: - self.backing_list = getattr(self.instance, self.backing_name) + self._backing_list = getattr(self._instance, self._backing_name) def _get_collection(self) -> Sequence[S]: - return self.backing_list + return self._backing_list def _safe_get_by_mrid(self, mrid: str) -> S | None: - found = next((element for element in self.backing_list if element.mrid == mrid), None) + found = next((element for element in self._backing_list if element.mrid == mrid), None) return found def append(self, item: S) -> None: @@ -81,23 +81,23 @@ def append(self, item: S) -> None: return if self.backfill is not None: - self.backfill.apply(item, self.instance) + self.backfill.apply(item, self._instance) if self.validate is not None: - self.validate(self.instance, item) + self.validate(self._instance, item) - self.backing_list.append(item) + self._backing_list.append(item) if self.sort_by is not None: - self.backing_list.sort(key=self.sort_by) + self._backing_list.sort(key=self.sort_by) def remove(self, item: S) -> None: - self.backing_list.remove(item) + self._backing_list.remove(item) def clear(self) -> None: - self.backing_list.clear() + self._backing_list.clear() def __repr__(self) -> str: - if self.instance is None: + if self._instance is None: return object.__repr__(self) - return repr(self.backing_list) + return repr(self._backing_list) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py index 9d1672c58..45ee6c87a 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -25,10 +25,10 @@ def __init__( self.validate = validate def _get(self) -> dict[str, S] | None: - return getattr(self.instance, self.backing_name) + return getattr(self._instance, self._backing_name) def _get_or_empty(self) -> dict[str, S]: - return getattr(self.instance, self.backing_name) or {} + return getattr(self._instance, self._backing_name) or {} def _get_collection(self) -> ValuesView[S]: return self._get_or_empty().values() @@ -44,15 +44,15 @@ def append(self, element: S) -> None: return if self.backfill is not None: - self.backfill.apply(element, self.instance) + self.backfill.apply(element, self._instance) if self.validate is not None: - self.validate(self.instance, element) + self.validate(self._instance, element) - existing = getattr(self.instance, self.backing_name) + existing = getattr(self._instance, self._backing_name) if existing is None: existing = {element.mrid: element} - setattr(self.instance, self.backing_name, existing) + setattr(self._instance, self._backing_name, existing) else: existing[element.mrid] = element @@ -70,10 +70,10 @@ def remove(self, item) -> None: self.clear() def clear(self) -> None: - setattr(self.instance, self.backing_name, None) + setattr(self._instance, self._backing_name, None) def __repr__(self) -> str: - if self.instance is None: + if self._instance is None: return object.__repr__(self) return repr(self._get_or_empty()) diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py index d697e1a23..3ec410014 100644 --- a/src/zepben/ewb/boilerplate/collections/wrapper.py +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -14,8 +14,8 @@ class _Wrapper(BackedDescriptor): - instance: Any - backing_name: Any + _instance: Any + _backing_name: Any def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ @@ -29,8 +29,8 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self: def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self.instance = None - self.backing_name = None + self._instance = None + self._backing_name = None def __get__(self, instance, _=None) -> Self: """ @@ -40,8 +40,8 @@ def __get__(self, instance, _=None) -> Self: if instance is None: return self obj = type(self)(*self._init_args, **self._init_kwargs) - obj.instance = instance - obj.backing_name = self.private_field.name + obj._instance = instance + obj._backing_name = self.private_field.name return obj @@ -73,9 +73,9 @@ class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection[T], ABC): def __set__(self, instance, value) -> None: if instance is None: return - if not hasattr(instance, self.backing_name): + if not hasattr(instance, self._backing_name): resolve_default(instance, self.private_field) - elif getattr(instance, self.backing_name): + elif getattr(instance, self._backing_name): raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") if value is not None: self.__get__(instance).extend(value) diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index ace55ca85..74a276f5a 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -89,7 +89,6 @@ class MyClass: return cls -@dataclass_transform(eq_default=False, order_default=False) def zb_dataclass(cls: type[T]) -> type[T]: """ Shorthand alias for ``@dataclass(init=False, eq=False, slots=True, repr=False)`` diff --git a/src/zepben/ewb/boilerplate/relations/__init__.py b/src/zepben/ewb/boilerplate/relations/__init__.py index e69de29bb..35e3122c5 100644 --- a/src/zepben/ewb/boilerplate/relations/__init__.py +++ b/src/zepben/ewb/boilerplate/relations/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py index 9c740cf8a..fd5e23859 100644 --- a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py +++ b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py @@ -25,7 +25,7 @@ def get_by_num(self, end_number: int) -> PowerTransformerEnd: end = next((it for it in self if it.end_number == end_number), None) if end: return end - raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self.instance)}") + raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self._instance)}") def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: """ @@ -39,4 +39,4 @@ def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: end = next((it for it in self if it.terminal == terminal), None) if end: return end - raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self.instance)}") + raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self._instance)}") diff --git a/src/zepben/ewb/boilerplate/relations/terminal_list.py b/src/zepben/ewb/boilerplate/relations/terminal_list.py index b46ada487..5292a9d90 100644 --- a/src/zepben/ewb/boilerplate/relations/terminal_list.py +++ b/src/zepben/ewb/boilerplate/relations/terminal_list.py @@ -14,5 +14,5 @@ class TerminalsList(LazyMridList[Terminal]): def get_by_sequence_number(self, sequence_number: int) -> Terminal: term = next((it for it in self if it.sequence_number == sequence_number), None) if term is None: - raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self.instance)}") + raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self._instance)}") return term diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index 8d7ef7724..6949c7d2d 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -57,9 +57,13 @@ class LvSubstation(EquipmentContainer): ) """[ZBEX] the ``LvFeeders`` that are normally energized by this ``LvSubstation``.""" - def num_normal_energized_lv_feeders(self) -> int: - """Get the number of entries in the normal ``LvFeeder`` collection.""" - return nlen(self._normal_energized_lv_feeders_by_id) + def normal_energized_lv_switch_feeders(self) -> Generator[LvFeeder, None, None]: + """ + Retrieves all normally energized LvFeeders that represent low voltage network connected below a switch on the edge of this LvSubstation. This is all + LvFeeders in the normalEnergizedLvFeeders that has a normalHeadTerminal attached to a Switch. + """ + # NOTE: import exists here due to a circular import problem + from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch for lv_feeder in self.normal_energized_lv_feeders: if (it := lv_feeder.normal_head_terminal) is not None and isinstance(it.conducting_equipment, Switch): diff --git a/test/boilerplate/test_mrid_list.py b/test/boilerplate/test_mrid_list.py new file mode 100644 index 000000000..6fb5c120f --- /dev/null +++ b/test/boilerplate/test_mrid_list.py @@ -0,0 +1,339 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Callable + +import pytest + +from zepben.ewb.boilerplate.collections.mrid_list import MridList +from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder + + +class CallbackBackfill: + def __init__( + self, + callback: Callable[[Owner, Feeder], None], + ): + self._callback = callback + + def apply(self, item: Feeder, owner: Owner): + self._callback(owner, item) + + +def record_backfill(owner: Owner, item: Feeder): + owner._events.append(("backfill", item.mrid)) + + +def reject_backfill(_owner: Owner, _item: Feeder): + raise ValueError("backfill failed") + + +def record_validation(owner: Owner, item: Feeder): + owner._events.append(("validate", item.mrid)) + + +def reject_validation(_owner: Owner, _item: Feeder): + raise ValueError("validation failed") + + +def validate_for_combined_test(owner: Owner, item: Feeder): + record_validation(owner, item) + + if item.mrid == "invalid": + raise ValueError("invalid feeder") + + +@dataclass +class Owner: + _feeders: list[Feeder] = field( + default_factory=list, + repr=False, + ) + _events: list[tuple[str, str]] = field( + default_factory=list, + repr=False, + ) + + feeders = MridList( + _feeders, + "Feeder", + ) + backfilled_feeders = MridList( + _feeders, + "Feeder", + backfill=CallbackBackfill(record_backfill), + ) + rejecting_backfill_feeders = MridList( + _feeders, + "Feeder", + backfill=CallbackBackfill(reject_backfill), + ) + validated_feeders = MridList( + _feeders, + "Feeder", + validate=record_validation, + ) + rejecting_validation_feeders = MridList( + _feeders, + "Feeder", + validate=reject_validation, + ) + backfilled_validated_feeders = MridList( + _feeders, + "Feeder", + backfill=CallbackBackfill(record_backfill), + validate=record_validation, + ) + sorted_feeders = MridList( + _feeders, + "Feeder", + sort_by=lambda feeder: feeder.mrid, + ) + fully_configured_feeders = MridList( + _feeders, + "Feeder", + backfill=CallbackBackfill(record_backfill), + validate=validate_for_combined_test, + sort_by=lambda feeder: feeder.mrid, + ) + + +@pytest.fixture +def owner() -> Owner: + owner = Owner() + + # All wrappers intentionally point at this same non-null backing list. + # Reset it and the callback log for each test. + owner._feeders.clear() + owner._events.clear() + + return owner + + +def test_append_adds_item_to_owner_backing_list(owner: Owner): + feeder = Feeder("feeder") + + owner.feeders.append(feeder) + + assert owner._feeders == [feeder] + + +def test_appending_same_instance_twice_only_adds_it_once( + owner: Owner, +): + feeder = Feeder("feeder") + + owner.feeders.append(feeder) + owner.feeders.append(feeder) + + assert owner._feeders == [feeder] + + +def test_appending_different_instance_with_same_mrid_raises( + owner: Owner, +): + existing = Feeder("duplicate") + owner.feeders.append(existing) + + with pytest.raises( + ValueError, + match=r"Feeder with mRID duplicate already exists", + ): + owner.feeders.append(Feeder("duplicate")) + + assert owner._feeders == [existing] + + +def test_rejected_duplicate_does_not_run_backfill_or_validation( + owner: Owner, +): + feeder = Feeder("feeder") + owner.backfilled_validated_feeders.append(feeder) + owner._events.clear() + + owner.backfilled_validated_feeders.append(feeder) + + assert owner._feeders == [feeder] + assert owner._events == [] + + +def test_append_applies_backfill(owner: Owner): + feeder = Feeder("feeder") + + owner.backfilled_feeders.append(feeder) + + assert owner._events == [("backfill", "feeder")] + assert owner._feeders == [feeder] + + +def test_backfill_failure_does_not_append_item(owner: Owner): + with pytest.raises(ValueError, match="backfill failed"): + owner.rejecting_backfill_feeders.append(Feeder("feeder")) + + assert owner._feeders == [] + + +def test_append_runs_validation(owner: Owner): + feeder = Feeder("feeder") + + owner.validated_feeders.append(feeder) + + assert owner._events == [("validate", "feeder")] + assert owner._feeders == [feeder] + + +def test_validation_failure_does_not_append_item(owner: Owner): + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_feeders.append(Feeder("feeder")) + + assert owner._feeders == [] + + +def test_append_runs_backfill_before_validation(owner: Owner): + feeder = Feeder("feeder") + + owner.backfilled_validated_feeders.append(feeder) + + assert owner._events == [ + ("backfill", "feeder"), + ("validate", "feeder"), + ] + assert owner._feeders == [feeder] + + +def test_append_sorts_owner_backing_list(owner: Owner): + second = Feeder("second") + first = Feeder("first") + + owner.sorted_feeders.append(second) + owner.sorted_feeders.append(first) + + assert owner._feeders == [first, second] + + +def test_remove_removes_item_from_owner_backing_list( + owner: Owner, +): + retained = Feeder("retained") + removed = Feeder("removed") + owner._feeders.extend([retained, removed]) + + owner.feeders.remove(removed) + + assert owner._feeders == [retained] + + +def test_clear_empties_owner_backing_list(owner: Owner): + owner._feeders.extend( + [ + Feeder("first"), + Feeder("second"), + ] + ) + + owner.feeders.clear() + + assert owner._feeders == [] + + +def test_repr_matches_owner_backing_list_repr(owner: Owner): + owner._feeders.extend( + [ + Feeder("first"), + Feeder("second"), + ] + ) + + assert repr(owner.feeders) == repr(owner._feeders) + + + + +def test_mrid_list_all_functionality_together(owner: Owner): + third = Feeder("third") + first = Feeder("first") + second = Feeder("second") + + owner.fully_configured_feeders.extend([third, first, second]) + + assert owner._feeders == [first, second, third] + assert len(owner.fully_configured_feeders) == 3 + assert list(owner.fully_configured_feeders) == [ + first, + second, + third, + ] + assert next(iter(owner.fully_configured_feeders)) is first + assert owner.fully_configured_feeders[0] is first + assert list(owner.fully_configured_feeders[1:]) == [ + second, + third, + ] + assert second in owner.fully_configured_feeders + assert ( + owner.fully_configured_feeders.get_by_mrid("second") + is second + ) + assert repr(owner.fully_configured_feeders) == repr( + owner._feeders + ) + + with pytest.raises(KeyError, match="missing"): + owner.fully_configured_feeders.get_by_mrid("missing") + + indexed: list[tuple[int, Feeder]] = [] + owner.fully_configured_feeders.for_each_indexed( + lambda index, item: indexed.append((index, item)) + ) + assert indexed == [ + (0, first), + (1, second), + (2, third), + ] + + events_before_duplicate = list(owner._events) + owner.fully_configured_feeders.append(first) + + assert owner._feeders == [first, second, third] + assert owner._events == events_before_duplicate + + with pytest.raises( + ValueError, + match=r"Feeder with mRID second already exists", + ): + owner.fully_configured_feeders.append(Feeder("second")) + + assert owner._feeders == [first, second, third] + assert owner._events == events_before_duplicate + + with pytest.raises(ValueError, match="invalid feeder"): + owner.fully_configured_feeders.append(Feeder("invalid")) + + assert owner._feeders == [first, second, third] + assert owner._events[-2:] == [ + ("backfill", "invalid"), + ("validate", "invalid"), + ] + + owner.fully_configured_feeders.remove(second) + + assert owner._feeders == [first, third] + + owner.fully_configured_feeders.clear() + + assert len(owner.fully_configured_feeders) == 0 + assert list(owner.fully_configured_feeders) == [] + assert owner._feeders == [] + assert owner._events[:6] == [ + ("backfill", "third"), + ("validate", "third"), + ("backfill", "first"), + ("validate", "first"), + ("backfill", "second"), + ("validate", "second"), + ] From 8493178d4979ff4de6d4fe11c4b5c44294dc744a Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Wed, 29 Jul 2026 17:16:28 +1000 Subject: [PATCH 14/54] Make list fields private; add a test; supposedly fix license issue Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/zepben/ewb/boilerplate/__init__.py b/src/zepben/ewb/boilerplate/__init__.py index 7ee764916..35e3122c5 100644 --- a/src/zepben/ewb/boilerplate/__init__.py +++ b/src/zepben/ewb/boilerplate/__init__.py @@ -2,4 +2,3 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. - From 08dd91e010bbe973689d91f187c31df390de4d14 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 11:41:46 +1000 Subject: [PATCH 15/54] Test Signed-off-by: Boris Filin --- test/boilerplate/test_mrid_list.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/boilerplate/test_mrid_list.py b/test/boilerplate/test_mrid_list.py index 6fb5c120f..e33b2f371 100644 --- a/test/boilerplate/test_mrid_list.py +++ b/test/boilerplate/test_mrid_list.py @@ -252,6 +252,8 @@ def test_repr_matches_owner_backing_list_repr(owner: Owner): assert repr(owner.feeders) == repr(owner._feeders) +def test_class_field_repr_uses_descriptor_repr() -> None: + assert repr(Owner.feeders) == object.__repr__(Owner.feeders) def test_mrid_list_all_functionality_together(owner: Owner): From 95934365c648d9e531a1f73d3aa236fb3c0f4717 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 12:36:01 +1000 Subject: [PATCH 16/54] Reorder things to minimise diff Signed-off-by: Boris Filin --- .../collections/lazy_collection.py | 2 +- .../ewb/boilerplate/collections/lazy_list.py | 2 +- .../ewb/boilerplate/collections/mrid_list.py | 6 +-- .../ewb/boilerplate/collections/mrid_map.py | 6 +-- .../iec61968/common/contact_details.py | 12 +++--- .../iec61970/base/feeder/lv_feeder.py | 35 ++++++++-------- .../iec61970/base/feeder/lv_substation.py | 13 +++--- .../protection/protection_relay_function.py | 10 ++--- .../ewb/model/cim/iec61968/common/location.py | 9 ++-- .../model/cim/iec61968/customers/customer.py | 9 ++-- .../iec61970/base/core/connectivity_node.py | 7 ++-- .../model/cim/iec61970/base/core/equipment.py | 39 +++++++++-------- .../model/cim/iec61970/base/core/feeder.py | 23 +++++----- .../base/core/power_system_resource.py | 11 +++-- .../base/core/sub_geographical_region.py | 10 ++--- .../cim/iec61970/base/core/substation.py | 42 +++++++++---------- .../base/diagramlayout/diagram_object.py | 22 +++++----- .../base/wires/ac_line_segment_phase.py | 4 +- .../model/cim/iec61970/base/wires/clamp.py | 11 ++--- .../ewb/model/cim/iec61970/base/wires/cut.py | 9 ++-- .../iec61970/base/wires/energy_consumer.py | 9 ++-- .../base/wires/energy_consumer_phase.py | 12 +++--- .../cim/iec61970/base/wires/energy_source.py | 9 ++-- .../base/wires/energy_source_phase.py | 10 ++--- .../wires/power_electronics_connection.py | 22 +++++----- .../power_electronics_connection_phase.py | 12 +++--- .../iec61970/base/wires/power_transformer.py | 20 ++++----- 27 files changed, 181 insertions(+), 195 deletions(-) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py index 860427abd..12478c4fe 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_collection.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_collection.py @@ -15,7 +15,7 @@ class LazyCollection(_IterableWrapper[T], AbstractBackedList[T]): def __init__( self, - private_field, + private_field: list[T] | None, validate=None, sort_by=None ) -> None: diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index ef466361f..7a37afcb9 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -10,7 +10,7 @@ class LazyList(LazyCollection[T]): def __init__( self, - private_field, + private_field: list[T] | None, element_description: str, validate=None, ) -> None: diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 25efba636..2d45ffcd7 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -16,9 +16,9 @@ class LazyMridList(LazyCollection[S], MridCollection[S]): def __init__( self, - private_field, + private_field: list[S] | None, element_description: str, - backfill: Backfill = None, + backfill: Backfill | None = None, validate=None, sort_by=None ) -> None: @@ -48,7 +48,7 @@ def __init__( self, private_field, element_description: str, - backfill: Backfill = None, + backfill: Backfill | None = None, validate=None, sort_by=None ) -> None: diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py index 45ee6c87a..011b07637 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -11,12 +11,12 @@ T = TypeVar("T") -class LazyMridMap(_IterableWrapper[T], MridCollection[S]): +class LazyMridMap(_IterableWrapper[S], MridCollection[S]): def __init__( self, - private_field, + private_field: list[S] | None, element_description: str, - backfill: Backfill = None, + backfill: Backfill | None = None, validate=None ) -> None: super().__init__(private_field) diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py index 4a08b7312..93ef787f3 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -67,14 +67,8 @@ def id(self) -> str: _phone_numbers: list[TelephoneNumber] | None = field(default=None) - phone_numbers: AbstractBackedList[TelephoneNumber] = LazyCollection(_phone_numbers) - """[ZBEX] Phone numbers.""" - _electronic_addresses: list[ElectronicAddress] | None = field(default=None) - electronic_addresses: AbstractBackedList[ElectronicAddress] = LazyCollection(_electronic_addresses) - """[ZBEX] Electronic addresses.""" - def __init__(self, id: str|None=None, *args, **kwargs): if id is not None: if "mrid" in kwargs: @@ -82,7 +76,6 @@ def __init__(self, id: str|None=None, *args, **kwargs): kwargs["mrid"] = id super(ContactDetails, self).__init__(*args, **kwargs) - def __str__(self): return f"ContactDetails({self.mrid})" @@ -90,6 +83,11 @@ def __hash__(self): # noinspection PyUnresolvedReferences return hash((type(self), *(getattr(self, s) for s in self.__slots__))) + phone_numbers: AbstractBackedList[TelephoneNumber] = LazyCollection(_phone_numbers) + """[ZBEX] Phone numbers.""" + + electronic_addresses: AbstractBackedList[ElectronicAddress] = LazyCollection(_electronic_addresses) + """[ZBEX] Electronic addresses.""" def __eq__(self, other: Any) -> bool: # diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py index 4ac09a559..384e29e13 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py @@ -55,24 +55,6 @@ class LvFeeder(EquipmentContainer): normal_energizing_lv_substation: 'LvSubstation | None' = None """[ZBEX] The normally energizing LvSubstation for this LvFeeder""" - current_equipment: MridCollection[Equipment] = LazyMridMap( - _current_equipment_by_id, - "A current Equipment", - ) - """Contained `Equipment` using the current state of the network.""" - - normal_energizing_feeders: MridCollection[Feeder] = LazyMridMap( - _normal_energizing_feeders_by_id, - "A Feeder", - ) - """[ZBEX] The HV/MV feeders that normally energize this LV feeder.""" - - current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( - _current_energizing_feeders_by_id, - "A Feeder", - ) - """[ZBEX] The HV/MV feeders that currently energize this LV feeder.""" - @property def normal_head_terminal(self) -> Optional[Terminal]: """ @@ -87,6 +69,23 @@ def normal_head_terminal(self, term: Optional[Terminal]): else: raise ValueError(f"normal_head_terminal for {str(self)} has already been set to {self._normal_head_terminal}, cannot reset this field to {term}") + normal_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _normal_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that normally energize this LV feeder.""" + + current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _current_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that currently energize this LV feeder.""" + + current_equipment: MridCollection[Equipment] = LazyMridMap( + _current_equipment_by_id, + "A current Equipment", + ) + """Contained `Equipment` using the current state of the network.""" # region deprecated list boilerplate # region current_equipment boilerplate diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index 6949c7d2d..2dfd1a0c1 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -44,19 +44,18 @@ class LvSubstation(EquipmentContainer): ) """[ZBEX] The HV/MV feeders that normally energize this ``LvSubstation``.""" - current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( - _current_energizing_feeders_by_id, - "A Feeder", - ) - """[ZBEX] The HV/MV feeders that currently energize this LV substation.""" - - normal_energized_lv_feeders: MridCollection[LvFeeder] = LazyMridMap( _normal_energized_lv_feeders_by_id, "An LvFeeder", ) """[ZBEX] the ``LvFeeders`` that are normally energized by this ``LvSubstation``.""" + current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _current_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that currently energize this LV substation.""" + def normal_energized_lv_switch_feeders(self) -> Generator[LvFeeder, None, None]: """ Retrieves all normally energized LvFeeders that represent low voltage network connected below a switch on the edge of this LvSubstation. This is all diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 580691d78..92be720c4 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -84,16 +84,16 @@ def relay_info(self): def relay_info(self, relay_info: Optional[RelayInfo]): self.asset_info = relay_info - time_limits: LazyList[float] = LazyList( - _time_limits, - "float" - ) - thresholds: LazyList[RelaySetting] = LazyList( _thresholds, "RelaySetting" ) + time_limits: LazyList[float] = LazyList( + _time_limits, + "float" + ) + sensors: MridCollection[Sensor] = LazyMridList( _sensors, "A Sensor", diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index 0e38da570..52d1686b0 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -31,15 +31,14 @@ class Location(IdentifiedObject): _position_points: Optional[List[PositionPoint]] = field(default=None) - points: LazyList[PositionPoint] = LazyList( - _position_points, - "PositionPoint", - ) - def __init__(self, *args, position_points=None, **kwargs): super(Location, self).__init__(*args, **kwargs) self.points.extend(position_points) + points: LazyList[PositionPoint] = LazyList( + _position_points, + "PositionPoint", + ) # region deprecated list boilerplate # diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index b4f7be250..ef3bc8537 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -36,15 +36,14 @@ class Customer(OrganisationRole): _customer_agreements: Optional[List[CustomerAgreement]] = field(default=None) - agreements: MridCollection[CustomerAgreement] = LazyMridList( - _customer_agreements, - "A CustomerAgreement", - ) - def __init__(self, *args, customer_agreements=None, **kwargs): super(Customer, self).__init__(*args, **kwargs) self.agreements.extend(customer_agreements) + agreements: MridCollection[CustomerAgreement] = LazyMridList( + _customer_agreements, + "A CustomerAgreement", + ) # region deprecated list boilerplate # region agreements boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index a103ed3f3..12dc7dd18 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -33,16 +33,15 @@ class ConnectivityNode(IdentifiedObject, WeakrefSlot): """ _terminals: List[Terminal] = field(default_factory=list) + def __iter__(self): + return iter(self._terminals) + terminals: MridCollection[Terminal] = LazyMridList( _terminals, "A Terminal" ) """The `Terminal`s attached to this `ConnectivityNode`""" - def __iter__(self): - return iter(self._terminals) - - def is_switched(self): return self.get_switch() is not None diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index bff37c204..5223d6307 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -50,27 +50,8 @@ class Equipment(PowerSystemResource, metaclass=ABCMeta): _usage_points: Optional[List[UsagePoint]] = field(default=None) _equipment_containers: Optional[List[EquipmentContainer]] = field(default=None) _operational_restrictions: Optional[List[OperationalRestriction]] = field(default=None) - _current_containers: Optional[List[EquipmentContainer]] = field(default=None) - - containers: MridCollection[EquipmentContainer] = LazyMridList( - _equipment_containers, - "An EquipmentContainer", - ) - - current_containers: MridCollection[EquipmentContainer] = LazyMridList( - _current_containers, - "A current EquipmentContainer", - ) - - usage_points: MridCollection[UsagePoint] = LazyMridList( - _usage_points, - "A UsagePoint", - ) - operational_restrictions: MridCollection[OperationalRestriction] = LazyMridList( - _operational_restrictions, - "An OperationalRestriction", - ) + _current_containers: Optional[List[EquipmentContainer]] = field(default=None) def __init__(self, *args, equipment_containers=None, **kwargs): super(Equipment, self).__init__(*args, **kwargs) @@ -143,6 +124,10 @@ def current_lv_feeders(self) -> Generator[LvFeeder, None, None]: """ return ngen(_of_type(self._current_containers, LvFeeder)) + containers: MridCollection[EquipmentContainer] = LazyMridList( + _equipment_containers, + "An EquipmentContainer", + ) def num_sites(self) -> int: """ @@ -150,6 +135,20 @@ def num_sites(self) -> int: """ return len(list(self.sites)) + current_containers: MridCollection[EquipmentContainer] = LazyMridList( + _current_containers, + "A current EquipmentContainer", + ) + + usage_points: MridCollection[UsagePoint] = LazyMridList( + _usage_points, + "A UsagePoint", + ) + + operational_restrictions: MridCollection[OperationalRestriction] = LazyMridList( + _operational_restrictions, + "An OperationalRestriction", + ) # region deprecated list boilerplate # region containers boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 7c5f87bcc..69352f970 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -63,6 +63,17 @@ def normal_head_terminal(self, term: Optional[Terminal]): else: raise ValueError(f"Feeder {self.mrid} has equipment assigned to it. Cannot update normalHeadTerminal on a feeder with equipment assigned.") + @property + @internal(_normal_energizing_substation) + def normal_energizing_substation(self): + """The substation that normally energizes the feeder. Also used for naming purposes.""" + return self._normal_energizing_substation + + @normal_energizing_substation.setter + @deprecated("normal_energizing_substation should never be set directly - it is automatically set when adding it to the `feeders` list") + def normal_energizing_substation(self, value): + self._normal_energizing_substation = value + current_equipment: MridCollection[Equipment] = LazyMridMap( _current_equipment_by_id, "A current Equipment", @@ -92,18 +103,6 @@ def normal_head_terminal(self, term: Optional[Terminal]): "An LvSubstation", ) """[ZBEX]""" - @property - @internal(_normal_energizing_substation) - def normal_energizing_substation(self): - """The substation that normally energizes the feeder. Also used for naming purposes.""" - return self._normal_energizing_substation - - @normal_energizing_substation.setter - @deprecated("normal_energizing_substation should never be set directly - it is automatically set when adding it to the `feeders` list") - def normal_energizing_substation(self, value): - self._normal_energizing_substation = value - - # region deprecated list boilerplate # region current_equipment boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py index 8482bba2c..4baa1d964 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py @@ -44,12 +44,6 @@ class PowerSystemResource(IdentifiedObject, metaclass=ABCMeta): _assets: Optional[List[Asset]] = field(default=None) - assets: MridCollection[Asset] = LazyMridList( - _assets, - "An Asset", - ) - - @property def has_controls(self) -> bool: """ @@ -57,6 +51,11 @@ def has_controls(self) -> bool: """ return nlen(self.num_controls) > 0 + assets: MridCollection[Asset] = LazyMridList( + _assets, + "An Asset", + ) + # region deprecated list boilerplate # region assets boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index 258c08430..118037b74 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -39,17 +39,17 @@ def geographical_region(self): """The geographical region to which this sub-geographical region is within.""" return self._geographical_region + @geographical_region.setter + @deprecated("Geographical region is a backfill property - it should only be set by adding the sub region to the sub regions list") + def geographical_region(self, value): + self._geographical_region = value + substations: MridCollection[Substation] = LazyMridList( _substations, "A Substation", backfill=Backfill(Substation.sub_geographical_region) ) - @geographical_region.setter - @deprecated("Geographical region is a backfill property - it should only be set by adding the sub region to the sub regions list") - def geographical_region(self, value): - self._geographical_region = value - # region deprecated list boilerplate # region substations boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index db3a11d05..587d03f2d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -36,33 +36,12 @@ class Substation(EquipmentContainer): _normal_energized_feeders: Optional[List[Feeder]] = field(default=None) - feeders: MridCollection[Feeder] = LazyMridList( - _normal_energized_feeders, - "A Feeder", - backfill=Backfill(Feeder.normal_energizing_substation) - ) - _loops: Optional[List[Loop]] = field(default=None) - loops: MridCollection[Loop] = LazyMridList( - _loops, - "A Loop", - ) - _energized_loops: Optional[List[Loop]] = field(default=None) - energized_loops: MridCollection[Loop] = LazyMridList( - _energized_loops, - "A Loop", - ) - _circuits: Optional[List[Circuit]] = field(default=None) - circuits: MridCollection[Circuit] = LazyMridList( - _circuits, - "A Circuit", - ) - def __init__(self, *args, normal_energized_feeders=None, **kwargs): super(Substation, self).__init__(*args, **kwargs) self.feeders.extend(normal_energized_feeders) @@ -80,6 +59,27 @@ def sub_geographical_region(self): def sub_geographical_region(self, value): self._sub_geographical_region = value + circuits: MridCollection[Circuit] = LazyMridList( + _circuits, + "A Circuit", + ) + + loops: MridCollection[Loop] = LazyMridList( + _loops, + "A Loop", + ) + + energized_loops: MridCollection[Loop] = LazyMridList( + _energized_loops, + "A Loop", + ) + + feeders: MridCollection[Feeder] = LazyMridList( + _normal_energized_feeders, + "A Feeder", + backfill=Backfill(Feeder.normal_energizing_substation) + ) + # region deprecated list boilerplate # region circuits boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 6390f2e6f..d360c603e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -32,12 +32,6 @@ class DiagramObject(IdentifiedObject): _diagram: Optional[Diagram] = field(default=None) - @property - @internal(_diagram) - def diagram(self): - """A diagram object is part of a diagram.""" - return self._diagram - identified_object_mrid: Optional[str] = None """The domain object to which this diagram object is associated.""" @@ -49,15 +43,16 @@ def diagram(self): _diagram_object_points: Optional[List[DiagramObjectPoint]] = field(default=None) - points: LazyList[DiagramObjectPoint] = LazyList( - _diagram_object_points, - "DiagramObjectPoint", - ) - def __init__(self, *args, diagram_object_points=None, **kwargs): super(DiagramObject, self).__init__(*args, **kwargs) self.points.extend(diagram_object_points) + @property + @internal(_diagram) + def diagram(self): + """A diagram object is part of a diagram.""" + return self._diagram + @diagram.setter @deprecated("diagram should never be set directly - it is automatically set when adding it to the `diagram_objects` list") def diagram(self, diag): @@ -66,6 +61,11 @@ def diagram(self, diag): else: raise ValueError(f"diagram for {str(self)} has already been set to {self._diagram}, cannot reset this field to {diag}") + points: LazyList[DiagramObjectPoint] = LazyList( + _diagram_object_points, + "DiagramObjectPoint", + ) + # region deprecated list boilerplate # # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py index 61fb22ee1..e5b0caedd 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py @@ -44,8 +44,6 @@ class AcLineSegmentPhase(PowerSystemResource): def ac_line_segment(self) -> 'AcLineSegment | None': return self._ac_line_segment - asset_info: WireInfo | None = None - @ac_line_segment.setter @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `phases` list") def ac_line_segment(self, ac_line_segment: 'AcLineSegment') -> None: @@ -53,3 +51,5 @@ def ac_line_segment(self, ac_line_segment: 'AcLineSegment') -> None: self._ac_line_segment = ac_line_segment else: raise ValueError(f"ac_line_segment has already been set to {self._ac_line_segment}. Cannot set this field again") + + asset_info: WireInfo | None = None diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index 6c07993e3..eb56b633c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -31,19 +31,20 @@ class Clamp(ConductingEquipment): _ac_line_segment: Optional['AcLineSegment'] = field(default=None) """The line segment to which the clamp is connected.""" + max_terminals = 1 + @property @internal(_ac_line_segment) def ac_line_segment(self) -> Optional['AcLineSegment']: """The line segment to which the clamp is connected.""" return self._ac_line_segment - max_terminals = 1 + @ac_line_segment.setter + @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `clamps` list") + def ac_line_segment(self, value): + self._ac_line_segment = value @property def length_from_t1_or_0(self) -> float: return self.length_from_terminal_1 or 0.0 - @ac_line_segment.setter - @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `clamps` list") - def ac_line_segment(self, value): - self._ac_line_segment = value diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index 7836d745e..0f1658bcd 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -42,11 +42,12 @@ def ac_line_segment(self) -> Optional['AcLineSegment']: """The line segment to which the cut is applied.""" return self._ac_line_segment - @property - def length_from_t1_or_0(self) -> float: - return self.length_from_terminal_1 or 0.0 - @ac_line_segment.setter @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `cuts` list") def ac_line_segment(self, value): self._ac_line_segment = value + + @property + def length_from_t1_or_0(self) -> float: + return self.length_from_terminal_1 or 0.0 + diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 0377b2b02..fb0c0e2f3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -53,6 +53,10 @@ class EnergyConsumer(EnergyConnection): q_fixed: Optional[float] = None """Power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" + def __init__(self, *args, energy_consumer_phases=None, **kwargs): + super(EnergyConsumer, self).__init__(*args, **kwargs) + self.phases.extend(energy_consumer_phases) + phases: MridCollection[EnergyConsumerPhase] = LazyMridList( _energy_consumer_phases, "An EnergyConsumerPhase", @@ -60,11 +64,6 @@ class EnergyConsumer(EnergyConnection): ) - def __init__(self, *args, energy_consumer_phases=None, **kwargs): - super(EnergyConsumer, self).__init__(*args, **kwargs) - self.phases.extend(energy_consumer_phases) - - # region deprecated list boilerplate # region phases boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py index fa1748731..96985a9d8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py @@ -25,12 +25,6 @@ class EnergyConsumerPhase(PowerSystemResource): _energy_consumer: Optional['EnergyConsumer'] = field(default=None) - @property - @internal(_energy_consumer) - def energy_consumer(self): - """The `EnergyConsumer` that has this phase.""" - return self._energy_consumer - phase: SinglePhaseKind = SinglePhaseKind.X """Phase of this energy consumer component. If the energy consumer is wye connected, the connection is from the indicated phase to the central ground or neutral point. If the energy consumer is delta connected, the phase indicates an energy consumer connected from the indicated phase to the next @@ -50,6 +44,12 @@ def energy_consumer(self): q_fixed: Optional[float] = None """Reactive power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" + @property + @internal(_energy_consumer) + def energy_consumer(self): + """The `EnergyConsumer` that has this phase.""" + return self._energy_consumer + @energy_consumer.setter @deprecated("energy_consumer should never be set directly - it is automatically set when adding it to the `phases` list") def energy_consumer(self, ec): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index 933408cd8..f0b1fd2a8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -117,17 +117,16 @@ class EnergySource(EnergyConnection): x0_max: Optional[float] = None """Maximum zero sequence Thevenin reactance.""" + def __init__(self, *args, energy_source_phases=None, **kwargs): + super(EnergySource, self).__init__(*args, **kwargs) + self.phases.extend(energy_source_phases) + phases: MridCollection[EnergySourcePhase] = LazyMridList( _energy_source_phases, "An EnergySourcePhase", backfill=Backfill(EnergySourcePhase.energy_source) ) - def __init__(self, *args, energy_source_phases=None, **kwargs): - super(EnergySource, self).__init__(*args, **kwargs) - self.phases.extend(energy_source_phases) - - # region deprecated list boilerplate # region phases boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py index 149166903..5a0f096df 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py @@ -27,17 +27,17 @@ class EnergySourcePhase(PowerSystemResource): _energy_source: Optional['EnergySource'] = field(default=None) + phase: SinglePhaseKind = SinglePhaseKind.NONE + """A `zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind.SinglePhaseKind` Phase of this energy source component. If the energy source is wye connected, + the connection is from the indicated phase to the central ground or neutral point. If the energy source is delta connected, the phase indicates an energy + source connected from the indicated phase to the next logical non-neutral phase.""" + @property @internal(_energy_source) def energy_source(self): """The `EnergySource` with this `EnergySourcePhase`""" return self._energy_source - phase: SinglePhaseKind = SinglePhaseKind.NONE - """A `zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind.SinglePhaseKind` Phase of this energy source component. If the energy source is wye connected, - the connection is from the indicated phase to the central ground or neutral point. If the energy source is delta connected, the phase indicates an energy - source connected from the indicated phase to the next logical non-neutral phase.""" - @energy_source.setter @deprecated("energy_source should never be set directly - it is automatically set when adding it to the `phases` list") def energy_source(self, es): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 9165bab93..19143bd57 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -149,6 +149,16 @@ class PowerElectronicsConnection(RegulatingCondEq): _power_electronics_connection_phases: Optional[List[PowerElectronicsConnectionPhase]] = field(default=None) """The individual units models for the power electronics connection.""" + def __init__( + self, + *args, + power_electronics_units=None, + power_electronics_connection_phases=None, + **kwargs + ): + super(PowerElectronicsConnection, self).__init__(*args, **kwargs) + self.units.extend(power_electronics_units) + self.phases.extend(power_electronics_connection_phases) @property def inv_watt_resp_v1(self): @@ -356,18 +366,6 @@ def inv_var_resp_q_at_v4(self, value): backfill=Backfill(PowerElectronicsConnectionPhase.power_electronics_connection) ) - def __init__( - self, - *args, - power_electronics_units=None, - power_electronics_connection_phases=None, - **kwargs - ): - super(PowerElectronicsConnection, self).__init__(*args, **kwargs) - self.units.extend(power_electronics_units) - self.phases.extend(power_electronics_connection_phases) - - # region deprecated list boilerplate # region units boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py index 6bdd66733..95a27a895 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py @@ -26,12 +26,6 @@ class PowerElectronicsConnectionPhase(PowerSystemResource): _power_electronics_connection: Optional['PowerElectronicsConnection'] = field(default=None) """The power electronics connection to which the phase belongs.""" - @property - @internal(_power_electronics_connection) - def power_electronics_connection(self): - """The power electronics connection to which the phase belongs.""" - return self._power_electronics_connection - p: Optional[float] = None """Active power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" @@ -45,6 +39,12 @@ def power_electronics_connection(self): q: Optional[float] = None """Reactive power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" + @property + @internal(_power_electronics_connection) + def power_electronics_connection(self): + """The power electronics connection to which the phase belongs.""" + return self._power_electronics_connection + @power_electronics_connection.setter @deprecated("power_electronics_connection should never be set directly - it is automatically set when adding it to the `phases` list") def power_electronics_connection(self, value): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index f05bde4a0..b8a65d7d9 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -91,6 +91,9 @@ class PowerTransformer(ConductingEquipment): The function of this transformer. """ + def __init__(self, *args, power_transformer_ends=None, **kwargs): + super(PowerTransformer, self).__init__(*args, **kwargs) + self.ends.extend(power_transformer_ends) ends: PowerTransformerEndList = PowerTransformerEndList( _power_transformer_ends, @@ -100,17 +103,6 @@ class PowerTransformer(ConductingEquipment): sort_by=lambda it: it.end_number ) - def __init__(self, *args, power_transformer_ends=None, **kwargs): - super(PowerTransformer, self).__init__(*args, **kwargs) - self.ends.extend(power_transformer_ends) - - def _validate_end(self, end: PowerTransformerEnd): - self._validate_reference_by_field(end, end.end_number, self.ends.get_by_num, "end_number") - - if end.end_number == 0: - end.end_number = self.num_ends() + 1 - - @property @deprecated("use asset_info instead.") def power_transformer_info(self) -> Optional[PowerTransformerInfo]: @@ -135,6 +127,12 @@ def get_base_voltage(self, terminal: Terminal = None): else: return None + def _validate_end(self, end: PowerTransformerEnd): + self._validate_reference_by_field(end, end.end_number, self.ends.get_by_num, "end_number") + + if end.end_number == 0: + end.end_number = self.num_ends() + 1 + # region deprecated list boilerplate # From 556b896d3eb3bd8659a94b515a086f5c7e01a2ae Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 13:08:53 +1000 Subject: [PATCH 17/54] ruff fix and newlines Signed-off-by: Boris Filin --- .../ewb/boilerplate/collections/abstract_backed_collection.py | 2 -- src/zepben/ewb/boilerplate/collections/lazy_collection.py | 2 -- src/zepben/ewb/boilerplate/dataclass_base.py | 1 - .../ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py | 2 +- src/zepben/ewb/model/cim/iec61968/common/location.py | 1 - src/zepben/ewb/model/cim/iec61968/customers/customer.py | 1 - .../ewb/model/cim/iec61968/metering/controlled_appliance.py | 2 +- .../ewb/model/cim/iec61970/base/core/connectivity_node.py | 2 +- src/zepben/ewb/model/cim/iec61970/base/core/curve.py | 2 +- src/zepben/ewb/model/cim/iec61970/base/core/equipment.py | 1 - src/zepben/ewb/model/cim/iec61970/base/core/feeder.py | 1 - src/zepben/ewb/model/cim/iec61970/base/core/substation.py | 1 - .../ewb/model/cim/iec61970/base/diagramlayout/diagram.py | 2 +- .../model/cim/iec61970/base/diagramlayout/diagram_object.py | 2 -- src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py | 1 - src/zepben/ewb/model/cim/iec61970/base/wires/cut.py | 1 - .../ewb/model/cim/iec61970/base/wires/energy_consumer.py | 1 - src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py | 1 - .../cim/iec61970/base/wires/per_length_phase_impedance.py | 2 +- .../cim/iec61970/base/wires/power_electronics_connection.py | 1 - .../ewb/model/cim/iec61970/base/wires/power_transformer.py | 1 - .../ewb/model/cim/iec61970/base/wires/power_transformer_end.py | 3 +-- test/boilerplate/test_mrid_list.py | 2 +- test/dataclass_descriptors/test_basic_descriptor.py | 2 +- .../networktrace/test_network_trace_step_path_provider.py | 1 - 25 files changed, 9 insertions(+), 29 deletions(-) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py index 8bbb19688..4235a3532 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -48,5 +48,3 @@ def for_each_indexed(self, action: Callable[[int, T], object]) -> None: """ for index, item in enumerate(self._get_collection()): action(index, item) - - diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py index 12478c4fe..90808ddde 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_collection.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_collection.py @@ -56,5 +56,3 @@ def __repr__(self) -> str: if self._instance is None: return object.__repr__(self) return repr(self._get_collection()) - - diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index 74a276f5a..82bb2a2b3 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -6,7 +6,6 @@ from dataclasses import dataclass, fields, MISSING, Field from typing import TypeVar, cast -from typing_extensions import dataclass_transform T = TypeVar("T") diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index c7441f355..b9dcbe8c7 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -92,4 +92,4 @@ def clear_delays(self) -> RelayInfo: # endregion - # endregion \ No newline at end of file + # endregion diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index 52d1686b0..be4bc42c2 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -12,7 +12,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index ef3bc8537..05a80ade8 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -14,7 +14,6 @@ from zepben.ewb.model.cim.iec61968.common.organisation_role import OrganisationRole from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection diff --git a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py index 91db333b4..9bef5881f 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from enum import Enum -from typing import List, Union +from typing import List from zepben.ewb import unique diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index 12dc7dd18..e732fe838 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -84,4 +84,4 @@ def clear_terminals(self) -> ConnectivityNode: # endregion - # endregion \ No newline at end of file + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index d918383f5..7e2a6765d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -90,4 +90,4 @@ def clear_data(self) -> "Curve": # endregion - # endregion \ No newline at end of file + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index 5223d6307..ee38a743e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -21,7 +21,6 @@ from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site from zepben.ewb.util import ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 69352f970..5e4bae259 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -235,4 +235,3 @@ def clear_current_energized_lv_substations(self) -> Feeder: # endregion # endregion - diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index 587d03f2d..b2bf02a2f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -13,7 +13,6 @@ from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill, internal diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index eb62a1e33..1548a2a90 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -72,4 +72,4 @@ def clear_diagram_objects(self) -> Diagram: return self # endregion - # endregion \ No newline at end of file + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index d360c603e..8440b2c94 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -12,7 +12,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject @@ -138,4 +137,3 @@ def clear_points(self) -> DiagramObject: # endregion # endregion - diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index eb56b633c..afd6f37ae 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -47,4 +47,3 @@ def ac_line_segment(self, value): @property def length_from_t1_or_0(self) -> float: return self.length_from_terminal_1 or 0.0 - diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index 0f1658bcd..d588a0e13 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -50,4 +50,3 @@ def ac_line_segment(self, value): @property def length_from_t1_or_0(self) -> float: return self.length_from_terminal_1 or 0.0 - diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index fb0c0e2f3..ab4329fc5 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -12,7 +12,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index f0b1fd2a8..d775c3ac2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -12,7 +12,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py index ddff061e4..906b58e87 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py @@ -71,4 +71,4 @@ def clear_data(self) -> PerLengthPhaseImpedance: # endregion data boilerplate - # endregion deprecated list boilerplate \ No newline at end of file + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 19143bd57..0d3659fe2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -12,7 +12,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index b8a65d7d9..dc59d9edf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -11,7 +11,6 @@ from dataclasses import field from typing import List, Optional, TYPE_CHECKING -from zepben.ewb import Alias from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.boilerplate.relations.power_transformer_end_list import PowerTransformerEndList diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index 5bc3609a5..792790068 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -13,7 +13,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS @@ -238,4 +237,4 @@ def clear_ratings(self) -> PowerTransformerEnd: # endregion - # endregion \ No newline at end of file + # endregion diff --git a/test/boilerplate/test_mrid_list.py b/test/boilerplate/test_mrid_list.py index e33b2f371..d9457f1fb 100644 --- a/test/boilerplate/test_mrid_list.py +++ b/test/boilerplate/test_mrid_list.py @@ -6,7 +6,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Callable +from typing import Callable import pytest diff --git a/test/dataclass_descriptors/test_basic_descriptor.py b/test/dataclass_descriptors/test_basic_descriptor.py index 8d6c756ed..a8166f7da 100644 --- a/test/dataclass_descriptors/test_basic_descriptor.py +++ b/test/dataclass_descriptors/test_basic_descriptor.py @@ -5,7 +5,7 @@ from dataclasses import field from zepben.ewb.boilerplate.dataclass_base import DataclassBase -from zepben.ewb import zb_dataclass, remove_descriptor_annotations +from zepben.ewb import zb_dataclass from zepben.ewb.boilerplate.backed_descriptor import BackedDescriptor diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index 8c943ad74..726cba3cc 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -12,7 +12,6 @@ from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.services.network.network_service import NetworkService -from zepben.ewb import NetworkStateOperators, TestNetworkBuilder, NetworkTraceStep, Terminal, NominalPhasePath, Breaker, AcLineSegment, Clamp, Cut from zepben.ewb.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider From 16f641401a92be172318aff9cfc705f80cc03f35 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 14:04:31 +1000 Subject: [PATCH 18/54] Comments on boilerplate; Match boilerplate file structure to JVM Signed-off-by: Boris Filin --- .../collections/abstract_backed_collection.py | 22 ++++---- .../collections/abstract_backed_list.py | 11 ++-- .../collections/lazy_collection.py | 29 +++++++++++ .../ewb/boilerplate/collections/lazy_list.py | 23 ++++++++- .../boilerplate/collections/lazy_mrid_list.py | 51 +++++++++++++++++++ .../{mrid_map.py => lazy_mrid_map.py} | 38 +++++++++++--- .../collections/mrid_collection.py | 8 ++- .../ewb/boilerplate/collections/mrid_list.py | 38 +++----------- .../ewb/boilerplate/collections/wrapper.py | 31 ++++++++++- .../relations/ac_line_segment_phase_list.py | 2 +- .../relations/battery_control_list.py | 2 +- .../relations/power_transformer_end_list.py | 2 +- .../boilerplate/relations/terminal_list.py | 2 +- .../extensions/iec61970/base/feeder/loop.py | 2 +- .../iec61970/base/feeder/lv_feeder.py | 2 +- .../iec61970/base/feeder/lv_substation.py | 2 +- .../protection/protection_relay_function.py | 2 +- .../protection/protection_relay_scheme.py | 2 +- .../protection/protection_relay_system.py | 2 +- .../assetinfo/power_transformer_info.py | 2 +- .../assetinfo/transformer_tank_info.py | 2 +- .../ewb/model/cim/iec61968/assets/asset.py | 2 +- .../model/cim/iec61968/customers/customer.py | 2 +- .../iec61968/customers/customer_agreement.py | 2 +- .../iec61968/customers/pricing_structure.py | 2 +- .../iec61968/infiec61968/infassets/pole.py | 2 +- .../model/cim/iec61968/metering/end_device.py | 2 +- .../cim/iec61968/metering/usage_point.py | 2 +- .../operations/operational_restriction.py | 2 +- .../base/auxiliaryequipment/sensor.py | 2 +- .../iec61970/base/core/connectivity_node.py | 2 +- .../model/cim/iec61970/base/core/equipment.py | 2 +- .../iec61970/base/core/equipment_container.py | 2 +- .../model/cim/iec61970/base/core/feeder.py | 2 +- .../iec61970/base/core/geographical_region.py | 2 +- .../base/core/power_system_resource.py | 2 +- .../base/core/sub_geographical_region.py | 2 +- .../cim/iec61970/base/core/substation.py | 2 +- .../iec61970/base/diagramlayout/diagram.py | 2 +- .../iec61970/base/wires/ac_line_segment.py | 2 +- .../iec61970/base/wires/energy_consumer.py | 2 +- .../cim/iec61970/base/wires/energy_source.py | 2 +- .../wires/power_electronics_connection.py | 2 +- .../iec61970/base/wires/protected_switch.py | 2 +- .../iec61970/base/wires/regulating_control.py | 2 +- .../base/wires/synchronous_machine.py | 2 +- .../iec61970/infiec61970/feeder/circuit.py | 2 +- 47 files changed, 234 insertions(+), 93 deletions(-) create mode 100644 src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py rename src/zepben/ewb/boilerplate/collections/{mrid_map.py => lazy_mrid_map.py} (67%) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py index 4235a3532..148c8fb36 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -16,19 +16,23 @@ def _get_collection(self) -> Collection[T]: ... @abstractmethod - def append(self, element: T) -> None: + def append(self, item: T, /) -> None: + """Append an item to the collection.""" ... - def extend(self, elements: Iterable[T] | None) -> None: - for element in elements or []: + def extend(self, items: Iterable[T] | None, /) -> None: + """Append each item to the collection.""" + for element in items or []: self.append(element) @abstractmethod - def remove(self, element: T) -> None: + def remove(self, item: T, /) -> None: + """Remove an item from the collection.""" ... @abstractmethod def clear(self) -> None: + """Remove all items from the collection.""" ... def __len__(self) -> int: @@ -37,14 +41,10 @@ def __len__(self) -> int: def __iter__(self) -> Iterator[T]: return iter(self._get_collection()) - def __contains__(self, element: object) -> bool: - return element in self._get_collection() + def __contains__(self, item: object) -> bool: + return item in self._get_collection() def for_each_indexed(self, action: Callable[[int, T], object]) -> None: - """ - Call the `action` on each item in the list - - :param action: An action to apply to each :class:`RelaySetting` in the `thresholds` collection, taking the index of the threshold, and the threshold itself. - """ + """Call the `action` on each item in the list.""" for index, item in enumerate(self._get_collection()): action(index, item) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py index 0e60de702..3caa14d52 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py @@ -24,8 +24,13 @@ def __getitem__(self, index: int) -> T: ... @overload - def __getitem__(self, index: slice) -> Sequence[T]: - ... + def __getitem__(self, index: int) -> T: ... + + @overload + def __getitem__(self, index: slice) -> Sequence[T]: ... def __getitem__(self, index: int | slice) -> T | Sequence[T]: - return self._get_collection()[index] + collection = self._get_collection() + if isinstance(index, slice): + return collection[index] + return collection[index] diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py index 90808ddde..bc2267843 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_collection.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_collection.py @@ -12,7 +12,31 @@ class LazyCollection(_IterableWrapper[T], AbstractBackedList[T]): + """ + Concrete collection wrapper that treats its backing field as a nullable + list. + A backing value of ``None`` is exposed as an empty collection. The backing + list is created when the first item is appended and reset to ``None`` when + the last item is removed or the collection is cleared. + + For example:: + + class Container: + _items = field(default=None) + items = LazyCollection(_items) + + container = Container() + + assert list(container.items) == [] + assert container._items is None + + container.items.append("value") + assert container._items == ["value"] + + container.items.clear() + assert container._items is None + """ def __init__( self, private_field: list[T] | None, @@ -30,6 +54,11 @@ def _get_collection(self) -> list[T]: return getattr(self._instance, self._backing_name) or [] def append(self, item: T) -> None: + """ + Append an item to the collection. + Run optional validation. + Sort the collection if key lambda is provided. + """ if self.validate is not None: self.validate(self._instance, item) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index 7a37afcb9..6296f9cd7 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -7,7 +7,21 @@ class LazyList(LazyCollection[T]): + """ + Lazy collection with list-style index-based insertion and deletion. + It retains the nullable backing-list behaviour of ``LazyCollection``, + creating the backing list when an item is inserted and resetting it to + ``None`` when the final item is deleted. + + For example:: + + container.items.insert(0, "value") + assert container._items == ["value"] + + del container.items[0] + assert container._items is None + """ def __init__( self, private_field: list[T] | None, @@ -19,7 +33,9 @@ def __init__( def insert(self, index: int, item: T) -> None: """ - Insert ``item`` at ``index``. + Insert an item into the collection at a given index. + Check for mRID collisions and run optional validation. + Sort the collection if key lambda is provided. """ size = len(self) @@ -45,6 +61,11 @@ def insert(self, index: int, item: T) -> None: existing.insert(index, item) def append(self, item: T) -> None: + """ + Append an item to the collection. + Run optional validation. + Sort the collection if key lambda is provided. + """ self.insert(len(self), item) def pop(self, index: int = -1) -> T: diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py new file mode 100644 index 000000000..d6d313f62 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py @@ -0,0 +1,51 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.mrid_collection import S, MridCollection + + +class LazyMridList(LazyCollection[S], MridCollection[S]): + """ + Nullable list implementation of :class:`MridCollection`. + + Inherits mRID lookup and uniqueness semantics from ``MridCollection`` and + lazy backing-list behavior from ``LazyCollection``. + """ + def __init__( + self, + private_field: list[S] | None, + element_description: str, + backfill: Backfill | None = None, + validate=None, + sort_by=None + ) -> None: + super().__init__(private_field, validate, sort_by) + self.element_description = element_description + self.backfill = backfill + + def _safe_get_by_mrid(self, mrid: str) -> S | None: + existing = self._get() + if existing is None: + return None + found = next((element for element in existing if element.mrid == mrid), None) + return found + + def append(self, item: S) -> None: + """ + Append an item to the collection. + Check for mRID collisions with existing items. + Optionally fill the backref field on the added item. + Run optional validation. + Sort the collection if key lambda is provided. + """ + if not self._can_add_by_mrid(item): + return + + if self.backfill is not None: + self.backfill.apply(item, self._instance) + + super().append(item) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py similarity index 67% rename from src/zepben/ewb/boilerplate/collections/mrid_map.py rename to src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py index 011b07637..c2d32a0d0 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py @@ -12,6 +12,24 @@ class LazyMridMap(_IterableWrapper[S], MridCollection[S]): + """ + Lazy mRID collection backed by a nullable dictionary. + + Items are stored by their ``mrid`` while iteration exposes the dictionary + values. A backing value of ``None`` is treated as an empty collection. The + dictionary is created when the first item is appended and reset to ``None`` + when the collection becomes empty. + + For example:: + + container.items.append(item) + + assert container._items == {item.mrid: item} + assert container.items.get_by_mrid(item.mrid) is item + + container.items.remove(item) + assert container._items is None + """ def __init__( self, private_field: list[S] | None, @@ -39,27 +57,33 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: def get_by_mrid(self, mrid: str) -> S: return self._get_or_empty()[mrid] - def append(self, element: S) -> None: - if not self._can_add_by_mrid(element): + def append(self, item: S) -> None: + """ + Add an item to the collection. + Check for mRID collisions with existing items. + Optionally fill the backref field on the added item. + Run optional validation. + """ + if not self._can_add_by_mrid(item): return if self.backfill is not None: - self.backfill.apply(element, self._instance) + self.backfill.apply(item, self._instance) if self.validate is not None: - self.validate(self._instance, element) + self.validate(self._instance, item) existing = getattr(self._instance, self._backing_name) if existing is None: - existing = {element.mrid: element} + existing = {item.mrid: item} setattr(self._instance, self._backing_name, existing) else: - existing[element.mrid] = element + existing[item.mrid] = item def __len__(self) -> int: return len(self._get_or_empty()) - def __contains__(self, item: S) -> bool: + def __contains__(self, item: object) -> bool: return self._get_or_empty().get(getattr(item, "mrid", None)) == item def remove(self, item) -> None: diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py index 2b7524ce1..867fe1b48 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_collection.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -16,7 +16,13 @@ class HasMrid(Protocol): class MridCollection(AbstractBackedCollection[S], ABC): - instance: Any + """ + Collection of objects identified by a unique ``mrid``. + + Provides lookup by mRID and rejects distinct objects with duplicate mRIDs. + """ + + _instance: Any element_description: str @abstractmethod diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 2d45ffcd7..81ed03f28 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -8,41 +8,10 @@ from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class LazyMridList(LazyCollection[S], MridCollection[S]): - def __init__( - self, - private_field: list[S] | None, - element_description: str, - backfill: Backfill | None = None, - validate=None, - sort_by=None - ) -> None: - super().__init__(private_field, validate, sort_by) - self.element_description = element_description - self.backfill = backfill - - def _safe_get_by_mrid(self, mrid: str) -> S | None: - existing = self._get() - if existing is None: - return None - found = next((element for element in existing if element.mrid == mrid), None) - return found - - def append(self, item: S) -> None: - if not self._can_add_by_mrid(item): - return - - if self.backfill is not None: - self.backfill.apply(item, self._instance) - - super().append(item) - - class MridList(_IterableWrapper[S], AbstractBackedList[S], MridCollection[S]): def __init__( self, @@ -77,6 +46,13 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: return found def append(self, item: S) -> None: + """ + Append an item to the collection. + Check for mRID collisions with existing items. + Optionally fill the backref field on the added item. + Run optional validation. + Sort the collection if key lambda is provided. + """ if not self._can_add_by_mrid(item): return diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py index 3ec410014..40bdcf683 100644 --- a/src/zepben/ewb/boilerplate/collections/wrapper.py +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -3,7 +3,10 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from __future__ import annotations + from abc import ABC +from dataclasses import Field from typing import Any, TypeVar from typing_extensions import Self, deprecated @@ -13,6 +16,29 @@ class _Wrapper(BackedDescriptor): + """ + Descriptor base for collection views backed by another dataclass field. + + Access through an instance creates a short-lived wrapper bound to that + instance and its backing field. For example:: + + class Container: + _items = field(default=None) + items = LazyCollection(_items) + + container = Container() + + # Calls Container.items.__get__(container, Container), returning a + # wrapper whose _instance is container and _backing_name is "_items". + bound_items = container.items + bound_items.append("value") + + assert container._items == ["value"] + + Access through the class returns the original shared descriptor:: + + assert isinstance(Container.items, LazyCollection) + """ _instance: Any _backing_name: Any @@ -48,6 +74,7 @@ def __get__(self, instance, _=None) -> Self: class _WrapperFgetFix(_Wrapper): """ This class exists to fix the tests that rely on the old lists being @property. + It implements a minimalistic callable fget with a name. TODO: Remove in a separate PR fixing tests """ def __init__(self, *args, **kwargs) -> None: @@ -73,7 +100,7 @@ class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection[T], ABC): def __set__(self, instance, value) -> None: if instance is None: return - if not hasattr(instance, self._backing_name): + if not hasattr(instance, self._backing_name) and isinstance(self.private_field, Field): resolve_default(instance, self.private_field) elif getattr(instance, self._backing_name): raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") @@ -81,3 +108,5 @@ def __set__(self, instance, value) -> None: self.__get__(instance).extend(value) else: self.__get__(instance).clear() + + diff --git a/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py index a19a660aa..0fa921cd1 100644 --- a/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py +++ b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py @@ -6,7 +6,7 @@ from __future__ import annotations from zepben.ewb import SinglePhaseKind -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList class AcLineSegmentPhaseList(LazyMridList): diff --git a/src/zepben/ewb/boilerplate/relations/battery_control_list.py b/src/zepben/ewb/boilerplate/relations/battery_control_list.py index dcdeb7439..233a59867 100644 --- a/src/zepben/ewb/boilerplate/relations/battery_control_list.py +++ b/src/zepben/ewb/boilerplate/relations/battery_control_list.py @@ -6,7 +6,7 @@ from __future__ import annotations from zepben.ewb import BatteryControlMode -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList class BatteryControlList(LazyMridList): diff --git a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py index fd5e23859..ede88484f 100644 --- a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py +++ b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList if TYPE_CHECKING: from zepben.ewb import PowerTransformerEnd, Terminal diff --git a/src/zepben/ewb/boilerplate/relations/terminal_list.py b/src/zepben/ewb/boilerplate/relations/terminal_list.py index 5292a9d90..3242be310 100644 --- a/src/zepben/ewb/boilerplate/relations/terminal_list.py +++ b/src/zepben/ewb/boilerplate/relations/terminal_list.py @@ -6,7 +6,7 @@ from __future__ import annotations from zepben.ewb import Terminal -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList class TerminalsList(LazyMridList[Terminal]): diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py index 2659d0d43..828b1c269 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py index 384e29e13..deab4947b 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py @@ -14,7 +14,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection -from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index 2dfd1a0c1..c18713852 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection -from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 92be720c4..8e1751acc 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -23,7 +23,7 @@ from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py index e99991643..01252e8f9 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py index c62658d45..ec0f4fda5 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py @@ -15,7 +15,7 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py index 3dfafdd20..04d5077c2 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py index a5653106e..4f29eb367 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset.py b/src/zepben/ewb/model/cim/iec61968/assets/asset.py index b28e0a2eb..ea8989474 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index 05a80ade8..90c2a0a4e 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61968.common.organisation_role import OrganisationRole from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py index 1d2ae9700..538806a6b 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61968.common.agreement import Agreement from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py index c8d60f991..d0c353800 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py index 451f7f25c..7c13ee54c 100644 --- a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py +++ b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61968.assets.structure import Structure from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py index 877a7ac53..ed4f559a2 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61968.assets.asset_container import AssetContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index 625c6a0d1..5f6d3f612 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -16,7 +16,7 @@ from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.util import nlen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py index e0f0a8145..2d31b9262 100644 --- a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py +++ b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py index 99094aa19..03266cdb2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index e732fe838..493e42c4d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -12,7 +12,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index ee38a743e..679013e06 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -21,7 +21,7 @@ from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site from zepben.ewb.util import ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py index d4c2bcc53..e979566fe 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py @@ -15,7 +15,7 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection -from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import ConnectivityNodeContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 5e4bae259..6415fec39 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -14,7 +14,7 @@ from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import internal -from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index 9a44f3aed..3ab1de3f0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py index 4baa1d964..56a48491d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py @@ -15,7 +15,7 @@ from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import nlen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index 118037b74..042cdcf2a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index b2bf02a2f..56e68b7e3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill, internal diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index 1548a2a90..e627bae01 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -15,7 +15,7 @@ from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_style import DiagramStyle diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index 4e4320a21..f88c7b2d0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index ab4329fc5..70711e37b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index d775c3ac2..08f11d428 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 0d3659fe2..caf4b3ebb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py index bb5091b87..03f914bac 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py index 5bfa3848c..bc6eac26e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py @@ -16,7 +16,7 @@ from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.regulating_control_mode_kind import RegulatingControlModeKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py index f98cfc0a7..5465514e1 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py @@ -12,7 +12,7 @@ from zepben.ewb.model.cim.iec61970.base.wires.rotating_machine import RotatingMachine from zepben.ewb.model.cim.iec61970.base.wires.synchronous_machine_kind import SynchronousMachineKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py index f0f1fd83e..5fda60a1b 100644 --- a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py +++ b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61970.base.wires.line import Line from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: From 462c51e7eec6dfb834d3a461b1ebadd9b0808af0 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 15:06:50 +1000 Subject: [PATCH 19/54] Update MANIFESTO.md; Rename lists to match JVM Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/MANIFESTO.md | 36 +++++-- .../collections/lazy_collection.py | 87 ---------------- .../collections/lazy_index_list.py | 88 +++++++++++++++++ .../ewb/boilerplate/collections/lazy_list.py | 99 +++++++++---------- .../boilerplate/collections/lazy_mrid_list.py | 4 +- .../ewb/boilerplate/collections/mrid_list.py | 2 +- .../boilerplate/relations/curve_data_list.py | 4 +- .../relations/phase_impedance_data_list.py | 4 +- .../relations/transformer_end_rated_s_list.py | 4 +- .../iec61968/assetinfo/relay_info.py | 4 +- .../iec61968/common/contact_details.py | 6 +- .../protection/protection_relay_function.py | 6 +- .../ewb/model/cim/iec61968/common/location.py | 4 +- .../iec61970/base/core/connectivity_node.py | 3 +- .../base/diagramlayout/diagram_object.py | 4 +- 15 files changed, 190 insertions(+), 165 deletions(-) delete mode 100644 src/zepben/ewb/boilerplate/collections/lazy_collection.py create mode 100644 src/zepben/ewb/boilerplate/collections/lazy_index_list.py diff --git a/src/zepben/ewb/boilerplate/MANIFESTO.md b/src/zepben/ewb/boilerplate/MANIFESTO.md index 53220a622..ea136d722 100644 --- a/src/zepben/ewb/boilerplate/MANIFESTO.md +++ b/src/zepben/ewb/boilerplate/MANIFESTO.md @@ -6,13 +6,13 @@ Yes, this is not your usual manifest We have removed the dataclassy library from the SDK, migrating to using the actual Python dataclass decorator. This was motivated by lack of ongoing support for dataclassy, as well as no-one on the team wanting to deal with its mess of magic code. -Our Python version of CIM has some very specific requirements leading to some unorthodox decisions (eg custom inits). Whatever your "why does it work this way" is, it has likely been considered, and many are provided below. +Our Python version of CIM has some very specific requirements leading to some unorthodox decisions (eg custom inits). Whatever your "why does it work this way" is, it has likely been considered, and many are answered below. This document is to serve as a guide in case you need to modify the SDK and are unfamiliar with how things are implemented around here. The following are some major decisions we have made and how they influence your potential contribution. ## Slots -Due to the immense size of grid models we are dealing with, every byte of memory matters, lest you fry your RAM with 120% load. By default, Python stores its values as a dictionary, which takes up hella space. Slots exist to fix that - a `__slots__` class variable describes the fixed memory layout of a class, making it rigid and a lot more compact. Every single one of our CIM classes uses slots, and all the new ones should too. `@dataclass` with slots enabled saves us specifying them explicitly at the cost of transparency - every field of such a class is a descriptor, and overwrites don't always work the way you expect them to. (For example, `@property` overwrite of a static field in a superclass is completely ignored by Python). +Due to the immense size of grid models we are dealing with, every byte of memory matters, lest you fry your RAM with 120% load. By default, Python stores instance values as a dictionary, which takes up hella space. Slots exist to fix that - a `__slots__` class variable describes the fixed attribute layout of a class, and through some dark magic (static instance-hashed attribute dictionaries) removes a lot of the memory overhead. Every single one of our CIM classes uses slots, and all the new ones should too. `@dataclass` with slots enabled saves us specifying them explicitly at the cost of transparency - every field of such a class is a descriptor, and overwrites don't always work the way you expect them to. (For example, `@property` overwrite of a static field in a superclass is completely ignored by Python). Our chosen approach is delegating as much functionality as we can to the native Python `@dataclass`, which lets you rely on its documentation and AI's knowledge of its quirks. It might be weird, but it is weird in consistent ways. No more reading a random biologist's library to figure out why your ConnectivityNode just did a barrel roll! @@ -20,7 +20,7 @@ Our chosen approach is delegating as much functionality as we can to the native We have chosen to use dataclasses simply because we have a lot of attributes, and any non-dataclass Python init requires a ridiculous amount of code duplication. This allows us to make use of its slot construction, as well as default values for fields that don't need to be passed in the init. -Unfortunately, dataclass generates custom `__init__` functions for the classes, but ONLY if you don't have an existing init already. That's why slightly tweaking class instantiation is quite complicated (requiring a custom decorator to rebuild the class from the original and the dataclass combined - I tried this). Instead, we went for a "clean slate" base class init approach, which, while stripping some of the dataclass functionality, is a lot more transparent when it explodes in your face. +Unfortunately, dataclass generates custom `__init__` functions for the classes, but ONLY if you don't have an existing init already. That's why slightly tweaking class instantiation is quite complicated (requiring a custom decorator to rebuild the class from the original and the dataclass combined - I tried this). Instead, we went for a "clean slate" base class init approach (explained below), which, while stripping some of the dataclass functionality, is a lot more transparent when it explodes in your face. ## Instantiation @@ -44,10 +44,34 @@ While it would be possible to bring args back if all subclass inits are removed ## Eq, Hash, Str, Repr -CIM classes need to be homogenous in their behaviour. Normally, dataclasses define these four methods on subclass level, but we need them to be propagated all the way up to `Identifiable` (or any overriding children such as `NameType`), where `__eq__` and `__hash__` both asses mrid equality, and `__str__` and `__rer__` represent the class as `{}` eg `Terminal{terminal_mrid}`. +CIM classes need to be homogenous in their behaviour. Normally, dataclasses define these four methods on subclass level, but we need them to be propagated all the way up to `Identifiable` (or any overriding children such as `NameType`), where `__eq__` and `__hash__` both asses memory reference equality, and `__str__` and `__repr__` represent the class as `{}` eg `Terminal{terminal_mrid}`. ## Descriptors -Properties are bulky and limited in functionality. There is a more general python concept called a Descriptor. This is any class implementing `__get__` and/or (usually and) `__set__` methods, which overwrite accessing a class member provided they are attached to the class and not its instance. This allows us to, for example, implement internally nullable fields. +Properties are bulky and limited in functionality. There is a more general python concept called a Descriptor. This is any class implementing `__get__` and/or (usually and) `__set__` methods, which overwrite accessing a class member (provided they are attached to the class and not its instance). This allows us to, for example, implement internally nullable fields. -These fields technically require no space in the object, since they access other values internally, but `@dataclass` assigns a slot to anything with a type. Obviously, if you want type hinting on member access, descriptor fields need to be typed. `@remove_descriptor_annotations` decorator takes care of that by stripping the types off of anything defining the aforementioned methods before it gets passed to a dataclass. Since this happens at runtime, IDE type-checking still gets the types, and `@dataclass` is no longer confused about which fields should be created. +These fields technically require no space in the object, since they access other values internally, but `@dataclass` assigns a slot to anything with a type. Obviously, if you want type hinting on member access, descriptor fields need to be typed. `@remove_descriptor_annotations` decorator (ordinarily part of `@zb_dataclass`) takes care of that by stripping the types off of anything defining the aforementioned methods before it gets passed to a dataclass. Since this happens at runtime, IDE type-checking still gets the types, and `@dataclass` is no longer confused about which fields should be created. + +## Lists + +Since CIM is UML-based, there are a lot of one-to-many and many-to-many class relationships, represented with lists. Most such relationships are usually empty. Python empty list is 56 bytes, whereas `None` is 16, so we want the lists to be nullable. + +Consequently, we have created a hierarchy of nullable descriptors backed by private fields. Each descriptor implements a list-like interface, allowing the user to interact with the relations normally, blissfully unaware of the nullable insanity going on under the hood. + +Due to the high diversity of the relationship behaviours, there is a lot of optional functionality shared by some these lists: + +- *mRID collision checks*: for mrid collections, we perform an mRID lookup when an item is added. If another item comes back, we check if the identities match. If they do, we disregard the addition. If they don't, we throw an appropriate error. +- *backfill*: some items contained in lists link back to the owner item. In that case, we perform a backfill to do this automatically. We also ensure that the item is not already linked to another owner. +- *validation*: some lists are create with validation lambdas, calling a validator function on the owner itself. This is useful when additional validation is required, eg sequence number matching. +- *sorting*: passing a sorting lambda to ordered collections automatically re-sorts the list after an item is added. + +Here are the concrete implementations of the lists that are used in the SDK: + +- `LazyList`: A nullable list of non-`Identifiable` objects that has validation and sorting built in. +- `LazyIndexList`: `LazyList` with index-based insertion and deletion, for strictly ordered relationships such as diagram points. + +The remaining collections all implement `MridCollection` - an interface that declares functionality of interaction with a collection of `Identifiable` objects, mainly focusing on `get_by_mrid`, `append`, `remove`, and `clear` methods. This allows us to have a common way of interacting with inter-object relationships, while hiding the underlying implementation (eg `list` vs `dict`) + +- `LazyMridList`: A `LazyList` with an mrid check and backfill added. `O(N)` mRID lookup. +- `MridList`: An mRID collection backed by a non-nullable list. Useful for relationships that are most likely to be filled (eg `ConnectivityNode.terminals`). `O(N)` mRID lookup. +- `LazyMridMap`: A nullable map of Identifiable objects keyed on their mRID. Supports mRID checks and `O(1)` mRID lookup. Does not support sorting (duh). Useful fot large-scale collections (for which the lookup speedup is significant) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py deleted file mode 100644 index bc2267843..000000000 --- a/src/zepben/ewb/boilerplate/collections/lazy_collection.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2026 Zeppelin Bend Pty Ltd -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -from typing import TypeVar - -from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList -from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper - - -T = TypeVar("T") - - -class LazyCollection(_IterableWrapper[T], AbstractBackedList[T]): - """ - Concrete collection wrapper that treats its backing field as a nullable - list. - - A backing value of ``None`` is exposed as an empty collection. The backing - list is created when the first item is appended and reset to ``None`` when - the last item is removed or the collection is cleared. - - For example:: - - class Container: - _items = field(default=None) - items = LazyCollection(_items) - - container = Container() - - assert list(container.items) == [] - assert container._items is None - - container.items.append("value") - assert container._items == ["value"] - - container.items.clear() - assert container._items is None - """ - def __init__( - self, - private_field: list[T] | None, - validate=None, - sort_by=None - ) -> None: - super().__init__(private_field) - self.validate = validate - self.sort_by = sort_by - - def _get(self) -> list[T] | None: - return getattr(self._instance, self._backing_name) - - def _get_collection(self) -> list[T]: - return getattr(self._instance, self._backing_name) or [] - - def append(self, item: T) -> None: - """ - Append an item to the collection. - Run optional validation. - Sort the collection if key lambda is provided. - """ - if self.validate is not None: - self.validate(self._instance, item) - - existing = getattr(self._instance, self._backing_name) - if existing is None: - existing = [item] - setattr(self._instance, self._backing_name, existing) - else: - existing.append(item) - - if self.sort_by is not None: - existing.sort(key=self.sort_by) - - def remove(self, item: T) -> None: - existing = self._get_collection() - existing.remove(item) - if not existing: - self.clear() - - def clear(self) -> None: - setattr(self._instance, self._backing_name, None) - - def __repr__(self) -> str: - if self._instance is None: - return object.__repr__(self) - return repr(self._get_collection()) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py new file mode 100644 index 000000000..e310a6c0b --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py @@ -0,0 +1,88 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from zepben.ewb.boilerplate.collections.lazy_list import LazyList, T + + +class LazyIndexList(LazyList[T]): + """ + Lazy collection with list-style index-based insertion and deletion. + + It retains the nullable backing-list behaviour of ``LazyCollection``, + creating the backing list when an item is inserted and resetting it to + ``None`` when the final item is deleted. + + For example:: + + container.items.insert(0, "value") + assert container._items == ["value"] + + del container.items[0] + assert container._items is None + """ + def __init__( + self, + private_field: list[T] | None, + element_description: str, + validate=None, + ) -> None: + super().__init__(private_field, validate=validate, sort_by=None) + self.element_description = element_description + + def insert(self, index: int, item: T) -> None: + """ + Insert an item into the collection at a given index. + Check for mRID collisions and run optional validation. + Sort the collection if key lambda is provided. + """ + size = len(self) + + if not 0 <= index <= size: + raise ValueError( + f"Unable to add {self.element_description} to " + f"{self._instance}. " + f"Sequence number {index} is invalid. " + f"Expected a value between 0 and {size}. " + "Make sure you are adding the items in order and there are " + "no gaps in the numbering." + ) + + if self.validate is not None: + self.validate(self._instance, item) + + existing = getattr(self._instance, self._backing_name) + + if existing is None: + existing = [item] + setattr(self._instance, self._backing_name, existing) + else: + existing.insert(index, item) + + def append(self, item: T) -> None: + """ + Append an item to the collection. + Run optional validation. + Sort the collection if key lambda is provided. + """ + self.insert(len(self), item) + + def pop(self, index: int = -1) -> T: + """ + Remove and return the item at ``index``. + + Uses normal Python list semantics, including support for negative + indexes and raising ``IndexError`` when the index is invalid. + """ + existing = getattr(self._instance, self._backing_name) + + if existing is None: + raise IndexError("pop from empty list") + + item = existing.pop(index) + + if not existing: + self.clear() + + return item diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index 6296f9cd7..e40248e95 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -2,63 +2,56 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import TypeVar -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection, T +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class LazyList(LazyCollection[T]): +T = TypeVar("T") + + +class LazyList(_IterableWrapper[T], AbstractBackedList[T]): """ - Lazy collection with list-style index-based insertion and deletion. + Concrete collection wrapper that treats its backing field as a nullable + list. - It retains the nullable backing-list behaviour of ``LazyCollection``, - creating the backing list when an item is inserted and resetting it to - ``None`` when the final item is deleted. + A backing value of ``None`` is exposed as an empty collection. The backing + list is created when the first item is appended and reset to ``None`` when + the last item is removed or the collection is cleared. For example:: - container.items.insert(0, "value") + class Container: + _items = field(default=None) + items = LazyCollection(_items) + + container = Container() + + assert list(container.items) == [] + assert container._items is None + + container.items.append("value") assert container._items == ["value"] - del container.items[0] + container.items.clear() assert container._items is None """ def __init__( self, private_field: list[T] | None, - element_description: str, validate=None, + sort_by=None ) -> None: - super().__init__(private_field, validate=validate, sort_by=None) - self.element_description = element_description - - def insert(self, index: int, item: T) -> None: - """ - Insert an item into the collection at a given index. - Check for mRID collisions and run optional validation. - Sort the collection if key lambda is provided. - """ - size = len(self) - - if not 0 <= index <= size: - raise ValueError( - f"Unable to add {self.element_description} to " - f"{self._instance}. " - f"Sequence number {index} is invalid. " - f"Expected a value between 0 and {size}. " - "Make sure you are adding the items in order and there are " - "no gaps in the numbering." - ) + super().__init__(private_field) + self.validate = validate + self.sort_by = sort_by - if self.validate is not None: - self.validate(self._instance, item) - - existing = getattr(self._instance, self._backing_name) + def _get(self) -> list[T] | None: + return getattr(self._instance, self._backing_name) - if existing is None: - existing = [item] - setattr(self._instance, self._backing_name, existing) - else: - existing.insert(index, item) + def _get_collection(self) -> list[T]: + return getattr(self._instance, self._backing_name) or [] def append(self, item: T) -> None: """ @@ -66,23 +59,29 @@ def append(self, item: T) -> None: Run optional validation. Sort the collection if key lambda is provided. """ - self.insert(len(self), item) - - def pop(self, index: int = -1) -> T: - """ - Remove and return the item at ``index``. + if self.validate is not None: + self.validate(self._instance, item) - Uses normal Python list semantics, including support for negative - indexes and raising ``IndexError`` when the index is invalid. - """ existing = getattr(self._instance, self._backing_name) - if existing is None: - raise IndexError("pop from empty list") + existing = [item] + setattr(self._instance, self._backing_name, existing) + else: + existing.append(item) - item = existing.pop(index) + if self.sort_by is not None: + existing.sort(key=self.sort_by) + def remove(self, item: T) -> None: + existing = self._get_collection() + existing.remove(item) if not existing: self.clear() - return item + def clear(self) -> None: + setattr(self._instance, self._backing_name, None) + + def __repr__(self) -> str: + if self._instance is None: + return object.__repr__(self) + return repr(self._get_collection()) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py index d6d313f62..d33a4b726 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py @@ -4,11 +4,11 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.boilerplate.collections.mrid_collection import S, MridCollection -class LazyMridList(LazyCollection[S], MridCollection[S]): +class LazyMridList(LazyList[S], MridCollection[S]): """ Nullable list implementation of :class:`MridCollection`. diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 81ed03f28..7fac3e6e4 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -15,7 +15,7 @@ class MridList(_IterableWrapper[S], AbstractBackedList[S], MridCollection[S]): def __init__( self, - private_field, + private_field: list[S], element_description: str, backfill: Backfill | None = None, validate=None, diff --git a/src/zepben/ewb/boilerplate/relations/curve_data_list.py b/src/zepben/ewb/boilerplate/relations/curve_data_list.py index 3ee44553f..85954a1d2 100644 --- a/src/zepben/ewb/boilerplate/relations/curve_data_list.py +++ b/src/zepben/ewb/boilerplate/relations/curve_data_list.py @@ -4,13 +4,13 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from typing import TYPE_CHECKING -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.lazy_list import LazyList if TYPE_CHECKING: from zepben.ewb import CurveData -class CurveDataList(LazyCollection): +class CurveDataList(LazyList): def get(self, x: float) -> 'CurveData': """ diff --git a/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py index dae48773f..f1f2951e4 100644 --- a/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py +++ b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py @@ -7,12 +7,12 @@ from typing import TYPE_CHECKING -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.lazy_list import LazyList if TYPE_CHECKING: from zepben.ewb import SinglePhaseKind, PhaseImpedanceData -class PhaseImpedanceDataList(LazyCollection): +class PhaseImpedanceDataList(LazyList): def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: """ Get the matrix entry for the corresponding to and from phases. diff --git a/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py index 580015532..f62647491 100644 --- a/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py +++ b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py @@ -6,10 +6,10 @@ from __future__ import annotations from zepben.ewb import TransformerEndRatedS, TransformerCoolingType -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.lazy_list import LazyList -class TransformerEndRatedSList(LazyCollection[TransformerEndRatedS]): +class TransformerEndRatedSList(LazyList[TransformerEndRatedS]): def get_by_cooling_type( self, diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index b9dcbe8c7..cf500fa23 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyList +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo @@ -31,7 +31,7 @@ class RelayInfo(AssetInfo): _reclose_delays: Optional[List[float]] = field(default=None) - reclose_delays: LazyList[float] = LazyList( + reclose_delays: LazyIndexList[float] = LazyIndexList( _reclose_delays, "float" ) diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py index 93ef787f3..4d03083a8 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -12,7 +12,7 @@ from zepben.ewb import zb_dataclass from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress @@ -83,10 +83,10 @@ def __hash__(self): # noinspection PyUnresolvedReferences return hash((type(self), *(getattr(self, s) for s in self.__slots__))) - phone_numbers: AbstractBackedList[TelephoneNumber] = LazyCollection(_phone_numbers) + phone_numbers: AbstractBackedList[TelephoneNumber] = LazyList(_phone_numbers) """[ZBEX] Phone numbers.""" - electronic_addresses: AbstractBackedList[ElectronicAddress] = LazyCollection(_electronic_addresses) + electronic_addresses: AbstractBackedList[ElectronicAddress] = LazyList(_electronic_addresses) """[ZBEX] Electronic addresses.""" def __eq__(self, other: Any) -> bool: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 8e1751acc..b92f7fe12 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -11,7 +11,7 @@ from typing import Optional, List, Callable, TYPE_CHECKING, Any from abc import ABCMeta -from zepben.ewb.boilerplate.collections.lazy_list import LazyList +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList if sys.version_info >= (3, 13): from warnings import deprecated else: @@ -84,12 +84,12 @@ def relay_info(self): def relay_info(self, relay_info: Optional[RelayInfo]): self.asset_info = relay_info - thresholds: LazyList[RelaySetting] = LazyList( + thresholds: LazyIndexList[RelaySetting] = LazyIndexList( _thresholds, "RelaySetting" ) - time_limits: LazyList[float] = LazyList( + time_limits: LazyIndexList[float] = LazyIndexList( _time_limits, "float" ) diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index be4bc42c2..c20302742 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyList +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject @@ -34,7 +34,7 @@ def __init__(self, *args, position_points=None, **kwargs): super(Location, self).__init__(*args, **kwargs) self.points.extend(position_points) - points: LazyList[PositionPoint] = LazyList( + points: LazyIndexList[PositionPoint] = LazyIndexList( _position_points, "PositionPoint", ) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index 493e42c4d..6fcda037d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -14,6 +14,7 @@ from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.collections.mrid_list import MridList from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -36,7 +37,7 @@ class ConnectivityNode(IdentifiedObject, WeakrefSlot): def __iter__(self): return iter(self._terminals) - terminals: MridCollection[Terminal] = LazyMridList( + terminals: MridCollection[Terminal] = MridList( _terminals, "A Terminal" ) diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 8440b2c94..a3cb235cf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -12,7 +12,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.lazy_list import LazyList +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint @@ -60,7 +60,7 @@ def diagram(self, diag): else: raise ValueError(f"diagram for {str(self)} has already been set to {self._diagram}, cannot reset this field to {diag}") - points: LazyList[DiagramObjectPoint] = LazyList( + points: LazyIndexList[DiagramObjectPoint] = LazyIndexList( _diagram_object_points, "DiagramObjectPoint", ) From 274de9ad4bcd2c762d04263e8925437101913e3e Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 15:45:05 +1000 Subject: [PATCH 20/54] Documentation (and other) tweaks Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/MANIFESTO.md | 18 +++++++++--------- .../collections/abstract_backed_list.py | 6 ++---- .../boilerplate/collections/lazy_index_list.py | 9 ++++++--- .../ewb/boilerplate/collections/lazy_list.py | 2 +- .../boilerplate/collections/lazy_mrid_list.py | 2 +- .../boilerplate/collections/lazy_mrid_map.py | 2 +- .../ewb/boilerplate/collections/mrid_list.py | 10 ++++++++++ .../ewb/boilerplate/collections/wrapper.py | 6 ++---- 8 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/zepben/ewb/boilerplate/MANIFESTO.md b/src/zepben/ewb/boilerplate/MANIFESTO.md index ea136d722..571788402 100644 --- a/src/zepben/ewb/boilerplate/MANIFESTO.md +++ b/src/zepben/ewb/boilerplate/MANIFESTO.md @@ -38,13 +38,13 @@ Since `@dataclass` generates an init for every subclass without one, the `@zb_da Positional args are very hard to get consistent with the use of `@dataclass` - to know the order, you must parse the inheritance tree. `@property` values are not included in dataclass inits by default, meaning that adding them as an argument would create arbitrary ordering, and that is a readability nightmare. Moreover, we have custom inits in subclasses, making maintaining the arg order functionally impossible. -That is why, from this version onwards, any argument that is not the object's `mrid` (or equivalent identifier) must be passed as a keyword argument (`MyClass("mrid", 42)` bad, `MyClass("mrid", thing=42)` good). +That is why, from this version onwards, any argument that is not the object's `mrid` (or equivalent identifier) must be passed as a keyword argument (`MyClass("mRID", 42)` bad, `MyClass("mRID", thing=42)` good). While it would be possible to bring args back if all subclass inits are removed (probable future feature), it is a terrible idea. Do not do it unless you are very sure of yourself. ## Eq, Hash, Str, Repr -CIM classes need to be homogenous in their behaviour. Normally, dataclasses define these four methods on subclass level, but we need them to be propagated all the way up to `Identifiable` (or any overriding children such as `NameType`), where `__eq__` and `__hash__` both asses memory reference equality, and `__str__` and `__repr__` represent the class as `{}` eg `Terminal{terminal_mrid}`. +CIM classes need to be homogenous in their behaviour. Normally, dataclasses define these four methods on subclass level, but we need them to be propagated all the way up to `Identifiable` (or any overriding children such as `NameType`), where `__eq__` and `__hash__` both asses memory reference equality, and `__str__` and `__repr__` represent the class as `{}` eg `Terminal{terminal_mRID}`. ## Descriptors @@ -58,20 +58,20 @@ Since CIM is UML-based, there are a lot of one-to-many and many-to-many class re Consequently, we have created a hierarchy of nullable descriptors backed by private fields. Each descriptor implements a list-like interface, allowing the user to interact with the relations normally, blissfully unaware of the nullable insanity going on under the hood. -Due to the high diversity of the relationship behaviours, there is a lot of optional functionality shared by some these lists: +Due to the high diversity of the relationship behaviours, there is a lot of optional functionality shared by some of these lists: -- *mRID collision checks*: for mrid collections, we perform an mRID lookup when an item is added. If another item comes back, we check if the identities match. If they do, we disregard the addition. If they don't, we throw an appropriate error. +- *mRID collision checks*: for mRID collections, we perform an mRID lookup when an item is added. If another item comes back, we check if the identities match. If they do, we disregard the addition. If they don't, we throw an appropriate error. - *backfill*: some items contained in lists link back to the owner item. In that case, we perform a backfill to do this automatically. We also ensure that the item is not already linked to another owner. -- *validation*: some lists are create with validation lambdas, calling a validator function on the owner itself. This is useful when additional validation is required, eg sequence number matching. +- *validation*: some lists are created with validation lambdas, calling a validator function on the owner itself. This is useful when additional validation is required, eg sequence number matching. - *sorting*: passing a sorting lambda to ordered collections automatically re-sorts the list after an item is added. Here are the concrete implementations of the lists that are used in the SDK: -- `LazyList`: A nullable list of non-`Identifiable` objects that has validation and sorting built in. +- `LazyList`: A nullable list of objects that has validation and sorting built in. Typically used for non-`Identifiable` relationships. - `LazyIndexList`: `LazyList` with index-based insertion and deletion, for strictly ordered relationships such as diagram points. -The remaining collections all implement `MridCollection` - an interface that declares functionality of interaction with a collection of `Identifiable` objects, mainly focusing on `get_by_mrid`, `append`, `remove`, and `clear` methods. This allows us to have a common way of interacting with inter-object relationships, while hiding the underlying implementation (eg `list` vs `dict`) +The remaining collections all implement `MridCollection` - an interface that declares functionality of interaction with a collection of `Identifiable` objects, mainly focusing on `get_by_mRID`, `append`, `remove`, and `clear` methods. This allows us to have a common way of interacting with inter-object relationships, while hiding the underlying implementation (eg `list` vs `dict`) -- `LazyMridList`: A `LazyList` with an mrid check and backfill added. `O(N)` mRID lookup. +- `LazyMridList`: A `LazyList` with an mRID check and backfill added. `O(N)` mRID lookup. - `MridList`: An mRID collection backed by a non-nullable list. Useful for relationships that are most likely to be filled (eg `ConnectivityNode.terminals`). `O(N)` mRID lookup. -- `LazyMridMap`: A nullable map of Identifiable objects keyed on their mRID. Supports mRID checks and `O(1)` mRID lookup. Does not support sorting (duh). Useful fot large-scale collections (for which the lookup speedup is significant) +- `LazyMridMap`: A nullable map of Identifiable objects keyed on their mRID. Supports mRID checks and `O(1)` mRID lookup. Does not support sorting (duh). Useful for large-scale collections (for which the lookup speedup is significant) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py index 3caa14d52..49f348e0b 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py @@ -24,10 +24,8 @@ def __getitem__(self, index: int) -> T: ... @overload - def __getitem__(self, index: int) -> T: ... - - @overload - def __getitem__(self, index: slice) -> Sequence[T]: ... + def __getitem__(self, index: slice) -> Sequence[T]: + ... def __getitem__(self, index: int | slice) -> T | Sequence[T]: collection = self._get_collection() diff --git a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py index e310a6c0b..421eac1a8 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py @@ -10,7 +10,7 @@ class LazyIndexList(LazyList[T]): """ Lazy collection with list-style index-based insertion and deletion. - It retains the nullable backing-list behaviour of ``LazyCollection``, + It retains the nullable backing-list behaviour of ``LazyList``, creating the backing list when an item is inserted and resetting it to ``None`` when the final item is deleted. @@ -34,8 +34,7 @@ def __init__( def insert(self, index: int, item: T) -> None: """ Insert an item into the collection at a given index. - Check for mRID collisions and run optional validation. - Sort the collection if key lambda is provided. + Run optional validation. """ size = len(self) @@ -86,3 +85,7 @@ def pop(self, index: int = -1) -> T: self.clear() return item + + def __delitem__(self, index: int, /) -> None: + """Remove the item at the given index.""" + self.remove(self[index]) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index e40248e95..c9cf9cec2 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -24,7 +24,7 @@ class LazyList(_IterableWrapper[T], AbstractBackedList[T]): class Container: _items = field(default=None) - items = LazyCollection(_items) + items = LazyList(_items) container = Container() diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py index d33a4b726..33c9f41b0 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py @@ -13,7 +13,7 @@ class LazyMridList(LazyList[S], MridCollection[S]): Nullable list implementation of :class:`MridCollection`. Inherits mRID lookup and uniqueness semantics from ``MridCollection`` and - lazy backing-list behavior from ``LazyCollection``. + lazy backing-list behavior from ``LazyList``. """ def __init__( self, diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py index c2d32a0d0..a5e5b36eb 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py @@ -32,7 +32,7 @@ class LazyMridMap(_IterableWrapper[S], MridCollection[S]): """ def __init__( self, - private_field: list[S] | None, + private_field: dict[str, S] | None, element_description: str, backfill: Backfill | None = None, validate=None diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 7fac3e6e4..b1764cead 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -13,6 +13,16 @@ class MridList(_IterableWrapper[S], AbstractBackedList[S], MridCollection[S]): + """ + mRID collection backed by a non-nullable list. + + Items are kept in insertion order and retrieved by mRID using a linear search. + Appending enforces mRID uniqueness and can optionally apply backfill, + validation, and sorting. + + Unlike ``LazyMridList``, clearing the collection leaves an empty backing list + rather than resetting the backing field to ``None``. + """ def __init__( self, private_field: list[S], diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py index 40bdcf683..a388be93b 100644 --- a/src/zepben/ewb/boilerplate/collections/wrapper.py +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -24,7 +24,7 @@ class _Wrapper(BackedDescriptor): class Container: _items = field(default=None) - items = LazyCollection(_items) + items = LazyList(_items) container = Container() @@ -37,7 +37,7 @@ class Container: Access through the class returns the original shared descriptor:: - assert isinstance(Container.items, LazyCollection) + assert isinstance(Container.items, LazyList) """ _instance: Any @@ -108,5 +108,3 @@ def __set__(self, instance, value) -> None: self.__get__(instance).extend(value) else: self.__get__(instance).clear() - - From c169433196d1a2450d7bb0315c1072f3019f415e Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 3 Aug 2026 12:11:29 +1000 Subject: [PATCH 21/54] Changelog and tests; backfill clear Signed-off-by: Boris Filin Signed-off-by: Boris Filin --- changelog.md | 1 + src/zepben/ewb/boilerplate/backfill.py | 12 +- .../test_abstract_backed_collection.py | 109 ++++++++ test/boilerplate/test_abstract_backed_list.py | 58 +++++ test/boilerplate/test_backfill.py | 92 +++++++ test/boilerplate/test_lazy_index_list.py | 136 ++++++++++ test/boilerplate/test_lazy_list.py | 146 +++++++++++ test/boilerplate/test_lazy_mrid_list.py | 88 +++++++ test/boilerplate/test_lazy_mrid_map.py | 175 +++++++++++++ test/boilerplate/test_mrid_collection.py | 86 +++++++ test/boilerplate/test_mrid_list.py | 238 ++++-------------- test/boilerplate/test_wrapper.py | 116 +++++++++ 12 files changed, 1067 insertions(+), 190 deletions(-) create mode 100644 test/boilerplate/test_abstract_backed_collection.py create mode 100644 test/boilerplate/test_abstract_backed_list.py create mode 100644 test/boilerplate/test_backfill.py create mode 100644 test/boilerplate/test_lazy_index_list.py create mode 100644 test/boilerplate/test_lazy_list.py create mode 100644 test/boilerplate/test_lazy_mrid_list.py create mode 100644 test/boilerplate/test_lazy_mrid_map.py create mode 100644 test/boilerplate/test_mrid_collection.py create mode 100644 test/boilerplate/test_wrapper.py diff --git a/changelog.md b/changelog.md index ee265bd01..a52f8189b 100644 --- a/changelog.md +++ b/changelog.md @@ -23,6 +23,7 @@ * Added `skip_install = true` to the lint environment to avoid installing package dependencies that aren't needed for linting. * Added E402 noqa comments for intentional mid-file imports used to avoid circular dependency issues in `dataclassy/dataclass.py`, `context_value_computer.py`, `queue_condition.py`, `direction_logger.py`, and `test_network_trace.py`. * Switched from using `dataclassy` to native dataclasses +* Every UML relationship is now a custom list wrapper that takes no memory and has a reference to the backing field - usage interface is nearly identical to lists ### Fixes * added python3.14 to compatibility list. diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py index 27df77c3f..c0cca6157 100644 --- a/src/zepben/ewb/boilerplate/backfill.py +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -21,7 +21,7 @@ def __init__(self, backfill_prop: property) -> None: self.backfill_prop = backfill_prop - def apply(self, element: S, owner: Any) -> None: + def _get_backing_name(self) -> str: name = self.backfill_prop.fget.__name__ target = getattr(self.backfill_prop.fget, "_internal_target", None) @@ -33,13 +33,21 @@ def apply(self, element: S, owner: Any) -> None: if backing_name is None: raise TypeError(f"Cannot determine backing name for {target!r}") + return backing_name + + def apply(self, element: S, owner: Any) -> None: + backing_name = self._get_backing_name() if getattr(element, backing_name) is None: setattr(element, backing_name, owner) ref = getattr(element, backing_name) if ref is not owner: - raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") + raise ValueError(f"{element} `{self.backfill_prop.fget.__name__}` property references {ref}, expected {owner}.") + + def clear(self, element: S) -> None: + backing_name = self._get_backing_name() + setattr(element, backing_name, None) def internal(target: Any) -> Callable[[F], F]: diff --git a/test/boilerplate/test_abstract_backed_collection.py b/test/boilerplate/test_abstract_backed_collection.py new file mode 100644 index 000000000..35306120f --- /dev/null +++ b/test/boilerplate/test_abstract_backed_collection.py @@ -0,0 +1,109 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection + + +class BackedCollection(AbstractBackedCollection[str]): + def __init__(self, items: list[str] | None = None): + self.items = items or [] + + def _get_collection(self) -> list[str]: + return self.items + + def append(self, item: str) -> None: + self.items.append(item) + + def remove(self, item: str) -> None: + self.items.remove(item) + + def clear(self) -> None: + self.items.clear() + + +def test_extend_appends_each_item_in_order(): + collection = BackedCollection(["first"]) + + collection.extend(["second", "third"]) + + assert collection.items == ["first", "second", "third"] + + +def test_extend_accepts_single_pass_iterable(): + collection = BackedCollection() + + collection.extend(item for item in ["first", "second"]) + + assert collection.items == ["first", "second"] + + +def test_extend_empty_iterable_does_nothing(): + collection = BackedCollection(["item"]) + + collection.extend([]) + + assert collection.items == ["item"] + + +def test_extend_none_does_nothing(): + collection = BackedCollection(["item"]) + + collection.extend(None) + + assert collection.items == ["item"] + + +def test_len_returns_backing_collection_size(): + collection = BackedCollection(["first", "second"]) + + assert len(collection) == 2 + + +def test_len_returns_zero_for_empty_backing_collection(): + assert len(BackedCollection()) == 0 + + +def test_iter_returns_items_in_backing_collection_order(): + collection = BackedCollection(["first", "second"]) + + assert list(collection) == ["first", "second"] + + +def test_iter_returns_no_items_for_empty_backing_collection(): + assert list(BackedCollection()) == [] + + +def test_contains_finds_item_in_backing_collection(): + collection = BackedCollection(["first", "second"]) + + assert "second" in collection + + +def test_contains_rejects_item_missing_from_backing_collection(): + collection = BackedCollection(["first", "second"]) + + assert "missing" not in collection + + +def test_for_each_indexed_visits_each_item_with_its_index(): + collection = BackedCollection(["first", "second"]) + visited: list[tuple[int, str]] = [] + + collection.for_each_indexed( + lambda index, item: visited.append((index, item)) + ) + + assert visited == [(0, "first"), (1, "second")] + + +def test_for_each_indexed_does_not_call_action_for_empty_collection(): + collection = BackedCollection() + visited: list[tuple[int, str]] = [] + + collection.for_each_indexed( + lambda index, item: visited.append((index, item)) + ) + + assert visited == [] diff --git a/test/boilerplate/test_abstract_backed_list.py b/test/boilerplate/test_abstract_backed_list.py new file mode 100644 index 000000000..ea086979d --- /dev/null +++ b/test/boilerplate/test_abstract_backed_list.py @@ -0,0 +1,58 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import pytest + +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList + + +class BackedList(AbstractBackedList[str]): + def __init__(self, items: list[str]): + self.items = items + + def _get_collection(self) -> list[str]: + return self.items + + def append(self, item: str) -> None: + self.items.append(item) + + def remove(self, item: str) -> None: + self.items.remove(item) + + def clear(self) -> None: + self.items.clear() + + +def test_getitem_returns_item_at_index(): + collection = BackedList(["first", "second"]) + + assert collection[1] == "second" + + +def test_getitem_supports_negative_index(): + collection = BackedList(["first", "second"]) + + assert collection[-1] == "second" + + +def test_getitem_raises_for_index_out_of_range(): + collection = BackedList(["item"]) + + with pytest.raises(IndexError): + collection[1] + + +def test_getitem_returns_backing_list_slice(): + collection = BackedList(["first", "second", "third"]) + + assert collection[1:] == ["second", "third"] + + +def test_getitem_supports_slice_step(): + collection = BackedList( + ["first", "second", "third", "fourth"] + ) + + assert collection[::2] == ["first", "third"] diff --git a/test/boilerplate/test_backfill.py b/test/boilerplate/test_backfill.py new file mode 100644 index 000000000..a57820c5a --- /dev/null +++ b/test/boilerplate/test_backfill.py @@ -0,0 +1,92 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.backed_descriptor import Alias +from zepben.ewb.boilerplate.backfill import Backfill, internal + + +@dataclass +class Container: + """Container exposing each supported kind of internal back-reference.""" + + field_backing: object | None = field(default=None) + descriptor_backing: object | None = field(default=None) + property_backing: object | None = field(default=None) + + descriptor = Alias(descriptor_backing) + + @property + @internal(field_backing) + def field_parent(self): + return self.field_backing + + @property + @internal(descriptor) + def descriptor_parent(self): + return self.descriptor + + @property + def internal_parent(self): + return self.property_backing + + @internal_parent.setter + def internal_parent(self, value): + self.property_backing = value + + @property + @internal(internal_parent) + def property_parent(self): + return self.internal_parent + + +# @dataclass populates Field.name after the automatic __set_name__ call. +Container.descriptor.__set_name__(Container, "descriptor") + + +@pytest.mark.parametrize( + ("property_name", "backing_name"), + [ + ("field_parent", "field_backing"), + ("descriptor_parent", "descriptor_backing"), + ("property_parent", "property_backing"), + ], +) +def test_apply_sets_back_reference( + property_name: str, + backing_name: str, +): + container = Container() + owner = object() + + Backfill(getattr(Container, property_name)).apply(container, owner) + + assert getattr(container, property_name) is owner + assert getattr(container, backing_name) is owner + + +@pytest.mark.parametrize( + ("property_name", "backing_name"), + [ + ("field_parent", "field_backing"), + ("descriptor_parent", "descriptor_backing"), + ("property_parent", "property_backing"), + ], +) +def test_clear_nulls_back_reference( + property_name: str, + backing_name: str, +): + owner = object() + container = Container() + setattr(container, backing_name, owner) + + Backfill(getattr(Container, property_name)).clear(container) + + assert getattr(container, property_name) is None + assert getattr(container, backing_name) is None diff --git a/test/boilerplate/test_lazy_index_list.py b/test/boilerplate/test_lazy_index_list.py new file mode 100644 index 000000000..1cf12ba68 --- /dev/null +++ b/test/boilerplate/test_lazy_index_list.py @@ -0,0 +1,136 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList + + +def record_validation(owner: Owner, item: str): + owner.events.append(item) + + +def reject_validation(_owner: Owner, _item: str): + raise ValueError("validation failed") + + +@dataclass +class Owner: + backing_items: list[str] | None = field(default=None) + events: list[str] = field(default_factory=list) + + items = LazyIndexList(backing_items, "Item") + validated_items = LazyIndexList( + backing_items, + "Item", + validate=record_validation, + ) + rejecting_items = LazyIndexList( + backing_items, + "Item", + validate=reject_validation, + ) + + +def test_insert_into_empty_creates_backing_list(): + owner = Owner() + + owner.items.insert(0, "item") + + assert owner.backing_items == ["item"] + + +def test_insert_places_item_at_requested_index(): + owner = Owner(["first", "third"]) + + owner.items.insert(1, "second") + + assert owner.backing_items == ["first", "second", "third"] + + +@pytest.mark.parametrize("index", [-1, 1]) +def test_insert_rejects_index_outside_valid_range(index: int): + owner = Owner() + + with pytest.raises( + ValueError, + match=rf"Sequence number {index} is invalid.*between 0 and 0", + ): + owner.items.insert(index, "item") + + assert owner.backing_items is None + + +def test_insert_validates_before_creating_backing_list(): + owner = Owner() + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_items.insert(0, "item") + + assert owner.backing_items is None + + +def test_insert_runs_validation(): + owner = Owner() + + owner.validated_items.insert(0, "item") + + assert owner.events == ["item"] + assert owner.backing_items == ["item"] + + +def test_append_inserts_at_end(): + owner = Owner(["first"]) + + owner.items.append("second") + + assert owner.backing_items == ["first", "second"] + + +def test_pop_returns_and_removes_requested_item(): + owner = Owner(["first", "second"]) + + popped = owner.items.pop(0) + + assert popped == "first" + assert owner.backing_items == ["second"] + + +def test_pop_uses_last_item_by_default_and_nulls_empty_backing(): + owner = Owner(["item"]) + + popped = owner.items.pop() + + assert popped == "item" + assert owner.backing_items is None + + +def test_pop_from_empty_raises(): + owner = Owner() + + with pytest.raises(IndexError, match="pop from empty list"): + owner.items.pop() + + assert owner.backing_items is None + + +def test_delete_removes_item_at_index(): + owner = Owner(["first", "second"]) + + del owner.items[0] + + assert owner.backing_items == ["second"] + + +def test_delete_last_item_nulls_backing_list(): + owner = Owner(["item"]) + + del owner.items[0] + + assert owner.backing_items is None diff --git a/test/boilerplate/test_lazy_list.py b/test/boilerplate/test_lazy_list.py new file mode 100644 index 000000000..a5c9c07e6 --- /dev/null +++ b/test/boilerplate/test_lazy_list.py @@ -0,0 +1,146 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_list import LazyList + + +def record_validation(owner: Owner, item: str): + owner.events.append(("validate", item)) + + +def reject_validation(_owner: Owner, _item: str): + raise ValueError("validation failed") + + +@dataclass +class Owner: + backing_items: list[str] | None = field( + default=None, + repr=False, + ) + events: list[tuple[str, str]] = field( + default_factory=list, + repr=False, + ) + + items = LazyList(backing_items) + validated_items = LazyList( + backing_items, + validate=record_validation, + ) + rejecting_validation_items = LazyList( + backing_items, + validate=reject_validation, + ) + sorted_items = LazyList( + backing_items, + sort_by=lambda item: item, + ) + + +def test_append_to_empty_creates_owner_backing_list(): + owner = Owner() + + owner.items.append("item") + + assert owner.backing_items == ["item"] + + +def test_append_adds_item_to_existing_owner_backing_list(): + owner = Owner() + owner.backing_items = ["first"] + + owner.items.append("second") + + assert owner.backing_items == ["first", "second"] + + +def test_append_runs_validation(): + owner = Owner() + + owner.validated_items.append("item") + + assert owner.events == [("validate", "item")] + assert owner.backing_items == ["item"] + + +def test_failed_append_to_empty_does_not_create_backing_list(): + owner = Owner() + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_items.append("item") + + assert owner.backing_items is None + + +def test_validation_failure_does_not_append_to_existing_list(): + owner = Owner() + owner.backing_items = ["existing"] + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_items.append("item") + + assert owner.backing_items == ["existing"] + + +def test_append_sorts_owner_backing_list(): + owner = Owner() + + owner.sorted_items.append("second") + owner.sorted_items.append("first") + + assert owner.backing_items == ["first", "second"] + + +def test_remove_removes_item_from_owner_backing_list(): + owner = Owner() + owner.backing_items = ["retained", "removed"] + + owner.items.remove("removed") + + assert owner.backing_items == ["retained"] + + +def test_remove_last_item_sets_owner_backing_list_to_null(): + owner = Owner() + owner.backing_items = ["item"] + + owner.items.remove("item") + + assert owner.backing_items is None + + +def test_clear_sets_owner_backing_list_to_null(): + owner = Owner() + owner.backing_items = ["first", "second"] + + owner.items.clear() + + assert owner.backing_items is None + + +def test_repr_matches_owner_backing_list_repr(): + owner = Owner() + owner.backing_items = ["first", "second"] + + assert owner.backing_items is not None + assert repr(owner.items) == repr(owner.backing_items) + + +def test_empty_repr_matches_empty_list_repr(): + owner = Owner() + + assert repr(owner.items) == repr([]) + assert owner.backing_items is None + + +def test_class_field_repr_uses_descriptor_repr() -> None: + assert repr(Owner.items) == object.__repr__(Owner.items) diff --git a/test/boilerplate/test_lazy_mrid_list.py b/test/boilerplate/test_lazy_mrid_list.py new file mode 100644 index 000000000..3874eb15b --- /dev/null +++ b/test/boilerplate/test_lazy_mrid_list.py @@ -0,0 +1,88 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList + + +@dataclass(eq=False) +class Item: + mrid: str + + +class RecordingBackfill: + def apply(self, item: Item, owner: Owner): + owner.events.append(("backfill", item.mrid)) + + +class RejectingBackfill: + def apply(self, _item: Item, _owner: Owner): + raise ValueError("backfill failed") + + +def record_validation(owner: Owner, item: Item): + owner.events.append(("validate", item.mrid)) + + +@dataclass +class Owner: + backing_items: list[Item] | None = field(default=None) + events: list[tuple[str, str]] = field(default_factory=list) + + items = LazyMridList(backing_items, "Item") + backfilled_items = LazyMridList( + backing_items, + "Item", + backfill=RecordingBackfill(), + validate=record_validation, + ) + rejecting_backfill_items = LazyMridList( + backing_items, + "Item", + backfill=RejectingBackfill(), + ) + + +def test_get_by_mrid_finds_item_in_backing_list(): + item = Item("item") + owner = Owner([item]) + + assert owner.items.get_by_mrid("item") is item + + +def test_get_by_mrid_treats_null_backing_as_empty(): + owner = Owner() + + with pytest.raises(KeyError, match="missing"): + owner.items.get_by_mrid("missing") + + assert owner.backing_items is None + + +def test_append_applies_backfill_before_superclass_validation(): + item = Item("item") + owner = Owner() + + owner.backfilled_items.append(item) + + assert owner.events == [ + ("backfill", "item"), + ("validate", "item"), + ] + assert owner.backing_items == [item] + + +def test_backfill_failure_does_not_call_superclass_append(): + owner = Owner() + + with pytest.raises(ValueError, match="backfill failed"): + owner.rejecting_backfill_items.append(Item("item")) + + assert owner.backing_items is None diff --git a/test/boilerplate/test_lazy_mrid_map.py b/test/boilerplate/test_lazy_mrid_map.py new file mode 100644 index 000000000..f6e073cfc --- /dev/null +++ b/test/boilerplate/test_lazy_mrid_map.py @@ -0,0 +1,175 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap + + +@dataclass(eq=False) +class Item: + mrid: str + + +class RecordingBackfill: + def apply(self, item: Item, owner: Owner): + owner.events.append(("backfill", item.mrid)) + + +class RejectingBackfill: + def apply(self, _item: Item, _owner: Owner): + raise ValueError("backfill failed") + + +def record_validation(owner: Owner, item: Item): + owner.events.append(("validate", item.mrid)) + + +def reject_validation(_owner: Owner, _item: Item): + raise ValueError("validation failed") + + +@dataclass +class Owner: + backing_items: dict[str, Item] | None = field(default=None) + events: list[tuple[str, str]] = field(default_factory=list) + + items = LazyMridMap(backing_items, "Item") + configured_items = LazyMridMap( + backing_items, + "Item", + backfill=RecordingBackfill(), + validate=record_validation, + ) + rejecting_backfill_items = LazyMridMap( + backing_items, + "Item", + backfill=RejectingBackfill(), + ) + rejecting_validation_items = LazyMridMap( + backing_items, + "Item", + validate=reject_validation, + ) + + +def test_append_to_empty_creates_map_keyed_by_mrid(): + item = Item("item") + owner = Owner() + + owner.items.append(item) + + assert owner.backing_items == {"item": item} + + +def test_append_to_existing_map_adds_mrid_entry(): + first = Item("first") + second = Item("second") + owner = Owner({"first": first}) + + owner.items.append(second) + + assert owner.backing_items == { + "first": first, + "second": second, + } + + +def test_append_runs_backfill_before_validation(): + item = Item("item") + owner = Owner() + + owner.configured_items.append(item) + + assert owner.events == [ + ("backfill", "item"), + ("validate", "item"), + ] + assert owner.backing_items == {"item": item} + + +def test_backfill_failure_does_not_create_map(): + owner = Owner() + + with pytest.raises(ValueError, match="backfill failed"): + owner.rejecting_backfill_items.append(Item("item")) + + assert owner.backing_items is None + + +def test_validation_failure_does_not_create_map(): + owner = Owner() + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_items.append(Item("item")) + + assert owner.backing_items is None + + +def test_lookup_and_indexing_use_mrid_keys(): + item = Item("item") + owner = Owner({"item": item}) + + assert owner.items.get_by_mrid("item") is item + assert owner.items["item"] is item + + with pytest.raises(KeyError, match="missing"): + owner.items.get_by_mrid("missing") + + with pytest.raises(KeyError, match="missing"): + owner.items["missing"] + + +def test_collection_view_exposes_map_values(): + first = Item("first") + second = Item("second") + owner = Owner({"first": first, "second": second}) + + assert len(owner.items) == 2 + assert list(owner.items) == [first, second] + assert first in owner.items + assert Item("first") not in owner.items + + +def test_remove_deletes_item_by_mrid(): + first = Item("first") + second = Item("second") + owner = Owner({"first": first, "second": second}) + + owner.items.remove(first) + + assert owner.backing_items == {"second": second} + + +def test_remove_last_item_nulls_backing_map(): + item = Item("item") + owner = Owner({"item": item}) + + owner.items.remove(item) + + assert owner.backing_items is None + + +def test_clear_nulls_backing_map(): + owner = Owner({"item": Item("item")}) + + owner.items.clear() + + assert owner.backing_items is None + + +def test_repr_matches_backing_map_repr(): + item = Item("item") + owner = Owner({"item": item}) + + assert repr(owner.items) == repr(owner.backing_items) + + +def test_class_field_repr_uses_descriptor_repr(): + assert repr(Owner.items) == object.__repr__(Owner.items) diff --git a/test/boilerplate/test_mrid_collection.py b/test/boilerplate/test_mrid_collection.py new file mode 100644 index 000000000..2ddd5d333 --- /dev/null +++ b/test/boilerplate/test_mrid_collection.py @@ -0,0 +1,86 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from dataclasses import dataclass + +import pytest + +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection + + +@dataclass +class Item: + mrid: str + + +class MridCollectionImpl(MridCollection[Item]): + def __init__(self, items: list[Item] | None = None): + self.items = items or [] + self.element_description = "Item" + self._instance = "owner" + + def _get_collection(self) -> list[Item]: + return self.items + + def _safe_get_by_mrid(self, mrid: str) -> Item | None: + return next( + (item for item in self.items if item.mrid == mrid), + None, + ) + + def append(self, item: Item) -> None: + if self._can_add_by_mrid(item): + self.items.append(item) + + def remove(self, item: Item) -> None: + self.items.remove(item) + + def clear(self) -> None: + self.items.clear() + + +def test_get_by_mrid_returns_matching_item(): + item = Item("item") + collection = MridCollectionImpl([item]) + + assert collection.get_by_mrid("item") is item + + +def test_get_by_mrid_raises_for_missing_item(): + collection = MridCollectionImpl() + + with pytest.raises(KeyError, match="missing"): + collection.get_by_mrid("missing") + + +def test_append_accepts_new_mrid(): + item = Item("item") + collection = MridCollectionImpl() + + collection.append(item) + + assert collection.items == [item] + + +def test_append_ignores_same_instance(): + item = Item("item") + collection = MridCollectionImpl([item]) + + collection.append(item) + + assert collection.items == [item] + + +def test_append_rejects_different_instance_with_same_mrid(): + existing = Item("duplicate") + collection = MridCollectionImpl([existing]) + + with pytest.raises( + ValueError, + match=r"Item with mRID duplicate already exists in owner", + ): + collection.append(Item("duplicate")) + + assert collection.items == [existing] diff --git a/test/boilerplate/test_mrid_list.py b/test/boilerplate/test_mrid_list.py index d9457f1fb..674bbe8b9 100644 --- a/test/boilerplate/test_mrid_list.py +++ b/test/boilerplate/test_mrid_list.py @@ -26,7 +26,7 @@ def apply(self, item: Feeder, owner: Owner): def record_backfill(owner: Owner, item: Feeder): - owner._events.append(("backfill", item.mrid)) + owner.events.append(("backfill", item.mrid)) def reject_backfill(_owner: Owner, _item: Feeder): @@ -34,202 +34,147 @@ def reject_backfill(_owner: Owner, _item: Feeder): def record_validation(owner: Owner, item: Feeder): - owner._events.append(("validate", item.mrid)) + owner.events.append(("validate", item.mrid)) def reject_validation(_owner: Owner, _item: Feeder): raise ValueError("validation failed") -def validate_for_combined_test(owner: Owner, item: Feeder): - record_validation(owner, item) - - if item.mrid == "invalid": - raise ValueError("invalid feeder") - - @dataclass class Owner: - _feeders: list[Feeder] = field( + backing_feeders: list[Feeder] = field( default_factory=list, repr=False, ) - _events: list[tuple[str, str]] = field( + events: list[tuple[str, str]] = field( default_factory=list, repr=False, ) feeders = MridList( - _feeders, + backing_feeders, "Feeder", ) backfilled_feeders = MridList( - _feeders, + backing_feeders, "Feeder", backfill=CallbackBackfill(record_backfill), ) rejecting_backfill_feeders = MridList( - _feeders, + backing_feeders, "Feeder", backfill=CallbackBackfill(reject_backfill), ) validated_feeders = MridList( - _feeders, + backing_feeders, "Feeder", validate=record_validation, ) rejecting_validation_feeders = MridList( - _feeders, + backing_feeders, "Feeder", validate=reject_validation, ) backfilled_validated_feeders = MridList( - _feeders, + backing_feeders, "Feeder", backfill=CallbackBackfill(record_backfill), validate=record_validation, ) sorted_feeders = MridList( - _feeders, + backing_feeders, "Feeder", sort_by=lambda feeder: feeder.mrid, ) - fully_configured_feeders = MridList( - _feeders, - "Feeder", - backfill=CallbackBackfill(record_backfill), - validate=validate_for_combined_test, - sort_by=lambda feeder: feeder.mrid, - ) -@pytest.fixture -def owner() -> Owner: +def test_append_adds_item_to_owner_backing_list(): owner = Owner() - - # All wrappers intentionally point at this same non-null backing list. - # Reset it and the callback log for each test. - owner._feeders.clear() - owner._events.clear() - - return owner - - -def test_append_adds_item_to_owner_backing_list(owner: Owner): feeder = Feeder("feeder") owner.feeders.append(feeder) - assert owner._feeders == [feeder] - - -def test_appending_same_instance_twice_only_adds_it_once( - owner: Owner, -): - feeder = Feeder("feeder") - - owner.feeders.append(feeder) - owner.feeders.append(feeder) - - assert owner._feeders == [feeder] + assert owner.backing_feeders == [feeder] -def test_appending_different_instance_with_same_mrid_raises( - owner: Owner, -): - existing = Feeder("duplicate") - owner.feeders.append(existing) - - with pytest.raises( - ValueError, - match=r"Feeder with mRID duplicate already exists", - ): - owner.feeders.append(Feeder("duplicate")) - - assert owner._feeders == [existing] - - -def test_rejected_duplicate_does_not_run_backfill_or_validation( - owner: Owner, -): - feeder = Feeder("feeder") - owner.backfilled_validated_feeders.append(feeder) - owner._events.clear() - - owner.backfilled_validated_feeders.append(feeder) - - assert owner._feeders == [feeder] - assert owner._events == [] - - -def test_append_applies_backfill(owner: Owner): +def test_append_applies_backfill(): + owner = Owner() feeder = Feeder("feeder") owner.backfilled_feeders.append(feeder) - assert owner._events == [("backfill", "feeder")] - assert owner._feeders == [feeder] + assert owner.events == [("backfill", "feeder")] + assert owner.backing_feeders == [feeder] -def test_backfill_failure_does_not_append_item(owner: Owner): +def test_backfill_failure_does_not_append_item(): + owner = Owner() + with pytest.raises(ValueError, match="backfill failed"): owner.rejecting_backfill_feeders.append(Feeder("feeder")) - assert owner._feeders == [] + assert owner.backing_feeders == [] -def test_append_runs_validation(owner: Owner): +def test_append_runs_validation(): + owner = Owner() feeder = Feeder("feeder") owner.validated_feeders.append(feeder) - assert owner._events == [("validate", "feeder")] - assert owner._feeders == [feeder] + assert owner.events == [("validate", "feeder")] + assert owner.backing_feeders == [feeder] + +def test_validation_failure_does_not_append_item(): + owner = Owner() -def test_validation_failure_does_not_append_item(owner: Owner): with pytest.raises(ValueError, match="validation failed"): owner.rejecting_validation_feeders.append(Feeder("feeder")) - assert owner._feeders == [] + assert owner.backing_feeders == [] -def test_append_runs_backfill_before_validation(owner: Owner): +def test_append_runs_backfill_before_validation(): + owner = Owner() feeder = Feeder("feeder") owner.backfilled_validated_feeders.append(feeder) - assert owner._events == [ + assert owner.events == [ ("backfill", "feeder"), ("validate", "feeder"), ] - assert owner._feeders == [feeder] + assert owner.backing_feeders == [feeder] -def test_append_sorts_owner_backing_list(owner: Owner): +def test_append_sorts_owner_backing_list(): + owner = Owner() second = Feeder("second") first = Feeder("first") owner.sorted_feeders.append(second) owner.sorted_feeders.append(first) - assert owner._feeders == [first, second] + assert owner.backing_feeders == [first, second] -def test_remove_removes_item_from_owner_backing_list( - owner: Owner, -): +def test_remove_removes_item_from_owner_backing_list(): + owner = Owner() retained = Feeder("retained") removed = Feeder("removed") - owner._feeders.extend([retained, removed]) + owner.backing_feeders.extend([retained, removed]) owner.feeders.remove(removed) - assert owner._feeders == [retained] + assert owner.backing_feeders == [retained] -def test_clear_empties_owner_backing_list(owner: Owner): - owner._feeders.extend( +def test_clear_empties_owner_backing_list(): + owner = Owner() + + owner.backing_feeders.extend( [ Feeder("first"), Feeder("second"), @@ -238,104 +183,21 @@ def test_clear_empties_owner_backing_list(owner: Owner): owner.feeders.clear() - assert owner._feeders == [] + assert owner.backing_feeders == [] + +def test_repr_matches_owner_backing_list_repr(): + owner = Owner() -def test_repr_matches_owner_backing_list_repr(owner: Owner): - owner._feeders.extend( + owner.backing_feeders.extend( [ Feeder("first"), Feeder("second"), ] ) - assert repr(owner.feeders) == repr(owner._feeders) + assert repr(owner.feeders) == repr(owner.backing_feeders) def test_class_field_repr_uses_descriptor_repr() -> None: assert repr(Owner.feeders) == object.__repr__(Owner.feeders) - - -def test_mrid_list_all_functionality_together(owner: Owner): - third = Feeder("third") - first = Feeder("first") - second = Feeder("second") - - owner.fully_configured_feeders.extend([third, first, second]) - - assert owner._feeders == [first, second, third] - assert len(owner.fully_configured_feeders) == 3 - assert list(owner.fully_configured_feeders) == [ - first, - second, - third, - ] - assert next(iter(owner.fully_configured_feeders)) is first - assert owner.fully_configured_feeders[0] is first - assert list(owner.fully_configured_feeders[1:]) == [ - second, - third, - ] - assert second in owner.fully_configured_feeders - assert ( - owner.fully_configured_feeders.get_by_mrid("second") - is second - ) - assert repr(owner.fully_configured_feeders) == repr( - owner._feeders - ) - - with pytest.raises(KeyError, match="missing"): - owner.fully_configured_feeders.get_by_mrid("missing") - - indexed: list[tuple[int, Feeder]] = [] - owner.fully_configured_feeders.for_each_indexed( - lambda index, item: indexed.append((index, item)) - ) - assert indexed == [ - (0, first), - (1, second), - (2, third), - ] - - events_before_duplicate = list(owner._events) - owner.fully_configured_feeders.append(first) - - assert owner._feeders == [first, second, third] - assert owner._events == events_before_duplicate - - with pytest.raises( - ValueError, - match=r"Feeder with mRID second already exists", - ): - owner.fully_configured_feeders.append(Feeder("second")) - - assert owner._feeders == [first, second, third] - assert owner._events == events_before_duplicate - - with pytest.raises(ValueError, match="invalid feeder"): - owner.fully_configured_feeders.append(Feeder("invalid")) - - assert owner._feeders == [first, second, third] - assert owner._events[-2:] == [ - ("backfill", "invalid"), - ("validate", "invalid"), - ] - - owner.fully_configured_feeders.remove(second) - - assert owner._feeders == [first, third] - - owner.fully_configured_feeders.clear() - - assert len(owner.fully_configured_feeders) == 0 - assert list(owner.fully_configured_feeders) == [] - assert owner._feeders == [] - assert owner._events[:6] == [ - ("backfill", "third"), - ("validate", "third"), - ("backfill", "first"), - ("validate", "first"), - ("backfill", "second"), - ("validate", "second"), - ] diff --git a/test/boilerplate/test_wrapper.py b/test/boilerplate/test_wrapper.py new file mode 100644 index 000000000..8fd3ba763 --- /dev/null +++ b/test/boilerplate/test_wrapper.py @@ -0,0 +1,116 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from dataclasses import dataclass, field + +import pytest + +# noinspection PyProtectedMember +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper, _Wrapper + + +class RecordingWrapper(_Wrapper): + def __init__(self, private_field, marker: str): + super().__init__(private_field) + self.marker = marker + + @property + def bound_instance(self): + return self._instance + + @property + def backing_name(self): + return self._backing_name + + +class ListWrapper(_IterableWrapper[str]): + def _get_collection(self) -> list[str]: + return getattr(self._instance, self._backing_name) + + def append(self, item: str) -> None: + self._get_collection().append(item) + + def remove(self, item: str) -> None: + self._get_collection().remove(item) + + def clear(self) -> None: + self._get_collection().clear() + + @property + def bound_instance(self): + return self._instance + + +@dataclass +class Owner: + backing_items: list[str] = field(default_factory=list) + + wrapped = RecordingWrapper(backing_items, marker="marker") + items = ListWrapper(backing_items) + + +# @dataclass populates Field.name after the automatic __set_name__ call. +Owner.wrapped.__set_name__(Owner, "wrapped") +Owner.items.__set_name__(Owner, "items") + + +def test_class_access_returns_shared_descriptor(): + assert isinstance(Owner.wrapped, RecordingWrapper) + + +def test_instance_access_returns_bound_copy_with_init_arguments(): + owner = Owner() + + first = owner.wrapped + second = owner.wrapped + + assert first is not Owner.wrapped + assert first is not second + assert first.marker == "marker" + assert first.bound_instance is owner + assert first.backing_name == "backing_items" + + +def test_fget_is_named_after_descriptor_and_returns_bound_wrapper(): + owner = Owner() + + wrapper = Owner.items.fget(owner) + + assert Owner.items.fget.__name__ == "items" + assert Owner.items.fget.__qualname__ == "Owner.items" + assert wrapper.bound_instance is owner + + +def test_assigning_iterable_to_empty_backing_extends_collection(): + owner = Owner() + + owner.items = ["first", "second"] + + assert owner.backing_items == ["first", "second"] + + +def test_assigning_none_to_empty_backing_keeps_it_empty(): + owner = Owner() + + owner.items = None + + assert owner.backing_items == [] + + +def test_assigning_to_non_empty_backing_raises(): + owner = Owner(["existing"]) + + with pytest.raises(ValueError, match="currently non-empty"): + owner.items = ["new"] + + assert owner.backing_items == ["existing"] + + +def test_assignment_resolves_missing_default_before_extending(): + owner = Owner.__new__(Owner) + + Owner.items.__set__(owner, ["item"]) + + assert owner.backing_items == ["item"] From c78d3ec2a54871e9c64969ba32a83251346eedde Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 3 Aug 2026 12:16:18 +1000 Subject: [PATCH 22/54] Changelog and backref clear Signed-off-by: Boris Filin --- changelog.md | 2 ++ src/zepben/ewb/boilerplate/collections/mrid_list.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/changelog.md b/changelog.md index a52f8189b..93e266fff 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ * `EnergySource.phases` * `PowerElectronicsConnection.phases` * ContactDetails are now Identifiable and no longer have default id generation. The constructor now requires a string. The `id` field is deprecated, to be replaced with mrid. +* Removing items with backref from a containing object's list automatically clears the backref field on the item - it is not accessible after removal. ### New Features * Added a `lint` tox environment that runs `ruff check .` to enforce code quality standards. The test environments now depend on lint passing first, so CI will fail if any new lint violations are introduced. @@ -24,6 +25,7 @@ * Added E402 noqa comments for intentional mid-file imports used to avoid circular dependency issues in `dataclassy/dataclass.py`, `context_value_computer.py`, `queue_condition.py`, `direction_logger.py`, and `test_network_trace.py`. * Switched from using `dataclassy` to native dataclasses * Every UML relationship is now a custom list wrapper that takes no memory and has a reference to the backing field - usage interface is nearly identical to lists +* UML relationships can now automatically set backref values where needed (eg `Cut.ac_line_segment`). They also clear the backref field when the item is removed. ### Fixes * added python3.14 to compatibility list. diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index b1764cead..971ad9226 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -79,8 +79,13 @@ def append(self, item: S) -> None: def remove(self, item: S) -> None: self._backing_list.remove(item) + if self.backfill is not None: + self.backfill.clear(item) def clear(self) -> None: + if self.backfill is not None: + for item in self._backing_list: + self.backfill.clear(item) self._backing_list.clear() def __repr__(self) -> str: From c4cf707abb017e2c302620f32d77f5c90722896d Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 3 Aug 2026 12:51:12 +1000 Subject: [PATCH 23/54] parity test Signed-off-by: Boris Filin --- test/boilerplate/test_mrid_collections.py | 77 +++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 test/boilerplate/test_mrid_collections.py diff --git a/test/boilerplate/test_mrid_collections.py b/test/boilerplate/test_mrid_collections.py new file mode 100644 index 000000000..7dee097af --- /dev/null +++ b/test/boilerplate/test_mrid_collections.py @@ -0,0 +1,77 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.collections.mrid_list import MridList + + +@dataclass(eq=False) +class Item: + mrid: str + + +@dataclass +class Container: + list_backing: list[Item] = field(default_factory=list) + lazy_list_backing: list[Item] | None = field(default=None) + lazy_map_backing: dict[str, Item] | None = field(default=None) + + list_items = MridList(list_backing, "Item") + lazy_list_items = LazyMridList(lazy_list_backing, "Item") + lazy_map_items = LazyMridMap(lazy_map_backing, "Item") + + +def assert_public_state( + collections: tuple[MridCollection[Item], ...], + expected: list[Item], +): + for collection in collections: + assert len(collection) == len(expected) + assert list(collection) == expected + + for item in expected: + assert item in collection + assert collection.get_by_mrid(item.mrid) is item + + +def test_mrid_collections_have_the_same_public_effects(): + container = Container() + collections = ( + container.list_items, + container.lazy_list_items, + container.lazy_map_items, + ) + first = Item("first") + second = Item("second") + + for collection in collections: + collection.append(first) + collection.append(second) + + assert_public_state(collections, [first, second]) + + for collection in collections: + collection.remove(first) + + assert_public_state(collections, [second]) + for collection in collections: + assert first not in collection + with pytest.raises(KeyError, match="first"): + collection.get_by_mrid("first") + + for collection in collections: + collection.clear() + + assert_public_state(collections, []) + for collection in collections: + assert second not in collection + with pytest.raises(KeyError, match="second"): + collection.get_by_mrid("second") From 3d72f9225ed671c980ce2dabda739609fc0a9cf9 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 3 Aug 2026 13:27:14 +1000 Subject: [PATCH 24/54] field init fix in dataclassbase Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/dataclass_base.py | 9 ++++++- .../test_dataclass_base.py | 24 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index 82bb2a2b3..b09f4e064 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -139,8 +139,9 @@ def __init__(self, **kwargs) -> None: if callable(getattr(self, "__post_init__", None)): raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") + field_dict = {f.name: f for f in fields(type(self))} # Manually assign defaults in dataclass fields - for f in fields(type(self)): + for f in field_dict.values(): # We cannot just check kwargs because fields could be set in subclass __init__'s if _is_set(self, f.name) or f.name in kwargs: continue @@ -150,4 +151,10 @@ def __init__(self, **kwargs) -> None: # Assign all of the kwargs manually. # str-based setattr triggers descriptors, allowing us to intercept __set__. for attr, value in kwargs.items(): + field = field_dict.get(attr) + if field is not None and not field.init: + raise TypeError( + f"{type(self).__name__}.__init__() got an " + f"unexpected keyword argument {attr!r}" + ) setattr(self, attr, value) diff --git a/test/dataclass_descriptors/test_dataclass_base.py b/test/dataclass_descriptors/test_dataclass_base.py index ebc954a1b..a453d2e24 100644 --- a/test/dataclass_descriptors/test_dataclass_base.py +++ b/test/dataclass_descriptors/test_dataclass_base.py @@ -32,6 +32,11 @@ class Child(Root): dc_default_factory: List[int] = field(default_factory=lambda : [33]) +@zb_dataclass +class InitFalseFields(DataclassBase): + with_default: int = field(default=42, init=False) + + def test_dataclass_base(): mrid = "CLARKSOOOOOOOON" @@ -72,3 +77,22 @@ def test_dataclass_base(): other = Child("mrid2", y=42) other.dc_default_factory.append(24) assert obj.dc_default_factory == [33] + + +def test_init_false_field_uses_default(): + obj = InitFalseFields() + + assert obj.with_default == 42 + + +def test_init_false_field_cannot_be_passed_to_constructor(): + with pytest.raises( + TypeError, + match="unexpected keyword argument 'with_default'", + ): + InitFalseFields(with_default=24) + + +def test_unknown_constructor_value_is_rejected_by_slots(): + with pytest.raises(AttributeError, match="unknown"): + Child("mrid", y=33, unknown=24) From 16541b4a31048eaa6423f8f809c8a350d3d5ea1c Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Wed, 8 Jul 2026 17:35:45 +1000 Subject: [PATCH 25/54] Implement list fixes Signed-off-by: Boris Filin # Conflicts: # src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py # src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py # src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py # test/dataclass_descriptors/test_dataclass_base.py # test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py # Conflicts: # src/zepben/ewb/model/cim/iec61970/base/core/feeder.py # src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py # src/zepben/ewb/model/cim/iec61970/base/core/substation.py --- .../ewb/boilerplate/backed_descriptor.py | 92 +---- src/zepben/ewb/boilerplate/dataclass_base.py | 130 ++++-- src/zepben/ewb/boilerplate/lazy_list.py | 193 +++++++++ src/zepben/ewb/boilerplate/mrid_list.py | 195 +++++++++ src/zepben/ewb/boilerplate/mrid_map.py | 145 +++++++ src/zepben/ewb/boilerplate/tst.py | 162 ++++++++ .../sqlite/network/network_database_reader.py | 2 +- .../metering/pan_demand_reponse_function.py | 4 - .../extensions/iec61970/base/feeder/loop.py | 225 ++++------- .../iec61970/base/feeder/lv_feeder.py | 252 ++++-------- .../iec61970/base/feeder/lv_substation.py | 256 +++++------- .../protection/protection_relay_function.py | 201 ++++------ .../protection/protection_relay_scheme.py | 78 ++-- .../protection/protection_relay_system.py | 78 ++-- .../assetinfo/power_transformer_info.py | 80 ++-- .../assetinfo/transformer_tank_info.py | 80 ++-- .../ewb/model/cim/iec61968/assets/asset.py | 138 +++---- .../model/cim/iec61968/customers/customer.py | 77 ++-- .../iec61968/customers/customer_agreement.py | 82 ++-- .../iec61968/customers/pricing_structure.py | 76 ++-- .../iec61968/infiec61968/infassets/pole.py | 76 ++-- .../iec61968/metering/controlled_appliance.py | 2 +- .../model/cim/iec61968/metering/end_device.py | 153 +++---- .../cim/iec61968/metering/usage_point.py | 183 ++++----- .../operations/operational_restriction.py | 76 ++-- .../base/auxiliaryequipment/sensor.py | 78 ++-- .../ewb/model/cim/iec61970/base/core/curve.py | 165 ++++---- .../model/cim/iec61970/base/core/equipment.py | 290 +++++--------- .../iec61970/base/core/equipment_container.py | 149 +++---- .../model/cim/iec61970/base/core/feeder.py | 374 ++++++------------ .../iec61970/base/core/geographical_region.py | 65 ++- .../ewb/model/cim/iec61970/base/core/name.py | 4 +- .../model/cim/iec61970/base/core/name_type.py | 5 +- .../base/core/power_system_resource.py | 76 ++-- .../base/core/sub_geographical_region.py | 64 +-- .../cim/iec61970/base/core/substation.py | 275 +++++-------- .../iec61970/base/diagramlayout/diagram.py | 100 ++--- .../generation/production/battery_unit.py | 110 +++--- .../iec61970/base/wires/ac_line_segment.py | 250 +++++------- .../base/wires/ac_line_segment_phase.py | 5 - .../iec61970/base/wires/energy_consumer.py | 65 ++- .../base/wires/energy_consumer_phase.py | 5 - .../cim/iec61970/base/wires/energy_source.py | 62 ++- .../base/wires/energy_source_phase.py | 5 - .../base/wires/per_length_phase_impedance.py | 127 +++--- .../wires/power_electronics_connection.py | 192 ++++----- .../iec61970/base/wires/power_transformer.py | 170 ++++---- .../iec61970/base/wires/protected_switch.py | 82 ++-- .../iec61970/base/wires/regulating_cond_eq.py | 4 - .../iec61970/base/wires/regulating_control.py | 77 ++-- .../base/wires/synchronous_machine.py | 83 ++-- .../iec61970/base/wires/transformer_end.py | 5 - .../iec61970/infiec61970/feeder/circuit.py | 144 +++---- .../test_basic_descriptor.py | 8 +- .../test_dataclass_base.py | 24 +- .../test_network_trace_step_path_provider.py | 1 + 56 files changed, 2735 insertions(+), 3365 deletions(-) create mode 100644 src/zepben/ewb/boilerplate/lazy_list.py create mode 100644 src/zepben/ewb/boilerplate/mrid_list.py create mode 100644 src/zepben/ewb/boilerplate/mrid_map.py create mode 100644 src/zepben/ewb/boilerplate/tst.py diff --git a/src/zepben/ewb/boilerplate/backed_descriptor.py b/src/zepben/ewb/boilerplate/backed_descriptor.py index 1ecf6d965..6b4afbb91 100644 --- a/src/zepben/ewb/boilerplate/backed_descriptor.py +++ b/src/zepben/ewb/boilerplate/backed_descriptor.py @@ -12,7 +12,6 @@ from dataclasses import Field from typing import TypeVar -T = TypeVar("T", bound=type) @@ -24,89 +23,26 @@ class BackedDescriptor: """ def __init__(self, private_field): - if not isinstance(private_field, Field): - raise TypeError("private_field parameter of the Descriptor constructor has to be an instance of dataclass Field.") - self.private_field: Field = private_field + if not isinstance(private_field, Field) and not isinstance(private_field, BackedDescriptor): + raise TypeError(f"private_field parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {private_field}") + self.private_field: Field | 'BackedDescriptor' = private_field + self.backing_name: str | None = None self.name = None def __set_name__(self, owner, name): - self.name = name + if name is None: + return + self.__name__ = self.name = name + if not self.backing_name: + self.backing_name = self.private_field.name def __get__(self, instance, _): - return getattr(instance, self.private_field.name) + return getattr(instance, self.backing_name) def __set__(self, instance, value): - return setattr(instance, self.private_field.name, value) + return setattr(instance, self.backing_name, value) +Alias = BackedDescriptor - -def remove_descriptor_annotations(cls: T) -> T: - """ - Remove annotations for class attributes that are data descriptors. - - This is intended for descriptor attributes that should remain class-level - descriptors (classes defining ``__get__``/``__set__`` methods, controlling attribute access), - rather than becoming dataclass fields/slots. Dataclasses decide - which fields to create from ``__annotations__``. By removing annotations for - descriptor-backed attributes before ``@dataclass`` runs, those attributes are - left alone and can continue to behave as descriptors. - - Use this decorator below ``@dataclass`` so that it is called first. Python - applies decorators from the bottom up:: - - @dataclass - @remove_descriptor_annotations - class MyClass: - _x: int = field(default=0) - - x: int = MyDescriptor("_x") - - In the example above, ``x`` is annotated, but its class value is a descriptor. - Without ``remove_descriptor_annotations``, ``@dataclass`` would treat ``x`` as - a dataclass field and may try to include it in generated fields, slots, init, - repr, etc. With this decorator, the annotation for ``x`` is removed before - dataclass processing, while normal fields such as ``_x`` are left intact. - - Dataclass ``Field`` instances are themselves descriptors and are thus skipped. - In the example above, the slot for ``_x`` is still created. - - Before decoration:: - - MyClass.__annotations__ == { - "_x": int, - "x": int, - } - - After ``remove_descriptor_annotations`` runs:: - - MyClass.__annotations__ == { - "_x": int, - } - - The class is then passed to ``@dataclass`` with only real dataclass fields - remaining in ``__annotations__``. - """ - # Get editable annotations - original_annotations = dict(getattr(cls, "__annotations__", {})) - tweaked_annotations = dict(original_annotations) - - # Get the current values of class fields. Before @dataclass is run, we see Descriptor instances here - cls_dict = vars(cls) - - for name in list(tweaked_annotations): - try: - value = cls_dict[name] - # Skip dataclass fields - they need annotations - if isinstance(value, Field): - continue - # Any descriptor needs to implement get or set - most likely both. - if hasattr(value, "__get__") or hasattr(value, "__set__"): - tweaked_annotations.pop(name) - # Values without defaults will error out - definitely not descriptors - except KeyError: - pass - - # Update annotations on the class - cls.__annotations__ = tweaked_annotations - - return cls +# Alias for dataclass with params. Makes it easier to edit params for all of CIM at once. +# zb_dataclass = dataclass(init=False, eq=False, slots=True, repr=False) diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index 58067ae4a..ceed79f33 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -2,7 +2,8 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from dataclasses import dataclass, fields, MISSING +from dataclasses import dataclass, fields, MISSING, Field +from typing import TypeVar def _is_set(obj: object, name: str) -> bool: @@ -24,44 +25,121 @@ class DataclassBase: For more motivation, refer to `MANIFESTO.md` """ def __init__(self, **kwargs) -> None: - # Assign all of the kwargs manually. - # str-based setattr triggers descriptors, allowing us to intercept __set__. - for attr, value in kwargs.items(): - setattr(self, attr, value) # Manually assign defaults in dataclass fields for f in fields(type(self)): # We cannot just check kwargs because fields could be set in subclass __init__'s - if _is_set(self, f.name): + if _is_set(self, f.name) or f.name in kwargs: continue - # Set field defaults - if f.default is not MISSING: - setattr(self, f.name, f.default) - elif f.default_factory is not MISSING: - setattr(self, f.name, f.default_factory()) + resolve_default(self, f) - # Ignore custom descriptors - elif hasattr(f, '__get__'): - continue + # Assign all of the kwargs manually. + # str-based setattr triggers descriptors, allowing us to intercept __set__. + for attr, value in kwargs.items(): + setattr(self, attr, value) - # Mimic default Python missing arg error - else: - raise TypeError( - f"Missing required field {f.name!r} " - f"for {type(self).__name__}" - ) # Currently post init implementation requires a lot of extra logic, # which would mess with readability. If you require it - add it. if callable(getattr(self, "__post_init__", None)): raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") +def resolve_default(instance, field: Field): + # Set field defaults + if field.default is not MISSING: + setattr(instance, field.name, field.default) + elif field.default_factory is not MISSING: + setattr(instance, field.name, field.default_factory()) + + # Ignore custom descriptors + elif hasattr(field, '__get__'): + return + + # Mimic default Python missing arg error + else: + raise TypeError( + f"Missing required field {field.name!r} " + f"for {type(instance).__name__}" + ) + +T = TypeVar("T", bound=type) + +def remove_descriptor_annotations(cls: T) -> T: + """ + Remove annotations for class attributes that are data descriptors. + + This is intended for descriptor attributes that should remain class-level + descriptors (classes defining ``__get__``/``__set__`` methods, controlling attribute access), + rather than becoming dataclass fields/slots. Dataclasses decide + which fields to create from ``__annotations__``. By removing annotations for + descriptor-backed attributes before ``@dataclass`` runs, those attributes are + left alone and can continue to behave as descriptors. + + Use this decorator below ``@dataclass`` so that it is called first. Python + applies decorators from the bottom up:: + + @dataclass + class MyClass: + _x: int = field(default=0) -# Alias for dataclass with params. Makes it easier to edit params for all of CIM at once. -zb_dataclass = dataclass(init=False, eq=False, slots=True, repr=False) -""" -Shorthand alias for ``@dataclass(init=False,eq=False,slots=True,repr=False)`` -Allows us to modify dataclass parameters for all of CIM from a single reference point -""" + x: int = MyDescriptor("_x") + In the example above, ``x`` is annotated, but its class value is a descriptor. + Without ``remove_descriptor_annotations``, ``@dataclass`` would treat ``x`` as + a dataclass field and may try to include it in generated fields, slots, init, + repr, etc. With this decorator, the annotation for ``x`` is removed before + dataclass processing, while normal fields such as ``_x`` are left intact. + + Dataclass ``Field`` instances are themselves descriptors and are thus skipped. + In the example above, the slot for ``_x`` is still created. + + Before decoration:: + + MyClass.__annotations__ == { + "_x": int, + "x": int, + } + + After ``remove_descriptor_annotations`` runs:: + + MyClass.__annotations__ == { + "_x": int, + } + + The class is then passed to ``@dataclass`` with only real dataclass fields + remaining in ``__annotations__``. + """ + # Get editable annotations + original_annotations = dict(getattr(cls, "__annotations__", {})) + tweaked_annotations = dict(original_annotations) + + # Get the current values of class fields. Before @dataclass is run, we see Descriptor instances here + cls_dict = vars(cls) + + for name in list(tweaked_annotations): + try: + value = cls_dict[name] + # Skip dataclass fields - they need annotations + if isinstance(value, Field): + continue + # Any descriptor needs to implement get or set - most likely both. + if hasattr(value, "__get__") or hasattr(value, "__set__"): + tweaked_annotations.pop(name) + # Values without defaults will error out - definitely not descriptors + except KeyError: + pass + + # Update annotations on the class + cls.__annotations__ = tweaked_annotations + + return cls + + +def zb_dataclass(cls: type[object]): + """ + Shorthand alias for ``@dataclass(init=False,eq=False,slots=True,repr=False)`` + Allows us to modify dataclass parameters for all of CIM from a single reference point + """ + cls = remove_descriptor_annotations(cls) + return dataclass(init=False, eq=False, slots=True, repr=False)(cls) diff --git a/src/zepben/ewb/boilerplate/lazy_list.py b/src/zepben/ewb/boilerplate/lazy_list.py new file mode 100644 index 000000000..c4980e993 --- /dev/null +++ b/src/zepben/ewb/boilerplate/lazy_list.py @@ -0,0 +1,193 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC +from collections.abc import Collection +from dataclasses import field +from typing import TypeVar, Protocol, Any, Iterable + +from typing_extensions import deprecated, Self + +from zepben.ewb import BackedDescriptor, remove_descriptor_annotations, resolve_default, DataclassBase, zb_dataclass + + +# from descriptor_fix import BackedDescriptor, remove_descriptor_annotations + + +class HasMrid(Protocol): + mrid: str + +T = TypeVar("T") +S = TypeVar("S", bound=HasMrid) + +class MutableCollection(Collection[T], Protocol[T]): + def append(self, item: T) -> None: ... + + def remove(self, item: T) -> None: ... + + def clear(self) -> None: ... + + def extend(self, items: Collection[T]) -> None: + for item in items: + self.append(item) + + def __next__(self): + # TODO: Remove test hack in separate PR + return next(iter(self)) + + def __eq__(self, other): + return isinstance(other, Iterable) and all(a == b for a, b in zip(self, other)) + + +class IndexedMutableCollection(MutableCollection, ABC): + def __getitem__(self, idx: int) -> T: ... + + + +class _Wrapper(BackedDescriptor): + + instance: Any + backing_name: Any + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + """ + This __new__ stores the init arguments, and otherwise acts as default. + We need this to efficiently and truly re-create the wrapper and bind it to an instance upon a get call. + """ + obj = super().__new__(cls) + obj._init_args = args + obj._init_kwargs = kwargs.copy() + return obj + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.instance = None + self.backing_name = None + + def __get__(self, instance, _=None) -> Self: + """ + This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. + From there on, the wrapper functions as a lazy list. + """ + if instance is None: + return self + obj = type(self)(*self._init_args, **self._init_kwargs) + obj.instance = instance + obj.backing_name = self.private_field.name + return obj + +class _WrapperFgetFix(_Wrapper): + """ + This class exists to fix the tests that rely on the old lists being @property. + TODO: Remove in a separate PR fixing tests + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + def fget(instance): + return self.__get__(instance) + self.fget = fget + + def __set_name__(self, owner, name): + super().__set_name__(owner, name) + self.fget.__name__ = name + self.fget.__qualname__ = f"{owner.__qualname__}.{name}" + + @deprecated("Lists are no longer a property") + def fget(self, instance): ... + +class _IterableWrapper(_WrapperFgetFix, IndexedMutableCollection, ABC): + """ + This class allows us to assign lists at init time to avoid special-case handling. + """ + def __set__(self, instance, value): + if instance is None: + return + if not hasattr(instance, self.backing_name): + resolve_default(instance, self.private_field) + elif getattr(instance, self.backing_name): + raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") + if value is not None: + self.__get__(instance).extend(value) + else: + self.__get__(instance).clear() + +class LazyValidatedList(_IterableWrapper, IndexedMutableCollection[T]): + def __init__(self, + private_field, + validate=None, + sort_by=None): + super().__init__(private_field) + self.validate = validate + self.sort_by = sort_by + + def _get(self): + return getattr(self.instance, self.backing_name) + + def _get_or_empty(self) -> list[T]: + return getattr(self.instance, self.backing_name) or [] + + # clearIfEmpty is inlined for performance + + def append(self, item): + if self.validate is not None: + self.validate(self.instance, item) + + existing = getattr(self.instance, self.backing_name) + if existing is None: + existing = [item] + setattr(self.instance, self.backing_name, existing) + else: + existing.append(item) + + if self.sort_by is not None: + existing.sort(key=self.sort_by) + + def __len__(self): + return len(self._get_or_empty()) + + def __iter__(self): + return iter(self._get_or_empty()) + + def __contains__(self, item): + return item in (self._get_or_empty()) + + def remove(self, item): + existing = self._get_or_empty() + existing.remove(item) + if not existing: + self.clear() + + def clear(self): + setattr(self.instance, self.backing_name, None) + + def __repr__(self): + if self.instance is None: + return self.__class__.__repr__() + return str(self._get_or_empty()) + + def __str__(self): + return self.__repr__() + + def __getitem__(self, item): + return (self._get_or_empty())[item] + + +if __name__ == '__main__': + @zb_dataclass + class C(DataclassBase): + _xs: list[int] | None = field(default=None) + xs: IndexedMutableCollection[int] = LazyValidatedList( + _xs, + validate=lambda self, it: print("validate", it), + sort_by=lambda it: -it + ) + + c = C(xs=[24, 42]) + print(c._xs) + c.xs.clear() + print(c._xs) + c.xs.append(1) + c.xs.append(2) + c.xs.append(3) + print(c._xs) diff --git a/src/zepben/ewb/boilerplate/mrid_list.py b/src/zepben/ewb/boilerplate/mrid_list.py new file mode 100644 index 000000000..2774ce025 --- /dev/null +++ b/src/zepben/ewb/boilerplate/mrid_list.py @@ -0,0 +1,195 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import abstractmethod, ABC +from dataclasses import dataclass, field, Field +from typing import Any, TypeVar, Protocol + +import pytest +from typing_extensions import Self + +from zepben.ewb import remove_descriptor_annotations, BackedDescriptor +from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList, MutableCollection, IndexedMutableCollection, _IterableWrapper + + +class HasMrid(Protocol): + mrid: str + +T = TypeVar("T") +S = TypeVar("S", bound=HasMrid) + +class MridCollection(MutableCollection[S], ABC): + instance: Any + element_description: str + + @abstractmethod + def _safe_get_by_mrid(self, mrid: str) -> S | None: ... + + def get_by_mrid(self, mrid: str) -> S: + res = self._safe_get_by_mrid(mrid) + if res is None: + raise KeyError + return res + + def _can_add_by_mrid(self, element: S) -> bool: + existing = self._safe_get_by_mrid(element.mrid) + + if existing is None: + return True + + if existing is not element: + raise ValueError( + f"{self.element_description} with mRID {element.mrid} " + f"already exists in {self.instance}." + ) + + return False + + +class LazyMridList(LazyValidatedList, MridCollection[S]): + def __init__(self, + private_field, + element_description: str, + validate=None, + sort_by=None): + super().__init__(private_field, validate, sort_by) + self.element_description = element_description + + def _safe_get_by_mrid(self, mrid: str) -> S | None: + existing = self._get() + if existing is None: + return None + found = next((element for element in existing if element.mrid == mrid), None) + return found + + def append(self, item: T): + if not self._can_add_by_mrid(item): + return + + super().append(item) + + +class MridList(_IterableWrapper, IndexedMutableCollection[S], MridCollection[S]): + def __init__(self, + private_field, + element_description: str, + validate=None, + sort_by=None): + super().__init__(private_field) + self.element_description = element_description + self.validate = validate + self.sort_by = sort_by + self.backing_list = None + + def __get__(self, instance, owner=None) -> Self: + obj = super().__get__(instance, owner) + if obj is not self: + # noinspection PyUnresolvedReferences + obj.__post_init__() + return obj + + def __post_init__(self): + self.backing_list = getattr(self.instance, self.backing_name) + + def _safe_get_by_mrid(self, mrid: str) -> S | None: + found = next((element for element in self.backing_list if element.mrid == mrid), None) + return found + + def append(self, item: S): + if not self._can_add_by_mrid(item): + return + + if self.validate is not None: + self.validate(self.instance, item) + + self.backing_list.append(item) + + if self.sort_by is not None: + self.backing_list.sort(key=self.sort_by) + + def __len__(self): + return len(self.backing_list) + + def __iter__(self): + return iter(self.backing_list) + + def __contains__(self, item): + return item in self.backing_list + + def remove(self, item): + self.backing_list.remove(item) + + def clear(self): + self.backing_list.clear() + + def __repr__(self): + return str(self.backing_list) + + def __str__(self): + return self.__repr__() + + def __getitem__(self, item): + return self.backing_list[item] + + +# +# class Backfill(Protocol[S]): +# def __init__(self, backref_field: Any): +# if not isinstance(backref_field, Field) and not isinstance(backref_field, BackedDescriptor): +# raise TypeError(f"backref_field parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {backref_field}") +# self.backref_field = backref_field +# +# def apply(self, element: S, owner: Any): +# setattr(element, self.backref_field.__name__, ) + + + + + +if __name__ == '__main__': + @dataclass(slots=True) + class Ido: + mrid: str + n: int = 42 + + @dataclass(slots=True) + class A: + # _x: list[Ido] = field(default_factory=list) + _x: list[Ido] = field(default=None) + # x: MridCollection[Ido] = MridList( + x: MridCollection[Ido] = LazyMridList( + _x, + "A Thingo", + validate=(lambda self, it: self.validate_x(it)), + sort_by=(lambda it: it.n), + ) + + def validate_x(self, item): + print(f"validating {item}") + + def __hash__(self): + return object.__hash__(self) + + o1 = Ido("1") + o2 = Ido("2", 24) + oX = Ido("2", 32) + + a = A() + print(a, a.x) + a.x.append(o1) + print(a, a.x) + a.x.append(o2) + print(a, a.x) + for t in a.x: + print(t) + a.x.remove(o1) + print(a, a.x) + a.x.remove(o2) + print(a, a.x) + + a.x.append(o2) + print(a, a.x) + with pytest.raises(ValueError): + a.x.append(oX) + print(a, a.x) diff --git a/src/zepben/ewb/boilerplate/mrid_map.py b/src/zepben/ewb/boilerplate/mrid_map.py new file mode 100644 index 000000000..c74c40fa6 --- /dev/null +++ b/src/zepben/ewb/boilerplate/mrid_map.py @@ -0,0 +1,145 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from dataclasses import dataclass, field +from typing import Protocol, TypeVar + +import pytest + +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.lazy_list import _IterableWrapper +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection + + +class HasMrid(Protocol): + mrid: str + +T = TypeVar("T") +S = TypeVar("S", bound=HasMrid) + + +class LazyMridMap(_IterableWrapper[S], MridCollection[S]): + def __init__( + self, + private_field, + element_description: str, + validate=None): + super().__init__(private_field) + self.validate = validate + self.element_description = element_description + + def _get(self): + return getattr(self.instance, self.backing_name) + + def _get_or_empty(self) -> dict[str, S]: + return getattr(self.instance, self.backing_name) or {} + + def _safe_get_by_mrid(self, mrid: str) -> S | None: + return self._get_or_empty().get(mrid, None) + + def get_by_mrid(self, mrid: str) -> S: + return self._get_or_empty()[mrid] + + def _can_add_by_mrid(self, element: S) -> bool: + existing = self._safe_get_by_mrid(element.mrid) + + if existing is None: + return True + + if existing is not element: + raise ValueError( + f"{self.element_description} with mRID {element.mrid} " + f"already exists in {self.instance}." + ) + + return False + + def append(self, element: S): + if not self._can_add_by_mrid(element): + return + + if self.validate is not None: + self.validate(self.instance, element) + + existing = getattr(self.instance, self.backing_name) + if existing is None: + existing = {element.mrid: element} + setattr(self.instance, self.backing_name, existing) + else: + existing[element.mrid] = element + + def __len__(self): + return len(self._get_or_empty()) + + def __iter__(self): + return iter(self._get_or_empty().values()) + + def __contains__(self, item: S): + return self._get_or_empty().get(item.mrid, None) == item + + def remove(self, item): + existing = self._get_or_empty() + + del existing[item.mrid] + if not existing: + self.clear() + + def clear(self): + setattr(self.instance, self.backing_name, None) + + def __repr__(self): + return str(self._get_or_empty()) + + def __str__(self): + return self.__repr__() + + def __getitem__(self, item): + return (self._get_or_empty())[item] + + +if __name__ == '__main__': + @dataclass(slots=True) + class Ido: + mrid: str + n: int = 42 + + @dataclass(slots=True) + class A: + _x: list[Ido] | None = field(default=None) + x: MridCollection[Ido] = LazyMridMap( + _x, + "A Thingo", + validate=(lambda self, it: self.validate_x(it)), + # cached=False + ) + + def validate_x(self, item): + print(f"{self} validating {item}") + + + def __hash__(self): + return object.__hash__(self) + + o1 = Ido("1") + o2 = Ido("2", 24) + oX = Ido("2", 32) + + a = A() + print(a, a.x) + a.x.append(o1) + print(a, a.x) + a.x.append(o2) + print(a, a.x) + for t in a.x: + print(t) + a.x.remove(o1) + print(a, a.x) + a.x.remove(o2) + print(a, a.x) + + a.x.append(o2) + print(a, a.x) + with pytest.raises(ValueError): + a.x.append(oX) + print(a, a.x) diff --git a/src/zepben/ewb/boilerplate/tst.py b/src/zepben/ewb/boilerplate/tst.py new file mode 100644 index 000000000..55ed4ce86 --- /dev/null +++ b/src/zepben/ewb/boilerplate/tst.py @@ -0,0 +1,162 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +import copy +import time +from abc import abstractmethod, ABC +from dataclasses import dataclass, field +from typing import Any +from typing_extensions import Self, deprecated +from zepben.ewb import remove_descriptor_annotations, BackedDescriptor, resolve_default + + +class _Wrapper(BackedDescriptor): + + instance: Any + backing_name: Any + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + """ + This __new__ stores the init arguments, and otherwise acts as default. + We need this to efficiently and truly re-create the wrapper and bind it to an instance upon a get call. + """ + obj = super().__new__(cls) + obj._init_args = args + obj._init_kwargs = kwargs.copy() + return obj + + def __get__(self, instance, _=None) -> Self: + """ + This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. + From there on, the wrapper functions as a lazy list. + """ + obj = type(self)(*self._init_args, **self._init_kwargs) + obj.instance = instance + obj.backing_name = self.private_field.name + return obj + +class _WrapperFgetFix(_Wrapper): + """ + This class exists to fix the tests that rely on the old lists being @property. + TODO: Remove in a separate PR fixing tests + """ + def __set_name__(self, owner, name): + super().__set_name__(owner, name) + + def fget(instance): + return self.__get__(instance) + + fget.__name__ = name + fget.__qualname__ = f"{owner.__qualname__}.{name}" + + self.fget = fget + + @deprecated("Lists are no longer a property") + def fget(self, instance): + # TODO: Remove test hack in separate PR + raise NotImplementedError() + +class _IterableWrapper(_WrapperFgetFix, ABC): + """ + This class allows us to assign lists at init time to avoid special-case handling. + """ + def __set__(self, instance, value): + if instance is None: + return + if not hasattr(instance, self.backing_name): + resolve_default(instance, self.private_field) + elif getattr(instance, self.backing_name): + raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") + if value is not None: + self.__get__(instance).extend(value) + else: + self.__get__(instance).clear() + + @abstractmethod + def extend(self, items): ... + + @abstractmethod + def clear(self): ... + + +class ListWrapper(_IterableWrapper): + + def clear(self): + self._set(None) + + def extend(self, items): + for item in items: + self.append(item) + + def __init__( + self, + private_field: Any, + validate = None, + sort_by = None + ): + super().__init__(private_field) + self.validate = validate + self.sort_by = sort_by + + def _get(self): + if self.instance is None: + return self + return getattr(self.instance, self.backing_name) + + def _set(self, value): + if self.instance is None: + return + setattr(self.instance, self.backing_name, value) + + def __str__(self): + return str(self._get()) + + def append(self, item): + l = self._get() + if l is None: + self._set([item]) + else: + l.append(item) + + + + +@dataclass +class C: + _x: list[int] | None = field(default=None) + x: ListWrapper = ListWrapper(_x, sort_by=lambda it: -it) + + +if __name__ == '__main__': + c = C([1, 2, 3]) + print(c) + print(c.x) + print(c.x.sort_by) + c.x.clear() + c.x.append(42) + print(c) + print(c.x) + + # class C: + # def __init__(self, x, f1, f2): + # self.x = x + # self.y = x + x + # self.f1 = f1 + # self.f2 = f2 + # + # N = 1_000_000 + # t = time.time() + # for _ in range(N): + # tmp = C("blahblahblah", lambda : 42, lambda : 24) + # print(time.time() - t) + # + # c = C("blahblahblah", lambda : 42, lambda : 24) + # t = time.time() + # for _ in range(N): + # tmp = copy.copy(c) + # print(time.time() - t) + ... \ No newline at end of file diff --git a/src/zepben/ewb/database/sqlite/network/network_database_reader.py b/src/zepben/ewb/database/sqlite/network/network_database_reader.py index 3f9ba1765..ad1005b02 100644 --- a/src/zepben/ewb/database/sqlite/network/network_database_reader.py +++ b/src/zepben/ewb/database/sqlite/network/network_database_reader.py @@ -139,7 +139,7 @@ def _validate_equipment_containers(self): for it in missing_containers: count_by_class[type(it).__name__] += 1 - for (className, count) in count_by_class: + for (className, count) in count_by_class.items(): self._logger.warning(f"{count} {className}s were missing an equipment container.") if count_by_class: diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py b/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py index 16c669dc1..29355bff7 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/metering/pan_demand_reponse_function.py @@ -27,10 +27,6 @@ class PanDemandResponseFunction(EndDeviceFunction): _appliance_bitmask: Optional[int] = None - def __init__(self, *args, appliance: Union[int, ControlledAppliance] = None, **kwargs): - super(PanDemandResponseFunction, self).__init__(*args, **kwargs) - if appliance is not None: - self.appliance = appliance @property def appliance(self) -> Optional[ControlledAppliance]: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py index 523435c10..79f3e77fc 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py @@ -8,11 +8,15 @@ __all__ = ["Loop"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import safe_remove, ngen, nlen, get_by_mrid -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.substation import Substation @@ -28,182 +32,121 @@ class Loop(IdentifiedObject): to many customers for more than a short time. """ - _circuits: Optional[List[Circuit]] = None - _substations: Optional[List[Substation]] = None - _energizing_substations: Optional[List[Substation]] = None - - def __init__(self, *args, circuits: List[Circuit] = None, substations: List[Substation] = None, energizing_substations: List[Substation] = None, **kwargs): - super(Loop, self).__init__(*args, **kwargs) - if circuits: - for term in circuits: - self.add_circuit(term) - - if substations: - for sub in substations: - self.add_substation(sub) - - if energizing_substations: - for sub in energizing_substations: - self.add_energizing_substation(sub) - - @property - def circuits(self) -> Generator[Circuit, None, None]: - """ - [ZBEX] Sub-transmission `Circuit`s that form part of this loop. - """ - return ngen(self._circuits) - - @property - def substations(self) -> Generator[Substation, None, None]: - """ - [ZBEX] The `Substation`s that are powered by this `Loop`. - """ - return ngen(self._substations) - - @property - def energizing_substations(self) -> Generator[Substation, None, None]: - """ - [ZBEX] The `Substation`s that normally energize this `Loop`. - """ - return ngen(self._energizing_substations) + _circuits: Optional[List[Circuit]] = field(default=None) + _substations: Optional[List[Substation]] = field(default=None) + _energizing_substations: Optional[List[Substation]] = field(default=None) + + circuits: MridCollection[Circuit] = LazyMridList( + _circuits, + "An Circuit", + ) + + substations: MridCollection[Substation] = LazyMridList( + _substations, + "An Substation", + ) + + energizing_substations: MridCollection[Substation] = LazyMridList( + _energizing_substations, + "An Substation", + ) + + + + + + + + + + + + + + + + + # region deprecated list boilerplate + # region circuits boilerplate + + @deprecated("Use len(obj.circuits) instead.") def num_circuits(self): - """Return the number of end `Circuit`s associated with this `Loop`""" - return nlen(self._circuits) + return len(self.circuits) + @deprecated("Use obj.circuits.get_by_mrid(mrid) instead.") def get_circuit(self, mrid: str) -> Circuit: - """ - Get the `Circuit` for this `Loop` identified by `mrid` - - `mrid` the mRID of the required `Circuit` - Returns The `Circuit` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._circuits, mrid) + return self.circuits.get_by_mrid(mrid) + @deprecated("Use obj.circuits.append(circuit) instead.") def add_circuit(self, circuit: Circuit) -> Loop: - """ - Associate an `Circuit` with this `Loop` - - `circuit` the `Circuit` to associate with this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if another `Circuit` with the same `mrid` already exists for this `Loop`. - """ - if self._validate_reference(circuit, self.get_circuit, "An Circuit"): - return self - self._circuits = list() if self._circuits is None else self._circuits - self._circuits.append(circuit) + self.circuits.append(circuit) return self + @deprecated("Use obj.circuits.remove(circuit) instead.") def remove_circuit(self, circuit: Circuit) -> Loop: - """ - Disassociate `circuit` from this `Loop` - - `circuit` the `Circuit` to disassociate from this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if `circuit` was not associated with this `Loop`. - """ - self._circuits = safe_remove(self._circuits, circuit) + self.circuits.remove(circuit) return self + @deprecated("Use obj.circuits.clear() instead.") def clear_circuits(self) -> Loop: - """ - Clear all end circuits. - Returns A reference to this `Loop` to allow fluent use. - """ - self._circuits = None + self.circuits.clear() return self + # endregion circuits boilerplate + + # region substations boilerplate + + @deprecated("Use len(obj.substations) instead.") def num_substations(self): - """Return the number of end `Substation`s associated with this `Loop`""" - return nlen(self._substations) + return len(self.substations) + @deprecated("Use obj.substations.get_by_mrid(mrid) instead.") def get_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `Loop` identified by `mrid` - - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._substations, mrid) + return self.substations.get_by_mrid(mrid) + @deprecated("Use obj.substations.append(substation) instead.") def add_substation(self, substation: Substation) -> Loop: - """ - Associate an `Substation` with this `Loop` - - `substation` the `Substation` to associate with this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `Loop`. - """ - if self._validate_reference(substation, self.get_substation, "An Substation"): - return self - self._substations = list() if self._substations is None else self._substations - self._substations.append(substation) + self.substations.append(substation) return self + @deprecated("Use obj.substations.remove(substation) instead.") def remove_substation(self, substation: Substation) -> Loop: - """ - Disassociate `substation` from this `Loop` - - `substation` the `Substation` to disassociate from this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `Loop`. - """ - self._substations = safe_remove(self._substations, substation) + self.substations.remove(substation) return self + @deprecated("Use obj.substations.clear() instead.") def clear_substations(self) -> Loop: - """ - Clear all end substations. - Returns A reference to this `Loop` to allow fluent use. - """ - self._substations = None + self.substations.clear() return self + # endregion substations boilerplate + + # region energizing_substations boilerplate + + @deprecated("Use len(obj.energizing_substations) instead.") def num_energizing_substations(self): - """Return the number of end `Substation`s associated with this `Loop`""" - return nlen(self._energizing_substations) + return len(self.energizing_substations) + @deprecated("Use obj.energizing_substations.get_by_mrid(mrid) instead.") def get_energizing_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `Loop` identified by `mrid` - - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energizing_substations, mrid) + return self.energizing_substations.get_by_mrid(mrid) + @deprecated("Use obj.energizing_substations.append(substation) instead.") def add_energizing_substation(self, substation: Substation) -> Loop: - """ - Associate an `Substation` with this `Loop` - - `substation` the `Substation` to associate with this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `Loop`. - """ - if self._validate_reference(substation, self.get_energizing_substation, "An Substation"): - return self - self._energizing_substations = list() if self._energizing_substations is None else self._energizing_substations - self._energizing_substations.append(substation) + self.energizing_substations.append(substation) return self + @deprecated("Use obj.energizing_substations.remove(substation) instead.") def remove_energizing_substation(self, substation: Substation) -> Loop: - """ - Disassociate `substation` from this `Loop` - - `substation` the `Substation` to disassociate from this `Loop`. - Returns A reference to this `Loop` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `Loop`. - """ - self._energizing_substations = safe_remove(self._energizing_substations, substation) + self.energizing_substations.remove(substation) return self + @deprecated("Use obj.energizing_substations.clear() instead.") def clear_energizing_substations(self) -> Loop: - """ - Clear all end energizing_substations. - Returns A reference to this `Loop` to allow fluent use. - """ - self._energizing_substations = None + self.energizing_substations.clear() return self + + # endregion energizing_substations boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py index f03a6e156..bf95e8f33 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py @@ -8,8 +8,15 @@ __all__ = ["LvFeeder"] import typing +from dataclasses import field from typing import Generator, Optional, Dict, List +from pygments.lexer import default +from typing_extensions import deprecated + +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.util import safe_remove_by_id, nlen, ngen @@ -40,39 +47,35 @@ class LvFeeder(EquipmentContainer): _normal_head_terminal: Terminal | None = None """[ZBEX] The normal head terminal or terminals of this LvFeeder""" - _normal_energizing_feeders: Dict[str, Feeder] | None = None + _normal_energizing_feeders_by_id: Dict[str, Feeder] | None = field(default=None) """[ZBEX] The feeders that energize this LV feeder in the normal state of the network.""" - _current_equipment: Dict[str, Equipment] | None = None + _current_equipment_by_id: Dict[str, Equipment] | None = field(default=None) """[ZBEX] The equipment contained in this LvFeeder in the current state of the network.""" - _current_energizing_feeders: Dict[str, Feeder] | None = None + _current_energizing_feeders_by_id: Dict[str, Feeder] | None = field(default=None) """[ZBEX] The feeders that energize this LV feeder in the current state of the network.""" normal_energizing_lv_substation: 'LvSubstation | None' = None """[ZBEX] The normally energizing LvSubstation for this LvFeeder""" - def __init__( - self, - *args, - normal_head_terminal: Terminal = None, - normal_energizing_feeders: List[Feeder] = None, - current_equipment: List[Equipment] = None, - current_energizing_feeders: List[Feeder] = None, - **kwargs - ): - super(LvFeeder, self).__init__(*args, **kwargs) - if normal_head_terminal: - self.normal_head_terminal = normal_head_terminal - if normal_energizing_feeders: - for feeder in normal_energizing_feeders: - self.add_normal_energizing_feeder(feeder) - if current_equipment: - for eq in current_equipment: - self.add_current_equipment(eq) - if current_energizing_feeders: - for feeder in current_energizing_feeders: - self.add_current_energizing_feeder(feeder) + current_equipment: MridCollection[Equipment] = LazyMridMap( + _current_equipment_by_id, + "A current Equipment", + ) + """Contained `Equipment` using the current state of the network.""" + + normal_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _normal_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that normally energize this LV feeder.""" + + current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _current_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that currently energize this LV feeder.""" @property def normal_head_terminal(self) -> Optional[Terminal]: @@ -88,185 +91,84 @@ def normal_head_terminal(self, term: Optional[Terminal]): else: raise ValueError(f"normal_head_terminal for {str(self)} has already been set to {self._normal_head_terminal}, cannot reset this field to {term}") - @property - def normal_energizing_feeders(self) -> Generator[Feeder, None, None]: - """ - [ZBEX] The HV/MV feeders that normally energize this LV feeder. - """ - return ngen(self._normal_energizing_feeders) - - @property - def current_energizing_feeders(self) -> Generator[Feeder, None, None]: - """ - [ZBEX] The HV/MV feeders that currently energize this LV feeder. - """ - return ngen(self._current_energizing_feeders) - @property - def current_equipment(self) -> Generator[Equipment, None, None]: - """ - Contained `Equipment` using the current state of the network. - """ - return ngen(self._current_equipment) + # region deprecated list boilerplate + # region current_equipment boilerplate + @deprecated("Use len(current_equipment) instead") def num_current_equipment(self): - """ - Returns The number of `Equipment` associated with this `LvFeeder` in the current state of the network. - """ - return nlen(self._current_equipment) + return len(self.current_equipment) + @deprecated("Use current_equipment.get_by_mrid(mrid) instead") def get_current_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` contained in this `LvFeeder` in the current state of the network, identified by `mrid` + return self.current_equipment.get_by_mrid(mrid) - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - if not self._current_equipment: - raise KeyError(mrid) - try: - return self._current_equipment[mrid] - except AttributeError: - raise KeyError(mrid) - - def add_current_equipment(self, equipment: Equipment) -> LvFeeder: - """ - Associate `equipment` with this `LvFeeder` in the current state of the network. - - `equipment` the `Equipment` to associate with this `LvFeeder` in the current state of the network. - Returns A reference to this `LvFeeder` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `LvFeeder`. - """ - if self._validate_reference(equipment, self.get_current_equipment, "An Equipment"): - return self - self._current_equipment = dict() if self._current_equipment is None else self._current_equipment - self._current_equipment[equipment.mrid] = equipment + @deprecated("Use current_equipment.append(equipment) instead") + def add_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.append(equipment) return self - def remove_current_equipment(self, equipment: Equipment) -> LvFeeder: - """ - Disassociate `equipment` from this `LvFeeder` in the current state of the network. - - `equipment` The `Equipment` to disassociate from this `LvFeeder` in the current state of the network. - Returns A reference to this `LvFeeder` to allow fluent use. - Raises `KeyError` if `equipment` was not associated with this `LvFeeder`. - """ - self._current_equipment = safe_remove_by_id(self._current_equipment, equipment) + @deprecated("Use current_equipment.remove(equipment) instead") + def remove_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.remove(equipment) return self - def clear_current_equipment(self) -> LvFeeder: - """ - Clear all `Equipment` from this `LvFeeder` in the current state of the network. - Returns A reference to this `LvFeeder` to allow fluent use. - """ - self._current_equipment = None + @deprecated("Use current_equipment.clear() instead") + def clear_current_equipment(self) -> EquipmentContainer: + self.current_equipment.clear() return self - def num_normal_energizing_feeders(self) -> int: - """ - Get the number of HV/MV feeders that normally energize this LV feeder. - """ - return nlen(self._normal_energizing_feeders) + # endregion + # region normal_energizing_feeders boilerplate - def get_normal_energizing_feeder(self, mrid: str) -> Feeder: - """ - Energizing feeder using the normal state of the network. + @deprecated("Use len(normal_energizing_feeders) instead") + def num_normal_energizing_feeders(self): + return len(self.normal_energizing_feeders) - @param mrid: The mrid of the `Feeder`. - @return A matching `Feeder` that energizes this `LvFeeder` in the normal state of the network. - @raise A `KeyError` if no matching `Feeder` was found. - """ - if not self._normal_energizing_feeders: - raise KeyError(mrid) - try: - return self._normal_energizing_feeders[mrid] - except AttributeError: - raise KeyError(mrid) - - def add_normal_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Associate this `LvFeeder` with a `Feeder` in the normal state of the network. + @deprecated("Use normal_energizing_feeders.get_by_mrid(mrid) instead") + def get_normal_energizing_feeder(self, mrid: str) -> Feeder: + return self.normal_energizing_feeders.get_by_mrid(mrid) - @param feeder: the HV/MV feeder to associate with this LV feeder in the normal state of the network. - @return: This `LvFeeder` for fluent use. - """ - if self._validate_reference(feeder, self.get_normal_energizing_feeder, "A Feeder"): - return self - self._normal_energizing_feeders = dict() if self._normal_energizing_feeders is None else self._normal_energizing_feeders - self._normal_energizing_feeders[feeder.mrid] = feeder + @deprecated("Use normal_energizing_feeders.append(lv_feeder) instead") + def add_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.normal_energizing_feeders.append(lv_feeder) return self - def remove_normal_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Disassociate this `LvFeeder` from a `Feeder` in the normal state of the network. - - @param feeder: the HV/MV feeder to disassociate from this LV feeder in the normal state of the network. - @return: This `LvFeeder` for fluent use. - @raise: A `ValueError` if `feeder` is not found in the normal energizing feeders collection. - """ - self._normal_energizing_feeders = safe_remove_by_id(self._normal_energizing_feeders, feeder) + @deprecated("Use normal_energizing_feeders.remove(lv_feeder) instead") + def remove_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.normal_energizing_feeders.remove(lv_feeder) return self + @deprecated("Use normal_energizing_feeders.clear() instead") def clear_normal_energizing_feeders(self) -> LvFeeder: - """ - Clear all `Feeder`s associated with `LvFeeder` in the normal state of the network. - - @return: This `LvFeeder` for fluent use. - """ - self._normal_energizing_feeders = None + self.normal_energizing_feeders.clear() return self - def num_current_energizing_feeders(self) -> int: - """ - Get the number of HV/MV feeders that currently energize this LV feeder. - """ - return nlen(self._current_energizing_feeders) + # endregion + # region current_energizing_feeders boilerplate - def get_current_energizing_feeder(self, mrid: str) -> Feeder: - """ - Energizing feeder using the current state of the network. + @deprecated("Use len(current_energizing_feeders) instead") + def num_current_energizing_feeders(self): + return len(self.current_energizing_feeders) - @param mrid: The mrid of the `Feeder`. - @return A matching `Feeder` that energizes this `LvFeeder` in the current state of the network. - @raise A `KeyError` if no matching `Feeder` was found. - """ - if not self._current_energizing_feeders: - raise KeyError(mrid) - try: - return self._current_energizing_feeders[mrid] - except AttributeError: - raise KeyError(mrid) - - def add_current_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Associate this `LvFeeder` with a `Feeder` in the current state of the network. + @deprecated("Use current_energizing_feeders.get_by_mrid(mrid) instead") + def get_current_energizing_feeder(self, mrid: str) -> Feeder: + return self.current_energizing_feeders.get_by_mrid(mrid) - @param feeder: the HV/MV feeder to associate with this LV feeder in the current state of the network. - @return: This `LvFeeder` for fluent use. - """ - if self._validate_reference(feeder, self.get_current_energizing_feeder, "A Feeder"): - return self - self._current_energizing_feeders = dict() if self._current_energizing_feeders is None else self._current_energizing_feeders - self._current_energizing_feeders[feeder.mrid] = feeder + @deprecated("Use current_energizing_feeders.append(lv_feeder) instead") + def add_current_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.current_energizing_feeders.append(lv_feeder) return self - def remove_current_energizing_feeder(self, feeder: Feeder) -> LvFeeder: - """ - Disassociate this `LvFeeder` from a `Feeder` in the current state of the network. - - @param feeder: the HV/MV feeder to disassociate from this LV feeder in the current state of the network. - @return: This `LvFeeder` for fluent use. - @raise: A `ValueError` if `feeder` is not found in the current energizing feeders collection. - """ - self._current_energizing_feeders = safe_remove_by_id(self._current_energizing_feeders, feeder) + @deprecated("Use current_energizing_feeders.remove(lv_feeder) instead") + def remove_current_energizing_feeder(self, lv_feeder: Feeder) -> LvFeeder: + self.current_energizing_feeders.remove(lv_feeder) return self + @deprecated("Use current_energizing_feeders.clear() instead") def clear_current_energizing_feeders(self) -> LvFeeder: - """ - Clear all `Feeder`s associated with `LvFeeder` in the current state of the network. - - @return: This `LvFeeder` for fluent use. - """ - self._current_energizing_feeders = None + self.current_energizing_feeders.clear() return self + + # endregion + # endregion diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index 9afc8a44e..4bd63d40e 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -3,11 +3,18 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from __future__ import annotations + __all__ = ['LvSubstation'] +from dataclasses import field from typing import Generator, TYPE_CHECKING -from zepben.ewb import ngen, nlen, safe_remove_by_id, get_by_mrid +from typing_extensions import deprecated + +from zepben.ewb import ngen, nlen, safe_remove_by_id, get_by_mrid, remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder @@ -28,198 +35,115 @@ class LvSubstation(EquipmentContainer): :var current_energizing_feeders: [ZBEX] the feeders that currently energize the substation. also used for naming purposes. """ - _normal_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = None - _current_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = None - _normal_energized_lv_feeders_by_id: dict[str | None, LvFeeder] | None = None - - def __init__( - self, - *args, - normal_energizing_feeders: list['Feeder'] | None = None, - current_energizing_feeders: list['Feeder'] | None = None, - normal_energized_lv_feeders: list[LvFeeder] | None = None, - **kwargs - ): - super(LvSubstation, self).__init__(*args, **kwargs) - if normal_energizing_feeders: - for lv_feeder in normal_energizing_feeders: - self.add_normal_energizing_feeder(lv_feeder) - if current_energizing_feeders: - for lv_feeder in current_energizing_feeders: - self.add_current_energizing_feeder(lv_feeder) - if normal_energized_lv_feeders: - for lv_feeder in normal_energized_lv_feeders: - self.add_normal_energized_lv_feeder(lv_feeder) - - @zbex - @property - def normal_energizing_feeders(self) -> Generator["Feeder", None, None]: - """[ZBEX] The HV/MV feeders that normally energize this ``LvSubstation``.""" - return ngen(self._normal_energizing_feeders_by_id) - - @zbex - @property - def normal_energized_lv_feeders(self) -> Generator[LvFeeder, None, None]: - """[ZBEX] the ``LvFeeders`` that are normally energized by this ``LvSubstation``.""" - return ngen(self._normal_energized_lv_feeders_by_id) - - @zbex - @property - def current_energizing_feeders(self) -> Generator['Feeder', None, None]: - """ - [ZBEX] The HV/MV feeders that currently energize this LV substation. - """ - return ngen(self._current_energizing_feeders_by_id) - - def normal_energized_lv_switch_feeders(self) -> Generator[LvFeeder, None, None]: - """ - Retrieves all normally energized LvFeeders that represent low voltage network connected below a switch on the edge of this LvSubstation. This is all - LvFeeders in the normalEnergizedLvFeeders that has a normalHeadTerminal attached to a Switch. - """ - # NOTE: import exists here due to a circular import problem - from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch + _normal_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = field(default=None) + _current_energizing_feeders_by_id: dict[str | None, 'Feeder'] | None = field(default=None) + _normal_energized_lv_feeders_by_id: dict[str | None, LvFeeder] | None = field(default=None) - for lv_feeder in self.normal_energized_lv_feeders: - if (it := lv_feeder.normal_head_terminal) is not None and isinstance(it.conducting_equipment, Switch): - yield lv_feeder + normal_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _normal_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that normally energize this ``LvSubstation``.""" - def num_normal_energizing_feeders(self) -> int: - """Get the number of entries in the normal ``Feeder`` collection.""" - return nlen(self._normal_energizing_feeders_by_id) + current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _current_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that currently energize this LV substation.""" - def get_normal_energizing_feeder(self, mrid: str) -> 'Feeder | None': - """ - Energizing feeder using the normal state of the network. - :param mrid: the mRID of the required normal ``Feeder`` - :returns: The ``Feeder`` with the specified ``mrid`` if it exists, otherwise null - """ - return get_by_mrid(self._normal_energizing_feeders_by_id, mrid) + normal_energized_lv_feeders: MridCollection[LvFeeder] = LazyMridMap( + _normal_energized_lv_feeders_by_id, + "An LvFeeder", + ) + """[ZBEX] the ``LvFeeders`` that are normally energized by this ``LvSubstation``.""" - def add_normal_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Associate this ``LvSubstation`` with a ``Feeder`` in the normal state of the network. + def num_normal_energized_lv_feeders(self) -> int: + """Get the number of entries in the normal ``LvFeeder`` collection.""" + return nlen(self._normal_energized_lv_feeders_by_id) - :param feeder: the HV/MV feeder to associate with this ``LvSubstation`` in the normal state of the network. - :returns: This ``LvSubstation`` for fluent use. - """ - if self._validate_reference(feeder, self.get_normal_energizing_feeder, "A Feeder"): - return self + for lv_feeder in self.normal_energized_lv_feeders: + if (it := lv_feeder.normal_head_terminal) is not None and isinstance(it.conducting_equipment, Switch): + yield lv_feeder - if self._normal_energizing_feeders_by_id is None: - self._normal_energizing_feeders_by_id = dict() - self._normal_energizing_feeders_by_id[feeder.mrid] = feeder - return self - def remove_normal_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Disassociate this ``LvSubstation`` from a ``Feeder`` in the normal state of the network. + # region deprecated list boilerplate + # region normal_energizing_feeders boilerplate - :param feeder: the HV/MV feeder to disassociate from this ``LvSubstation`` in the normal state of the network. - :returns: true if a matching feeder is removed from the collection. - """ - self._normal_energizing_feeders_by_id = safe_remove_by_id(self._normal_energizing_feeders_by_id, feeder) - return self + @deprecated("Use len(normal_energizing_feeders) instead") + def num_normal_energizing_feeders(self): + return len(self.normal_energizing_feeders) - def clear_normal_energizing_feeders(self) -> "LvSubstation": - """ - Clear all ``Feeder``'s associated with this ``LvSubstation`` in the normal state of the network. + @deprecated("Use normal_energizing_feeders.get_by_mrid(mrid) instead") + def get_normal_energizing_feeder(self, mrid: str) -> Feeder: + return self.normal_energizing_feeders.get_by_mrid(mrid) - :returns: This ``LvSubstation`` for fluent use. - """ - self._normal_energizing_feeders_by_id = None + @deprecated("Use normal_energizing_feeders.append(lv_feeder) instead") + def add_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.normal_energizing_feeders.append(lv_feeder) return self - def num_current_energizing_feeders(self) -> int: - """ - Get the number of entries in the current ``Feeder`` collection. - """ - return nlen(self._current_energizing_feeders_by_id) - - def get_current_energizing_feeder(self, mrid: str) -> 'Feeder | None': - """ - Retrieve an energizing feeder using the current state of the network. - - :param mrid: the mRID of the required current ``Feeder`` - :returns: The ``Feeder`` with the specified ``mRID`` if it exists, otherwise null - """ - return get_by_mrid(self._current_energizing_feeders_by_id, mrid) - - def add_current_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Associate this ``LvSubstation`` with a ``Feeder`` in the current state of the network. - - :param feeder: the HV/MV feeder to associate with this ``LvSubstation`` in the current state of the network. - :returns: This ``LvSubstation`` for fluent use. - """ - if self._validate_reference(feeder, self.get_current_energizing_feeder, "A Feeder"): - return self - if self._current_energizing_feeders_by_id is None: - self._current_energizing_feeders_by_id = dict() - self._current_energizing_feeders_by_id[feeder.mrid] = feeder + @deprecated("Use normal_energizing_feeders.remove(lv_feeder) instead") + def remove_normal_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.normal_energizing_feeders.remove(lv_feeder) return self - def remove_current_energizing_feeder(self, feeder: 'Feeder') -> "LvSubstation": - """ - Disassociate this ``LvSubstation`` from a ``Feeder`` in the current state of the network. - - :param feeder: the HV/MV feeder to disassociate from this LvSubstation the current state of the network. - :returns: true if a matching feeder is removed from the collection. - """ - self._current_energizing_feeders_by_id = safe_remove_by_id(self._current_energizing_feeders_by_id, feeder) + @deprecated("Use normal_energizing_feeders.clear() instead") + def clear_normal_energizing_feeders(self) -> LvSubstation: + self.normal_energizing_feeders.clear() return self - def clear_current_energizing_feeders(self) -> "LvSubstation": - """ - Clear all ``Feeder``'s associated with this ``LvSubstation`` in the current state of the network. + # endregion + # region current_energizing_feeders boilerplate - :returns: This ``LvSubstation`` for fluent use. - """ - self._current_energizing_feeders_by_id = None - return self + @deprecated("Use len(current_energizing_feeders) instead") + def num_current_energizing_feeders(self): + return len(self.current_energizing_feeders) - def num_normal_energized_lv_feeders(self) -> int: - """Get the number of entries in the normal ``LvFeeder`` collection.""" - return nlen(self._normal_energized_lv_feeders_by_id) + @deprecated("Use current_energizing_feeders.get_by_mrid(mrid) instead") + def get_current_energizing_feeder(self, mrid: str) -> Feeder: + return self.current_energizing_feeders.get_by_mrid(mrid) - def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder | None: - """ - Retrieve an energized ``LvFeeder`` using the normal state of the network. + @deprecated("Use current_energizing_feeders.append(lv_feeder) instead") + def add_current_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.current_energizing_feeders.append(lv_feeder) + return self - :param mrid: the mRID of the required normal ``LvFeeder`` - :returns: The ``LvFeeder`` with the specified ``mRID`` if it exists, otherwise null - """ - return get_by_mrid(self._normal_energized_lv_feeders_by_id, mrid) + @deprecated("Use current_energizing_feeders.remove(lv_feeder) instead") + def remove_current_energizing_feeder(self, lv_feeder: Feeder) -> LvSubstation: + self.current_energizing_feeders.remove(lv_feeder) + return self - def add_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> "LvSubstation": - """ - Associate this ``LvSubstation`` with an ``LvFeeder`` in the normal state of the network. + @deprecated("Use current_energizing_feeders.clear() instead") + def clear_current_energizing_feeders(self) -> LvSubstation: + self.current_energizing_feeders.clear() + return self - :param lv_feeder: the ``LvFeeder`` to associate with this feeder in the normal state of the network. - :returns: This ``LvSubstation`` for fluent use. - """ - if self._validate_reference(lv_feeder, self.get_normal_energized_lv_feeder, "An LvFeeder"): - return self + # endregion + # region normal_energized_lv_feeders boilerplate - if self._normal_energized_lv_feeders_by_id is None: - self._normal_energized_lv_feeders_by_id = dict() - self._normal_energized_lv_feeders_by_id[lv_feeder.mrid] = lv_feeder - return self + @deprecated("Use len(normal_energized_lv_feeders) instead") + def num_normal_energized_lv_feeders(self): + return len(self.normal_energized_lv_feeders) - def remove_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> "LvSubstation": - """ - Disassociate this ``LvSubstation`` from an ``LvFeeder`` in the normal state of the network. + @deprecated("Use normal_energized_lv_feeders.get_by_mrid(mrid) instead") + def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder: + return self.normal_energized_lv_feeders.get_by_mrid(mrid) - :param lv_feeder: the ``LvFeeder`` to disassociate from this HV/MV feeder in the normal state of the network. - """ - self._normal_energized_lv_feeders_by_id = safe_remove_by_id(self._normal_energized_lv_feeders_by_id, lv_feeder) + @deprecated("Use normal_energized_lv_feeders.append(lv_feeder) instead") + def add_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> LvSubstation: + self.normal_energized_lv_feeders.append(lv_feeder) return self - def clear_normal_energized_lv_feeders(self) -> "LvSubstation": - """ - Clear all ``LvFeeder``'s associated with this ``LvSubstation`` in the normal state of the network. + @deprecated("Use normal_energized_lv_feeders.remove(lv_feeder) instead") + def remove_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> LvSubstation: + self.normal_energized_lv_feeders.remove(lv_feeder) + return self - :returns: This ``LvSubstation`` for fluent use. - """ - self._normal_energized_lv_feeders_by_id = None + @deprecated("Use normal_energized_lv_feeders.clear() instead") + def clear_normal_energized_lv_feeders(self) -> LvSubstation: + self.normal_energized_lv_feeders.clear() return self + + # endregion + # endregion diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index cd0eb9908..639328c83 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -15,6 +15,7 @@ from warnings import deprecated else: from typing_extensions import deprecated +from dataclasses import field from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.model.cim.extensions.iec61970.base.protection.power_direction_kind import PowerDirectionKind @@ -22,6 +23,8 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.util import require, nlen, ngen, safe_remove, get_by_mrid +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61968.assetinfo.relay_info import RelayInfo @@ -59,11 +62,11 @@ class ProtectionRelayFunction(PowerSystemResource, metaclass=ABCMeta): power_direction: PowerDirectionKind = PowerDirectionKind.UNKNOWN """[ZBEX] The flow of the power direction used by this ProtectionRelayFunction.""" - _sensors: Optional[List[Sensor]] = None + _sensors: Optional[List[Sensor]] = field(default=None) - _protected_switches: Optional[List[ProtectedSwitch]] = None + _protected_switches: Optional[List[ProtectedSwitch]] = field(default=None) - _schemes: Optional[List[ProtectionRelayScheme]] = None + _schemes: Optional[List[ProtectionRelayScheme]] = field(default=None) _time_limits: Optional[List[float]] = None @@ -312,155 +315,121 @@ def clear_time_limits(self) -> ProtectionRelayFunction: self._time_limits = None return self - def num_sensors(self) -> int: - """ - Get the number of :class:`Sensors` for this :class:`ProtectionRelayFunction`. + sensors: MridCollection[Sensor] = LazyMridList( + _sensors, + "A Sensor", + ) - :return: The number of :class:`Sensors` for this :class:`ProtectionRelayFunction`. - """ - return nlen(self._sensors) - def get_sensor(self, mrid: str) -> Sensor: - """ - Get a sensor :class:`Sensor` for this :class:`ProtectionRelayFunction` by its mrid. - :param mrid: The mrid of the desired :class:`Sensor`. - :returns: The :class:`Sensor` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._sensors, mrid) - def add_sensor(self, sensor: Sensor) -> ProtectionRelayFunction: - """ - Associate this :class:`ProtectionRelayFunction` with a :class:`Sensor`. - :param sensor: The :class:`Sensor` to associate with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if self._validate_reference(sensor, self.get_sensor, "A Sensor"): - return self - self._sensors = list() if self._sensors is None else self._sensors - self._sensors.append(sensor) - return self - def remove_sensor(self, sensor: Optional[Sensor]) -> ProtectionRelayFunction: - """ - Disassociate this :class:`ProtectionRelayFunction` from a :class:`Sensor`. + protected_switches: MridCollection[ProtectedSwitch] = LazyMridList( + _protected_switches, + "A ProtectedSwitch", + ) - :param sensor: The :class:`Sensor` to disassociate from this :class:`ProtectionRelayFunction`. - :raises ValueError: If sensor was not associated with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._sensors = safe_remove(self._sensors, sensor) - return self - def clear_sensors(self) -> ProtectionRelayFunction: + + + def remove_protected_switch(self, protected_switch: Optional[ProtectedSwitch]) -> ProtectionRelayFunction: """ - Disassociate all :class:`Sensors` from this :class:`ProtectionRelayFunction`. + Disassociate this :class:`ProtectionRelayFunction` from a :class:`ProtectedSwitch`. + :param protected_switch: The :class:`ProtectedSwitch` to disassociate from this :class:`ProtectionRelayFunction`. + :raises ValueError: If protected_switch was not associated with this :class:`ProtectionRelayFunction`. :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. """ - self._sensors = None + self._sensors = safe_remove(self._protected_switches, protected_switch) return self - def num_protected_switches(self) -> int: - """ - Get the number of :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. - :return: The number of :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. - """ - return nlen(self._protected_switches) + schemes: MridCollection[ProtectionRelayScheme] = LazyMridList( + _schemes, + "A ProtectionRelayScheme", + ) - def get_protected_switch(self, mrid: str) -> ProtectedSwitch: - """ - Get a :class:`ProtectedSwitch` operated by this :class:`ProtectionRelayFunction` by its mrid. - :param mrid: The mrid of the desired :class:`ProtectedSwitch`. - :returns: The :class:`ProtectedSwitch` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._protected_switches, mrid) - def add_protected_switch(self, protected_switch: ProtectedSwitch) -> ProtectionRelayFunction: - """ - Associate this :class:`ProtectionRelayFunction` with a :class:`ProtectedSwitch` it operates. - :param protected_switch: The :class:`ProtectedSwitch` to associate with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if self._validate_reference(protected_switch, self.get_protected_switch, "A ProtectedSwitch"): - return self - self._protected_switches = list() if self._protected_switches is None else self._protected_switches - self._protected_switches.append(protected_switch) + + + # region deprecated list boilerplate + # region sensors boilerplate + + @deprecated("Use len(obj.sensors) instead.") + def num_sensors(self) -> int: + return len(self.sensors) + + @deprecated("Use obj.sensors.get_by_mrid(mrid) instead.") + def get_sensor(self, mrid: str) -> Sensor: + return self.sensors.get_by_mrid(mrid) + + @deprecated("Use obj.sensors.append(sensor) instead.") + def add_sensor(self, sensor: Sensor) -> ProtectionRelayFunction: + self.sensors.append(sensor) return self - def remove_protected_switch(self, protected_switch: Optional[ProtectedSwitch]) -> ProtectionRelayFunction: - """ - Disassociate this :class:`ProtectionRelayFunction` from a :class:`ProtectedSwitch`. + @deprecated("Use obj.sensors.remove(sensor) instead.") + def remove_sensor(self, sensor: Optional[Sensor]) -> ProtectionRelayFunction: + self.sensors.remove(sensor) + return self - :param protected_switch: The :class:`ProtectedSwitch` to disassociate from this :class:`ProtectionRelayFunction`. - :raises ValueError: If protected_switch was not associated with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._sensors = safe_remove(self._protected_switches, protected_switch) + @deprecated("Use obj.sensors.clear() instead.") + def clear_sensors(self) -> ProtectionRelayFunction: + self.sensors.clear() return self - def clear_protected_switches(self) -> ProtectionRelayFunction: - """ - Disassociate all :class:`ProtectedSwitches` from this :class:`ProtectionRelayFunction`. + # endregion sensors boilerplate - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._protected_switches = None + # region protected_switches boilerplate + + @deprecated("Use len(obj.protected_switches) instead.") + def num_protected_switches(self) -> int: + return len(self.protected_switches) + + @deprecated("Use obj.protected_switches.get_by_mrid(mrid) instead.") + def get_protected_switch(self, mrid: str) -> ProtectedSwitch: + return self.protected_switches.get_by_mrid(mrid) + + @deprecated("Use obj.protected_switches.append(protected_switch) instead.") + def add_protected_switch(self, protected_switch: ProtectedSwitch) -> ProtectionRelayFunction: + self.protected_switches.append(protected_switch) return self - def num_schemes(self) -> int: - """ - Get the number of :class:`ProtectionRelaySchemes` this :class:`ProtectionRelayFunction` operates under. + @deprecated("Use obj.protected_switches.clear() instead.") + def clear_protected_switches(self) -> ProtectionRelayFunction: + self.protected_switches.clear() + return self - :return: The number of:class:`ProtectionRelaySchemes` operated by this :class:`ProtectionRelayFunction`. - """ - return nlen(self._schemes) + # endregion protected_switches boilerplate - def get_scheme(self, mrid: str) -> ProtectionRelayScheme: - """ - Get a :class:`ProtectionRelayScheme` this :class:`ProtectionRelayFunction` operates under by its mRID. + # region schemes boilerplate - :param mrid: The mRID of the desired :class:`ProtectionRelayScheme`. - :returns: The :class:`ProtectionRelayScheme` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._schemes, mrid) + @deprecated("Use len(obj.schemes) instead.") + def num_schemes(self) -> int: + return len(self.schemes) - def add_scheme(self, scheme: ProtectionRelayScheme) -> ProtectionRelayFunction: - """ - Associate this :class:`ProtectionRelayFunction` with a :class:`ProtectionRelayScheme` it operates under. + @deprecated("Use obj.schemes.get_by_mrid(mrid) instead.") + def get_scheme(self, mrid: str) -> ProtectionRelayScheme: + return self.schemes.get_by_mrid(mrid) - :param scheme: The :class:`ProtectionRelayScheme` to associate with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if self._validate_reference(scheme, self.get_scheme, "A ProtectionRelayScheme"): - return self - self._schemes = list() if self._schemes is None else self._schemes - self._schemes.append(scheme) + @deprecated("Use obj.schemes.append(scheme) instead.") + def add_scheme(self, scheme: ProtectionRelayScheme) -> ProtectionRelayFunction: + self.schemes.append(scheme) return self + @deprecated("Use obj.schemes.remove(scheme) instead.") def remove_scheme(self, scheme: Optional[ProtectionRelayScheme]) -> ProtectionRelayFunction: - """ - Disassociate this :class:`ProtectionRelayFunction` from a :class:`ProtectionRelayScheme`. - - :param scheme: The :class:`ProtectionRelayScheme` to disassociate from this :class:`ProtectionRelayFunction`. - :raises ValueError: If scheme was not associated with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._schemes = safe_remove(self._schemes, scheme) + self.schemes.remove(scheme) return self + @deprecated("Use obj.schemes.clear() instead.") def clear_schemes(self) -> ProtectionRelayFunction: - """ - Disassociate all :class:`ProtectionRelaySchemes` from this :class:`ProtectionRelayFunction`. - - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._schemes = None + self.schemes.clear() return self + + # endregion schemes boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py index 2d6a42987..dbeb9cf07 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py @@ -8,11 +8,15 @@ __all__ = ["ProtectionRelayScheme"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import ngen, get_by_mrid, nlen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_system import ProtectionRelaySystem @@ -30,70 +34,40 @@ class ProtectionRelayScheme(IdentifiedObject): system: Optional[ProtectionRelaySystem] = None """[ZBEX] The system this scheme belongs to.""" - _functions: Optional[List[ProtectionRelayFunction]] = None + _functions: Optional[List[ProtectionRelayFunction]] = field(default=None) - def __init__(self, *args, functions: Optional[List[ProtectionRelayFunction]] = None, **kwargs): - super(ProtectionRelayScheme, self).__init__(*args, **kwargs) - if functions is not None: - for function in functions: - self.add_function(function) + functions: MridCollection[ProtectionRelayFunction] = LazyMridList( + _functions, + "A ProtectionRelayFunction", + ) - @property - def functions(self) -> Generator[ProtectionRelayFunction, None, None]: - """ - [ZBEX] 6Yields all the functions operated as part of this :class:`ProtectionRelayScheme`. - :return: A generator that iterates over all functions operated as part of this :class:`ProtectionRelayScheme`. - """ - return ngen(self._functions) + # region deprecated list boilerplate + # region functions boilerplate + @deprecated("Use len(obj.functions) instead.") def num_functions(self) -> int: - """ - Get the number of :class:`ProtectionRelayFunctions` operated as part of this :class:`ProtectionRelayScheme`. - - :return: The number of :class:`ProtectionRelayFunctions` operated as part of this :class:`ProtectionRelayScheme`. - """ - return nlen(self._functions) + return len(self.functions) + @deprecated("Use obj.functions.get_by_mrid(mrid) instead.") def get_function(self, mrid: str) -> ProtectionRelayFunction: - """ - Get a :class:`ProtectionRelayFunction` operated as part of this :class:`ProtectionRelayScheme`. - - :param mrid: The mrid of the desired :class:`ProtectionRelayFunction`. - :returns: The :class:`ProtectionRelayFunction` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._functions, mrid) + return self.functions.get_by_mrid(mrid) + @deprecated("Use obj.functions.append(function) instead.") def add_function(self, function: ProtectionRelayFunction) -> ProtectionRelayScheme: - """ - Associate a :class:`ProtectionRelayFunction` with this :class:`ProtectionRelayScheme`. - - :param function: The :class:`ProtectionRelayFunction` to associate with this :class:`ProtectionRelayScheme`. - :return: A reference to this :class:`ProtectionRelayScheme` for fluent use. - """ - if self._validate_reference(function, self.get_function, "A ProtectionRelayFunction"): - return self - self._functions = list() if self._functions is None else self._functions - self._functions.append(function) + self.functions.append(function) return self + @deprecated("Use obj.functions.remove(function) instead.") def remove_function(self, function: Optional[ProtectionRelayFunction]) -> ProtectionRelayScheme: - """ - Disassociate this :class:`ProtectionRelayScheme` from a :class:`ProtectionRelayFunction`. - - :param function: The :class:`ProtectionRelayFunction` to disassociate from this :class:`ProtectionRelayScheme`. - :raises ValueError: If function was not associated with this :class:`ProtectionRelayScheme`. - :return: A reference to this :class:`ProtectionRelayScheme` for fluent use. - """ - self._functions = safe_remove(self._functions, function) + self.functions.remove(function) return self + @deprecated("Use obj.functions.clear() instead.") def clear_function(self) -> ProtectionRelayScheme: - """ - Disassociate all :class:`ProtectionRelayFunctions` from this :class:`ProtectionRelayScheme`. - - :return: A reference to this :class:`ProtectionRelayScheme` for fluent use. - """ - self._functions = None + self.functions.clear() return self + + # endregion functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py index d0245a0e2..49276fd6d 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py @@ -8,12 +8,16 @@ __all__ = ["ProtectionRelaySystem"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.util import ngen, get_by_mrid, nlen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_scheme import ProtectionRelayScheme @@ -30,70 +34,40 @@ class ProtectionRelaySystem(Equipment): protection_kind: ProtectionKind = ProtectionKind.UNKNOWN """[ZBEX] The kind of protection being provided by this protection equipment.""" - _schemes: Optional[List[ProtectionRelayScheme]] = None + _schemes: Optional[List[ProtectionRelayScheme]] = field(default=None) - def __init__(self, *args, schemes: Optional[List[ProtectionRelayScheme]] = None, **kwargs): - super(ProtectionRelaySystem, self).__init__(*args, **kwargs) - if schemes is not None: - for scheme in schemes: - self.add_scheme(scheme) + schemes: MridCollection[ProtectionRelayScheme] = LazyMridList( + _schemes, + "A ProtectionRelayScheme", + ) - @property - def schemes(self) -> Generator[ProtectionRelayScheme, None, None]: - """ - [ZBEX] Yields all the schemes implemented by this :class:`ProtectionRelaySystem`. - :return: A generator that iterates over all the schemes implemented by this :class:`ProtectionRelaySystem`. - """ - return ngen(self._schemes) + # region deprecated list boilerplate + # region schemes boilerplate + @deprecated("Use len(obj.schemes) instead.") def num_schemes(self) -> int: - """ - Get the number of :class:`ProtectionRelaySchemes` for this :class:`ProtectionRelaySystem`. - - :return: The number of :class:`ProtectionRelaySchemes` for this :class:`ProtectionRelaySystem`. - """ - return nlen(self._schemes) + return len(self.schemes) + @deprecated("Use obj.schemes.get_by_mrid(mrid) instead.") def get_scheme(self, mrid: str) -> ProtectionRelayScheme: - """ - Get a :class:`ProtectionRelayScheme` for this :class:`ProtectionRelaySystem` by its mRID. - - :param mrid: The mRID of the desired :class:`ProtectionRelayScheme`. - :returns: The :class:`ProtectionRelayScheme` with the specified mrid if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._schemes, mrid) + return self.schemes.get_by_mrid(mrid) + @deprecated("Use obj.schemes.append(scheme) instead.") def add_scheme(self, scheme: ProtectionRelayScheme) -> ProtectionRelaySystem: - """ - Add a :class:`ProtectionRelayScheme` to this :class:`ProtectionRelaySystem`. - - :param scheme: The :class:`ProtectionRelayScheme` to add. - :return: A reference to this :class:`ProtectionRelaySystem` for fluent use. - """ - if self._validate_reference(scheme, self.get_scheme, "A ProtectionRelayScheme"): - return self - self._schemes = list() if self._schemes is None else self._schemes - self._schemes.append(scheme) + self.schemes.append(scheme) return self + @deprecated("Use obj.schemes.remove(scheme) instead.") def remove_scheme(self, scheme: Optional[ProtectionRelayScheme]) -> ProtectionRelaySystem: - """ - Remove a :class:`ProtectionRelayScheme` from this :class:`ProtectionRelaySystem`. - - :param scheme: The :class:`ProtectionRelayScheme` to remove. - :raises ValueError: If scheme was not associated with this :class:`ProtectionRelaySystem`. - :return: A reference to this :class:`ProtectionRelaySystem` for fluent use. - """ - self._schemes = safe_remove(self._schemes, scheme) + self.schemes.remove(scheme) return self + @deprecated("Use obj.schemes.clear() instead.") def clear_scheme(self) -> ProtectionRelaySystem: - """ - Remove all :class:`ProtectionRelaySchemes` from this :class:`ProtectionRelaySystem`. - - :return: A reference to this :class:`ProtectionRelaySystem` for fluent use. - """ - self._schemes = None + self.schemes.clear() return self + + # endregion schemes boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py index c774d5006..e966a3e14 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py @@ -8,11 +8,15 @@ __all__ = ["PowerTransformerInfo"] from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.transformer_tank_info import TransformerTankInfo @@ -22,21 +26,14 @@ class PowerTransformerInfo(AssetInfo): """Set of power transformer data, from an equipment library.""" - _transformer_tank_infos: Optional[List[TransformerTankInfo]] = None + _transformer_tank_infos: Optional[List[TransformerTankInfo]] = field(default=None) """Data for all the tanks described by this power transformer data.""" - def __init__(self, *args, transformer_tank_infos: List[TransformerTankInfo] = None, **kwargs): - super(PowerTransformerInfo, self).__init__(*args, **kwargs) - if transformer_tank_infos: - for ti in transformer_tank_infos: - self.add_transformer_tank_info(ti) + transformer_tank_infos: MridCollection[TransformerTankInfo] = LazyMridList( + _transformer_tank_infos, + "A TransformerTankInfo", + ) - @property - def transformer_tank_infos(self) -> Generator[TransformerTankInfo, None, None]: - """ - The `TransformerTankInfo`s of this `PowerTransformerInfo`. - """ - return ngen(self._transformer_tank_infos) def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance]: """ @@ -51,55 +48,32 @@ def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance] else: return None + # region deprecated list boilerplate + # region transformer_tank_infos boilerplate + + @deprecated("Use len(obj.transformer_tank_infos) instead.") def num_transformer_tank_infos(self): - """ - Get the number of `TransformerTankInfo`s associated with this `PowerTransformerInfo`. - """ - return nlen(self._transformer_tank_infos) + return len(self.transformer_tank_infos) + @deprecated("Use obj.transformer_tank_infos.get_by_mrid(mrid) instead.") def get_transformer_tank_info(self, mrid: str) -> TransformerTankInfo: - """ - Get the `TransformerTankInfo` for this `PowerTransformerInfo` identified by `mrid`. - - `mrid` the mRID of the required `TransformerTankInfo` - Returns The `TransformerTankInfo` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._transformer_tank_infos, mrid) + return self.transformer_tank_infos.get_by_mrid(mrid) + @deprecated("Use obj.transformer_tank_infos.append(tti) instead.") def add_transformer_tank_info(self, tti: TransformerTankInfo) -> PowerTransformerInfo: - """ - `tti` The `TransformerTankInfo` to - associate with this `PowerTransformerInfo`. - - Returns A reference to this `PowerTransformerInfo` to allow fluent use. - - Raises `ValueError` if another `TransformerTankInfo` with the same `mrid` already - exists in this `PowerTransformerInfo` - """ - if self._validate_reference(tti, self.get_transformer_tank_info, "A TransformerTankInfo"): - return self - - self._transformer_tank_infos = list() if self._transformer_tank_infos is None else self._transformer_tank_infos - self._transformer_tank_infos.append(tti) + self.transformer_tank_infos.append(tti) return self + @deprecated("Use obj.transformer_tank_infos.remove(tti) instead.") def remove_transformer_tank_info(self, tti: TransformerTankInfo) -> PowerTransformerInfo: - """ - Disassociate an `TransformerTankInfo` from this `PowerTransformerInfo`. - - `tti` the `TransformerTankInfo` to - disassociate with this `PowerTransformerInfo`. - Raises `ValueError` if `tti` was not associated with this `PowerTransformerInfo`. - Returns A reference to this `Asset` to allow fluent use. - """ - self._transformer_tank_infos = safe_remove(self._transformer_tank_infos, tti) + self.transformer_tank_infos.remove(tti) return self + @deprecated("Use obj.transformer_tank_infos.clear() instead.") def clear_transformer_tank_infos(self) -> PowerTransformerInfo: - """ - Clears all `TransformerTankInfo`. - Returns self - """ - self._transformer_tank_infos = None + self.transformer_tank_infos.clear() return self + + # endregion transformer_tank_infos boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py index 1d827d563..a0e0c406f 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py @@ -8,11 +8,15 @@ __all__ = ["TransformerTankInfo"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.util import nlen, ngen, safe_remove, get_by_mrid -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo @@ -26,21 +30,14 @@ class TransformerTankInfo(AssetInfo): power_transformer_info: Optional[PowerTransformerInfo] = None """Power transformer data that this tank description is part of.""" - _transformer_end_infos: Optional[List[TransformerEndInfo]] = None + _transformer_end_infos: Optional[List[TransformerEndInfo]] = field(default=None) """Data for all the ends described by this transformer tank data.""" - def __init__(self, *args, transformer_end_infos: List[TransformerEndInfo] = None, **kwargs): - super(TransformerTankInfo, self).__init__(*args, **kwargs) - if transformer_end_infos: - for tei in transformer_end_infos: - self.add_transformer_end_info(tei) + transformer_end_infos: MridCollection[TransformerEndInfo] = LazyMridList( + _transformer_end_infos, + "A TransformerEndInfo", + ) - @property - def transformer_end_infos(self) -> Generator[TransformerEndInfo, None, None]: - """ - The `TransformerEndInfo`s of this `TransformerTankInfo`. - """ - return ngen(self._transformer_end_infos) def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance]: """ @@ -56,55 +53,32 @@ def resistance_reactance(self, end_number: int) -> Optional[ResistanceReactance] else: return None + # region deprecated list boilerplate + # region transformer_end_infos boilerplate + + @deprecated("Use len(obj.transformer_end_infos) instead.") def num_transformer_end_infos(self): - """ - Get the number of `TransformerEndInfo`s associated with this `TransformerTankInfo`. - """ - return nlen(self._transformer_end_infos) + return len(self.transformer_end_infos) + @deprecated("Use obj.transformer_end_infos.get_by_mrid(mrid) instead.") def get_transformer_end_info(self, mrid: str) -> TransformerEndInfo: - """ - Get the `TransformerEndInfo` for this `TransformerTankInfo` identified by `mrid`. - - `mrid` the mRID of the required `TransformerEndInfo` - Returns The `TransformerEndInfo` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._transformer_end_infos, mrid) + return self.transformer_end_infos.get_by_mrid(mrid) + @deprecated("Use obj.transformer_end_infos.append(tei) instead.") def add_transformer_end_info(self, tei: TransformerEndInfo) -> TransformerTankInfo: - """ - `tei` The `TransformerEndInfo` to - associate with this `TransformerTankInfo`. - - Returns A reference to this `TransformerTankInfo` to allow fluent use. - - Raises `ValueError` if another `TransformerEndInfo` with the same `mrid` already - exists in this `TransformerTankInfo` - """ - if self._validate_reference(tei, self.get_transformer_end_info, "A TransformerEndInfo"): - return self - - self._transformer_end_infos = list() if self._transformer_end_infos is None else self._transformer_end_infos - self._transformer_end_infos.append(tei) + self.transformer_end_infos.append(tei) return self + @deprecated("Use obj.transformer_end_infos.remove(tei) instead.") def remove_transformer_end_info(self, tei: TransformerEndInfo) -> TransformerTankInfo: - """ - Disassociate an `TransformerEndInfo` from this `TransformerTankInfo`. - - `tei` the `TransformerEndInfo` to - disassociate with this `TransformerTankInfo`. - Raises `ValueError` if `tei` was not associated with this `TransformerTankInfo`. - Returns A reference to this `TransformerTankInfo` to allow fluent use. - """ - self._transformer_end_infos = safe_remove(self._transformer_end_infos, tei) + self.transformer_end_infos.remove(tei) return self + @deprecated("Use obj.transformer_end_infos.clear() instead.") def clear_transformer_end_infos(self) -> TransformerTankInfo: - """ - Clears all `TransformerEndInfo`. - Returns self - """ - self._transformer_end_infos = None + self.transformer_end_infos.clear() return self + + # endregion transformer_end_infos boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset.py b/src/zepben/ewb/model/cim/iec61968/assets/asset.py index 9ca727541..afb538c2c 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset.py @@ -9,10 +9,14 @@ from typing import Optional, Generator, List, TYPE_CHECKING from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset_organisation_role import AssetOrganisationRole @@ -32,126 +36,74 @@ class Asset(IdentifiedObject, metaclass=ABCMeta): location: Optional[Location] = None """`zepben.ewb.model.cim.iec61968.common.location.Location` of this asset""" - _organisation_roles: Optional[List[AssetOrganisationRole]] = None + _organisation_roles: Optional[List[AssetOrganisationRole]] = field(default=None) - _power_system_resources: Optional[List[PowerSystemResource]] = None + _power_system_resources: Optional[List[PowerSystemResource]] = field(default=None) - def __init__(self, *args, organisation_roles: List[AssetOrganisationRole] = None, power_system_resources: List[PowerSystemResource] = None, **kwargs): - super(Asset, self).__init__(*args, **kwargs) - if organisation_roles: - for role in organisation_roles: - self.add_organisation_role(role) + organisation_roles: MridCollection[AssetOrganisationRole] = LazyMridList( + _organisation_roles, + "An AssetOrganisationRole", + ) - if power_system_resources: - for resource in power_system_resources: - self.add_power_system_resource(resource) + power_system_resources: MridCollection[PowerSystemResource] = LazyMridList( + _power_system_resources, + "An PowerSystemResource", + ) - @property - def organisation_roles(self) -> Generator[AssetOrganisationRole, None, None]: - """ - The `AssetOrganisationRole`s of this `Asset`. - """ - return ngen(self._organisation_roles) - @property - def power_system_resources(self) -> Generator[PowerSystemResource, None, None]: - """ - The `PowerSystemResource`s of this `Asset`. - """ - return ngen(self._power_system_resources) + # region deprecated list boilerplate + # region organisation_roles boilerplate + @deprecated("Use len(obj.organisation_roles) instead.") def num_organisation_roles(self) -> int: - """ - Get the number of `AssetOrganisationRole`s associated with this `Asset`. - """ - return nlen(self._organisation_roles) + return len(self.organisation_roles) + @deprecated("Use obj.organisation_roles.get_by_mrid(mrid) instead.") def get_organisation_role(self, mrid: str) -> AssetOrganisationRole: - """ - Get the `AssetOrganisationRole` for this asset identified by `mrid`. - - `mrid` the mRID of the required `AssetOrganisationRole` - Returns The `AssetOrganisationRole` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._organisation_roles, mrid) + return self.organisation_roles.get_by_mrid(mrid) + @deprecated("Use obj.organisation_roles.append(role) instead.") def add_organisation_role(self, role: AssetOrganisationRole) -> Asset: - """ - `role` The `AssetOrganisationRole` to associate with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - Raises `ValueError` if another `AssetOrganisationRole` with the same `mrid` already exists in this `Asset` - """ - if self._validate_reference(role, self.get_organisation_role, "An AssetOrganisationRole"): - return self - - self._organisation_roles = list() if self._organisation_roles is None else self._organisation_roles - self._organisation_roles.append(role) + self.organisation_roles.append(role) return self + @deprecated("Use obj.organisation_roles.remove(role) instead.") def remove_organisation_role(self, role: AssetOrganisationRole) -> Asset: - """ - Disassociate an `AssetOrganisationRole` from this `Asset`. - - `role` the `AssetOrganisationRole` to disassociate from this `Asset`. - Raises `ValueError` if `role` was not associated with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - """ - self._organisation_roles = safe_remove(self._organisation_roles, role) + self.organisation_roles.remove(role) return self + @deprecated("Use obj.organisation_roles.clear() instead.") def clear_organisation_roles(self) -> Asset: - """ - Clear all organisation roles. - Returns self - """ - self._organisation_roles = None + self.organisation_roles.clear() return self + # endregion organisation_roles boilerplate + + # region power_system_resources boilerplate + + @deprecated("Use len(obj.power_system_resources) instead.") def num_power_system_resources(self) -> int: - """ - Get the number of `PowerSystemResource`s associated with this `Asset`. - """ - return nlen(self._power_system_resources) + return len(self.power_system_resources) + @deprecated("Use obj.power_system_resources.get_by_mrid(mrid) instead.") def get_power_system_resource(self, mrid: str) -> PowerSystemResource: - """ - Get the `PowerSystemResource` for this asset identified by `mrid`. - - `mrid` the mRID of the required `PowerSystemResource` - Returns The `PowerSystemResource` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_system_resources, mrid) + return self.power_system_resources.get_by_mrid(mrid) + @deprecated("Use obj.power_system_resources.append(resource) instead.") def add_power_system_resource(self, resource: PowerSystemResource) -> Asset: - """ - `resource` The `PowerSystemResource` to associate with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - Raises `ValueError` if another `PowerSystemResource` with the same `mrid` already exists in this `Asset` - """ - if self._validate_reference(resource, self.get_power_system_resource, "An PowerSystemResource"): - return self - - self._power_system_resources = list() if self._power_system_resources is None else self._power_system_resources - self._power_system_resources.append(resource) + self.power_system_resources.append(resource) return self + @deprecated("Use obj.power_system_resources.remove(resource) instead.") def remove_power_system_resource(self, resource: PowerSystemResource) -> Asset: - """ - Disassociate an `PowerSystemResource` from this `Asset`. - - `resource` the `PowerSystemResource` to disassociate from this `Asset`. - Raises `ValueError` if `resource` was not associated with this `Asset`. - Returns A reference to this `Asset` to allow fluent use. - """ - self._power_system_resources = safe_remove(self._power_system_resources, resource) + self.power_system_resources.remove(resource) return self + @deprecated("Use obj.power_system_resources.clear() instead.") def clear_power_system_resources(self) -> Asset: - """ - Clear all power system resources. - Returns self - """ - self._power_system_resources = None + self.power_system_resources.clear() return self + + # endregion power_system_resources boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index f1425e257..9787f551d 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -8,11 +8,15 @@ __all__ = ["Customer"] from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.organisation_role import OrganisationRole from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer_agreement import CustomerAgreement @@ -30,66 +34,41 @@ class Customer(OrganisationRole): special_need: Optional[str] = None """A special service need such as life support, hospitals, etc.""" - _customer_agreements: Optional[List[CustomerAgreement]] = None + _customer_agreements: Optional[List[CustomerAgreement]] = field(default=None) - def __init__(self, *args, customer_agreements: List[CustomerAgreement] = None, **kwargs): - super(Customer, self).__init__(*args, **kwargs) - if customer_agreements: - for agreement in customer_agreements: - self.add_agreement(agreement) + agreements: MridCollection[CustomerAgreement] = LazyMridList( + _customer_agreements, + "A CustomerAgreement", + ) + customer_agreements = Alias(agreements) - @property - def agreements(self) -> Generator[CustomerAgreement, None, None]: - """ - The `CustomerAgreement`s for this `Customer`. - """ - return ngen(self._customer_agreements) + # region deprecated list boilerplate + # region agreements boilerplate + + @deprecated("Use len(obj.agreements) instead.") def num_agreements(self) -> int: - """ - Get the number of `CustomerAgreement`s associated with this `Customer`. - """ - return nlen(self._customer_agreements) + return len(self.agreements) + @deprecated("Use obj.agreements.get_by_mrid(mrid) instead.") def get_agreement(self, mrid: str) -> CustomerAgreement: - """ - Get the `CustomerAgreement` for this `Customer` identified by `mrid`. - - `mrid` the mRID of the required `customer_agreement.CustomerAgreement` - Returns the `CustomerAgreement` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._customer_agreements, mrid) + return self.agreements.get_by_mrid(mrid) + @deprecated("Use obj.agreements.append(customer_agreement) instead.") def add_agreement(self, customer_agreement: CustomerAgreement) -> Customer: - """ - Associate a `CustomerAgreement` with this `Customer`. - `customer_agreement` The `customer_agreement.CustomerAgreement` to associate with this `Customer`. - Returns A reference to this `Customer` to allow fluent use. - Raises `ValueError` if another `CustomerAgreement` with the same `mrid` already exists for this `Customer` - """ - if self._validate_reference(customer_agreement, self.get_agreement, "A CustomerAgreement"): - return self - - self._customer_agreements = list() if self._customer_agreements is None else self._customer_agreements - self._customer_agreements.append(customer_agreement) + self.agreements.append(customer_agreement) return self + @deprecated("Use obj.agreements.remove(customer_agreement) instead.") def remove_agreement(self, customer_agreement: CustomerAgreement) -> Customer: - """ - Disassociate `customer_agreement` from this `Customer`. - - `customer_agreement` the `customer_agreement.CustomerAgreement` to disassociate with this `Customer`. - Returns A reference to this `Customer` to allow fluent use. - Raises `ValueError` if `customer_agreement` was not associated with this `Customer`. - """ - self._customer_agreements = safe_remove(self._customer_agreements, customer_agreement) + self.agreements.remove(customer_agreement) return self + @deprecated("Use obj.agreements.clear() instead.") def clear_agreements(self) -> Customer: - """ - Clear all customer agreements. - Returns self - """ - self._customer_agreements = None + self.agreements.clear() return self + + # endregion agreements boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py index 4bfaf89da..9d6b0a4e0 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py @@ -8,10 +8,14 @@ __all__ = ["CustomerAgreement"] from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.agreement import Agreement from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer import Customer @@ -29,15 +33,7 @@ class CustomerAgreement(Agreement): _customer: Optional[Customer] = None """The `zepben.ewb.model.cim.iec61968.customers.customer.Customer` that has this `CustomerAgreement`.""" - _pricing_structures: Optional[List[PricingStructure]] = None - - def __init__(self, *args, customer: Customer = None, pricing_structures: List[PricingStructure] = None, **kwargs): - super(CustomerAgreement, self).__init__(*args, **kwargs) - if customer: - self.customer = customer - if pricing_structures: - for ps in pricing_structures: - self.add_pricing_structure(ps) + _pricing_structures: Optional[List[PricingStructure]] = field(default=None) @property def customer(self): @@ -51,59 +47,39 @@ def customer(self, cust): else: raise ValueError(f"customer for {str(self)} has already been set to {self._customer}, cannot reset this field to {cust}") - @property - def pricing_structures(self) -> Generator[PricingStructure, None, None]: - """ - The `PricingStructure`s of this `CustomerAgreement`. - """ - return ngen(self._pricing_structures) + pricing_structures: MridCollection[PricingStructure] = LazyMridList( + _pricing_structures, + "A PricingStructure", + ) + + + # region deprecated list boilerplate + # region pricing_structures boilerplate + + @deprecated("Use len(obj.pricing_structures) instead.") def num_pricing_structures(self): - """ - The number of `PricingStructure`s associated with this `CustomerAgreement` - """ - return nlen(self._pricing_structures) + return len(self.pricing_structures) + @deprecated("Use obj.pricing_structures.get_by_mrid(mrid) instead.") def get_pricing_structure(self, mrid: str) -> PricingStructure: - """ - Get the `PricingStructure` for this `CustomerAgreement` identified by `mrid` - - `mrid` the mRID of the required `PricingStructure` - Returns the `PricingStructure` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._pricing_structures, mrid) + return self.pricing_structures.get_by_mrid(mrid) + @deprecated("Use obj.pricing_structures.append(ps) instead.") def add_pricing_structure(self, ps: PricingStructure) -> CustomerAgreement: - """ - Associate `ps` with this `CustomerAgreement` - - `ps` the `PricingStructure` to associate with this `CustomerAgreement`. - Returns A reference to this `CustomerAgreement` to allow fluent use. - Raises `ValueError` if another `PricingStructure` with the same `mrid` already exists for this `CustomerAgreement` - """ - if self._validate_reference(ps, self.get_pricing_structure, "A PricingStructure"): - return self - - self._pricing_structures = list() if self._pricing_structures is None else self._pricing_structures - self._pricing_structures.append(ps) + self.pricing_structures.append(ps) return self + @deprecated("Use obj.pricing_structures.remove(ps) instead.") def remove_pricing_structure(self, ps: PricingStructure) -> CustomerAgreement: - """ - Disassociate `ps` from this `CustomerAgreement` - - `ps` the `PricingStructure` to disassociate from this `CustomerAgreement`. - Returns A reference to this `CustomerAgreement` to allow fluent use. - Raises `ValueError` if `ps` was not associated with this `CustomerAgreement`. - """ - self._pricing_structures = safe_remove(self._pricing_structures, ps) + self.pricing_structures.remove(ps) return self + @deprecated("Use obj.pricing_structures.clear() instead.") def clear_pricing_structures(self) -> CustomerAgreement: - """ - Clear all pricing structures. - Returns a reference to this `CustomerAgreement` to allow fluent use. - """ - self._pricing_structures = None + self.pricing_structures.clear() return self + + # endregion pricing_structures boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py index 55070f585..be237a622 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py @@ -8,10 +8,14 @@ __all__ = ["PricingStructure"] from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.tariff import Tariff @@ -29,68 +33,42 @@ class PricingStructure(Document): :var code: Unique user-allocated key for this pricing structure, used by company representatives to identify the correct price structure for allocating to a customer. For rate schedules it is often prefixed by a state code. """ - _tariffs: Optional[List[Tariff]] = None + _tariffs: Optional[List[Tariff]] = field(default=None) code: str | None = None - def __init__(self, *args, tariffs: List[Tariff] = None, **kwargs): - super(PricingStructure, self).__init__(*args, **kwargs) - if tariffs: - for tariff in tariffs: - self.add_tariff(tariff) + tariffs: MridCollection[Tariff] = LazyMridList( + _tariffs, + "A Tariff", + ) - @property - def tariffs(self) -> Generator[Tariff, None, None]: - """ - The `Tariff`s of this `PricingStructure`. - """ - return ngen(self._tariffs) + # region deprecated list boilerplate + # region tariffs boilerplate + + @deprecated("Use len(obj.tariffs) instead.") def num_tariffs(self): - """ - Returns The number of `Tariff`s associated with this `PricingStructure` - """ - return nlen(self._tariffs) + return len(self.tariffs) + @deprecated("Use obj.tariffs.get_by_mrid(mrid) instead.") def get_tariff(self, mrid: str) -> Tariff: - """ - Get the `Tariff` for this `PricingStructure` identified by `mrid` - - `mrid` the mRID of the required `Tariff` - Returns The `Tariff` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._tariffs, mrid) + return self.tariffs.get_by_mrid(mrid) + @deprecated("Use obj.tariffs.append(tariff) instead.") def add_tariff(self, tariff: Tariff) -> PricingStructure: - """ - Associate a `Tariff` with this `PricingStructure`. - - `tariff` the `Tariff` to associate with this `PricingStructure`. - Returns A reference to this `PricingStructure` to allow fluent use. - Raises `ValueError` if another `Tariff` with the same `mrid` already exists for this `PricingStructure`. - """ - if self._validate_reference(tariff, self.get_tariff, "A Tariff"): - return self - self._tariffs = list() if self._tariffs is None else self._tariffs - self._tariffs.append(tariff) + self.tariffs.append(tariff) return self + @deprecated("Use obj.tariffs.remove(tariff) instead.") def remove_tariff(self, tariff: Tariff) -> PricingStructure: - """ - Disassociate `tariff` from this `PricingStructure`. - - `tariff` the `Tariff` to disassociate from this `PricingStructure`. - Returns A reference to this `PricingStructure` to allow fluent use. - Raises `ValueError` if `tariff` was not associated with this `PricingStructure`. - """ - self._tariffs = safe_remove(self._tariffs, tariff) + self.tariffs.remove(tariff) return self + @deprecated("Use obj.tariffs.clear() instead.") def clear_tariffs(self) -> PricingStructure: - """ - Clear all tariffs. - Returns A reference to this `PricingStructure` to allow fluent use. - """ - self._tariffs = None + self.tariffs.clear() return self + + # endregion tariffs boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py index 2a6b1c6b9..a3ad34d3a 100644 --- a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py +++ b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py @@ -8,10 +8,14 @@ __all__ = ["Pole"] from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.structure import Structure from zepben.ewb.util import get_by_mrid, ngen, nlen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.streetlight import Streetlight @@ -24,66 +28,40 @@ class Pole(Structure): classification: Optional[str] = None """Pole class: 1, 2, 3, 4, 5, 6, 7, H1, H2, Other, Unknown.""" - _streetlights: Optional[List[Streetlight]] = None + _streetlights: Optional[List[Streetlight]] = field(default=None) - def __init__(self, *args, streetlights: List[Streetlight] = None, **kwargs): - super(Pole, self).__init__(*args, **kwargs) - if streetlights: - for light in streetlights: - self.add_streetlight(light) + streetlights: MridCollection[Streetlight] = LazyMridList( + _streetlights, + "A Streetlight", + ) - @property - def streetlights(self) -> Generator[Streetlight, None, None]: - """ - The `Streetlight`s of this `Pole`. - """ - return ngen(self._streetlights) + # region deprecated list boilerplate + # region streetlights boilerplate + + @deprecated("Use len(obj.streetlights) instead.") def num_streetlights(self) -> int: - """ - Get the number of `Streetlight`s associated with this `Pole`. - """ - return nlen(self._streetlights) + return len(self.streetlights) + @deprecated("Use obj.streetlights.get_by_mrid(mrid) instead.") def get_streetlight(self, mrid: str) -> Streetlight: - """ - Get the `Streetlight` for this asset identified by `mrid`. - - `mrid` the mRID of the required `Streetlight` - Returns The `Streetlight` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._streetlights, mrid) + return self.streetlights.get_by_mrid(mrid) + @deprecated("Use obj.streetlights.append(streetlight) instead.") def add_streetlight(self, streetlight: Streetlight) -> Pole: - """ - Associate a `Streetlight` with this `Pole` - - `streetlight` the `Streetlight` to associate with this `Pole`. - Returns A reference to this `Pole` to allow fluent use. - Raises `ValueError` if another `Streetlight` with the same `mrid` already exists in this `Pole` - """ - if self._validate_reference(streetlight, self.get_streetlight, "A Streetlight"): - return self - - self._streetlights = list() if self._streetlights is None else self._streetlights - self._streetlights.append(streetlight) + self.streetlights.append(streetlight) return self + @deprecated("Use obj.streetlights.remove(streetlight) instead.") def remove_streetlight(self, streetlight: Streetlight) -> Pole: - """ - Disassociate `streetlight` from this `Pole` - `streetlight` the `Streetlight` to disassociate from this `Pole`. - Raises `ValueError` if `streetlight` was not associated with this `Pole`. - Returns A reference to this `Pole` to allow fluent use. - """ - self._streetlights = safe_remove(self._streetlights, streetlight) + self.streetlights.remove(streetlight) return self + @deprecated("Use obj.streetlights.clear() instead.") def clear_streetlights(self) -> Pole: - """ - Clear all Streetlights. - Returns self - """ - self._streetlights = None + self.streetlights.clear() return self + + # endregion streetlights boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py index 943eaebf0..91db333b4 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py @@ -69,7 +69,7 @@ class ControlledAppliance: _bitmask: int - def __init__(self, appliances: Union[int, Appliance, List[Appliance]]): + def __init__(self, appliances: int | Appliance | List[Appliance]): if isinstance(appliances, int): self._bitmask = appliances elif isinstance(appliances, Appliance): diff --git a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py index 59bd836e1..bc4e3daff 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py @@ -9,10 +9,14 @@ from typing import Optional, List, Generator, TYPE_CHECKING from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_container import AssetContainer from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location @@ -42,127 +46,74 @@ class EndDevice(AssetContainer, metaclass=ABCMeta): service_location: Optional[Location] = None """Service `zepben.ewb.model.cim.iec61968.common.location.Location` whose service delivery is measured by this `EndDevice`.""" - _usage_points: Optional[List[UsagePoint]] = None - - _functions: Optional[List[EndDeviceFunction]] = None - - def __init__(self, *args, usage_points: List[UsagePoint] = None, functions: List[EndDeviceFunction] = None, **kwargs): - super(EndDevice, self).__init__(*args, **kwargs) - if usage_points: - for up in usage_points: - self.add_usage_point(up) - if functions: - for edf in functions: - self.add_function(edf) - - @property - def usage_points(self) -> Generator[UsagePoint, None, None]: - """ - The `UsagePoint`s associated with this `EndDevice` - """ - return ngen(self._usage_points) - - @property - def functions(self) -> Generator[EndDeviceFunction, None, None]: - """ - The `EndDeviceFunction`s associated with this `EndDevice` - """ - return ngen(self._functions) + _usage_points: Optional[List[UsagePoint]] = field(default=None) + _functions: Optional[List[EndDeviceFunction]] = field(default=None) + + usage_points: MridCollection[UsagePoint] = LazyMridList( + _usage_points, + "A UsagePoint", + ) + + functions: MridCollection[EndDeviceFunction] = LazyMridList( + _functions, + "An EndDeviceFunction", + ) + + + # region deprecated list boilerplate + # region usage_points boilerplate + + @deprecated("Use len(obj.usage_points) instead.") def num_usage_points(self): - """ - Returns The number of `UsagePoint`s associated with this `EndDevice` - """ - return nlen(self._usage_points) + return len(self.usage_points) + @deprecated("Use obj.usage_points.get_by_mrid(mrid) instead.") def get_usage_point(self, mrid: str) -> UsagePoint: - """ - Get the `UsagePoint` for this `EndDevice` identified by `mrid` - - `mrid` the mRID of the required `UsagePoint` - Returns The `UsagePoint` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._usage_points, mrid) + return self.usage_points.get_by_mrid(mrid) + @deprecated("Use obj.usage_points.append(up) instead.") def add_usage_point(self, up: UsagePoint) -> EndDevice: - """ - Associate `up` to this `EndDevice`. - - `up` the `UsagePoint` to associate with this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if another `UsagePoint` with the same `mrid` already exists for this `EndDevice`. - """ - if self._validate_reference(up, self.get_usage_point, "A UsagePoint"): - return self - self._usage_points = list() if self._usage_points is None else self._usage_points - self._usage_points.append(up) + self.usage_points.append(up) return self + @deprecated("Use obj.usage_points.remove(up) instead.") def remove_usage_point(self, up: UsagePoint) -> EndDevice: - """ - Disassociate `up` from this `EndDevice` - - `up` the `UsagePoint` to disassociate from this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `EndDevice`. - """ - self._usage_points = safe_remove(self._usage_points, up) + self.usage_points.remove(up) return self + @deprecated("Use obj.usage_points.clear() instead.") def clear_usage_points(self) -> EndDevice: - """ - Clear all usage_points. - Returns A reference to this `EndDevice` to allow fluent use. - """ - self._usage_points = None + self.usage_points.clear() return self + # endregion usage_points boilerplate + + # region functions boilerplate + + @deprecated("Use len(obj.functions) instead.") def num_functions(self): - """ - Returns The number of `EndDeviceFunction`s associated with this `EndDevice` - """ - return nlen(self._functions) + return len(self.functions) + @deprecated("Use obj.functions.get_by_mrid(mrid) instead.") def get_function(self, mrid: str) -> EndDeviceFunction: - """ - Get the `EndDeviceFunction` for this `EndDevice` identified by `mrid` - - `mrid` the mRID of the required `EndDeviceFunction` - Returns The `EndDeviceFunction` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._functions, mrid) - - def add_function(self, edf: EndDeviceFunction) -> EndDevice: - """ - Associate `edf` to this `EndDevice`. - - `edf` the `EndDeviceFunction` to associate with this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if another `EndDeviceFunction` with the same `mrid` already exists for this `EndDevice`. - """ - if self._validate_reference(edf, self.get_function, "An EndDeviceFunction"): - return self - self._functions = list() if self._functions is None else self._functions - self._functions.append(edf) + return self.functions.get_by_mrid(mrid) + + @deprecated("Use obj.functions.append(edf) instead.") + def add_function(self, edf: 'EndDeviceFunction') -> 'EndDevice': + self.functions.append(edf) return self + @deprecated("Use obj.functions.remove(edf) instead.") def remove_function(self, edf: EndDeviceFunction) -> EndDevice: - """ - Disassociate `edf` from this `EndDevice` - - `up` the `EndDeviceFunction` to disassociate from this `EndDevice`. - Returns A reference to this `EndDevice` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `EndDevice`. - """ - self._functions = safe_remove(self._functions, edf) + self.functions.remove(edf) return self + @deprecated("Use obj.functions.clear() instead.") def clear_functions(self) -> EndDevice: - """ - Clear all end_device_functions. - Returns A reference to this `EndDevice` to allow fluent use. - """ - self._functions = None + self.functions.clear() return self + + # endregion functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index 40a2c2cc7..dc8235b9b 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -8,12 +8,16 @@ __all__ = ["UsagePoint"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location @@ -56,125 +60,27 @@ class UsagePoint(IdentifiedObject): four-wire, s12n (splitSecondary12N) is single-phase, three-wire, and s1n and s2n are single-phase, two-wire. """ - _equipment: list[Equipment] | None = None - _end_devices: list[EndDevice] | None = None + _equipment: list[Equipment] | None = field(default=None) + _end_devices: list[EndDevice] | None = field(default=None) _contacts: list[ContactDetails] | None = None - def __init__(self, *args, equipment: List[Equipment] = None, end_devices: List[EndDevice] = None, contacts: List[ContactDetails] = None, **kwargs): + def __init__(self, *args, contacts: List[ContactDetails] = None, **kwargs): super(UsagePoint, self).__init__(*args, **kwargs) - if equipment: - for eq in equipment: - self.add_equipment(eq) - if end_devices: - for ed in end_devices: - self.add_end_device(ed) if contacts: for c in contacts: self.add_contact(c) - @property - def end_devices(self) -> Generator[EndDevice, None, None]: - """ - The `EndDevice`'s (Meter's) associated with this `UsagePoint`. - """ - return ngen(self._end_devices) - - @property - def equipment(self) -> Generator[Equipment, None, None]: - """ - The `zepben.model.Equipment` associated with this `UsagePoint`. - """ - return ngen(self._equipment) - def is_metered(self): - """ - Check whether this `UsagePoint` is metered. A `UsagePoint` is metered if it's associated with at least one `EndDevice`. - Returns True if this `UsagePoint` has an `EndDevice`, False otherwise. - """ - return nlen(self._end_devices) > 0 - - @property - def contacts(self) -> Generator[ContactDetails, None, None]: - """[ZBEX] All contact details for this `UsagePoint`""" - return ngen(self._contacts) - - def num_contacts(self): - """Get the number of entries in the `ContactDetails` collection""" - return nlen(self._contacts) - - def get_contact(self, _id: str) -> ContactDetails: - """All End devices at this usage point.""" # TODO: again, lol, also jvmsdk - try: - return next((it for it in self.contacts if it.id == _id)) - except StopIteration: - raise KeyError(_id) - - def add_contact(self, contact: ContactDetails) -> UsagePoint: - """Add a `ContactDetails` to this `UsagePoint`""" - if self._validate_reference(contact, self.get_contact, "A ContactDetails"): - return self - - if self._contacts is None: - self._contacts = list() - self._contacts.append(contact) - return self + end_devices: MridCollection[EndDevice] = LazyMridList( + _end_devices, + "An EndDevice", + ) - def remove_contact(self, contact: ContactDetails) -> UsagePoint: - self._contacts = safe_remove(self._contacts, contact) - return self - - def clear_contacts(self) -> UsagePoint: - self._contacts = None - return self - - def num_end_devices(self): - """ - Returns The number of `EndDevice`s associated with this `UsagePoint` - """ - return nlen(self._end_devices) - - def get_end_device(self, mrid: str) -> EndDevice: - """ - Get the `EndDevice` for this `UsagePoint` identified by `mrid` - - `mrid` The mRID of the required `EndDevice` - Returns The `EndDevice` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._end_devices, mrid) - - def add_end_device(self, end_device: EndDevice) -> UsagePoint: - """ - Associate an `EndDevice` with this `UsagePoint` - - `end_device` The `EndDevice` to associate with this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if another `EndDevice` with the same `mrid` already exists for this `UsagePoint`. - """ - if self._validate_reference(end_device, self.get_end_device, "An EndDevice"): - return self - self._end_devices = list() if self._end_devices is None else self._end_devices - self._end_devices.append(end_device) - return self - - def remove_end_device(self, end_device: EndDevice) -> UsagePoint: - """ - Disassociate `end_device` from this `UsagePoint`. - - `end_device` The `EndDevice` to disassociate from this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if `end_device` was not associated with this `UsagePoint`. - """ - self._end_devices = safe_remove(self._end_devices, end_device) - return self + equipment: MridCollection[Equipment] = LazyMridList( + _equipment, + "An Equipment", + ) - def clear_end_devices(self) -> UsagePoint: - """ - Clear all end_devices. - Returns A reference to this `UsagePoint` to allow fluent use. - """ - self._end_devices = None - return self def num_equipment(self): """ @@ -225,3 +131,60 @@ def clear_equipment(self) -> UsagePoint: """ self._equipment = None return self + + # region deprecated list boilerplate + # region end_devices boilerplate + + @deprecated("Use len(obj.end_devices) instead.") + def num_end_devices(self): + return len(self.end_devices) + + @deprecated("Use obj.end_devices.get_by_mrid(mrid) instead.") + def get_end_device(self, mrid: str) -> EndDevice: + return self.end_devices.get_by_mrid(mrid) + + @deprecated("Use obj.end_devices.append(end_device) instead.") + def add_end_device(self, end_device: EndDevice) -> UsagePoint: + self.end_devices.append(end_device) + return self + + @deprecated("Use obj.end_devices.remove(end_device) instead.") + def remove_end_device(self, end_device: EndDevice) -> UsagePoint: + self.end_devices.remove(end_device) + return self + + @deprecated("Use obj.end_devices.clear() instead.") + def clear_end_devices(self) -> UsagePoint: + self.end_devices.clear() + return self + + # endregion end_devices boilerplate + + # region equipment boilerplate + + @deprecated("Use len(obj.equipment) instead.") + def num_equipment(self): + return len(self.equipment) + + @deprecated("Use obj.equipment.get_by_mrid(mrid) instead.") + def get_equipment(self, mrid: str) -> Equipment: + return self.equipment.get_by_mrid(mrid) + + @deprecated("Use obj.equipment.append(equipment) instead.") + def add_equipment(self, equipment: Equipment) -> UsagePoint: + self.equipment.append(equipment) + return self + + @deprecated("Use obj.equipment.remove(equipment) instead.") + def remove_equipment(self, equipment: Equipment) -> UsagePoint: + self.equipment.remove(equipment) + return self + + @deprecated("Use obj.equipment.clear() instead.") + def clear_equipment(self) -> UsagePoint: + self.equipment.clear() + return self + + # endregion equipment boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py index 4ce33da14..5b79d57ee 100644 --- a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py +++ b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py @@ -8,10 +8,14 @@ __all__ = ["OperationalRestriction"] from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment @@ -29,66 +33,40 @@ class OperationalRestriction(Document): They then apply operational restrictions in the operational systems to warn operators of potential problems. After appropriate inspection and maintenance, the operational restrictions may be removed. """ - _equipment: Optional[List[Equipment]] = None + _equipment: Optional[List[Equipment]] = field(default=None) - def __init__(self, *args, equipment: List[Equipment] = None, **kwargs): - super(OperationalRestriction, self).__init__(*args, **kwargs) - if equipment: - for eq in equipment: - self.add_equipment(eq) + equipment: MridCollection[Equipment] = LazyMridList( + _equipment, + "An Equipment", + ) - @property - def equipment(self) -> Generator[Equipment, None, None]: - """ - The `Equipment` to which this `OperationalRestriction` applies. - """ - return ngen(self._equipment) + # region deprecated list boilerplate + # region equipment boilerplate + + @deprecated("Use len(obj.equipment) instead.") def num_equipment(self): - """ - Returns the number of `Equipment` associated with this `OperationalRestriction` - """ - return nlen(self._equipment) + return len(self.equipment) + @deprecated("Use obj.equipment.get_by_mrid(mrid) instead.") def get_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `OperationalRestriction` identified by `mrid` - - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._equipment, mrid) + return self.equipment.get_by_mrid(mrid) + @deprecated("Use obj.equipment.append(equipment) instead.") def add_equipment(self, equipment: Equipment) -> OperationalRestriction: - """ - Associate an `Equipment` with this `OperationalRestriction` - - `equipment` The `Equipment` to associate with this `OperationalRestriction`. - Returns A reference to this `OperationalRestriction` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `OperationalRestriction`. - """ - if self._validate_reference(equipment, self.get_equipment, "An Equipment"): - return self - self._equipment = list() if self._equipment is None else self._equipment - self._equipment.append(equipment) + self.equipment.append(equipment) return self + @deprecated("Use obj.equipment.remove(equipment) instead.") def remove_equipment(self, equipment: Equipment) -> OperationalRestriction: - """ - Disassociate `equipment` from this `OperationalRestriction`. - - `equipment` The `Equipment` to disassociate from this `OperationalRestriction`. - Returns A reference to this `OperationalRestriction` to allow fluent use. - Raises `ValueError` if `equipment` was not associated with this `OperationalRestriction`. - """ - self._equipment = safe_remove(self._equipment, equipment) + self.equipment.remove(equipment) return self + @deprecated("Use obj.equipment.clear() instead.") def clear_equipment(self) -> OperationalRestriction: - """ - Clear all equipment. - Returns A reference to this `OperationalRestriction` to allow fluent use. - """ - self._equipment = None + self.equipment.clear() return self + + # endregion equipment boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py index 6299e06ae..e8370543a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py @@ -9,10 +9,14 @@ from typing import Generator, Optional, List, TYPE_CHECKING, Iterable from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction @@ -25,71 +29,41 @@ class Sensor(AuxiliaryEquipment, metaclass=ABCMeta): used in control or be recorded. """ - _relay_functions: Optional[List[ProtectionRelayFunction]] = None + _relay_functions: Optional[List[ProtectionRelayFunction]] = field(default=None) """The relay functions influenced by this [Sensor].""" - def __init__(self, *args, relay_functions: Iterable[ProtectionRelayFunction] = None, **kwargs): - super(Sensor, self).__init__(*args, **kwargs) - if relay_functions is not None: - for relay_function in relay_functions: - self.add_relay_function(relay_function) + relay_functions: MridCollection[ProtectionRelayFunction] = LazyMridList( + _relay_functions, + "A ProtectionRelayFunction", + ) - @property - def relay_functions(self) -> Generator[ProtectionRelayFunction, None, None]: - """ - Yields all the :class:`ProtectionRelayFunction` that are influenced by this :class:`Sensor`. - :return: A generator that iterates over all ProtectionRelayFunction influenced by this Sensor. - """ - return ngen(self._relay_functions) + # region deprecated list boilerplate + # region relay_functions boilerplate + @deprecated("Use len(obj.relay_functions) instead.") def num_relay_functions(self) -> int: - """ - Get the number of :class:`ProtectionRelayFunction` that are influenced by this :class:`Sensor`. - - :return: The number of ProtectionRelayFunction influenced by this Sensor. - """ - return nlen(self._relay_functions) + return len(self.relay_functions) + @deprecated("Use obj.relay_functions.get_by_mrid(mrid) instead.") def get_relay_function(self, mrid: str) -> ProtectionRelayFunction: - """ - Get a :class:`ProtectionRelayFunction` that are influenced by this :class:`Sensor`. - - :param mrid: The mRID of the desired ProtectionRelayFunction - :return: The ProtectionRelayFunction with the specified mRID if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._relay_functions, mrid) + return self.relay_functions.get_by_mrid(mrid) + @deprecated("Use obj.relay_functions.append(protection_relay_function) instead.") def add_relay_function(self, protection_relay_function: ProtectionRelayFunction) -> Sensor: - """ - Associate this :class:`Sensor` with a :class:`ProtectionRelayFunction` it is influencing. - - :param protection_relay_function: The ProtectionRelayFunction to associate with this Sensor. - :return: A reference to this Sensor for fluent use. - """ - if self._validate_reference(protection_relay_function, self.get_relay_function, "A ProtectionRelayFunction"): - return self - - self._relay_functions = list() if self._relay_functions is None else self._relay_functions - self._relay_functions.append(protection_relay_function) + self.relay_functions.append(protection_relay_function) return self + @deprecated("Use obj.relay_functions.remove(protection_relay_function) instead.") def remove_relay_function(self, protection_relay_function: ProtectionRelayFunction) -> Sensor: - """ - Disassociate this :class:`Sensor` from a :class:`ProtectionRelayFunction` it is influencing. - - :param protection_relay_function: The ProtectionRelayFunction to disassociate from this Sensor. - :return: A reference to this Sensor for fluent use. - """ - self._relay_functions = safe_remove(self._relay_functions, protection_relay_function) + self.relay_functions.remove(protection_relay_function) return self + @deprecated("Use obj.relay_functions.clear() instead.") def clear_relay_function(self) -> Sensor: - """ - Disassociate all :class:`ProtectionRelayFunction` from this :class:`Sensor`. - - :return: A reference to this Sensor for fluent use. - """ - self._relay_functions = None + self.relay_functions.clear() return self + + # endregion relay_functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index 175605f39..821e87af8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -5,22 +5,53 @@ __all__ = ["Curve"] +from dataclasses import field from typing import Optional, List, Generator from abc import ABCMeta +from typing_extensions import deprecated + +from zepben.ewb import remove_descriptor_annotations, zb_dataclass +from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList from zepben.ewb.model.cim.iec61970.base.core.curve_data import CurveData from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import require, ngen, nlen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +class CurveDataList(LazyValidatedList[CurveData]): + + def get(self, x: float) -> CurveData: + """ + Get the :class:`CurveData` identified by its `x_value`. + + :param x: The X value of the required :class:`CurveData`. + :returns: The :class:`CurveData` with the specified `x` if it exists. + :raises KeyError: When no `CurveData` was found with `x`. + """ + curve_data = next((it for it in self if it.x_value == x), None) + if curve_data: + return curve_data + raise KeyError(x) + + def remove_data_at(self, x: float) -> CurveData: + """ + Disassociate a :class:`CurveData` from this collection based on its `x_value`. + + :param x: The :class:`CurveData` to disassociate from this :class:`Curve`. + :returns: A reference to the removed :class:`CurveData`. + :raises IndexError: If no :class:`CurveData` with a value of `x` was not associated with this :class:`Curve`. + """ + data = self.get(x) + self.remove(data) + return data + @zb_dataclass class Curve(IdentifiedObject, metaclass=ABCMeta): """ The Curve class is a multipurpose functional relationship between an independent variable (X-axis) and dependent (Y-axis) variables. """ - _data: Optional[List[CurveData]] = None + _data: list[CurveData] | None = field(default=None) def __init__(self, *args, data: List[CurveData] = None, **kwargs): """ @@ -31,98 +62,68 @@ def __init__(self, *args, data: List[CurveData] = None, **kwargs): for curve_data in data: self.add_curve_data(curve_data) - @property - def data(self) -> Generator[CurveData, None, None]: - """ - The point data values that define this curve, sorted by `x_value` in ascending order. - """ - return ngen(self._data) + data: CurveDataList[CurveData] = CurveDataList( + _data, + validate=lambda self, it: self._validate_data(it), + sort_by=lambda it: it.x_value + ) - def num_data(self): - """Return the number of :class:`CurveData` associated with this :class:`Curve`.""" - return nlen(self._data) + def _validate_data(self, curve_data: CurveData): + require(all([it.x_value != curve_data.x_value for it in self.data]), + lambda: f"Unable to add datapoint to {self}. x_value {curve_data.x_value} is invalid, as data with same x_value already exist in this Curve.") - def get_data(self, x: float) -> CurveData: - """ - Get the :class:`CurveData` for this :class:`Curve` identified by its `x_value`. - :param x: The X value of the required :class:`CurveData`. - :returns: The :class:`CurveData` with the specified `x` if it exists. - :raises KeyError: When no `CurveData` was found with `x`. - """ - if self._data: - curve_data = next((it for it in self._data if it.x_value == x), None) - if curve_data: - return curve_data - raise KeyError(x) + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. - def __getitem__(self, x: float) -> CurveData: - """ - Get the :class:`CurveData` for this :class:`Curve` identified by its `x_value`. - - :param x: The X value of the required :class:`CurveData`. - :returns: The :class:`CurveData` with the specified `x` if it exists. - :raises IndexError: When no `CurveData` was found with `x`. - """ - return self.get_data(x) - - def add_data(self, x: float, y1: float, y2: Optional[float], y3: Optional[float]) -> 'Curve': - """ - Add a data point to this :class:`Curve`. + # region data boilerplate - :param x: The data value of the X-axis variable, depending on the X-axis units. - :param y1: The data value of the first Y-axis variable, depending on the Y-axis units. - :param y2: The data value of the second Y-axis variable (if present), depending on the Y-axis units. - :param y3: The data value of the third Y-axis variable (if present), depending on the Y-axis units. - :raises ValueError: if a :class:`CurveData` for the provided `x` value already exists for this :class:`Curve`. - """ - require(all([it.x_value != x for it in self.data]), - lambda: f"Unable to add datapoint to {self}. x_value {x} is invalid, as data with same x_value already exist in this Curve.") + @deprecated("Use len(data) instead.") + def num_data(self): + return len(self.data) - if self._data is None: - self._data = [] - self._data.append(CurveData(x, y1, y2, y3)) - self._data.sort(key=lambda it: it.x_value) + @deprecated("Use data.get(x) instead.") + def get_data(self, x: float) -> CurveData: + return self.data.get(x) + @deprecated("Use data.get(x) instead.") + def __getitem__(self, x: float) -> CurveData: + return self.data.get(x) + + @deprecated("Use data.append(CurveData(x, y1, y2, y3)) instead.") + def add_data( + self, + x: float, + y1: float, + y2: Optional[float], + y3: Optional[float], + ) -> "Curve": + self.data.append(CurveData(x, y1, y2, y3)) return self - def add_curve_data(self, curve_data: CurveData) -> 'Curve': - """ - Associate a :class:`CurveData` with this :class:`Curve`. - - :param curve_data: The :class:`CurveData` to associate with this :class:`Curve`. - :returns: A reference to this :class:`Curve` to allow fluent use. - :raises ValueError: If another :class:`CurveData` with the same `x_value` already exists for this :class:`Curve`. - """ - return self.add_data(curve_data.x_value, curve_data.y1_value, curve_data.y2_value, curve_data.y3_value) - - def remove_data(self, curve_data: CurveData) -> 'Curve': - """ - Disassociate a :class:`CurveData` from this :class:`Curve`. + @deprecated("Use data.append(curve_data) instead.") + def add_curve_data(self, curve_data: CurveData) -> "Curve": + self.data.append(curve_data) + return self - :param curve_data: The :class:`CurveData` to disassociate from this :class:`Curve`. - :returns: A reference to this :class:`Curve` to allow fluent use. - :raises ValueError: If `curve_data` was not associated with this :class:`Curve`. - """ - self._data = safe_remove(self._data, curve_data) + @deprecated("Use data.remove(curve_data) instead.") + def remove_data(self, curve_data: CurveData) -> "Curve": + self.data.remove(curve_data) return self + @deprecated("Use data.remove_data_at(x) instead.") def remove_data_at(self, x: float) -> CurveData: - """ - Disassociate a :class:`CurveData` from this :class:`Curve` based on its `x_value`. - - :param x: The :class:`CurveData` to disassociate from this :class:`Curve`. - :returns: A reference to the removed :class:`CurveData`. - :raises IndexError: If no :class:`CurveData` with a value of `x` was not associated with this :class:`Curve`. - """ - data = self.get_data(x) - self._data = safe_remove(self._data, data) - return data + return self.data.remove_data_at(x) - def clear_data(self) -> 'Curve': - """ - Clear all :class:`CurveData` associated with this :class:`Curve`. - :returns: A reference to this :class:`Curve` to allow fluent use. - """ - self._data = None + @deprecated("Use data.clear() instead.") + def clear_data(self) -> "Curve": + self.data.clear() return self + + # endregion + + # endregion \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index a741c0752..fd41ad360 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -10,6 +10,8 @@ import datetime from typing import Optional, Generator, List, TYPE_CHECKING, TypeVar, Type from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_substation import LvSubstation @@ -18,7 +20,9 @@ from zepben.ewb.model.cim.iec61970.base.core.substation import Substation from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.metering.usage_point import UsagePoint @@ -42,25 +46,32 @@ class Equipment(PowerSystemResource, metaclass=ABCMeta): commissioned_date: Optional[datetime.datetime] = None """The date this equipment was commissioned into service.""" - _usage_points: Optional[List[UsagePoint]] = None - _equipment_containers: Optional[List[EquipmentContainer]] = None - _operational_restrictions: Optional[List[OperationalRestriction]] = None - _current_containers: Optional[List[EquipmentContainer]] = None - - def __init__(self, *args, usage_points: List[UsagePoint] = None, equipment_containers: List[EquipmentContainer] = None, operational_restrictions: List[OperationalRestriction] = None, current_containers: List[EquipmentContainer] = None, **kwargs): - super(Equipment, self).__init__(*args, **kwargs) - if usage_points: - for up in usage_points: - self.add_usage_point(up) - if equipment_containers: - for container in equipment_containers: - self.add_container(container) - if operational_restrictions: - for restriction in operational_restrictions: - self.add_operational_restriction(restriction) - if current_containers: - for cf in current_containers: - self.add_current_container(cf) + _usage_points: Optional[List[UsagePoint]] = field(default=None) + _equipment_containers: Optional[List[EquipmentContainer]] = field(default=None) + _operational_restrictions: Optional[List[OperationalRestriction]] = field(default=None) + _current_containers: Optional[List[EquipmentContainer]] = field(default=None) + + containers: MridCollection[EquipmentContainer] = LazyMridList( + _equipment_containers, + "An EquipmentContainer", + ) + equipment_containers = Alias(containers) + + current_containers: MridCollection[EquipmentContainer] = LazyMridList( + _current_containers, + "A current EquipmentContainer", + ) + + usage_points: MridCollection[UsagePoint] = LazyMridList( + _usage_points, + "A UsagePoint", + ) + + operational_restrictions: MridCollection[OperationalRestriction] = LazyMridList( + _operational_restrictions, + "An OperationalRestriction", + ) + @property def sites(self) -> Generator['Site', None, None]: @@ -129,12 +140,6 @@ def current_lv_feeders(self) -> Generator[LvFeeder, None, None]: """ return ngen(_of_type(self._current_containers, LvFeeder)) - @property - def containers(self) -> Generator[EquipmentContainer, None, None]: - """ - The `EquipmentContainer`s this equipment belongs to. - """ - return ngen(self._equipment_containers) def num_sites(self) -> int: """ @@ -142,235 +147,126 @@ def num_sites(self) -> int: """ return len(list(self.sites)) - @property - def current_containers(self) -> Generator[EquipmentContainer, None, None]: - """ - The `EquipmentContainer`s this equipment belongs to in the current state of the network. - """ - return ngen(self._current_containers) - @property - def usage_points(self) -> Generator[UsagePoint, None, None]: - """ - The `UsagePoint`s for this equipment. - """ - return ngen(self._usage_points) - - @property - def operational_restrictions(self) -> Generator[OperationalRestriction, None, None]: - """ - The `OperationalRestriction`s that this equipment is associated with. - """ - return ngen(self._operational_restrictions) + # region deprecated list boilerplate + # region containers boilerplate + @deprecated("Use len(obj.containers) instead.") def num_containers(self) -> int: - """ - Returns The number of `EquipmentContainer`s associated with this `Equipment` - """ - return nlen(self._equipment_containers) + return len(self.containers) + @deprecated("Use len(obj.containers) instead.") def num_substations(self) -> int: - """ - Returns The number of `zepben.ewb.model.cim.iec61970.base.core.substation.Substation`s associated with this `Equipment` - """ - return len(list(_of_type(self._equipment_containers, Substation))) + return len(self.containers) + @deprecated("Use len(obj.containers) instead.") def num_normal_feeders(self) -> int: - """ - Returns The number of normal `Feeder`s associated with this `Equipment` - """ - return len(list(_of_type(self._equipment_containers, Feeder))) + return len(self.containers) + @deprecated("Use obj.containers.get_by_mrid(mrid) instead.") def get_container(self, mrid: str) -> EquipmentContainer: - """ - Get the `EquipmentContainer` for this `Equipment` identified by `mrid` - - `mrid` The mRID of the required `EquipmentContainer` - Returns The `EquipmentContainer` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._equipment_containers, mrid) + return self.containers.get_by_mrid(mrid) + @deprecated("Use obj.containers.append(ec) instead.") def add_container(self, ec: EquipmentContainer) -> Equipment: - """ - Associate an `EquipmentContainer` with this `Equipment` - - `ec` The `EquipmentContainer` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `EquipmentContainer` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(ec, self.get_container, "An EquipmentContainer"): - return self - self._equipment_containers = list() if self._equipment_containers is None else self._equipment_containers - self._equipment_containers.append(ec) + self.containers.append(ec) return self + @deprecated("Use obj.containers.remove(ec) instead.") def remove_container(self, ec: EquipmentContainer) -> Equipment: - """ - Disassociate `ec` from this `Equipment`. - - `ec` The `EquipmentContainer` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `ec` was not associated with this `Equipment`. - """ - self._equipment_containers = safe_remove(self._equipment_containers, ec) + self.containers.remove(ec) return self + @deprecated("Use obj.containers.clear() instead.") def clear_containers(self) -> Equipment: - """ - Clear all equipment. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._equipment_containers = None + self.containers.clear() return self + # endregion containers boilerplate + + # region current_containers boilerplate + + @deprecated("Use len(obj.current_containers) instead.") def num_current_containers(self) -> int: - """ - Returns The number of `EquipmentContainer`s associated with this `Equipment` - """ - return nlen(self._current_containers) + return len(self.current_containers) + @deprecated("Use obj.current_containers.get_by_mrid(mrid) instead.") def get_current_container(self, mrid: str) -> EquipmentContainer: - """ - Get the `EquipmentContainer` for this `Equipment` in the current state of the network, identified by `mrid` - - `mrid` The mRID of the required `EquipmentContainer` - Returns The `EquipmentContainer` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._current_containers, mrid) + return self.current_containers.get_by_mrid(mrid) + @deprecated("Use obj.current_containers.append(equipment_container) instead.") def add_current_container(self, equipment_container: EquipmentContainer) -> Equipment: - """ - Associate `equipment_container` with this `Equipment` in the current state of the network. - - `equipment_container` The `EquipmentContainer` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `EquipmentContainer` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(equipment_container, self.get_current_container, "A current EquipmentContainer"): - return self - self._current_containers = list() if self._current_containers is None else self._current_containers - self._current_containers.append(equipment_container) + self.current_containers.append(equipment_container) return self + @deprecated("Use obj.current_containers.remove(equipment_container) instead.") def remove_current_container(self, equipment_container: EquipmentContainer) -> Equipment: - """ - Disassociate `equipment_container` from this `Equipment` in the current state of the network. - - `equipment_container` The `EquipmentContainer` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `equipment_container` was not associated with this `Equipment`. - """ - self._current_containers = safe_remove(self._current_containers, equipment_container) + self.current_containers.remove(equipment_container) return self + @deprecated("Use obj.current_containers.clear() instead.") def clear_current_containers(self) -> Equipment: - """ - Clear all current `EquipmentContainer`s in the current state of the network. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._current_containers = None + self.current_containers.clear() return self + # endregion current_containers boilerplate + + # region usage_points boilerplate + + @deprecated("Use len(obj.usage_points) instead.") def num_usage_points(self) -> int: - """ - Returns The number of `UsagePoint`s associated with this `Equipment` - """ - return nlen(self._usage_points) + return len(self.usage_points) + @deprecated("Use obj.usage_points.get_by_mrid(mrid) instead.") def get_usage_point(self, mrid: str) -> UsagePoint: - """ - Get the `UsagePoint` for this `Equipment` identified by `mrid` - - `mrid` The mRID of the required `UsagePoint` - Returns The `UsagePoint` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._usage_points, mrid) + return self.usage_points.get_by_mrid(mrid) + @deprecated("Use obj.usage_points.append(up) instead.") def add_usage_point(self, up: UsagePoint) -> Equipment: - """ - Associate `up` with this `Equipment`. - - `up` the `UsagePoint` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `UsagePoint` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(up, self.get_usage_point, "A UsagePoint"): - return self - self._usage_points = list() if self._usage_points is None else self._usage_points - self._usage_points.append(up) + self.usage_points.append(up) return self + @deprecated("Use obj.usage_points.remove(up) instead.") def remove_usage_point(self, up: UsagePoint) -> Equipment: - """ - Disassociate `up` from this `Equipment`. - - `up` The `UsagePoint` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `Equipment`. - """ - self._usage_points = safe_remove(self._usage_points, up) + self.usage_points.remove(up) return self + @deprecated("Use obj.usage_points.clear() instead.") def clear_usage_points(self) -> Equipment: - """ - Clear all usage_points. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._usage_points = None + self.usage_points.clear() return self + # endregion usage_points boilerplate + + # region operational_restrictions boilerplate + + @deprecated("Use len(obj.operational_restrictions) instead.") def num_operational_restrictions(self) -> int: - """ - Returns The number of `OperationalRestriction`s associated with this `Equipment` - """ - return nlen(self._operational_restrictions) + return len(self.operational_restrictions) + @deprecated("Use obj.operational_restrictions.get_by_mrid(mrid) instead.") def get_operational_restriction(self, mrid: str) -> OperationalRestriction: - """ - Get the `OperationalRestriction` for this `Equipment` identified by `mrid` - - `mrid` The mRID of the required `OperationalRestriction` - Returns The `OperationalRestriction` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._operational_restrictions, mrid) + return self.operational_restrictions.get_by_mrid(mrid) + @deprecated("Use obj.operational_restrictions.append(op) instead.") def add_operational_restriction(self, op: OperationalRestriction) -> Equipment: - """ - Associate `op` with this `Equipment`. - - `op` The `OperationalRestriction` to associate with this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if another `OperationalRestriction` with the same `mrid` already exists for this `Equipment`. - """ - if self._validate_reference(op, self.get_operational_restriction, "An OperationalRestriction"): - return self - self._operational_restrictions = list() if self._operational_restrictions is None else self._operational_restrictions - self._operational_restrictions.append(op) + self.operational_restrictions.append(op) return self + @deprecated("Use obj.operational_restrictions.remove(op) instead.") def remove_operational_restriction(self, op: OperationalRestriction) -> Equipment: - """ - Disassociate `up` from this `Equipment`. - - `op` The `OperationalRestriction` to disassociate from this `Equipment`. - Returns A reference to this `Equipment` to allow fluent use. - Raises `ValueError` if `op` was not associated with this `Equipment`. - """ - self._operational_restrictions = safe_remove(self._operational_restrictions, op) + self.operational_restrictions.remove(op) return self + @deprecated("Use obj.operational_restrictions.clear() instead.") def clear_operational_restrictions(self) -> Equipment: - """ - Clear all `OperationalRestrictions`. - Returns A reference to this `Equipment` to allow fluent use. - """ - self._operational_restrictions = None + self.operational_restrictions.clear() return self + # endregion operational_restrictions boilerplate + + # endregion deprecated list boilerplate + def _of_type(containers: Optional[List[EquipmentContainer]], ectype: Type[TEquipmentContainer]) -> Generator[TEquipmentContainer, None, None]: yield from (ec for ec in containers if isinstance(ec, ectype)) if containers is not None else {} diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py index 6f7290e57..2a2dadeeb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py @@ -7,9 +7,15 @@ __all__ = ['EquipmentContainer'] +from dataclasses import field from typing import Optional, Dict, Generator, List, TYPE_CHECKING, TypeVar, Iterable, Type from abc import ABCMeta +from typing_extensions import deprecated + +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import ConnectivityNodeContainer from zepben.ewb.util import nlen, ngen, safe_remove_by_id from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -32,28 +38,17 @@ class EquipmentContainer(ConnectivityNodeContainer, metaclass=ABCMeta): Unless overridden, all functions operating on currentEquipment simply operate on the equipment collection. i.e. currentEquipment = equipment """ - _equipment: Optional[Dict[str, Equipment]] = None + _equipment_by_id: Dict[str, Equipment] | None = field(default=None) """Map of Equipment in this EquipmentContainer by their mRID""" - def __init__(self, *args, equipment: List[Equipment] = None, **kwargs): - super(EquipmentContainer, self).__init__(*args, **kwargs) - if equipment: - for eq in equipment: - self.add_equipment(eq) + equipment: MridCollection[Equipment] = LazyMridMap( + _equipment_by_id, + "An Equipment", + ) + """The `Equipment` contained in this `EquipmentContainer`""" - @property - def equipment(self) -> Generator[Equipment, None, None]: - """ - The `Equipment` contained in this `EquipmentContainer` - """ - return ngen(self._equipment) - - @property - def current_equipment(self) -> Generator[Equipment, None, None]: - """ - Contained `Equipment` using the current state of the network. - """ - return self.equipment + current_equipment = Alias(equipment) + """Contained `Equipment` using the current state of the network.""" def current_feeders(self) -> Generator[Feeder, None, None]: """ @@ -61,7 +56,7 @@ def current_feeders(self) -> Generator[Feeder, None, None]: Returns the current feeders for all associated feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.current_feeders: if f not in seen: seen.add(f.mrid) @@ -73,7 +68,7 @@ def normal_feeders(self) -> Generator[Feeder, None, None]: Returns the normal feeders for all associated feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.normal_feeders: if f not in seen: seen.add(f.mrid) @@ -85,7 +80,7 @@ def current_lv_feeders(self) -> Generator[LvFeeder, None, None]: Returns the normal LV feeders for all associated LV feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.current_lv_feeders: if f not in seen: seen.add(f.mrid) @@ -97,7 +92,7 @@ def normal_lv_feeders(self) -> Generator[LvFeeder, None, None]: Returns the normal LV feeders for all associated LV feeders """ seen = set() - for equip in self._equipment.values(): + for equip in self._equipment_by_id.values(): for f in equip.normal_lv_feeders: if f not in seen: seen.add(f.mrid) @@ -146,103 +141,59 @@ def edge_terminals(self, state_operator: 'Type[NetworkStateOperators]' = None) - seen.add(ct.from_terminal) yield ct.from_terminal + + + # region deprecated list boilerplate + # region equipment boilerplate + + @deprecated("Use len(self.equipment) instead") def num_equipment(self): - """ - Returns The number of `Equipment` associated with this `EquipmentContainer` - """ - return nlen(self._equipment) + return len(self.equipment) + @deprecated("Use self.equipment.get_by_mrid(mrid) instead") def get_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `EquipmentContainer` identified by `mrid` - - `mrid` the mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - if not self._equipment: - raise KeyError(mrid) - try: - return self._equipment[mrid] - except AttributeError: - raise KeyError(mrid) + return self.equipment.get_by_mrid(mrid) + @deprecated("Use equipment.append(equipment) instead") def add_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Associate `equipment` with this `EquipmentContainer`. - - `equipment` The `Equipment` to associate with this `EquipmentContainer`. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `EquipmentContainer`. - """ - if self._validate_reference(equipment, self.get_equipment, "An Equipment"): - return self - if self._equipment is None: - self._equipment = dict() - self._equipment[equipment.mrid] = equipment + self.equipment.append(equipment) return self + @deprecated("Use equipment.remove(equipment) instead") def remove_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Disassociate `equipment` from this `EquipmentContainer` - - `equipment` The `Equipment` to disassociate with this `EquipmentContainer`. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `KeyError` if `equipment` was not associated with this `EquipmentContainer`. - """ - self._equipment = safe_remove_by_id(self._equipment, equipment) + self.equipment.remove(equipment) return self + @deprecated("Use equipment.clear() instead") def clear_equipment(self) -> EquipmentContainer: - """ - Clear all equipment. - Returns A reference to this `EquipmentContainer` to allow fluent use. - """ - self._equipment = None + self.equipment.clear() return self - def num_current_equipment(self) -> int: - """ - Returns The number of `Equipment` contained in this `EquipmentContainer` in the current state of the network. - """ - return self.num_equipment() + # endregion + # region current_equipment boilerplate - def get_current_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` contained in this `EquipmentContainer` in the current state of the network, identified by `mrid` + @deprecated("Use len(current_equipment) instead") + def num_current_equipment(self): + return len(self.current_equipment) - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return self.get_equipment(mrid) + @deprecated("Use current_equipment.get_by_mrid(mrid) instead") + def get_current_equipment(self, mrid: str) -> Equipment: + return self.current_equipment.get_by_mrid(mrid) + @deprecated("Use current_equipment.append(equipment) instead") def add_current_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Associate `equipment` with this `EquipmentContainer` in the current state of the network. - - `equipment` the `Equipment` to associate with this `EquipmentContainer` in the current state of the network. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `EquipmentContainer`. - """ - self.add_equipment(equipment) + self.current_equipment.append(equipment) return self + @deprecated("Use current_equipment.remove(equipment) instead") def remove_current_equipment(self, equipment: Equipment) -> EquipmentContainer: - """ - Disassociate `equipment` from this `EquipmentContainer` in the current state of the network. - - `equipment` The `Equipment` to disassociate from this `EquipmentContainer` in the current state of the network. - Returns A reference to this `EquipmentContainer` to allow fluent use. - Raises `KeyError` if `equipment` was not associated with this `EquipmentContainer`. - """ - self.remove_equipment(equipment) + self.current_equipment.remove(equipment) return self + @deprecated("Use current_equipment.clear() instead") def clear_current_equipment(self) -> EquipmentContainer: - """ - Clear all `Equipment` from this `EquipmentContainer` in the current state of the network. - Returns A reference to this `EquipmentContainer` to allow fluent use. - """ - self.clear_equipment() + self.current_equipment.clear() return self + + # endregion + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 21f9dae7e..88e28b34c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -7,11 +7,14 @@ __all__ = ["Feeder"] +from dataclasses import field from typing import Optional, Dict, List, Generator, TYPE_CHECKING from typing_extensions import deprecated -from zepben.ewb import get_by_mrid +from zepben.ewb import get_by_mrid, remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.util import ngen, nlen, safe_remove_by_id @@ -35,52 +38,21 @@ class Feeder(EquipmentContainer): _normal_head_terminal: Terminal | None = None """The normal head terminal or terminals of the feeder.""" - _normal_energizing_substation: Substation | None = None + normal_energizing_substation: Substation | None = None + """The substation that normally energizes the feeder. Also used for naming purposes.""" - _current_equipment: Dict[str, Equipment] | None = None + _current_equipment_by_id: Dict[str, Equipment] | None = field(default=None) """The equipment contained in this feeder in the current state of the network.""" - _normal_energized_lv_feeders: Dict[str, LvFeeder] | None = None + _normal_energized_lv_feeders_by_id: Dict[str, LvFeeder] | None = field(default=None) """The LV feeders that are energized by this feeder in the normal state of the network.""" - _current_energized_lv_feeders: Dict[str, LvFeeder] | None = None + _current_energized_lv_feeders_by_id: Dict[str, LvFeeder] | None = field(default=None) """The LV feeders that are energized by this feeder in the current state of the network.""" - _normal_energized_lv_substations: Dict[str, 'LvSubstation'] | None = None - _current_energized_lv_substations: Dict[str, 'LvSubstation'] | None = None - - def __init__( - self, - *args, - normal_head_terminal: Terminal = None, - normal_energizing_substation: Substation = None, - current_equipment: List[Equipment] = None, - normal_energized_lv_feeders: List[LvFeeder] = None, - current_energized_lv_feeders: List[LvFeeder] = None, - normal_energized_lv_substations: List[LvSubstation] = None, - current_energized_lv_substations: List[LvSubstation] = None, - **kwargs, - ): - super(Feeder, self).__init__(*args, **kwargs) - if normal_head_terminal: - self.normal_head_terminal = normal_head_terminal - if normal_energizing_substation: - self.normal_energizing_substation = normal_energizing_substation - if normal_energized_lv_feeders: - for lv_feeder in normal_energized_lv_feeders: - self.add_normal_energized_lv_feeder(lv_feeder) - if current_equipment: - for eq in current_equipment: - self.add_current_equipment(eq) - if current_energized_lv_feeders: - for lv_feeder in current_energized_lv_feeders: - self.add_current_energized_lv_feeder(lv_feeder) - if normal_energized_lv_substations: - for lv_substation in normal_energized_lv_substations: - self.add_normal_energized_lv_substation(lv_substation) - if current_energized_lv_substations: - for lv_substation in current_energized_lv_substations: - self.add_current_energized_lv_substation(lv_substation) + _normal_energized_lv_substations_by_id: Dict[str, 'LvSubstation'] | None = field(default=None) + _current_energized_lv_substations_by_id: Dict[str, 'LvSubstation'] | None = field(default=None) + @property def normal_head_terminal(self) -> Optional[Terminal]: @@ -94,6 +66,35 @@ def normal_head_terminal(self, term: Optional[Terminal]): else: raise ValueError(f"Feeder {self.mrid} has equipment assigned to it. Cannot update normalHeadTerminal on a feeder with equipment assigned.") + current_equipment: MridCollection[Equipment] = LazyMridMap( + _current_equipment_by_id, + "A current Equipment", + ) + """Contained `Equipment` using the current state of the network.""" + + normal_energized_lv_feeders: MridCollection[LvFeeder] = LazyMridMap( + _normal_energized_lv_feeders_by_id, + "An LvFeeder", + ) + """The LV feeders that are normally energized by this feeder.""" + + current_energized_lv_feeders: MridCollection[LvFeeder] = LazyMridMap( + _current_energized_lv_feeders_by_id, + "An LvFeeder", + ) + """[ZBEX] The LV feeders that are currently energized by this feeder.""" + + normal_energized_lv_substations: MridCollection[LvSubstation] = LazyMridMap( + _normal_energized_lv_substations_by_id, + "An LvSubstation", + ) + """[ZBEX]""" + + current_energized_lv_substations: MridCollection[LvSubstation] = LazyMridMap( + _current_energized_lv_substations_by_id, + "An LvSubstation", + ) + """[ZBEX]""" @property def normal_energizing_substation(self): """The substation that normally energizes the feeder. Also used for naming purposes.""" @@ -111,270 +112,137 @@ def current_equipment(self) -> Generator[Equipment, None, None]: """ return ngen(self._current_equipment) - @property - def normal_energized_lv_feeders(self) -> Generator[LvFeeder, None, None]: - """ - The LV feeders that are normally energized by this feeder. - """ - return ngen(self._normal_energized_lv_feeders) - - @zbex - @property - def current_energized_lv_feeders(self) -> Generator[LvFeeder, None, None]: - """ - The LV feeders that are currently energized by this feeder. - """ - return ngen(self._current_energized_lv_feeders) - - @zbex - @property - def normal_energized_lv_substations(self) -> Generator['LvSubstation', None, None]: - return ngen(self._normal_energized_lv_substations) - @zbex - @property - def current_energized_lv_substations(self) -> Generator['LvSubstation', None, None]: - return ngen(self._current_energized_lv_substations) + # region deprecated list boilerplate + # region current_equipment boilerplate + @deprecated("Use len(current_equipment) instead") def num_current_equipment(self): - """ - :returns: The number of `Equipment` associated with this `Feeder` - """ - return nlen(self._current_equipment) + return len(self.current_equipment) + @deprecated("Use current_equipment.get_by_mrid(mrid) instead") def get_current_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `Feeder` identified by `mrid` - - `mrid` The mRID of the required `Equipment` - :returns: The `Equipment` with the specified `mrid` if it exists - :raises: `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._current_equipment, mrid) - - def add_current_equipment(self, equipment: Equipment) -> Feeder: - """ - Associate `equipment` with this `Feeder`. + return self.current_equipment.get_by_mrid(mrid) - `equipment` the `Equipment` to associate with this `Feeder`. - :returns: A reference to this `Feeder` to allow fluent use. - :raises: `ValueError` if another `Equipment` with the same `mrid` already exists for this `Feeder`. - """ - if self._validate_reference(equipment, self.get_current_equipment, "An Equipment"): - return self - self._current_equipment = dict() if self._current_equipment is None else self._current_equipment - self._current_equipment[equipment.mrid] = equipment + @deprecated("Use current_equipment.append(equipment) instead") + def add_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.append(equipment) return self - def remove_current_equipment(self, equipment: Equipment) -> Feeder: - """ - Disassociate `equipment` from this `Feeder` - - `equipment` The `Equipment` to disassociate from this `Feeder`. - :returns: A reference to this `Feeder` to allow fluent use. - :raises: `KeyError` if `equipment` was not associated with this `Feeder`. - """ - self._current_equipment = safe_remove_by_id(self._current_equipment, equipment) + @deprecated("Use current_equipment.remove(equipment) instead") + def remove_current_equipment(self, equipment: Equipment) -> EquipmentContainer: + self.current_equipment.remove(equipment) return self - def clear_current_equipment(self) -> Feeder: - """ - Clear all equipment. - :returns: A reference to this `Feeder` to allow fluent use. - """ - self._current_equipment = None + @deprecated("Use current_equipment.clear() instead") + def clear_current_equipment(self) -> EquipmentContainer: + self.current_equipment.clear() return self - def num_normal_energized_lv_feeders(self) -> int: - """ - Get the number of LV feeders that are normally energized by this feeder. - """ - return nlen(self._normal_energized_lv_feeders) + # endregion + # region normal_energized_lv_feeders boilerplate - def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder: - """ - Energized LvFeeder in the normal state of the network. + @deprecated("Use len(normal_energized_lv_feeders) instead") + def num_normal_energized_lv_feeders(self): + return len(self.normal_energized_lv_feeders) - :param mrid: The mrid of the `LvFeeder`. - :returns: A matching `LvFeeder` that is energized by this `Feeder` in the normal state of the network. - :raise sA `KeyError` if no matching `LvFeeder` was found. - """ - return get_by_mrid(self._normal_energized_lv_feeders, mrid) + @deprecated("Use normal_energized_lv_feeders.get_by_mrid(mrid) instead") + def get_normal_energized_lv_feeder(self, mrid: str) -> LvFeeder: + return self.normal_energized_lv_feeders.get_by_mrid(mrid) + @deprecated("Use normal_energized_lv_feeders.append(lv_feeder) instead") def add_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Associate this `Feeder` with an `LvFeeder` in the normal state of the network. - - :param lv_feeder: the LV feeder to associate with this feeder in the normal state of the network. - :return: This `Feeder` for fluent use. - """ - if self._validate_reference(lv_feeder, self.get_normal_energized_lv_feeder, "An LvFeeder"): - return self - self._normal_energized_lv_feeders = dict() if self._normal_energized_lv_feeders is None else self._normal_energized_lv_feeders - self._normal_energized_lv_feeders[lv_feeder.mrid] = lv_feeder + self.normal_energized_lv_feeders.append(lv_feeder) return self + @deprecated("Use normal_energized_lv_feeders.remove(lv_feeder) instead") def remove_normal_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Disassociate this `Feeder` from an `LvFeeder` in the normal state of the network. - - :param lv_feeder: the LV feeder to disassociate from this feeder in the normal state of the network. - :return: This `Feeder` for fluent use. - :raises: A `ValueError` if `lv_feeder` is not found in the normal energized lv feeders collection. - """ - self._normal_energized_lv_feeders = safe_remove_by_id(self._normal_energized_lv_feeders, lv_feeder) + self.normal_energized_lv_feeders.remove(lv_feeder) return self + @deprecated("Use normal_energized_lv_feeders.clear() instead") def clear_normal_energized_lv_feeders(self) -> Feeder: - """ - Clear all `LvFeeder`s associated with `Feeder` in the normal state of the network. - - :return: This `Feeder` for fluent use. - """ - self._normal_energized_lv_feeders = None + self.normal_energized_lv_feeders.clear() return self - def num_current_energized_lv_feeders(self) -> int: - """ - Get the number of LV feeders that are currently energized by this feeder. - """ - return nlen(self._current_energized_lv_feeders) + # endregion + # region current_energized_lv_feeders boilerplate - def get_current_energized_lv_feeder(self, mrid: str) -> LvFeeder: - """ - Energized LvFeeder in the current state of the network. + @deprecated("Use len(current_energized_lv_feeders) instead") + def num_current_energized_lv_feeders(self): + return len(self.current_energized_lv_feeders) - :param mrid: The mrid of the `LvFeeder`. - :return: A matching `LvFeeder` that is energized by this `Feeder` in the current state of the network. - :raises: A `KeyError` if no matching `LvFeeder` was found. - """ - return get_by_mrid(self._current_energized_lv_feeders, mrid) + @deprecated("Use current_energized_lv_feeders.get_by_mrid(mrid) instead") + def get_current_energized_lv_feeder(self, mrid: str) -> LvFeeder: + return self.current_energized_lv_feeders.get_by_mrid(mrid) + @deprecated("Use current_energized_lv_feeders.append(lv_feeder) instead") def add_current_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Associate this `Feeder` with an `LvFeeder` in the current state of the network. - - :param lv_feeder: the LV feeder to associate with this feeder in the current state of the network. - :return: This `Feeder` for fluent use. - """ - if self._validate_reference(lv_feeder, self.get_current_energized_lv_feeder, "An LvFeeder"): - return self - self._current_energized_lv_feeders = dict() if self._current_energized_lv_feeders is None else self._current_energized_lv_feeders - self._current_energized_lv_feeders[lv_feeder.mrid] = lv_feeder + self.current_energized_lv_feeders.append(lv_feeder) return self + @deprecated("Use current_energized_lv_feeders.remove(lv_feeder) instead") def remove_current_energized_lv_feeder(self, lv_feeder: LvFeeder) -> Feeder: - """ - Disassociate this `Feeder` from an `LvFeeder` in the current state of the network. - - :param lv_feeder: the LV feeder to disassociate from this feeder in the current state of the network. - :return: This `Feeder` for fluent use. - :raises: A `ValueError` if `lv_feeder` is not found in the current energized lv feeders collection. - """ - self._current_energized_lv_feeders = safe_remove_by_id(self._current_energized_lv_feeders, lv_feeder) + self.current_energized_lv_feeders.remove(lv_feeder) return self + @deprecated("Use current_energized_lv_feeders.clear() instead") def clear_current_energized_lv_feeders(self) -> Feeder: - """ - Clear all `LvFeeder`s associated with `Feeder` in the current state of the network. - - :return: This `Feeder` for fluent use. - """ - self._current_energized_lv_feeders = None + self.current_energized_lv_feeders.clear() return self - def num_normal_energized_lv_substations(self) -> int: - """ - Get the number of entries in the normal [LvSubstation] collection. - """ - return nlen(self._normal_energized_lv_substations) - - def get_normal_energized_lv_substation(self, mrid: str) -> 'LvSubstation | None': - """ - Retrieve an energized LvSubstation using the normal state of the network. + # endregion + # region normal_energized_lv_substations boilerplate - :param mrid: the mRID of the required normal [LvSubstation] - :returns: The [LvSubstation] with the specified [mRID] if it exists, otherwise null - """ - return get_by_mrid(self._normal_energized_lv_substations, mrid) + @deprecated("Use len(normal_energized_lv_substations) instead") + def num_normal_energized_lv_substations(self): + return len(self.normal_energized_lv_substations) - def add_normal_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Associate this [Feeder] with a [LvSubstation] in the normal state of the network. + @deprecated("Use normal_energized_lv_substations.get_by_mrid(mrid) instead") + def get_normal_energized_lv_substation(self, mrid: str) -> LvSubstation: + return self.normal_energized_lv_substations.get_by_mrid(mrid) - :param lv_substation: the [LvSubstation] to associate with this LV feeder in the normal state of the network. - :returns: This [Feeder] for fluent use. - """ - if self._validate_reference(lv_substation, self.get_normal_energized_lv_substation, "An LvSubstation"): - return self - if self._normal_energized_lv_substations is None: - self._normal_energized_lv_substations = dict() - self._normal_energized_lv_substations[lv_substation.mrid] = lv_substation + @deprecated("Use normal_energized_lv_substations.append(lv_substation) instead") + def add_normal_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.normal_energized_lv_substations.append(lv_substation) return self - def remove_normal_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Disassociate this [Feeder] from a [LvSubstation] in the normal state of the network. - - :param lv_substation: the [LvSubstation] to disassociate from this LV feeder in the normal state of the network. - :returns: true if a matching [LvSubstation] is removed from the collection. - """ - self._normal_energized_lv_substations = safe_remove_by_id(self._normal_energized_lv_substations, lv_substation) + @deprecated("Use normal_energized_lv_substations.remove(lv_substation) instead") + def remove_normal_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.normal_energized_lv_substations.remove(lv_substation) return self - def clear_normal_energized_lv_substations(self) -> "Feeder": - """ - Clear all [LvSubstation]'s associated with this [Feeder] in the normal state of the network. - - :returns: This [Feeder] for fluent use. - """ - self._normal_energized_lv_substations = None + @deprecated("Use normal_energized_lv_substations.clear() instead") + def clear_normal_energized_lv_substations(self) -> Feeder: + self.normal_energized_lv_substations.clear() return self - def num_current_energized_lv_substations(self) -> int: - """ - Get the number of entries in the current [LvSubstation] collection. - """ - return nlen(self._current_energized_lv_substations) - - def get_current_energized_lv_substation(self, mrid: str) -> 'LvSubstation | None': - """ - Retrieve an energized LvSubstation using the current state of the network. + # endregion + # region current_energized_lv_substations boilerplate - :param mrid: the mRID of the required current [LvSubstation] - :returns: The [LvSubstation] with the specified [mRID] if it exists, otherwise null - """ - return get_by_mrid(self._current_energized_lv_substations, mrid) + @deprecated("Use len(current_energized_lv_substations) instead") + def num_current_energized_lv_substations(self): + return len(self.current_energized_lv_substations) - def add_current_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Associate this [Feeder] with a [LvSubstation] in the current state of the network. + @deprecated("Use current_energized_lv_substations.get_by_mrid(mrid) instead") + def get_current_energized_lv_substation(self, mrid: str) -> LvSubstation: + return self.current_energized_lv_substations.get_by_mrid(mrid) - :param lv_substation: the [LvSubstation] to associate with this LV feeder in the current state of the network. - :returns: This [Feeder] for fluent use. - """ - if self._validate_reference(lv_substation, self.get_current_energized_lv_substation, "An LvSubstation"): - return self - if self._current_energized_lv_substations is None: - self._current_energized_lv_substations = dict() - self._current_energized_lv_substations[lv_substation.mrid] = lv_substation + @deprecated("Use current_energized_lv_substations.append(lv_substation) instead") + def add_current_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.current_energized_lv_substations.append(lv_substation) return self - def remove_current_energized_lv_substation(self, lv_substation: 'LvSubstation') -> "Feeder": - """ - Disassociate this [Feeder] from a [LvSubstation] in the current state of the network. + @deprecated("Use current_energized_lv_substations.remove(lv_substation) instead") + def remove_current_energized_lv_substation(self, lv_substation: LvSubstation) -> Feeder: + self.current_energized_lv_substations.remove(lv_substation) + return self - :param lv_substation: the [LvSubstation] to disassociate from this LV feeder in the current state of the network. - :returns: true if a matching [LvSubstation] is removed from the collection. - """ - self._current_energized_lv_substations = safe_remove_by_id(self._current_energized_lv_substations, lv_substation) + @deprecated("Use current_energized_lv_substations.clear() instead") + def clear_current_energized_lv_substations(self) -> Feeder: + self.current_energized_lv_substations.clear() return self - def clear_current_energized_lv_substations(self) -> "Feeder": - """ - Clear all [LvSubstation]'s associated with this [Feeder] in the current state of the network. + # endregion + # endregion - :returns: This [Feeder] for fluent use. - """ - self._current_energized_lv_substations = None - return self diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index 801654734..ac5448eb2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -8,11 +8,15 @@ __all__ = ["GeographicalRegion"] from typing import Optional, List, Generator +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList @zb_dataclass @@ -20,7 +24,7 @@ class GeographicalRegion(IdentifiedObject): """ A geographical region of a power system network phases. """ - _sub_geographical_regions: Optional[List[SubGeographicalRegion]] = None + _sub_geographical_regions: Optional[List[SubGeographicalRegion]] = field(default=None) def __init__(self, *args, sub_geographical_regions: List[SubGeographicalRegion] = None, **kwargs): super(GeographicalRegion, self).__init__(*args, **kwargs) @@ -28,28 +32,12 @@ def __init__(self, *args, sub_geographical_regions: List[SubGeographicalRegion] for sgr in sub_geographical_regions: self.add_sub_geographical_region(sgr) - @property - def sub_geographical_regions(self) -> Generator[SubGeographicalRegion, None, None]: - """ - The `SubGeographicalRegion`s of this `GeographicalRegion`. - """ - return ngen(self._sub_geographical_regions) - - def num_sub_geographical_regions(self) -> int: - """ - Returns The number of `SubGeographicalRegion`s associated with this `GeographicalRegion` - """ - return nlen(self._sub_geographical_regions) - def get_sub_geographical_region(self, mrid: str) -> SubGeographicalRegion: - """ - Get the `SubGeographicalRegion` for this `GeographicalRegion` identified by `mrid` + sub_geographical_regions: MridCollection[SubGeographicalRegion] = LazyMridList( + _sub_geographical_regions, + "A SubGeographicalRegion", + ) - `mrid` The mRID of the required `SubGeographicalRegion` - Returns The `SubGeographicalRegion` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._sub_geographical_regions, mrid) def add_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: """ @@ -73,20 +61,29 @@ def add_sub_geographical_region(self, sub_geographical_region: SubGeographicalRe self._sub_geographical_regions.append(sub_geographical_region) return self + + + # region deprecated list boilerplate + # region sub_geographical_regions boilerplate + + @deprecated("Use len(obj.sub_geographical_regions) instead.") + def num_sub_geographical_regions(self) -> int: + return len(self.sub_geographical_regions) + + @deprecated("Use obj.sub_geographical_regions.get_by_mrid(mrid) instead.") + def get_sub_geographical_region(self, mrid: str) -> SubGeographicalRegion: + return self.sub_geographical_regions.get_by_mrid(mrid) + + @deprecated("Use obj.sub_geographical_regions.remove(sub_geographical_region) instead.") def remove_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: - """ - Disassociate `sub_geographical_region` from this `GeographicalRegion` - `sub_geographical_region` The `SubGeographicalRegion` to disassociate from this `GeographicalRegion`. - Returns A reference to this `GeographicalRegion` to allow fluent use. - Raises `ValueError` if `sub_geographical_region` was not associated with this `GeographicalRegion`. - """ - self._sub_geographical_regions = safe_remove(self._sub_geographical_regions, sub_geographical_region) + self.sub_geographical_regions.remove(sub_geographical_region) return self + @deprecated("Use obj.sub_geographical_regions.clear() instead.") def clear_sub_geographical_regions(self) -> GeographicalRegion: - """ - Clear all SubGeographicalRegions. - Returns A reference to this `GeographicalRegion` to allow fluent use. - """ - self._sub_geographical_regions = None + self.sub_geographical_regions.clear() return self + + # endregion sub_geographical_regions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name.py b/src/zepben/ewb/model/cim/iec61970/base/core/name.py index 05adcfda9..d457580f6 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name.py @@ -9,8 +9,7 @@ from typing import TYPE_CHECKING, Optional -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.backed_descriptor import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable if TYPE_CHECKING: @@ -19,7 +18,6 @@ @zb_dataclass -@remove_descriptor_annotations class Name(Identifiable): """ The Name class provides the means to define any number of human-readable names for an object. A name is **not** to be used for defining inter-object diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py index 7e0231238..a15d9a1b0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py @@ -12,8 +12,8 @@ from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable from zepben.ewb.model.cim.iec61970.base.core.name import Name -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.backed_descriptor import BackedDescriptor, remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.descriptor_fix import BackedDescriptor if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject @@ -21,7 +21,6 @@ @zb_dataclass -@remove_descriptor_annotations class NameType(Identifiable): """ Type of name. Possible values for attribute 'name' are implementation dependent but standard profiles may specify types. An enterprise may have multiple diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py index 6368c13ec..50331de93 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py @@ -9,10 +9,14 @@ from typing import Optional, TYPE_CHECKING, List, Generator, Iterable from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset import Asset @@ -38,13 +42,13 @@ class PowerSystemResource(IdentifiedObject, metaclass=ABCMeta): num_controls: Optional[int] = None """Number of Control's known to associate with this [PowerSystemResource]""" - _assets: Optional[List[Asset]] = None + _assets: Optional[List[Asset]] = field(default=None) + + assets: MridCollection[Asset] = LazyMridList( + _assets, + "An Asset", + ) - def __init__(self, *args, assets: Iterable[Asset] = None, **kwargs): - super(PowerSystemResource, self).__init__(*args, **kwargs) - if assets: - for asset in assets: - self.add_asset(asset) @property def has_controls(self) -> bool: @@ -53,57 +57,33 @@ def has_controls(self) -> bool: """ return nlen(self.num_controls) > 0 - @property - def assets(self) -> Generator[Asset, None, None]: - """ - The `Asset`s of this `PowerSystemResource`. - """ - return ngen(self._assets) + # region deprecated list boilerplate + # region assets boilerplate + + @deprecated("Use len(obj.assets) instead.") def num_assets(self) -> int: - """ - Get the number of `Asset`s associated with this `PowerSystemResource`. - """ - return nlen(self._assets) + return len(self.assets) + @deprecated("Use obj.assets.get_by_mrid(mrid) instead.") def get_asset(self, mrid: str) -> Asset: - """ - Get the `Asset` associated with this `PowerSystemResource` identified by `mrid`. - - `mrid` the mRID of the required `Asset` - Returns The `Asset` with the specified `mrid`. - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._assets, mrid) + return self.assets.get_by_mrid(mrid) + @deprecated("Use obj.assets.append(asset) instead.") def add_asset(self, asset: Asset) -> PowerSystemResource: - """ - `asset` The `Asset` to associate with this `PowerSystemResource`. - Returns A reference to this `PowerSystemResource` to allow fluent use. - Raises `ValueError` if another `Asset` with the same `mrid` already exists in this `PowerSystemResource` - """ - if self._validate_reference(asset, self.get_asset, "An Asset"): - return self - - self._assets = list() if self._assets is None else self._assets - self._assets.append(asset) + self.assets.append(asset) return self + @deprecated("Use obj.assets.remove(asset) instead.") def remove_asset(self, asset: Asset) -> PowerSystemResource: - """ - Disassociate an `Asset` from this `PowerSystemResource`. - - `asset` the `Asset` to disassociate from this `PowerSystemResource`. - Raises `ValueError` if `asset` was not associated with this `PowerSystemResource`. - Returns A reference to this `PowerSystemResource` to allow fluent use. - """ - self._assets = safe_remove(self._assets, asset) + self.assets.remove(asset) return self + @deprecated("Use obj.assets.clear() instead.") def clear_assets(self) -> PowerSystemResource: - """ - Clear all assets. - Returns self - """ - self._assets = None + self.assets.clear() return self + + # endregion assets boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index 869eef810..a1b7fd736 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -8,12 +8,14 @@ __all__ = ["SubGeographicalRegion"] from typing import Optional, List, Generator, TYPE_CHECKING - +from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.geographical_region import GeographicalRegion @@ -26,9 +28,10 @@ class SubGeographicalRegion(IdentifiedObject): A subset of a geographical region of a power system network model. """ - _geographical_region: Optional[GeographicalRegion] = None + geographical_region: Optional[GeographicalRegion] = None + """The geographical region to which this sub-geographical region is within.""" - _substations: Optional[List[Substation]] = None + _substations: Optional[List[Substation]] = field(default=None) def __init__(self, *args, substations: List[Substation] = None, **kwargs): super(SubGeographicalRegion, self).__init__(*args, **kwargs) @@ -47,28 +50,17 @@ def geographical_region(self): def geographical_region(self, value): self._geographical_region = value - @property - def substations(self) -> Generator[Substation, None, None]: - """ - All substations belonging to this sub geographical region. - """ - return ngen(self._substations) - def num_substations(self) -> int: """ Returns The number of `Substation`s associated with this `SubGeographicalRegion` """ return nlen(self._substations) - def get_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `SubGeographicalRegion` identified by `mrid` + substations: MridCollection[Substation] = LazyMridList( + _substations, + "A Substation", + ) - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._substations, mrid) def add_substation(self, substation: Substation) -> SubGeographicalRegion: """ @@ -93,21 +85,29 @@ def add_substation(self, substation: Substation) -> SubGeographicalRegion: self._substations.append(substation) return self - def remove_substation(self, substation: Substation) -> SubGeographicalRegion: - """ - Disassociate `substation` from this `GeographicalRegion` - `substation` The `Substation` to disassociate from this `SubGeographicalRegion`. - Returns A reference to this `SubGeographicalRegion` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `SubGeographicalRegion`. - """ - self._substations = safe_remove(self._substations, substation) + + # region deprecated list boilerplate + # region substations boilerplate + + @deprecated("Use len(obj.substations) instead.") + def num_substations(self) -> int: + return len(self.substations) + + @deprecated("Use obj.substations.get_by_mrid(mrid) instead.") + def get_substation(self, mrid: str) -> Substation: + return self.substations.get_by_mrid(mrid) + + @deprecated("Use obj.substations.remove(substation) instead.") + def remove_substation(self, substation: Substation) -> SubGeographicalRegion: + self.substations.remove(substation) return self + @deprecated("Use obj.substations.clear() instead.") def clear_substations(self) -> SubGeographicalRegion: - """ - Clear all `Substations`. - Returns A reference to this `SubGeographicalRegion` to allow fluent use. - """ - self._substations = None + self.substations.clear() return self + + # endregion substations boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index ed515fe95..41fead9ba 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -8,12 +8,14 @@ __all__ = ["Substation"] from typing import Optional, Generator, List, TYPE_CHECKING - +from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop @@ -31,29 +33,18 @@ class Substation(EquipmentContainer): _sub_geographical_region: Optional[SubGeographicalRegion] = None - _normal_energized_feeders: Optional[List[Feeder]] = None - - _loops: Optional[List[Loop]] = None + _normal_energized_feeders: Optional[List[Feeder]] = field(default=None) - _energized_loops: Optional[List[Loop]] = None + _loops: Optional[List[Loop]] = field(default=None) - _circuits: Optional[List[Circuit]] = None + _energized_loops: Optional[List[Loop]] = field(default=None) - def __init__(self, *args, normal_energized_feeders: List[Feeder] = None, loops: List[Loop] = None, energized_loops: List[Loop] = None, circuits: List[Circuit] = None, **kwargs): - super(Substation, self).__init__(*args, **kwargs) - if normal_energized_feeders: - for feeder in normal_energized_feeders: - self.add_feeder(feeder) - if loops: - for loop in loops: - self.add_loop(loop) - if energized_loops: - for loop in energized_loops: - self.add_energized_loop(loop) - if circuits: - for circuit in circuits: - self.add_circuit(circuit) + _circuits: Optional[List[Circuit]] = field(default=None) + circuits: MridCollection[Circuit] = LazyMridList( + _circuits, + "A Circuit", + ) @property def sub_geographical_region(self): @@ -72,42 +63,23 @@ def circuits(self) -> Generator[Circuit, None, None]: """ return ngen(self._circuits) - @property - def loops(self) -> Generator[Loop, None, None]: - """ - The `Loop` originating from this substation. - """ - return ngen(self._loops) + loops: MridCollection[Loop] = LazyMridList( + _loops, + "A Loop", + ) - @property - def energized_loops(self) -> Generator[Loop, None, None]: - """ - The `Loop`s originating from this substation that are energised. - """ - return ngen(self._energized_loops) + energized_loops: MridCollection[Loop] = LazyMridList( + _energized_loops, + "A Loop", + ) - @property - def feeders(self) -> Generator[Feeder, None, None]: - """ - The normal energized feeders of the substation. Also used for naming purposes. - """ - return ngen(self._normal_energized_feeders) + feeders: MridCollection[Feeder] = LazyMridList( + _normal_energized_feeders, + "A Feeder", + ) + normal_energized_feeders = Alias(feeders) - def num_circuits(self): - """ - Returns The number of `Circuit`s associated with this `Substation` - """ - return nlen(self._circuits) - - def get_circuit(self, mrid: str) -> Circuit: - """ - Get the `Circuit` for this `Substation` identified by `mrid` - `mrid` The mRID of the required `Circuit` - Returns The `Circuit` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._circuits, mrid) def add_circuit(self, circuit: Circuit) -> Substation: """ @@ -123,175 +95,110 @@ def add_circuit(self, circuit: Circuit) -> Substation: self._circuits.append(circuit) return self - def remove_circuit(self, circuit: Circuit) -> Substation: - """ - Disassociate `circuit` from this `Substation` - `circuit` The `Circuit` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `circuit` was not associated with this `Substation`. - """ - self._circuits = safe_remove(self._circuits, circuit) + + # region deprecated list boilerplate + # region circuits boilerplate + + @deprecated("Use len(obj.circuits) instead.") + def num_circuits(self): + return len(self.circuits) + + @deprecated("Use obj.circuits.get_by_mrid(mrid) instead.") + def get_circuit(self, mrid: str) -> Circuit: + return self.circuits.get_by_mrid(mrid) + + @deprecated("Use obj.circuits.append(circuit) instead.") + def add_circuit(self, circuit: Circuit) -> Substation: + self.circuits.append(circuit) + return self + + @deprecated("Use obj.circuits.remove(circuit) instead.") + def remove_circuit(self, circuit: Circuit) -> Substation: + self.circuits.remove(circuit) return self + @deprecated("Use obj.circuits.clear() instead.") def clear_circuits(self) -> Substation: - """ - Clear all current `Circuit`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._circuits = None + self.circuits.clear() return self + # endregion circuits boilerplate + + # region loops boilerplate + + @deprecated("Use len(obj.loops) instead.") def num_loops(self): - """ - Returns The number of `Loop`s associated with this `Substation` - """ - return nlen(self._loops) + return len(self.loops) + @deprecated("Use obj.loops.get_by_mrid(mrid) instead.") def get_loop(self, mrid: str) -> Loop: - """ - Get the `Loop` for this `Substation` identified by `mrid` - - `mrid` The mRID of the required `Loop` - Returns The `Loop` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._loops, mrid) + return self.loops.get_by_mrid(mrid) + @deprecated("Use obj.loops.append(loop) instead.") def add_loop(self, loop: Loop) -> Substation: - """ - Associate a `Loop` with this `Substation` - - `loop` The `Loop` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Loop` with the same `mrid` already exists for this `Substation`. - """ - if self._validate_reference(loop, self.get_loop, "A Loop"): - return self - self._loops = list() if self._loops is None else self._loops - self._loops.append(loop) + self.loops.append(loop) return self + @deprecated("Use obj.loops.remove(loop) instead.") def remove_loop(self, loop: Loop) -> Substation: - """ - Disassociate `loop` from this `Substation` - - `loop` The `Loop` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `loop` was not associated with this `Substation`. - """ - self._loops = safe_remove(self._loops, loop) + self.loops.remove(loop) return self + @deprecated("Use obj.loops.clear() instead.") def clear_loops(self) -> Substation: - """ - Clear all current `Loop`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._loops = None + self.loops.clear() return self + # endregion loops boilerplate + + # region energized_loops boilerplate + + @deprecated("Use len(obj.energized_loops) instead.") def num_energized_loops(self): - """ - Returns The number of `Loop`s associated with this `Substation` - """ - return nlen(self._energized_loops) + return len(self.energized_loops) + @deprecated("Use obj.energized_loops.get_by_mrid(mrid) instead.") def get_energized_loop(self, mrid: str) -> Loop: - """ - Get the `Loop` for this `Substation` identified by `mrid` - - `mrid` The mRID of the required `Loop` - Returns The `Loop` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energized_loops, mrid) + return self.energized_loops.get_by_mrid(mrid) + @deprecated("Use obj.energized_loops.append(loop) instead.") def add_energized_loop(self, loop: Loop) -> Substation: - """ - Associate a `Loop` with this `Substation` - - `loop` The `Loop` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Loop` with the same `mrid` already exists for this `Substation`. - """ - if self._validate_reference(loop, self.get_energized_loop, "A Loop"): - return self - self._energized_loops = list() if self._energized_loops is None else self._energized_loops - self._energized_loops.append(loop) + self.energized_loops.append(loop) return self + @deprecated("Use obj.energized_loops.remove(loop) instead.") def remove_energized_loop(self, loop: Loop) -> Substation: - """ - Disassociate `loop` from this `Substation` - - `loop` The `Loop` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `loop` was not associated with this `Substation`. - """ - self._energized_loops = safe_remove(self._energized_loops, loop) + self.energized_loops.remove(loop) return self + @deprecated("Use obj.energized_loops.clear() instead.") def clear_energized_loops(self) -> Substation: - """ - Clear all current `Loop`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._energized_loops = None + self.energized_loops.clear() return self - def num_feeders(self): - """ - Returns The number of `Feeder`s associated with this `Substation` - """ - return nlen(self._normal_energized_feeders) - - def get_feeder(self, mrid: str) -> Feeder: - """ - Get the `Feeder` for this `Substation` identified by `mrid` - - `mrid` The mRID of the required `Feeder` - Returns The `Feeder` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._normal_energized_feeders, mrid) - - def add_feeder(self, feeder: Feeder) -> Substation: - """ - Associate a `Feeder` with this `Substation` - - `feeder` The `Feeder` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Feeder` with the same `mrid` already exists for this `Substation`, or if - `feeder.normal_energizing_substation` is not this `Substation`. - """ - if self._validate_reference(feeder, self.get_feeder, "A Feeder"): - return self + # endregion energized_loops boilerplate - if feeder.normal_energizing_substation is None: - feeder.normal_energizing_substation = self + # region feeders boilerplate - require(feeder.normal_energizing_substation is self, lambda: f"{feeder} `normal_energizing_substation` property references {feeder.normal_energizing_substation}, expected {self}.") + @deprecated("Use len(obj.feeders) instead.") + def num_feeders(self): + return len(self.feeders) - self._normal_energized_feeders = list() if self._normal_energized_feeders is None else self._normal_energized_feeders - self._normal_energized_feeders.append(feeder) - return self + @deprecated("Use obj.feeders.get_by_mrid(mrid) instead.") + def get_feeder(self, mrid: str) -> Feeder: + return self.feeders.get_by_mrid(mrid) + @deprecated("Use obj.feeders.remove(feeder) instead.") def remove_feeder(self, feeder: Feeder) -> Substation: - """ - Disassociate `feeder` from this `Substation` - - `feeder` The `Feeder` to disassociate from this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if `feeder` was not associated with this `Substation`. - """ - self._normal_energized_feeders = safe_remove(self._normal_energized_feeders, feeder) + self.feeders.remove(feeder) return self + @deprecated("Use obj.feeders.clear() instead.") def clear_feeders(self) -> Substation: - """ - Clear all current `Feeder`s. - Returns A reference to this `Substation` to allow fluent use. - """ - self._normal_energized_feeders = None + self.feeders.clear() return self + + # endregion feeders boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index 9f48f929e..8eba60fbe 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -7,13 +7,19 @@ __all__ = ["Diagram"] -from typing import Optional, Dict, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing import Optional, Dict, List, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_style import DiagramStyle from zepben.ewb.model.cim.iec61970.base.diagramlayout.orientation_kind import OrientationKind -from zepben.ewb.util import nlen, ngen, require, safe_remove_by_id -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.util import require if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject @@ -32,80 +38,50 @@ class Diagram(IdentifiedObject): orientation_kind: OrientationKind = OrientationKind.POSITIVE """Coordinate system orientation of the diagram.""" - _diagram_objects: Optional[Dict[str, DiagramObject]] = None - - def __init__(self, *args, diagram_objects: List[DiagramObject] = None, **kwargs): - super(Diagram, self).__init__(*args, **kwargs) - if diagram_objects: - for obj in diagram_objects: - self.add_diagram_object(obj) + _diagram_objects: Dict[str, DiagramObject] | None = field(default=None) - @property - def diagram_objects(self) -> Generator[DiagramObject, None, None]: - """ - The diagram objects belonging to this diagram. - """ - return ngen(self._diagram_objects) - - def num_diagram_objects(self): - """ - Returns The number of `DiagramObject`s associated with this `Diagram` - """ - return nlen(self._diagram_objects) - - def get_diagram_object(self, mrid: str) -> DiagramObject: - """ - Get the `DiagramObject` for this `Diagram` identified by `mrid` - - `mrid` the mRID of the required `DiagramObject` - Returns The `DiagramObject` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - if not self._diagram_objects: - raise KeyError(mrid) - try: - return self._diagram_objects[mrid] - except AttributeError: - raise KeyError(mrid) + diagram_objects: MridCollection[DiagramObject] = LazyMridMap( + _diagram_objects, + "A DiagramObject", + validate=lambda self, it: self._validate_diagram_object(it) + ) + """The diagram objects belonging to this diagram.""" - def add_diagram_object(self, diagram_object: DiagramObject) -> Diagram: + def _validate_diagram_object(self, diagram_object: DiagramObject) -> None: """ - Associate a `DiagramObject` with this `Diagram`. - - `diagram_object` the `DiagramObject` to associate with this `Diagram`. - Returns The previous `DiagramObject` stored by `diagram_object`s mrid, otherwise `diagram_object` is returned - if there was no previous value. - Raises `ValueError` if another `DiagramObject` with the same `mrid` already exists for this `Diagram`, or if `diagram_object.diagram` is not this - `Diagram`. + Fill and validate the `diagram` backref for the `diagram_object` """ if not diagram_object.diagram: diagram_object.diagram = self require(diagram_object.diagram is self, lambda: f"{str(diagram_object)} `diagram` property references " f"{str(diagram_object.diagram)}, expected {str(self)}.") - if self._validate_reference(diagram_object, self.get_diagram_object, "A DiagramObject"): - return self - self._diagram_objects = dict() if self._diagram_objects is None else self._diagram_objects - self._diagram_objects[diagram_object.mrid] = diagram_object + # region deprecated list boilerplate + # region cuts boilerplate + + @deprecated("Use len(diagram_objects) instead") + def num_diagram_objects(self): + return len(self.diagram_objects) + @deprecated("Use diagram_objects.get_by_mrid(mrid) instead") + def get_diagram_object(self, mrid: str) -> DiagramObject: + return self.diagram_objects.get_by_mrid(mrid) + + @deprecated("Use diagram_objects.append(diagram_object) instead") + def add_diagram_object(self, diagram_object: DiagramObject) -> Diagram: + self.diagram_objects.append(diagram_object) return self + @deprecated("Use diagram_objects.remove(diagram_object) instead") def remove_diagram_object(self, diagram_object: DiagramObject) -> Diagram: - """ - Disassociate `diagram_object` from this `Diagram` - - `diagram_object` the `DiagramObject` to disassociate with this `Diagram`. - Returns A reference to this `Diagram` to allow fluent use. - Raises `KeyError` if `diagram_object` was not associated with this `Diagram`. - """ - self._diagram_objects = safe_remove_by_id(self._diagram_objects, diagram_object) + self.diagram_objects.remove(diagram_object) return self + @deprecated("Use diagram_objects.clear() instead") def clear_diagram_objects(self) -> Diagram: - """ - Clear all `DiagramObject`s. - Returns A reference to this `Diagram` to allow fluent use. - """ - self._diagram_objects = None + self.diagram_objects.clear() return self + + # endregion + # endregion \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py index c31fe2691..1dd0e62ed 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py +++ b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py @@ -3,30 +3,45 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from __future__ import annotations + __all__ = ["BatteryUnit"] from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control_mode import BatteryControlMode from zepben.ewb.model.cim.iec61970.base.generation.production.battery_state_kind import BatteryStateKind from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control import BatteryControl +class BatteryControlList(LazyMridList): + def get_by_mode(self, control_mode: BatteryControlMode): + """ + Get the `BatteryControl` identified by its `control_mode` + + `control_mode` the `BatteryControlMode` of the desired `BatteryControl` + Returns The `BatteryControl` with the specified `control_mode` if it exists + Raises `KeyError` if a `BatteryControl` with `control_mode` wasn't present. + """ + for control in self: + if control.control_mode == control_mode: + return control + raise IndexError(f"No BatteryControl with a control_mode of {control_mode} was found in BatteryUnit {str(self)}") + + @zb_dataclass class BatteryUnit(PowerElectronicsUnit): """An electrochemical energy storage device.""" - def __init__(self, *args, controls: List['BatteryControl'] = None, **kwargs): - super(BatteryUnit, self).__init__(*args, **kwargs) - if controls: - for bc in controls: - self.add_control(bc) - battery_state: BatteryStateKind = BatteryStateKind.UNKNOWN """The current state of the battery (charging, full, etc.).""" @@ -36,75 +51,46 @@ def __init__(self, *args, controls: List['BatteryControl'] = None, **kwargs): stored_e: Optional[int] = None """Amount of energy currently stored in watt hours (Wh). The attribute shall be a positive value or zero and lower than `rated_e`.""" - _controls: Optional[List['BatteryControl']] = None + _controls: Optional[List['BatteryControl']] = field(default=None) # NOTE: This is called `num_battery_controls` because `num_controls` is already used by `PowerSystemResource`. - @property - def controls(self) -> Generator['BatteryControl', None, None]: - """ - [ZBEX] The `BatteryControl`s associated with this `BatteryUnit` - """ - return ngen(self._controls) + controls: BatteryControlList['BatteryControl'] = BatteryControlList( + _controls, + "A BatteryControl", + ) + + + # region deprecated list boilerplate + # region controls boilerplate + + @deprecated("Use len(obj.controls) instead.") def num_battery_controls(self): - """ - Returns The number of `BatteryControl`s associated with this `BatteryUnit` - """ - return nlen(self._controls) + return len(self.controls) + @deprecated("Use obj.controls.get_by_mrid(mrid) instead.") def get_control(self, mrid: str) -> 'BatteryControl': - """ - Get the `BatteryControl` for this `BatteryUnit` identified by `mrid` - - `mrid` the mRID of the required `BatteryControl` - Returns The `BatteryControl` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._controls, mrid) + return self.controls.get_by_mrid(mrid) + @deprecated("Use obj.controls.get_by_mode(control_mode) instead.") def get_control_by_mode(self, control_mode: BatteryControlMode) -> 'BatteryControl': - """ - Get the `BatteryControl` for this `BatteryUnit` identified by its `control_mode` - - `control_mode` the `BatteryControlMode` of the desired `BatteryControl` - Returns The `BatteryControl` with the specified `control_mode` if it exists - Raises `KeyError` if a `BatteryControl` with `control_mode` wasn't present. - """ - if self._controls: - for control in self._controls: - if control.control_mode == control_mode: - return control - raise IndexError(f"No BatteryControl with a control_mode of {control_mode} was found in BatteryUnit {str(self)}") + return self.controls.get_by_mode(control_mode) + @deprecated("Use obj.controls.append(bc) instead.") def add_control(self, bc: 'BatteryControl') -> 'BatteryUnit': - """ - Associate `bc` to this `BatteryUnit`. - - `bc` the `BatteryControl` to associate with this `BatteryUnit`. - Returns A reference to this `BatteryUnit` to allow fluent use. - Raises `ValueError` if another `BatteryControl` with the same `mrid` already exists for this `BatteryUnit`. - """ - if self._validate_reference(bc, self.get_control, "A BatteryControl"): - return self - self._controls = list() if self._controls is None else self._controls - self._controls.append(bc) + self.controls.append(bc) return self + @deprecated("Use obj.controls.remove(bc) instead.") def remove_control(self, bc: 'BatteryControl') -> 'BatteryUnit': - """ - Disassociate `bc` from this `BatteryUnit` - - `bc` the `BatteryControl` to disassociate from this `BatteryUnit`. - Returns A reference to this `BatteryUnit` to allow fluent use. - Raises `ValueError` if `up` was not associated with this `BatteryUnit`. - """ - self._controls = safe_remove(self._controls, bc) + self.controls.remove(bc) return self + @deprecated("Use obj.controls.clear() instead.") def clear_controls(self) -> 'BatteryUnit': - """ - Clear all battery_controls. - Returns A reference to this `BatteryUnit` to allow fluent use. - """ - self._controls = None + self.controls.clear() return self + + # endregion controls boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index c6734028d..9c6c9d284 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -8,12 +8,16 @@ __all__ = ["AcLineSegment"] from typing import Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase from zepben.ewb.model.cim.iec61970.base.wires.conductor import Conductor from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo @@ -24,6 +28,18 @@ from zepben.ewb.model.cim.iec61970.base.wires.per_length_sequence_impedance import PerLengthSequenceImpedance +class AcLineSegmentPhaseList(LazyMridList): + def get_by_phase(self, phase: SinglePhaseKind): + """ + The individual phase models for an AcLineSegment. + `phase` the phase of the required [AcLineSegmentPhase] + """ + res = next((it for it in self if it.phase == phase), None) + if res is None: + raise KeyError(phase) + return res + + @zb_dataclass class AcLineSegment(Conductor): """ @@ -43,9 +59,9 @@ class AcLineSegment(Conductor): per_length_impedance: 'PerLengthImpedance | None' = None """A `zepben.ewb.model.cim.iec61970.base.wires.PerLengthImpedance` describing this AcLineSegment""" - _cuts: list['Cut'] | None = None - _clamps: list['Clamp'] | None = None - _phases: list['AcLineSegmentPhase'] | None = None + _cuts: list['Cut'] | None = field(default=None) + _clamps: list['Clamp'] | None = field(default=None) + _phases: list['AcLineSegmentPhase'] | None = field(default=None) @property def per_length_sequence_impedance(self) -> Optional['PerLengthSequenceImpedance']: @@ -79,27 +95,20 @@ def per_length_phase_impedance(self) -> Optional['PerLengthPhaseImpedance']: def per_length_phase_impedance(self, value: Optional['PerLengthPhaseImpedance']): self.per_length_impedance = value - @property - def cuts(self) -> Generator['Cut', None, None]: - """The `Cut`s for this `AcLineSegment`.""" - return ngen(self._cuts) - @property - def clamps(self) -> Generator['Clamp', None, None]: - """The `Clamp`s for this `AcLineSegment`.""" - return ngen(self._clamps) + cuts: MridCollection['Cut'] = LazyMridList( + _cuts, + "A Cut", + validate=lambda self, it: self._validate_cut(it) + ) - def _validate_cut(self, cut: 'Cut') -> bool: + def _validate_cut(self, cut: 'Cut') -> None: """ Validate a cut against this `AcLineSegment`'s `Cut`s. :param cut: The `Cut` to validate. - :return: True if `cut` is already associated with this `AcLineSegment`, otherwise False. - :raise ValueError: If `cut.ac_line_segment` is not this `AcLineSegment`, or if this `AcLineSegment` has a different `Cut` with the same mRID. + :raise ValueError: If `cut.ac_line_segment` is not this `AcLineSegment` """ - if self._validate_reference(cut, self.get_cut, "A Cut"): - return True - if not cut.ac_line_segment: cut.ac_line_segment = self @@ -107,19 +116,21 @@ def _validate_cut(self, cut: 'Cut') -> bool: cut.ac_line_segment is self, lambda: f"{cut} `ac_line_segment` property references {cut.ac_line_segment}, expected {str(self)}.", ) - return False - def _validate_clamp(self, clamp: 'Clamp') -> bool: + + clamps: MridCollection['Clamp'] = LazyMridList( + _clamps, + "A Clamp", + validate=lambda self, it: self._validate_clamp(it) + ) + + def _validate_clamp(self, clamp: 'Clamp') -> None: """ Validate a clamp against this `AcLineSegment`'s `Clamp`s. :param clamp: The `Clamp` to validate. - :return: True if `clamp` is already associated with this `AcLineSegment`, otherwise False. - :raise ValueError: If `clamp.ac_line_segment` is not this `AcLineSegment`, or if this `AcLineSegment` has a different `Clamp` with the same mRID. + :raise ValueError: If `clamp.ac_line_segment` is not this `AcLineSegment` """ - if self._validate_reference(clamp, self.get_clamp, "A Clamp"): - return True - if not clamp.ac_line_segment: clamp.ac_line_segment = self @@ -127,14 +138,27 @@ def _validate_clamp(self, clamp: 'Clamp') -> bool: clamp.ac_line_segment is self, lambda: f"{clamp} `ac_line_segment` property references {clamp.ac_line_segment}, expected {str(self)}.", ) - return False - @property - def phases(self) -> Generator['AcLineSegmentPhase', None, None]: + + phases: AcLineSegmentPhaseList = AcLineSegmentPhaseList( + _phases, + "An AcLineSegmentPhase", + validate=lambda self, it: self._validate_phase(it), + sort_by=lambda it: it.sequence_number or 0 + ) + + def _validate_phase(self, phase: AcLineSegmentPhase) -> None: """ - The individual phase models for this AcLineSegment. The returned collection is read only. + Validate a phase against this `AcLineSegment`'s `Phase`s. + + :param phase: The `Phase` to validate. + :raise ValueError: If `phase.ac_line_segment` is not this `AcLineSegment` """ - return ngen(self._phases) + if phase.ac_line_segment is None: + phase.ac_line_segment = self + + require(phase.ac_line_segment is self, lambda: f"{phase} `ac_line_segment` property references {phase.ac_line_segment}, expected {self}.") + def wire_info_for_phase(self, phase: SinglePhaseKind) -> 'WireInfo | None': """ @@ -150,162 +174,90 @@ def wire_info_for_phase(self, phase: SinglePhaseKind) -> 'WireInfo | None': else: return self.asset_info + # region deprecated list boilerplate + # region cuts boilerplate + + @deprecated("Use len(obj.cuts) instead.") def num_cuts(self): - """ - Get the number of `Cut`s for this `AcLineSegment`. - """ - return nlen(self._cuts) + return len(self.cuts) + @deprecated("Use obj.cuts.get_by_mrid(mrid) instead.") def get_cut(self, mrid: str) -> 'Cut': - """ - Get the `Cut` for this `AcLineSegment` identified by `mrid` - - :param mrid: The mRID of the required `Cut` - :return: The `Cut` with the specified `mrid` if it exists - :raise KeyError: If the `mrid` wasn't present. - """ - return get_by_mrid(self._cuts, mrid) + return self.cuts.get_by_mrid(mrid) + @deprecated("Use obj.cuts.append(cut) instead.") def add_cut(self, cut: 'Cut') -> 'AcLineSegment': - """ - Associate a `Cut` with this `AcLineSegment`. - - :param cut: the `Cut` to associate with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - :raise ValueError: If another `Cut` with the same `mrid` already exists for this `AcLineSegment`. - """ - if self._validate_cut(cut): - return self - - self._cuts = list() if self._cuts is None else self._cuts - self._cuts.append(cut) + self.cuts.append(cut) return self + @deprecated("Use obj.cuts.remove(cut) instead.") def remove_cut(self, cut: 'Cut') -> 'AcLineSegment': - """ - :param cut: The `Cut` to disassociate from this `AcLineSegment`. - :raise ValueError: If `cut` was not associated with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._cuts = safe_remove(self._cuts, cut) + self.cuts.remove(cut) return self + @deprecated("Use obj.cuts.clear() instead.") def clear_cuts(self) -> 'AcLineSegment': - """ - Clear all `Cut`s. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._cuts.clear() + self.cuts.clear() return self + # endregion cuts boilerplate + + # region clamps boilerplate + + @deprecated("Use len(obj.clamps) instead.") def num_clamps(self): - """ - Get the number of `Clamp`s for this `AcLineSegment`. - """ - return nlen(self._clamps) + return len(self.clamps) + @deprecated("Use obj.clamps.get_by_mrid(mrid) instead.") def get_clamp(self, mrid: str) -> 'Clamp': - """ - Get the `Clamp` for this `AcLineSegment` identified by `mrid` - - :param mrid: The mRID of the required `Clamp` - :return: The `Clamp` with the specified `mrid` if it exists - :raise KeyError: If the `mrid` wasn't present. - """ - return get_by_mrid(self._clamps, mrid) + return self.clamps.get_by_mrid(mrid) + @deprecated("Use obj.clamps.append(clamp) instead.") def add_clamp(self, clamp: 'Clamp') -> 'AcLineSegment': - """ - Associate a `Clamp` with this `AcLineSegment`. - - :param clamp: the `Clamp` to associate with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - :raise ValueError: If another `Clamp` with the same `mrid` already exists for this `AcLineSegment`. - """ - if self._validate_clamp(clamp): - return self - - self._clamps = list() if self._clamps is None else self._clamps - self._clamps.append(clamp) + self.clamps.append(clamp) return self + @deprecated("Use obj.clamps.remove(clamp) instead.") def remove_clamp(self, clamp: 'Clamp') -> 'AcLineSegment': - """ - :param clamp: The `Clamp` to disassociate from this `AcLineSegment`. - :raise ValueError: If `clamp` was not associated with this `AcLineSegment`. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._clamps = safe_remove(self._clamps, clamp) + self.clamps.remove(clamp) return self + @deprecated("Use obj.clamps.clear() instead.") def clear_clamps(self) -> 'AcLineSegment': - """ - Clear all `Clamp`s. - :return: A reference to this `AcLineSegment` to allow fluent use. - """ - self._clamps.clear() + self.clamps.clear() return self + # endregion clamps boilerplate + + # region phases boilerplate + + @deprecated("Use len(obj.phases) instead.") def num_phases(self) -> int: - """ - Get the number of entries in the [AcLineSegmentPhase] collection. - """ - return nlen(self._phases) + return len(self.phases) + @deprecated("Use obj.phases.get_by_mrid(identifier) or obj.phases.get_by_phase(identifier) instead.") def get_phase(self, identifier: 'str | SinglePhaseKind') -> 'AcLineSegmentPhase | None': - """ - The individual phase models for this AcLineSegment. - - :param identifier: the mRID or ``SinglePhaseKind`` of the required [AcLineSegmentPhase] - :returns: The [AcLineSegmentPhase] with the specified [mRID] if it exists, otherwise null - """ if isinstance(identifier, str): - if self._phases is not None: - return get_by_mrid(self._phases, identifier) - + return self.phases.get_by_mrid(identifier) elif isinstance(identifier, SinglePhaseKind): - for it in self._phases: - if it == identifier: - return it - - raise KeyError(identifier) + return self.phases.get_by_phase(identifier) + raise KeyError(identifier) # Wrong error, but consistent with previous functionality - deprecated regardless. + @deprecated("Use obj.phases.append(phase) instead.") def add_phase(self, phase: AcLineSegmentPhase) -> 'AcLineSegment': - """ - Add an [AcLineSegmentPhase] to this [AcLineSegment]. - - :param phase: The [AcLineSegmentPhase] to add. - :returns: This [AcLineSegment] for fluent use. - """ - if self._validate_reference(phase, self.get_phase, "An AcLineSegmentPhase"): - return self - - if phase.ac_line_segment is None: - phase.ac_line_segment = self - - require(phase.ac_line_segment is self, lambda: f"{phase} `ac_line_segment` property references {phase.ac_line_segment}, expected {self}.") - - if self._phases is None: - self._phases = list() - self._phases.append(phase) - self._phases.sort(key=lambda it: it.sequence_number or 0) + self.phases.append(phase) return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: AcLineSegmentPhase) -> 'AcLineSegment': - """ - Remove an [AcLineSegmentPhase] from this [AcLineSegment]. - - :param phase: The [AcLineSegmentPhase] to remove. - :returns: true if [phase] is removed from the collection. - """ - self._phases = safe_remove(self._phases, phase) + self.phases.remove(phase) return self + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> 'AcLineSegment': - """ - Clear all [AcLineSegmentPhase]'s from this [AcLineSegment]. - - :returns: This [AcLineSegment] for fluent use. - """ - self._phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py index 297c5a50d..929c6e1f3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py @@ -37,11 +37,6 @@ class AcLineSegmentPhase(PowerSystemResource): sequence_number: int | None = None _ac_line_segment: AcLineSegment | None = None - def __init__(self, *args, ac_line_segment: AcLineSegment = None, **kwargs): - super(AcLineSegmentPhase, self).__init__(*args, **kwargs) - if ac_line_segment is not None: - self.ac_line_segment = ac_line_segment - @property def ac_line_segment(self) -> 'AcLineSegment | None': return self._ac_line_segment diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 32247cf9c..be5dfb6db 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -7,8 +7,14 @@ __all__ = ["EnergyConsumer"] -from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing import Optional, List, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.phase_shunt_connection_kind import PhaseShuntConnectionKind from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require @@ -22,7 +28,7 @@ class EnergyConsumer(EnergyConnection): """Generic user of energy - a point of consumption on the power system phases. May also represent a pro-sumer with negative p/q values. """ - _energy_consumer_phases: Optional[List[EnergyConsumerPhase]] = None + _energy_consumer_phases: Optional[List[EnergyConsumerPhase]] = field(default=None) """The individual phase models for this energy consumer.""" customer_count: Optional[int] = None @@ -49,30 +55,25 @@ class EnergyConsumer(EnergyConnection): q_fixed: Optional[float] = None """Power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" - def __init__(self, *args, energy_consumer_phases: List[EnergyConsumerPhase] = None, **kwargs): - super(EnergyConsumer, self).__init__(*args, **kwargs) - if energy_consumer_phases: - for phase in energy_consumer_phases: - self.add_phase(phase) + phases: MridCollection[EnergyConsumerPhase] = LazyMridList( + _energy_consumer_phases, + "An EnergyConsumerPhase", + ) + # TODO: Remove hack + # Old inits used the wrong name. This allows backwards compatibility + energy_consumer_phases: None = Alias(phases) + - @property - def phases(self) -> Generator[EnergyConsumerPhase, None, None]: - """The individual phase models for this energy consumer.""" - return ngen(self._energy_consumer_phases) + # region deprecated list boilerplate + # region phases boilerplate + @deprecated("Use len(obj.phases) instead.") def num_phases(self): - """Get the number of `EnergySourcePhase`s for this `EnergyConsumer`.""" - return nlen(self._energy_consumer_phases) + return len(self.phases) + @deprecated("Use obj.phases.get_by_mrid(mrid) instead.") def get_phase(self, mrid: str) -> EnergyConsumerPhase: - """ - Get the `EnergyConsumerPhase` for this `EnergyConsumer` identified by `mrid` - - `mrid` The mRID of the required `EnergyConsumerPhase` - Returns The `EnergyConsumerPhase` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energy_consumer_phases, mrid) + return self.phases.get_by_mrid(mrid) def add_phase(self, phase: EnergyConsumerPhase) -> EnergyConsumer: """ @@ -95,22 +96,16 @@ def add_phase(self, phase: EnergyConsumerPhase) -> EnergyConsumer: self._energy_consumer_phases.append(phase) return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: EnergyConsumerPhase) -> EnergyConsumer: - """ - Disassociate `phase` from this `OperationalRestriction`. - - `phase` the `EnergyConsumerPhase` to disassociate with this `EnergyConsumer`. - Raises `KeyError` if `phase` was not associated with this `EnergyConsumer`. - Returns A reference to this `EnergyConsumer` to allow fluent use. - Raises `ValueError` if `phase` was not associated with this `EnergyConsumer`. - """ - self._energy_consumer_phases = safe_remove(self._energy_consumer_phases, phase) + self.phases.remove(phase) return self + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> EnergyConsumer: - """ - Clear all phases. - Returns A reference to this `EnergyConsumer` to allow fluent use. - """ - self._energy_consumer_phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py index a46682b4c..304091e80 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py @@ -42,11 +42,6 @@ class EnergyConsumerPhase(PowerSystemResource): q_fixed: Optional[float] = None """Reactive power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" - def __init__(self, *args, energy_consumer: 'EnergyConsumer' = None, **kwargs): - super(EnergyConsumerPhase, self).__init__(*args, **kwargs) - if energy_consumer: - self.energy_consumer = energy_consumer - @property def energy_consumer(self): """The `EnergyConsumer` that has this phase.""" diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index a544a2230..48d1d0fc5 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -8,10 +8,14 @@ __all__ = ["EnergySource"] from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase @@ -23,7 +27,7 @@ class EnergySource(EnergyConnection): A generic equivalent for an energy supplier on a transmission or distribution voltage level. """ - _energy_source_phases: Optional[List[EnergySourcePhase]] = None + _energy_source_phases: Optional[List[EnergySourcePhase]] = field(default=None) active_power: Optional[float] = None """ @@ -113,32 +117,23 @@ class EnergySource(EnergyConnection): x0_max: Optional[float] = None """Maximum zero sequence Thevenin reactance.""" - def __init__(self, *args, energy_source_phases: List[EnergySourcePhase] = None, **kwargs): - super(EnergySource, self).__init__(*args, **kwargs) - if energy_source_phases: - for phase in energy_source_phases: - self.add_phase(phase) + phases: MridCollection[EnergySourcePhase] = LazyMridList( + _energy_source_phases, + "An EnergySourcePhase", + ) + energy_source_phases = Alias(phases) - @property - def phases(self) -> Generator[EnergySourcePhase, None, None]: - """ - The `EnergySourcePhase`s for this `EnergySource`. - """ - return ngen(self._energy_source_phases) + # region deprecated list boilerplate + # region phases boilerplate + + @deprecated("Use len(obj.phases) instead.") def num_phases(self): - """Return the number of `EnergySourcePhase`s associated with this `EnergySource`""" - return nlen(self._energy_source_phases) + return len(self.phases) + @deprecated("Use obj.phases.get_by_mrid(mrid) instead.") def get_phase(self, mrid: str) -> EnergySourcePhase: - """ - Get the `EnergySourcePhase` for this `EnergySource` identified by `mrid` - - `mrid` the mRID of the required `EnergySourcePhase` - Returns The `EnergySourcePhase` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._energy_source_phases, mrid) + return self.phases.get_by_mrid(mrid) def add_phase(self, phase: EnergySourcePhase) -> EnergySource: """ @@ -161,21 +156,16 @@ def add_phase(self, phase: EnergySourcePhase) -> EnergySource: self._energy_source_phases.append(phase) return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: EnergySourcePhase) -> EnergySource: - """ - Disassociate an `phase` from this `EnergySource` - - `phase` the `EnergySourcePhase` to disassociate from this `EnergySource`. - Returns A reference to this `EnergySource` to allow fluent use. - Raises `ValueError` if `phase` was not associated with this `EnergySource`. - """ - self._energy_source_phases = safe_remove(self._energy_source_phases, phase) + self.phases.remove(phase) return self + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> EnergySource: - """ - Clear all phases. - Returns A reference to this `EnergySource` to allow fluent use. - """ - self._energy_source_phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py index a818d0eab..4b2fcfe5a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py @@ -31,11 +31,6 @@ class EnergySourcePhase(PowerSystemResource): the connection is from the indicated phase to the central ground or neutral point. If the energy source is delta connected, the phase indicates an energy source connected from the indicated phase to the next logical non-neutral phase.""" - def __init__(self, *args, energy_source: 'EnergySource' = None, **kwargs): - super(EnergySourcePhase, self).__init__(*args, **kwargs) - if energy_source: - self.energy_source = energy_source - @property def energy_source(self): """The `EnergySource` with this `EnergySourcePhase`""" diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py index edb0557cb..7b7709f66 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py @@ -3,53 +3,25 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from __future__ import annotations + __all__ = ["PerLengthPhaseImpedance"] -from typing import Optional, List, Generator +from dataclasses import field +from typing import List, Generator + +from typing_extensions import deprecated +from zepben.ewb import remove_descriptor_annotations, zb_dataclass +from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.phase_impedance_data import PhaseImpedanceData from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.util import require, ngen, nlen, safe_remove, none -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass - - -@zb_dataclass -class PerLengthPhaseImpedance(PerLengthImpedance): - """ - Impedance and admittance parameters per unit length for n-wire unbalanced lines, in matrix form. - """ - _data: Optional[List[PhaseImpedanceData]] = None - def __init__(self, *args, data: List[PhaseImpedanceData] = None, **kwargs): - """ - `data` A list of `PhaseImpedanceData`s to associate with this `PerLengthPhaseImpedance`. - """ - super(PerLengthPhaseImpedance, self).__init__(*args, **kwargs) - if data: - for phase_data in data: - self.add_data(phase_data) - - @property - def data(self) -> Generator[PhaseImpedanceData, None, None]: - """ - The point data values that define this phase_impedance, sorted by `x_value` in ascending order. - """ - return ngen(self._data) - - @property - def diagonal(self) -> Generator[PhaseImpedanceData, None, None]: - """ - Get only the diagonal elements of the matrix, i.e toPhase == fromPhase. - """ - return ngen(pid for pid in self._data if pid.from_phase == pid.to_phase) - - def num_data(self): - """Return the number of :class:`PhaseImpedanceData` associated with this :class:`PerLengthPhaseImpedance`.""" - return nlen(self._data) - - def get_data(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: +class PhaseImpedanceDataList(LazyValidatedList): + def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: """ Get the matrix entry for the corresponding to and from phases. @@ -58,44 +30,69 @@ def get_data(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> Ph :returns: The :class:`PhaseImpedanceData` with the specified `from_phase` and `to_phase` if it exists. :raises KeyError: When no `PhaseImpedanceData` was found with a matching `from_phase` and `to_phase`. """ - if self._data: - phase_impedance_data = next((it for it in self._data if it.from_phase == from_phase and it.to_phase == to_phase), None) - if phase_impedance_data: - return phase_impedance_data + phase_impedance_data = next((it for it in self if it.from_phase == from_phase and it.to_phase == to_phase), None) + if phase_impedance_data: + return phase_impedance_data + raise KeyError((from_phase, to_phase)) - def add_data(self, phase_impedance_data: PhaseImpedanceData) -> 'PerLengthPhaseImpedance': + @property + def diagonal(self): """ - Add a :class:`PhaseImpedanceData` to this :class:`PerLengthPhaseImpedance`. - - :param phase_impedance_data: The :class:`PhaseImpedanceData` to add. - :returns: A reference to this :class:`PerLengthPhaseImpedance` to allow fluent use. - :raises ValueError: If another :class:`PhaseImpedanceData` with the same `from_phase` and `to_phase` already exists for this :class:`PerLengthPhaseImpedance`. + Get only the diagonal elements of the matrix, i.e toPhase == fromPhase. """ + return (pid for pid in self if pid.from_phase == pid.to_phase) + +@zb_dataclass +class PerLengthPhaseImpedance(PerLengthImpedance): + """ + Impedance and admittance parameters per unit length for n-wire unbalanced lines, in matrix form. + """ + + _data: List[PhaseImpedanceData] | None = field(default=None) + + data: PhaseImpedanceDataList[PhaseImpedanceData] = PhaseImpedanceDataList( + _data, + validate=lambda self, it: self._validate_data(it) + ) + + def _validate_data(self, phase_impedance_data: PhaseImpedanceData): require(none([it.from_phase == phase_impedance_data.from_phase and it.to_phase == phase_impedance_data.to_phase for it in self.data]), lambda: f"""Unable to add PhaseImpedanceData to {self}. A PhaseImpedanceData with from_phase {phase_impedance_data.from_phase} and to_phase {phase_impedance_data.to_phase} already exists in this PerLengthPhaseImpedance.""") - self._data = self._data or [] - self._data.append(phase_impedance_data) - return self + # region deprecated list boilerplate + # region data boilerplate - def remove_data(self, phase_impedance_data: PhaseImpedanceData) -> 'PerLengthPhaseImpedance': - """ - Remove a :class:`PhaseImpedanceData` from this :class:`PerLengthPhaseImpedance`. + @property + @deprecated("Use data.diagonal instead") + def diagonal(self) -> Generator[PhaseImpedanceData, None, None]: + return self.data.diagonal - :param phase_impedance_data: The :class:`PhaseImpedanceData` to remove from this :class:`PerLengthPhaseImpedance`. - :returns: A reference to this :class:`PerLengthPhaseImpedance` to allow fluent use. - :raises ValueError: If `phase_impedance_data` was not associated with this :class:`PerLengthPhaseImpedance`. - """ - self._data = safe_remove(self._data, phase_impedance_data) + @deprecated("Use len(data) instead.") + def num_data(self): + return len(self.data) + + @deprecated("Use data.get(from_phase, to_phase) instead.") + def get_data(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: + return self.data.get(from_phase, to_phase) + + @deprecated("Use data.append(phase_impedance_data) instead.") + def add_data(self, phase_impedance_data: 'PhaseImpedanceData') -> PerLengthPhaseImpedance: + self.data.append(phase_impedance_data) return self - def clear_data(self) -> 'PerLengthPhaseImpedance': - """ - Clear all :class:`PhaseImpedanceData` associated with this :class:`PerLengthPhaseImpedance`. - :returns: A reference to this :class:`PerLengthPhaseImpedance` to allow fluent use. - """ - self._data = None + @deprecated("Use data.remove(phase_impedance_data) instead.") + def remove_data(self, phase_impedance_data: 'PhaseImpedanceData') -> PerLengthPhaseImpedance: + self.data.remove(phase_impedance_data) + return self + + @deprecated("Use data.clear() instead.") + def clear_data(self) -> PerLengthPhaseImpedance: + self.data.clear() return self + + # endregion data boilerplate + + # endregion deprecated list boilerplate \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 7d680df12..5649efe89 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -8,10 +8,13 @@ __all__ = ["PowerElectronicsConnection"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove, require +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit @@ -136,35 +139,33 @@ class PowerElectronicsConnection(RegulatingCondEq): Permitted range is between -1.0 and 1.0 (inclusive), with a negative sign referring to “sink”. """ - _power_electronics_units: Optional[List[PowerElectronicsUnit]] = None + _power_electronics_units: Optional[List[PowerElectronicsUnit]] = field(default=None) """An AC network connection may have several power electronics units connecting through it.""" - _power_electronics_connection_phases: Optional[List[PowerElectronicsConnectionPhase]] = None + _power_electronics_connection_phases: Optional[List[PowerElectronicsConnectionPhase]] = field(default=None) """The individual units models for the power electronics connection.""" - def __init__( - self, - *args, - power_electronics_units: List[PowerElectronicsUnit] = None, - power_electronics_connection_phases: List[PowerElectronicsConnectionPhase] = None, - inv_watt_resp_v1=None, - inv_watt_resp_v2=None, - inv_watt_resp_v3=None, - inv_watt_resp_v4=None, - inv_watt_resp_p_at_v1=None, - inv_watt_resp_p_at_v2=None, - inv_watt_resp_p_at_v3=None, - inv_watt_resp_p_at_v4=None, - inv_var_resp_v1=None, - inv_var_resp_v2=None, - inv_var_resp_v3=None, - inv_var_resp_v4=None, - inv_var_resp_q_at_v1=None, - inv_var_resp_q_at_v2=None, - inv_var_resp_q_at_v3=None, - inv_var_resp_q_at_v4=None, - **kwargs, - ): + def __init__(self, + *args, + power_electronics_units: List[PowerElectronicsUnit] = None, + power_electronics_connection_phases: List[PowerElectronicsConnectionPhase] = None, + inv_watt_resp_v1=None, + inv_watt_resp_v2=None, + inv_watt_resp_v3=None, + inv_watt_resp_v4=None, + inv_watt_resp_p_at_v1=None, + inv_watt_resp_p_at_v2=None, + inv_watt_resp_p_at_v3=None, + inv_watt_resp_p_at_v4=None, + inv_var_resp_v1=None, + inv_var_resp_v2=None, + inv_var_resp_v3=None, + inv_var_resp_v4=None, + inv_var_resp_q_at_v1=None, + inv_var_resp_q_at_v2=None, + inv_var_resp_q_at_v3=None, + inv_var_resp_q_at_v4=None, + **kwargs): super(PowerElectronicsConnection, self).__init__(*args, **kwargs) if power_electronics_units: for unit in power_electronics_units: @@ -417,84 +418,22 @@ def inv_var_resp_q_at_v4(self, value): require(value is None or -0.6 <= value <= 0.0, lambda: f"inv_var_resp_q_at_v4 [{value}] must be between -0.6 and 0.0.") self._inv_var_resp_q_at_v4 = value - @property - def units(self) -> Generator[PowerElectronicsUnit, None, None]: - """ - The `PowerElectronicsUnit`s for this `PowerElectronicsConnection`. - """ - return ngen(self._power_electronics_units) + units: MridCollection[PowerElectronicsUnit] = LazyMridList( + _power_electronics_units, + "A PowerElectronicsUnit", + ) - @property - def phases(self) -> Generator[PowerElectronicsConnectionPhase, None, None]: - """ - The `PowerElectronicsConnectionPhase`s for this `PowerElectronicsConnection`. - """ - return ngen(self._power_electronics_connection_phases) + phases: MridCollection[PowerElectronicsConnectionPhase] = LazyMridList( + _power_electronics_connection_phases, + "A PowerElectronicsConnectionPhase", + ) - def num_units(self): - """Return the number of `PowerElectronicsUnit`s associated with this `PowerElectronicsConnection`""" - return nlen(self._power_electronics_units) - def get_unit(self, mrid: str) -> PowerElectronicsUnit: - """ - Get the `PowerElectronicsUnit` for this - `PowerElectronicsConnection` identified by `mrid` - `mrid` the mRID of the required `PowerElectronicsUnit` - Returns The `PowerElectronicsUnit` with the specified `mrid` - if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_electronics_units, mrid) - def add_unit(self, unit: PowerElectronicsUnit) -> PowerElectronicsConnection: - """ - Associate an `PowerElectronicsUnit` with this - `PowerElectronicsConnection` - `unit` the `PowerElectronicsUnit` to associate with this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if another `PowerElectronicsUnit` with the same `mrid` already exists for this `PowerElectronicsConnection`. - """ - if self._validate_reference(unit, self.get_unit, "A PowerElectronicsUnit"): - return self - self._power_electronics_units = list() if self._power_electronics_units is None else self._power_electronics_units - self._power_electronics_units.append(unit) - return self - def remove_unit(self, unit: PowerElectronicsUnit) -> PowerElectronicsConnection: - """ - Disassociate `unit` from this `PowerElectronicsConnection` - - `unit` the `PowerElectronicsUnit` to disassociate from this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if `unit` was not associated with this `PowerElectronicsConnection`. - """ - self._power_electronics_units = safe_remove(self._power_electronics_units, unit) - return self - - def clear_units(self) -> PowerElectronicsConnection: - """ - Clear all units. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - """ - self._power_electronics_units = None - return self - - def num_phases(self): - """Return the number of `PowerElectronicsConnectionPhase`s associated with this `PowerElectronicsConnection`""" - return nlen(self._power_electronics_connection_phases) - - def get_phase(self, mrid: str) -> PowerElectronicsConnectionPhase: - """ - Get the `PowerElectronicsConnectionPhase` for this `PowerElectronicsConnection` identified by `mrid` - - `mrid` the mRID of the required `PowerElectronicsConnectionPhase` - Returns The `PowerElectronicsConnectionPhase` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_electronics_connection_phases, mrid) def add_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: """ @@ -517,21 +456,56 @@ def add_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsC self._power_electronics_connection_phases.append(phase) return self - def remove_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: - """ - Disassociate `phase` from this `PowerElectronicsConnection` - `phase` the `PowerElectronicsConnectionPhase` to disassociate from this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if `phase` was not associated with this `PowerElectronicsConnection`. - """ - self._power_electronics_connection_phases = safe_remove(self._power_electronics_connection_phases, phase) + + # region deprecated list boilerplate + # region units boilerplate + + @deprecated("Use len(obj.units) instead.") + def num_units(self): + return len(self.units) + + @deprecated("Use obj.units.get_by_mrid(mrid) instead.") + def get_unit(self, mrid: str) -> PowerElectronicsUnit: + return self.units.get_by_mrid(mrid) + + @deprecated("Use obj.units.append(unit) instead.") + def add_unit(self, unit: PowerElectronicsUnit) -> PowerElectronicsConnection: + self.units.append(unit) + return self + + @deprecated("Use obj.units.remove(unit) instead.") + def remove_unit(self, unit: PowerElectronicsUnit) -> PowerElectronicsConnection: + self.units.remove(unit) return self + @deprecated("Use obj.units.clear() instead.") + def clear_units(self) -> PowerElectronicsConnection: + self.units.clear() + return self + + # endregion units boilerplate + + # region phases boilerplate + + @deprecated("Use len(obj.phases) instead.") + def num_phases(self): + return len(self.phases) + + @deprecated("Use obj.phases.get_by_mrid(mrid) instead.") + def get_phase(self, mrid: str) -> PowerElectronicsConnectionPhase: + return self.phases.get_by_mrid(mrid) + + @deprecated("Use obj.phases.remove(phase) instead.") + def remove_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: + self.phases.remove(phase) + return self + + @deprecated("Use obj.phases.clear() instead.") def clear_phases(self) -> PowerElectronicsConnection: - """ - Clear all phases. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - """ - self._power_electronics_connection_phases = None + self.phases.clear() return self + + # endregion phases boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index fc67591d6..ca4215c3c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -8,7 +8,12 @@ __all__ = ["PowerTransformer"] import sys -from typing import List, Optional, Generator, TYPE_CHECKING +from dataclasses import field +from typing import List, Optional, TYPE_CHECKING + +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList + if sys.version_info >= (3, 13): from warnings import deprecated else: @@ -19,7 +24,7 @@ from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_construction_kind import TransformerConstructionKind from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_function_kind import TransformerFunctionKind from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.util import require, nlen, get_by_mrid, ngen, safe_remove +from zepben.ewb.util import require if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo @@ -27,6 +32,36 @@ from zepben.ewb.model.cim.iec61970.base.wires.power_transformer_end import PowerTransformerEnd +class PowerTransformerEndList(LazyMridList['PowerTransformerEnd']): + + def get_by_num(self, end_number: int) -> PowerTransformerEnd: + """ + Get the `PowerTransformerEnd` on this `PowerTransformer` by its `end_number`. + + `end_number` The `end_number` of the `PowerTransformerEnd` in relation to this `PowerTransformer`s VectorGroup. + Returns The `PowerTransformerEnd` referred to by `end_number` + Raises IndexError if no `PowerTransformerEnd` was found with end_number `end_number`. + """ + end = next((it for it in self if it.end_number == end_number), None) + if end: + return end + raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self.instance)}") + + def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: + """ + Get the `PowerTransformerEnd` on this `PowerTransformer` by its `terminal`. + + `terminal` The `terminal` to find a `PowerTransformerEnd` for. + Returns The `PowerTransformerEnd` connected to the specified `terminal` + Raises IndexError if no `PowerTransformerEnd` connected to `terminal` was found on this `PowerTransformer`. + """ + + end = next((it for it in self if it.terminal == terminal), None) + if end: + return end + raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self.instance)}") + + @zb_dataclass class PowerTransformer(ConductingEquipment): """ @@ -68,7 +103,7 @@ class PowerTransformer(ConductingEquipment): numerical sequence if they are numbered: the phasors are assumed to rotate in a counter-clockwise sense. """ - _power_transformer_ends: Optional[List[PowerTransformerEnd]] = None + _power_transformer_ends: List[PowerTransformerEnd] | None = field(default=None) transformer_utilisation: Optional[float] = None """ @@ -94,10 +129,17 @@ def __init__(self, *args, power_transformer_ends: List[PowerTransformerEnd] = No end.power_transformer = self self.add_end(end) - @property - def ends(self) -> Generator[PowerTransformerEnd, None, None]: - """The `PowerTransformerEnd`s for this `PowerTransformer`.""" - return ngen(self._power_transformer_ends) + ends: PowerTransformerEndList = PowerTransformerEndList( + _power_transformer_ends, + "A PowerTransformerEnd", + validate=lambda self, it: self._validate_end(it), + sort_by=lambda it: it.end_number + ) + + @deprecated("...") + def num_ends(self): + return len(self.ends) + @property @deprecated("use asset_info instead.") @@ -123,74 +165,19 @@ def get_base_voltage(self, terminal: Terminal = None): else: return None - def _validate_end(self, end: PowerTransformerEnd) -> bool: - """ - Validate an end against this `PowerTransformer`'s `PowerTransformerEnd`s. - - `end` The `PowerTransformerEnd` to validate. - Returns True if `end` is already associated with this `PowerTransformer`, otherwise False. - Raises `ValueError` if `end.power_transformer` is not this `PowerTransformer`, or if this `PowerTransformer` has a different `PowerTransformerEnd` - with the same mRID. - """ - if self._validate_reference(end, self.get_end_by_mrid, "A PowerTransformerEnd"): - return True - - if self._validate_reference_by_field(end, end.end_number, self.get_end_by_num, "end_number"): - return True - - if not end.power_transformer: - end.power_transformer = self - - require( - end.power_transformer is self, - lambda: f"{end} `power_transformer` property references {end.power_transformer}, expected {str(self)}.", - ) - return False - - def num_ends(self): - """ - Get the number of `PowerTransformerEnd`s for this `PowerTransformer`. - """ - return nlen(self._power_transformer_ends) - + @deprecated("...") def get_end_by_mrid(self, mrid: str) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` for this `PowerTransformer` identified by `mrid` - - `mrid` the mRID of the required `PowerTransformerEnd` - Returns The `PowerTransformerEnd` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._power_transformer_ends, mrid) + return self.ends.get_by_mrid(mrid) + @deprecated("...") def get_end_by_num(self, end_number: int) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `end_number`. - - `end_number` The `end_number` of the `PowerTransformerEnd` in relation to this `PowerTransformer`s VectorGroup. - Returns The `PowerTransformerEnd` referred to by `end_number` - Raises IndexError if no `PowerTransformerEnd` was found with end_number `end_number`. - """ - if self._power_transformer_ends: - for end in self._power_transformer_ends: - if end.end_number == end_number: - return end - raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self)}") + return self.ends.get_by_num(end_number) + @deprecated("...") def get_end_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `terminal`. - - `terminal` The `terminal` to find a `PowerTransformerEnd` for. - Returns The `PowerTransformerEnd` connected to the specified `terminal` - Raises IndexError if no `PowerTransformerEnd` connected to `terminal` was found on this `PowerTransformer`. - """ - if self._power_transformer_ends: - for end in self._power_transformer_ends: - if end.terminal is terminal: - return end - raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self)}") + return self.ends.get_by_terminal(terminal) + @deprecated("...") def add_end(self, end: PowerTransformerEnd) -> PowerTransformer: """ Associate a `PowerTransformerEnd` with this `PowerTransformer`. If `end.end_number` == 0, the end will be assigned an end_number of @@ -200,30 +187,37 @@ def add_end(self, end: PowerTransformerEnd) -> PowerTransformer: Returns A reference to this `PowerTransformer` to allow fluent use. Raises `ValueError` if another `PowerTransformerEnd` with the same `mrid` already exists for this `PowerTransformer`. """ - if self._validate_end(end): - return self - - if end.end_number == 0: - end.end_number = self.num_ends() + 1 - - self._power_transformer_ends = list() if self._power_transformer_ends is None else self._power_transformer_ends - self._power_transformer_ends.append(end) - self._power_transformer_ends.sort(key=lambda t: t.end_number) + self.ends.append(end) return self + @deprecated("...") def remove_end(self, end: PowerTransformerEnd) -> PowerTransformer: - """ - `end` the `PowerTransformerEnd` to disassociate from this `PowerTransformer`. - Raises `ValueError` if `end` was not associated with this `PowerTransformer`. - Returns A reference to this `PowerTransformer` to allow fluent use. - """ - self._power_transformer_ends = safe_remove(self._power_transformer_ends, end) + self.ends.remove(end) return self + @deprecated("...") def clear_ends(self) -> PowerTransformer: + self.ends.clear() + return self + + def _validate_end(self, end: PowerTransformerEnd): """ - Clear all `PowerTransformerEnd`s. - Returns A reference to this `PowerTransformer` to allow fluent use. + Validate an end against this `PowerTransformer`'s `PowerTransformerEnd`s. + + `end` The `PowerTransformerEnd` to validate. + Returns True if `end` is already associated with this `PowerTransformer`, otherwise False. + Raises `ValueError` if `end.power_transformer` is not this `PowerTransformer`, or if this `PowerTransformer` has a different `PowerTransformerEnd` + with the same mRID. """ - self._power_transformer_ends.clear() - return self + self._validate_reference_by_field(end, end.end_number, self.get_end_by_num, "end_number") + + if not end.power_transformer: + end.power_transformer = self + + require( + end.power_transformer is self, + lambda: f"{end} `power_transformer` property references {end.power_transformer}, expected {str(self)}.", + ) + + if end.end_number == 0: + end.end_number = self.num_ends() + 1 diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py index 9f3eee533..f1f0c046b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py @@ -9,10 +9,14 @@ from typing import Optional, List, Generator, TYPE_CHECKING, Iterable from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.util import get_by_mrid, ngen, nlen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction @@ -27,74 +31,40 @@ class ProtectedSwitch(Switch, metaclass=ABCMeta): breaking_capacity: Optional[int] = None """The maximum fault current in amps a breaking device can break safely under prescribed conditions of use.""" - _relay_functions: Optional[List[ProtectionRelayFunction]] = None + _relay_functions: Optional[List[ProtectionRelayFunction]] = field(default=None) - def __init__( - self, - *args, - relay_functions: Iterable[ProtectionRelayFunction] = None, - **kwargs - ): - super(ProtectedSwitch, self).__init__(*args, **kwargs) + relay_functions: MridCollection[ProtectionRelayFunction] = LazyMridList( + _relay_functions, + "A ProtectionRelayFunction", + ) - # breaking_capacity is handled via dataclassy. - if relay_functions is not None: - for relay_function in relay_functions: - self.add_relay_function(relay_function) - @property - def relay_functions(self) -> Generator[ProtectionRelayFunction, None, None]: - """ - Yields all :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - - :return: A generator that iterates over all :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - """ - return ngen(self._relay_functions) + # region deprecated list boilerplate + # region relay_functions boilerplate + @deprecated("Use len(obj.relay_functions) instead.") def num_relay_functions(self) -> int: - """ - Get the number of :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - - :return: The number of :class:`ProtectionRelayFunctions` operating this :class:`ProtectedSwitch`. - """ - return nlen(self._relay_functions) + return len(self.relay_functions) + @deprecated("Use obj.relay_functions.get_by_mrid(mrid) instead.") def get_relay_function(self, mrid: str) -> ProtectionRelayFunction: - """ - Get a :class:`ProtectionRelayFunction` operating this :class:`ProtectedSwitch` with the specified `mrid`. - - :param mrid: The mRID of the desired :class:`ProtectionRelayFunction` - :return: The :class:`ProtectionRelayFunction` with the specified mRID if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._relay_functions, mrid) + return self.relay_functions.get_by_mrid(mrid) + @deprecated("Use obj.relay_functions.append(relay_function) instead.") def add_relay_function(self, relay_function: ProtectionRelayFunction) -> ProtectedSwitch: - """ - Associate this :class:`ProtectedSwitch` with a :class:`ProtectionRelayFunction` operating it. - :param relay_function: The :class:`ProtectionRelayFunction` to associate with this :class:`ProtectedSwitch`. - :return: A reference to this :class:`ProtectedSwitch` for fluent use. - """ - if self._validate_reference(relay_function, self.get_relay_function, "A ProtectionRelayFunction"): - return self - - self._relay_functions = list() if self._relay_functions is None else self._relay_functions - self._relay_functions.append(relay_function) + self.relay_functions.append(relay_function) return self + @deprecated("Use obj.relay_functions.remove(relay_function) instead.") def remove_relay_function(self, relay_function: Optional[ProtectionRelayFunction]) -> ProtectedSwitch: - """ - Disassociate this :class:`ProtectedSwitch` from a :class:`ProtectionRelayFunction`. - :param relay_function: The :class:`ProtectionRelayFunction` to disassociate from this :class:`ProtectedSwitch`. - :return: A reference to this :class:`ProtectedSwitch` for fluent use. - """ - self._relay_functions = safe_remove(self._relay_functions, relay_function) + self.relay_functions.remove(relay_function) return self + @deprecated("Use obj.relay_functions.clear() instead.") def clear_relay_functions(self) -> ProtectedSwitch: - """ - Disassociate all :class:`ProtectionRelayFunction` from this :class:`ProtectedSwitch`. - :return: A reference to this :class:`ProtectedSwitch` for fluent use. - """ - self._relay_functions = None + self.relay_functions.clear() return self + + # endregion relay_functions boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py index 21095033d..8554de6a6 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_cond_eq.py @@ -29,10 +29,6 @@ class RegulatingCondEq(EnergyConnection, metaclass=ABCMeta): _regulating_control: Optional[RegulatingControl] = None - def __init__(self, *args, regulating_control: Optional[RegulatingControl] = None, **kwargs): - super(RegulatingCondEq, self).__init__(*args, **kwargs) - if regulating_control: - self.regulating_control = regulating_control @property def regulating_control(self): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py index ef87daaff..4a11127a4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py @@ -9,12 +9,16 @@ from typing import Optional, List, Generator, Iterable, TYPE_CHECKING from abc import ABCMeta +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.regulating_control_mode_kind import RegulatingControlModeKind from zepben.ewb.util import nlen, get_by_mrid, safe_remove, ngen -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal @@ -107,70 +111,41 @@ class RegulatingControl(PowerSystemResource, metaclass=ABCMeta): regulators, shunt compensators, or battery units. """ - _regulating_cond_eq: Optional[List[RegulatingCondEq]] = None + _regulating_cond_eq: Optional[List[RegulatingCondEq]] = field(default=None) """The [RegulatingCondEq] that are controlled by this regulating control scheme.""" - def __init__(self, *args, regulating_conducting_equipment: Optional[Iterable[RegulatingCondEq]] = None, **kwargs): - super(RegulatingControl, self).__init__(*args, **kwargs) - if regulating_conducting_equipment is not None: - for eq in regulating_conducting_equipment: - self.add_regulating_cond_eq(eq) + regulating_conducting_equipment: MridCollection[RegulatingCondEq] = LazyMridList( + _regulating_cond_eq, + "A RegulatingCondEq", + ) - @property - def regulating_conducting_equipment(self) -> Generator[RegulatingCondEq, None, None]: - """ - Yields all the :class:`RegulatingCondEq` that are controlled by this :class:`RegulatingControl`. - :return: A generator that iterates over all RegulatingCondEq controlled by this RegulatingControl. - """ - return ngen(self._regulating_cond_eq) + # region deprecated list boilerplate + # region regulating_conducting_equipment boilerplate + @deprecated("Use len(obj.regulating_conducting_equipment) instead.") def num_regulating_cond_eq(self) -> int: - """ - Get the number of :class:`RegulatingCondEq` that are controlled by this :class:`RegulatingControl`. - - :return: The number of RegulatingCondEq that are controlled by this RegulatingControl. - """ - return nlen(self._regulating_cond_eq) + return len(self.regulating_conducting_equipment) + @deprecated("Use obj.regulating_conducting_equipment.get_by_mrid(mrid) instead.") def get_regulating_cond_eq(self, mrid: str) -> RegulatingCondEq: - """ - Get a :class:`RegulatingCondEq` controlled by this :class:`RegulatingControl`. - - :param mrid: The mRID of the desired RegulatingCondEq - :return: The RegulatingCondEq with the specified mRID if it exists, otherwise None. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._regulating_cond_eq, mrid) + return self.regulating_conducting_equipment.get_by_mrid(mrid) + @deprecated("Use obj.regulating_conducting_equipment.append(regulating_cond_eq) instead.") def add_regulating_cond_eq(self, regulating_cond_eq: RegulatingCondEq) -> RegulatingControl: - """ - Associate this :class:`RegulatingControl` with a :class:`RegulatingCondEq` it is controlling. - - :param regulating_cond_eq: The RegulatingCondEq to associate with this RegulatingControl. - :return: A reference to this RegulatingControl for fluent use. - """ - if self._validate_reference(regulating_cond_eq, self.get_regulating_cond_eq, "A RegulatingCondEq"): - return self - - self._regulating_cond_eq = list() if self._regulating_cond_eq is None else self._regulating_cond_eq - self._regulating_cond_eq.append(regulating_cond_eq) + self.regulating_conducting_equipment.append(regulating_cond_eq) return self + @deprecated("Use obj.regulating_conducting_equipment.remove(regulating_cond_eq) instead.") def remove_regulating_cond_eq(self, regulating_cond_eq: Optional[RegulatingCondEq]) -> RegulatingControl: - """ - Disassociate this :class:`RegulatingControl` from a :class:`RegulatingCondEq`. - - :param regulating_cond_eq: The RegulatingCondEq to disassociate from this RegulatingControl. - :return: A reference to this RegulatingControl for fluent use. - """ - self._regulating_cond_eq = safe_remove(self._regulating_cond_eq, regulating_cond_eq) + self.regulating_conducting_equipment.remove(regulating_cond_eq) return self + @deprecated("Use obj.regulating_conducting_equipment.clear() instead.") def clear_regulating_cond_eq(self) -> RegulatingControl: - """ - Disassociate all :class:`RegulatingCondEq` from this :class:`RegulatingControl`. - :return: A reference to this RegulatingControl for fluent use. - """ - self._regulating_cond_eq = None + self.regulating_conducting_equipment.clear() return self + + # endregion regulating_conducting_equipment boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py index 0082bce55..602212359 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py @@ -6,11 +6,15 @@ __all__ = ["SynchronousMachine"] from typing import Optional, List, Generator, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.rotating_machine import RotatingMachine from zepben.ewb.model.cim.iec61970.base.wires.synchronous_machine_kind import SynchronousMachineKind from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.reactive_capability_curve import ReactiveCapabilityCurve @@ -23,7 +27,7 @@ class SynchronousMachine(RotatingMachine): synchronous condenser or pump. """ - _reactive_capability_curves: Optional[List['ReactiveCapabilityCurve']] = None + _reactive_capability_curves: Optional[List['ReactiveCapabilityCurve']] = field(default=None) base_q: Optional[float] = None """Default base reactive power value in VAr. This value represents the initial reactive power that can be used by any application function.""" @@ -106,65 +110,42 @@ class SynchronousMachine(RotatingMachine): operating_mode: SynchronousMachineKind = SynchronousMachineKind.UNKNOWN """Current mode of operation.""" - def __init__(self, *args, curves: List['ReactiveCapabilityCurve'] = None, **kwargs): - """ - `reactive_capability_curves` A list of `ReactiveCapabilityCurve`s to associate with this `SynchronousMachine`. - """ - super(SynchronousMachine, self).__init__(*args, **kwargs) - if curves: - for rcc in curves: - self.add_curve(rcc) - - @property - def curves(self) -> Generator['ReactiveCapabilityCurve', None, None]: - """ - The available reactive capability curves for this synchronous machine. The first shall be the default for this :class:`SynchronousMachine`. - """ - return ngen(self._reactive_capability_curves) + curves: MridCollection['ReactiveCapabilityCurve'] = LazyMridList( + _reactive_capability_curves, + "A ReactiveCapabilityCurve", + ) + + + + + + # region deprecated list boilerplate + # region curves boilerplate + + @deprecated("Use len(obj.curves) instead.") def num_curves(self): - """Return the number of :class:`ReactiveCapabilityCurve`s associated with this :class:`SynchronousMachine`.""" - return nlen(self._reactive_capability_curves) + return len(self.curves) + @deprecated("Use obj.curves.get_by_mrid(mrid) instead.") def get_curve(self, mrid: str) -> 'ReactiveCapabilityCurve': - """ - Get the :class:`ReactiveCapabilityCurve` for this :class:`SynchronousMachine` identified by `mrid` - - :param mrid: The mRID of the required :class:`ReactiveCapabilityCurve`. - :returns: The :class:`ReactiveCapabilityCurve` with the specified `mrid` if it exists. - :raises KeyError: If `mrid` wasn't present. - """ - return get_by_mrid(self._reactive_capability_curves, mrid) + return self.curves.get_by_mrid(mrid) + @deprecated("Use obj.curves.append(curve) instead.") def add_curve(self, curve: 'ReactiveCapabilityCurve') -> 'SynchronousMachine': - """ - Associate a :class:`ReactiveCapabilityCurve` with this :class:`SynchronousMachine`. - - :param curve: The :class:`ReactiveCapabilityCurve` to associate with this :class:`SynchronousMachine`. - :returns: A reference to this :class:`SynchronousMachine` to allow fluent use. - :raises ValueError: If another :class:`ReactiveCapabilityCurve` with the same `mrid` already exists for this :class:`SynchronousMachine`. - """ - if self._validate_reference(curve, self.get_curve, "A ReactiveCapabilityCurve"): - return self - self._reactive_capability_curves = self._reactive_capability_curves or [] - self._reactive_capability_curves.append(curve) + self.curves.append(curve) return self + @deprecated("Use obj.curves.remove(curve) instead.") def remove_curve(self, curve: 'ReactiveCapabilityCurve') -> 'SynchronousMachine': - """ - Disassociate a :class:`ReactiveCapabilityCurve` from this :class:`SynchronousMachine`. - - :param curve: The :class:`ReactiveCapabilityCurve` to disassociate from this :class:`SynchronousMachine`. - :returns: A reference to this :class:`SynchronousMachine` to allow fluent use. - :raises ValueError: If `curve` was not associated with this :class:`SynchronousMachine`. - """ - self._reactive_capability_curves = safe_remove(self._reactive_capability_curves, curve) + self.curves.remove(curve) return self + @deprecated("Use obj.curves.clear() instead.") def clear_curves(self) -> 'SynchronousMachine': - """ - Clear all :class:`ReactiveCapabilityCurve` associated with this :class:`SynchronousMachine`. - :returns: A reference to this :class:`SynchronousMachine` to allow fluent use. - """ - self._reactive_capability_curves = None + self.curves.clear() return self + + # endregion curves boilerplate + + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py index 1efccef84..e0ebe95bc 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/transformer_end.py @@ -53,11 +53,6 @@ class TransformerEnd(IdentifiedObject, metaclass=ABCMeta): """(accurate for 2- or 3-winding transformers only) Pi-model impedances of this transformer end. By convention, for a two winding transformer, the full values of the transformer should be entered on the high voltage end (endNumber=1).""" - def __init__(self, *args, terminal: Optional['Terminal'] = None, **kwargs): - super(TransformerEnd, self).__init__(*args, **kwargs) - if terminal is not None: - self.terminal = terminal - @property def terminal(self) -> Optional['Terminal']: """ diff --git a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py index b36289d3a..c2ae3ff63 100644 --- a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py +++ b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py @@ -8,10 +8,14 @@ __all__ = ["Circuit"] from typing import Optional, Generator, List, TYPE_CHECKING +from dataclasses import field +from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.line import Line from zepben.ewb.util import ngen, get_by_mrid, safe_remove, nlen -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop @@ -24,123 +28,73 @@ class Circuit(Line): """Missing description""" loop: Optional[Loop] = None - _end_terminals: Optional[List[Terminal]] = None - _end_substations: Optional[List[Substation]] = None - - def __init__(self, *args, end_terminals: List[Terminal] = None, end_substations: List[Substation] = None, **kwargs): - super(Circuit, self).__init__(*args, **kwargs) - if end_terminals: - for term in end_terminals: - self.add_end_terminal(term) - - if end_substations: - for sub in end_substations: - self.add_end_substation(sub) - - @property - def end_terminals(self) -> Generator[Terminal, None, None]: - """ - The `Terminal`s representing the ends for this `Circuit`. - """ - return ngen(self._end_terminals) - - @property - def end_substations(self) -> Generator[Substation, None, None]: - """ - The `Substations`s representing the ends for this `Circuit`. - """ - return ngen(self._end_substations) + _end_terminals: Optional[List[Terminal]] = field(default=None) + _end_substations: Optional[List[Substation]] = field(default=None) + end_terminals: MridCollection[Terminal] = LazyMridList( + _end_terminals, + "An Terminal", + ) + + end_substations: MridCollection[Substation] = LazyMridList( + _end_substations, + "An Substation", + ) + + + # region deprecated list boilerplate + # region end_terminals boilerplate + + @deprecated("Use len(obj.end_terminals) instead.") def num_end_terminals(self): - """Return the number of end `Terminal`s associated with this `Circuit`""" - return nlen(self._end_terminals) + return len(self.end_terminals) + @deprecated("Use obj.end_terminals.get_by_mrid(mrid) instead.") def get_end_terminal(self, mrid: str) -> Terminal: - """ - Get the `Terminal` for this `Circuit` identified by `mrid` - - `mrid` the mRID of the required `Terminal` - Returns The `Terminal` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._end_terminals, mrid) + return self.end_terminals.get_by_mrid(mrid) + @deprecated("Use obj.end_terminals.append(terminal) instead.") def add_end_terminal(self, terminal: Terminal) -> Circuit: - """ - Associate an `Terminal` with this `Circuit` - - `terminal` the `Terminal` to associate with this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if another `Terminal` with the same `mrid` already exists for this `Circuit`. - """ - if self._validate_reference(terminal, self.get_end_terminal, "An Terminal"): - return self - self._end_terminals = list() if self._end_terminals is None else self._end_terminals - self._end_terminals.append(terminal) + self.end_terminals.append(terminal) return self + @deprecated("Use obj.end_terminals.remove(terminal) instead.") def remove_end_terminal(self, terminal: Terminal) -> Circuit: - """ - Disassociate `terminal` from this `Circuit` - - `terminal` the `Terminal` to disassociate from this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if `terminal` was not associated with this `Circuit`. - """ - self._end_terminals = safe_remove(self._end_terminals, terminal) + self.end_terminals.remove(terminal) return self + @deprecated("Use obj.end_terminals.clear() instead.") def clear_end_terminals(self) -> Circuit: - """ - Clear all end terminals. - Returns A reference to this `Circuit` to allow fluent use. - """ - self._end_terminals = None + self.end_terminals.clear() return self + # endregion end_terminals boilerplate + + # region end_substations boilerplate + + @deprecated("Use len(obj.end_substations) instead.") def num_end_substations(self): - """Return the number of end `Substation`s associated with this `Circuit`""" - return nlen(self._end_substations) + return len(self.end_substations) + @deprecated("Use obj.end_substations.get_by_mrid(mrid) instead.") def get_end_substation(self, mrid: str) -> Substation: - """ - Get the `Substation` for this `Circuit` identified by `mrid` - - `mrid` the mRID of the required `Substation` - Returns The `Substation` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._end_substations, mrid) + return self.end_substations.get_by_mrid(mrid) + @deprecated("Use obj.end_substations.append(substation) instead.") def add_end_substation(self, substation: Substation) -> Circuit: - """ - Associate an `Substation` with this `Circuit` - - `substation` the `Substation` to associate with this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `Circuit`. - """ - if self._validate_reference(substation, self.get_end_substation, "An Substation"): - return self - self._end_substations = list() if self._end_substations is None else self._end_substations - self._end_substations.append(substation) + self.end_substations.append(substation) return self + @deprecated("Use obj.end_substations.remove(substation) instead.") def remove_end_substation(self, substation: Substation) -> Circuit: - """ - Disassociate `substation` from this `Circuit` - - `substation` the `Substation` to disassociate from this `Circuit`. - Returns A reference to this `Circuit` to allow fluent use. - Raises `ValueError` if `substation` was not associated with this `Circuit`. - """ - self._end_substations = safe_remove(self._end_substations, substation) + self.end_substations.remove(substation) return self + @deprecated("Use obj.end_substations.clear() instead.") def clear_end_substations(self) -> Circuit: - """ - Clear all end substations. - Returns A reference to this `Circuit` to allow fluent use. - """ - self._end_substations = None + self.end_substations.clear() return self + + # endregion end_substations boilerplate + + # endregion deprecated list boilerplate diff --git a/test/dataclass_descriptors/test_basic_descriptor.py b/test/dataclass_descriptors/test_basic_descriptor.py index 121a04dec..6941d9682 100644 --- a/test/dataclass_descriptors/test_basic_descriptor.py +++ b/test/dataclass_descriptors/test_basic_descriptor.py @@ -4,14 +4,14 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import field -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass, DataclassBase -from zepben.ewb.boilerplate.backed_descriptor import remove_descriptor_annotations, BackedDescriptor +from zepben.ewb.dataclass_descriptors.dataclass_base import DataclassBase +from zepben.ewb import zb_dataclass, remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.descriptor_fix import BackedDescriptor @zb_dataclass -@remove_descriptor_annotations class DescriptorTest(DataclassBase): - _x: int = field() + _x: int = field(default=0) x: int = BackedDescriptor(_x) diff --git a/test/dataclass_descriptors/test_dataclass_base.py b/test/dataclass_descriptors/test_dataclass_base.py index c6ae10269..f35c26ee5 100644 --- a/test/dataclass_descriptors/test_dataclass_base.py +++ b/test/dataclass_descriptors/test_dataclass_base.py @@ -7,13 +7,10 @@ import pytest -from zepben.ewb import Cut -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass, DataclassBase -from zepben.ewb.boilerplate.backed_descriptor import remove_descriptor_annotations +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass, DataclassBase @zb_dataclass -@remove_descriptor_annotations class Root(DataclassBase): mrid: str y: int @@ -26,15 +23,13 @@ def __init__(self, mrid, *_, **kwargs): self.mrid = mrid super(Root, self).__init__(**kwargs) - @zb_dataclass -@remove_descriptor_annotations class Child(Root): x: float = 42.0 z: str = "abc" dc_default: int = field(default=99) - dc_default_factory: List[int] = field(default_factory=lambda: [33]) + dc_default_factory: List[int] = field(default_factory=lambda : [33]) def test_dataclass_base(): @@ -48,8 +43,8 @@ def test_dataclass_base(): obj = Child(mrid, y=33, z="Hello there") # Memory layout correct # noinspection PyUnresolvedReferences - all_slots = set(obj.__slots__).union(set(Root.__slots__)) # Python 3.11+ stores parent slots only in parent - assert all_slots == {'mrid', 'y', 'x', 'z', 'dc_default', 'dc_default_factory'} + all_slots = set(obj.__slots__).union(set(Root.__slots__)) # Python 3.11+ stores parent slots only in parent + assert all_slots == {'mrid', 'y', 'x', 'z', 'dc_default', 'dc_default_factory'} # positional arg assert obj.mrid == mrid @@ -77,14 +72,3 @@ def test_dataclass_base(): other = Child("mrid2", y=42) other.dc_default_factory.append(24) assert obj.dc_default_factory == [33] - - -def test_identifiable_mrid(): - obj = Cut("it") - assert obj.mrid == "it" - - obj = Cut(mrid="it") - assert obj.mrid == "it" - - with pytest.raises(TypeError): - Cut("it", mrid="it") diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index 726cba3cc..8c943ad74 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -12,6 +12,7 @@ from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.services.network.network_service import NetworkService +from zepben.ewb import NetworkStateOperators, TestNetworkBuilder, NetworkTraceStep, Terminal, NominalPhasePath, Breaker, AcLineSegment, Clamp, Cut from zepben.ewb.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider From 4e8371a1b979b9d3226b99c73e045511b0b67087 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 20 Jul 2026 17:05:19 +1000 Subject: [PATCH 26/54] Backfill Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/mrid_list.py | 38 +++++--- src/zepben/ewb/boilerplate/mrid_map.py | 9 +- .../model/cim/iec61970/base/core/feeder.py | 2 +- .../iec61970/base/core/geographical_region.py | 39 +++------ .../iec61970/base/core/identified_object.py | 2 +- .../base/core/sub_geographical_region.py | 37 ++------ .../cim/iec61970/base/core/substation.py | 30 +++---- .../iec61970/base/diagramlayout/diagram.py | 21 +---- .../iec61970/base/wires/ac_line_segment.py | 86 +++++-------------- .../model/cim/iec61970/base/wires/clamp.py | 6 +- .../ewb/model/cim/iec61970/base/wires/cut.py | 5 +- .../iec61970/base/wires/energy_consumer.py | 33 ++----- .../cim/iec61970/base/wires/energy_source.py | 35 ++------ .../wires/power_electronics_connection.py | 43 +++------- .../power_electronics_connection_phase.py | 6 +- .../iec61970/base/wires/power_transformer.py | 71 +++++++-------- 16 files changed, 156 insertions(+), 307 deletions(-) diff --git a/src/zepben/ewb/boilerplate/mrid_list.py b/src/zepben/ewb/boilerplate/mrid_list.py index 2774ce025..178b19a6b 100644 --- a/src/zepben/ewb/boilerplate/mrid_list.py +++ b/src/zepben/ewb/boilerplate/mrid_list.py @@ -4,12 +4,13 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import abstractmethod, ABC from dataclasses import dataclass, field, Field +from types import MemberDescriptorType from typing import Any, TypeVar, Protocol import pytest from typing_extensions import Self -from zepben.ewb import remove_descriptor_annotations, BackedDescriptor +from zepben.ewb import remove_descriptor_annotations, BackedDescriptor, require from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList, MutableCollection, IndexedMutableCollection, _IterableWrapper @@ -47,14 +48,36 @@ def _can_add_by_mrid(self, element: S) -> bool: return False +class Backfill: + def __init__(self, backfill_prop: Any): + if not any(isinstance(backfill_prop, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): + raise TypeError(f"backfill_prop parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {backfill_prop}") + self.backfill_prop = backfill_prop + + def apply(self, element: S, owner: Any): + if isinstance(self.backfill_prop, property): + name = self.backfill_prop.fget.__name__ + else: + name = self.backfill_prop.__name__ + + if getattr(element, name) is None: + setattr(element, name, owner) + + ref = getattr(element, name) + if ref is not owner: + raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") + + class LazyMridList(LazyValidatedList, MridCollection[S]): def __init__(self, private_field, element_description: str, + backfill: Backfill = None, validate=None, sort_by=None): super().__init__(private_field, validate, sort_by) self.element_description = element_description + self.backfill = backfill def _safe_get_by_mrid(self, mrid: str) -> S | None: existing = self._get() @@ -67,6 +90,9 @@ def append(self, item: T): if not self._can_add_by_mrid(item): return + if self.backfill is not None: + self.backfill.apply(item, self.instance) + super().append(item) @@ -133,16 +159,6 @@ def __getitem__(self, item): return self.backing_list[item] -# -# class Backfill(Protocol[S]): -# def __init__(self, backref_field: Any): -# if not isinstance(backref_field, Field) and not isinstance(backref_field, BackedDescriptor): -# raise TypeError(f"backref_field parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {backref_field}") -# self.backref_field = backref_field -# -# def apply(self, element: S, owner: Any): -# setattr(element, self.backref_field.__name__, ) - diff --git a/src/zepben/ewb/boilerplate/mrid_map.py b/src/zepben/ewb/boilerplate/mrid_map.py index c74c40fa6..0f71c474c 100644 --- a/src/zepben/ewb/boilerplate/mrid_map.py +++ b/src/zepben/ewb/boilerplate/mrid_map.py @@ -9,7 +9,7 @@ from zepben.ewb import remove_descriptor_annotations from zepben.ewb.dataclass_descriptors.lazy_list import _IterableWrapper -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, Backfill class HasMrid(Protocol): @@ -24,10 +24,12 @@ def __init__( self, private_field, element_description: str, + backfill: Backfill = None, validate=None): super().__init__(private_field) - self.validate = validate self.element_description = element_description + self.backfill = backfill + self.validate = validate def _get(self): return getattr(self.instance, self.backing_name) @@ -59,6 +61,9 @@ def append(self, element: S): if not self._can_add_by_mrid(element): return + if self.backfill is not None: + self.backfill.apply(element, self.instance) + if self.validate is not None: self.validate(self.instance, element) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 88e28b34c..83a62f353 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -38,7 +38,7 @@ class Feeder(EquipmentContainer): _normal_head_terminal: Terminal | None = None """The normal head terminal or terminals of the feeder.""" - normal_energizing_substation: Substation | None = None + normal_energizing_substation: Substation | None = field(default=None) """The substation that normally energizes the feeder. Also used for naming purposes.""" _current_equipment_by_id: Dict[str, Equipment] | None = field(default=None) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index ac5448eb2..923ca240e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -7,16 +7,15 @@ __all__ = ["GeographicalRegion"] -from typing import Optional, List, Generator from dataclasses import field +from typing import Optional, List + from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList @zb_dataclass @@ -36,33 +35,10 @@ def __init__(self, *args, sub_geographical_regions: List[SubGeographicalRegion] sub_geographical_regions: MridCollection[SubGeographicalRegion] = LazyMridList( _sub_geographical_regions, "A SubGeographicalRegion", + backfill=Backfill(SubGeographicalRegion.geographical_region) ) - def add_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: - """ - Associate a `SubGeographicalRegion` with this `GeographicalRegion` - - `sub_geographical_region` The `SubGeographicalRegion` to associate with this `GeographicalRegion`. - Returns A reference to this `GeographicalRegion` to allow fluent use. - Raises `ValueError` if another `SubGeographicalRegion` with the same `mrid` already exists for this `GeographicalRegion`, or if - `sub_geographical_region.geographical_region` is not this `GeographicalRegion`. - """ - if self._validate_reference(sub_geographical_region, self.get_sub_geographical_region, "A SubGeographicalRegion"): - return self - - if sub_geographical_region.geographical_region is None: - sub_geographical_region.geographical_region = self - - require(sub_geographical_region.geographical_region is self, lambda: f"{sub_geographical_region} `geographical_region` property references " + - f"{sub_geographical_region.geographical_region}, expected {self}.") - - self._sub_geographical_regions = list() if self._sub_geographical_regions is None else self._sub_geographical_regions - self._sub_geographical_regions.append(sub_geographical_region) - return self - - - # region deprecated list boilerplate # region sub_geographical_regions boilerplate @@ -74,6 +50,11 @@ def num_sub_geographical_regions(self) -> int: def get_sub_geographical_region(self, mrid: str) -> SubGeographicalRegion: return self.sub_geographical_regions.get_by_mrid(mrid) + @deprecated("Use obj.sub_geographical_regions.append(sub_geographical_region) instead.") + def add_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: + self.sub_geographical_regions.append(sub_geographical_region) + return self + @deprecated("Use obj.sub_geographical_regions.remove(sub_geographical_region) instead.") def remove_sub_geographical_region(self, sub_geographical_region: SubGeographicalRegion) -> GeographicalRegion: self.sub_geographical_regions.remove(sub_geographical_region) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py b/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py index 4c9b6c0a2..ef9b28821 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/identified_object.py @@ -62,7 +62,7 @@ def __init__(self, mrid: str, *args, names: Optional[List[Name]] = None, **kwarg def __str__(self): class_name = f'{self.__class__.__name__}' - if self.name: + if getattr(self, "name", None): return f'{class_name}{{{self.mrid}|{self.name}}}' return f'{class_name}{{{self.mrid}}}' diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index a1b7fd736..e401267ce 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -15,11 +15,11 @@ from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.model.cim.iec61970.base.core.substation import Substation if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.geographical_region import GeographicalRegion - from zepben.ewb.model.cim.iec61970.base.core.substation import Substation @zb_dataclass @@ -28,7 +28,7 @@ class SubGeographicalRegion(IdentifiedObject): A subset of a geographical region of a power system network model. """ - geographical_region: Optional[GeographicalRegion] = None + geographical_region: Optional[GeographicalRegion] = field(default=None) """The geographical region to which this sub-geographical region is within.""" _substations: Optional[List[Substation]] = field(default=None) @@ -59,34 +59,10 @@ def num_substations(self) -> int: substations: MridCollection[Substation] = LazyMridList( _substations, "A Substation", + backfill=Backfill(Substation.sub_geographical_region) ) - def add_substation(self, substation: Substation) -> SubGeographicalRegion: - """ - Associate a `Substation` with this `GeographicalRegion` - - `substation` the `Substation` to associate with this `SubGeographicalRegion`. - - Returns A reference to this `SubGeographicalRegion` to allow fluent use. - - Raises `ValueError` if another `Substation` with the same `mrid` already exists for this `SubGeographicalRegion`, or if - `substation.sub_geographical_region` is not this `SubGeographicalRegion`. - """ - if self._validate_reference(substation, self.get_substation, "A Substation"): - return self - - if substation.sub_geographical_region is None: - substation.sub_geographical_region = self - - require(substation.sub_geographical_region is self, lambda: f"{substation} `sub_geographical_region` property references {substation.sub_geographical_region}, expected {self}.") - - self._substations = list() if self._substations is None else self._substations - self._substations.append(substation) - return self - - - # region deprecated list boilerplate # region substations boilerplate @@ -98,6 +74,11 @@ def num_substations(self) -> int: def get_substation(self, mrid: str) -> Substation: return self.substations.get_by_mrid(mrid) + @deprecated("Use obj.substations.append(substation) instead.") + def add_substation(self, substation: Substation) -> SubGeographicalRegion: + self.substations.append(substation) + return self + @deprecated("Use obj.substations.remove(substation) instead.") def remove_substation(self, substation: Substation) -> SubGeographicalRegion: self.substations.remove(substation) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index 41fead9ba..d34be25fb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -15,11 +15,11 @@ from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop - from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion from zepben.ewb.model.cim.iec61970.infiec61970.feeder.circuit import Circuit @@ -31,7 +31,8 @@ class Substation(EquipmentContainer): is passed for the purposes of switching or modifying its characteristics. """ - _sub_geographical_region: Optional[SubGeographicalRegion] = None + sub_geographical_region: Optional[SubGeographicalRegion] = field(default=None) + """The SubGeographicalRegion containing the substation.""" _normal_energized_feeders: Optional[List[Feeder]] = field(default=None) @@ -76,27 +77,11 @@ def circuits(self) -> Generator[Circuit, None, None]: feeders: MridCollection[Feeder] = LazyMridList( _normal_energized_feeders, "A Feeder", + backfill=Backfill(Feeder.normal_energizing_substation) ) normal_energized_feeders = Alias(feeders) - - def add_circuit(self, circuit: Circuit) -> Substation: - """ - Associate a `Circuit` with this `Substation` - - `circuit` The `Circuit` to associate with this `Substation`. - Returns A reference to this `Substation` to allow fluent use. - Raises `ValueError` if another `Circuit` with the same `mrid` already exists for this `Substation`. - """ - if self._validate_reference(circuit, self.get_circuit, "A Circuit"): - return self - self._circuits = list() if self._circuits is None else self._circuits - self._circuits.append(circuit) - return self - - - # region deprecated list boilerplate # region circuits boilerplate @@ -189,6 +174,11 @@ def num_feeders(self): def get_feeder(self, mrid: str) -> Feeder: return self.feeders.get_by_mrid(mrid) + @deprecated("Use obj.feeders.append(feeder) instead.") + def add_feeder(self, feeder: Feeder) -> Substation: + self.feeders.append(feeder) + return self + @deprecated("Use obj.feeders.remove(feeder) instead.") def remove_feeder(self, feeder: Feeder) -> Substation: self.feeders.remove(feeder) diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index 8eba60fbe..0767d2244 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -8,21 +8,17 @@ __all__ = ["Diagram"] from dataclasses import field -from typing import Optional, Dict, List, TYPE_CHECKING +from typing import Dict from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, Backfill from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject +from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_style import DiagramStyle from zepben.ewb.model.cim.iec61970.base.diagramlayout.orientation_kind import OrientationKind -from zepben.ewb.util import require - -if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject @zb_dataclass @@ -43,19 +39,10 @@ class Diagram(IdentifiedObject): diagram_objects: MridCollection[DiagramObject] = LazyMridMap( _diagram_objects, "A DiagramObject", - validate=lambda self, it: self._validate_diagram_object(it) + backfill=Backfill(DiagramObject.diagram) ) """The diagram objects belonging to this diagram.""" - def _validate_diagram_object(self, diagram_object: DiagramObject) -> None: - """ - Fill and validate the `diagram` backref for the `diagram_object` - """ - if not diagram_object.diagram: - diagram_object.diagram = self - require(diagram_object.diagram is self, lambda: f"{str(diagram_object)} `diagram` property references " - f"{str(diagram_object.diagram)}, expected {str(self)}.") - # region deprecated list boilerplate # region cuts boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index 9c6c9d284..b0a5eae4a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -7,23 +7,20 @@ __all__ = ["AcLineSegment"] -from typing import Optional, Generator, TYPE_CHECKING from dataclasses import field +from typing import Optional, TYPE_CHECKING + from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase +from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp from zepben.ewb.model.cim.iec61970.base.wires.conductor import Conductor +from zepben.ewb.model.cim.iec61970.base.wires.cut import Cut from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo - from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp - from zepben.ewb.model.cim.iec61970.base.wires.cut import Cut - from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.per_length_phase_impedance import PerLengthPhaseImpedance from zepben.ewb.model.cim.iec61970.base.wires.per_length_sequence_impedance import PerLengthSequenceImpedance @@ -59,9 +56,9 @@ class AcLineSegment(Conductor): per_length_impedance: 'PerLengthImpedance | None' = None """A `zepben.ewb.model.cim.iec61970.base.wires.PerLengthImpedance` describing this AcLineSegment""" - _cuts: list['Cut'] | None = field(default=None) - _clamps: list['Clamp'] | None = field(default=None) - _phases: list['AcLineSegmentPhase'] | None = field(default=None) + _cuts: list[Cut] | None = field(default=None) + _clamps: list[Clamp] | None = field(default=None) + _phases: list[AcLineSegmentPhase] | None = field(default=None) @property def per_length_sequence_impedance(self) -> Optional['PerLengthSequenceImpedance']: @@ -96,69 +93,26 @@ def per_length_phase_impedance(self, value: Optional['PerLengthPhaseImpedance']) self.per_length_impedance = value - cuts: MridCollection['Cut'] = LazyMridList( + cuts: MridCollection[Cut] = LazyMridList( _cuts, "A Cut", - validate=lambda self, it: self._validate_cut(it) + backfill=Backfill(Cut.ac_line_segment) ) - def _validate_cut(self, cut: 'Cut') -> None: - """ - Validate a cut against this `AcLineSegment`'s `Cut`s. - - :param cut: The `Cut` to validate. - :raise ValueError: If `cut.ac_line_segment` is not this `AcLineSegment` - """ - if not cut.ac_line_segment: - cut.ac_line_segment = self - - require( - cut.ac_line_segment is self, - lambda: f"{cut} `ac_line_segment` property references {cut.ac_line_segment}, expected {str(self)}.", - ) - - clamps: MridCollection['Clamp'] = LazyMridList( + clamps: MridCollection[Clamp] = LazyMridList( _clamps, "A Clamp", - validate=lambda self, it: self._validate_clamp(it) + backfill=Backfill(Clamp.ac_line_segment) ) - def _validate_clamp(self, clamp: 'Clamp') -> None: - """ - Validate a clamp against this `AcLineSegment`'s `Clamp`s. - - :param clamp: The `Clamp` to validate. - :raise ValueError: If `clamp.ac_line_segment` is not this `AcLineSegment` - """ - if not clamp.ac_line_segment: - clamp.ac_line_segment = self - - require( - clamp.ac_line_segment is self, - lambda: f"{clamp} `ac_line_segment` property references {clamp.ac_line_segment}, expected {str(self)}.", - ) - - phases: AcLineSegmentPhaseList = AcLineSegmentPhaseList( _phases, "An AcLineSegmentPhase", - validate=lambda self, it: self._validate_phase(it), + backfill=Backfill(AcLineSegmentPhase.ac_line_segment), sort_by=lambda it: it.sequence_number or 0 ) - def _validate_phase(self, phase: AcLineSegmentPhase) -> None: - """ - Validate a phase against this `AcLineSegment`'s `Phase`s. - - :param phase: The `Phase` to validate. - :raise ValueError: If `phase.ac_line_segment` is not this `AcLineSegment` - """ - if phase.ac_line_segment is None: - phase.ac_line_segment = self - - require(phase.ac_line_segment is self, lambda: f"{phase} `ac_line_segment` property references {phase.ac_line_segment}, expected {self}.") - def wire_info_for_phase(self, phase: SinglePhaseKind) -> 'WireInfo | None': """ @@ -182,16 +136,16 @@ def num_cuts(self): return len(self.cuts) @deprecated("Use obj.cuts.get_by_mrid(mrid) instead.") - def get_cut(self, mrid: str) -> 'Cut': + def get_cut(self, mrid: str) -> Cut: return self.cuts.get_by_mrid(mrid) @deprecated("Use obj.cuts.append(cut) instead.") - def add_cut(self, cut: 'Cut') -> 'AcLineSegment': + def add_cut(self, cut: Cut) -> 'AcLineSegment': self.cuts.append(cut) return self @deprecated("Use obj.cuts.remove(cut) instead.") - def remove_cut(self, cut: 'Cut') -> 'AcLineSegment': + def remove_cut(self, cut: Cut) -> 'AcLineSegment': self.cuts.remove(cut) return self @@ -209,16 +163,16 @@ def num_clamps(self): return len(self.clamps) @deprecated("Use obj.clamps.get_by_mrid(mrid) instead.") - def get_clamp(self, mrid: str) -> 'Clamp': + def get_clamp(self, mrid: str) -> Clamp: return self.clamps.get_by_mrid(mrid) @deprecated("Use obj.clamps.append(clamp) instead.") - def add_clamp(self, clamp: 'Clamp') -> 'AcLineSegment': + def add_clamp(self, clamp: Clamp) -> 'AcLineSegment': self.clamps.append(clamp) return self @deprecated("Use obj.clamps.remove(clamp) instead.") - def remove_clamp(self, clamp: 'Clamp') -> 'AcLineSegment': + def remove_clamp(self, clamp: Clamp) -> 'AcLineSegment': self.clamps.remove(clamp) return self diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index e7067475a..355dd6288 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -5,10 +5,9 @@ __all__ = ["Clamp"] +from dataclasses import field from typing import Optional, TYPE_CHECKING -from typing_extensions import deprecated - from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -26,7 +25,8 @@ class Clamp(ConductingEquipment): length_from_terminal_1: Optional[float] = None """The length to the place where the clamp is located starting from side one of the line segment, i.e. the line segment terminal with sequence number equal to 1.""" - _ac_line_segment: Optional['AcLineSegment'] = None + ac_line_segment: Optional['AcLineSegment'] = field(default=None) + """The line segment to which the clamp is connected.""" max_terminals = 1 diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index 19d9f08bf..d8dc18b72 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -5,10 +5,9 @@ __all__ = ["Cut"] +from dataclasses import field from typing import Optional, TYPE_CHECKING -from typing_extensions import deprecated - from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -32,6 +31,8 @@ class Cut(Switch): length_from_terminal_1: Optional[float] = None """The length to the place where the cut is located starting from side one of the cut line segment, i.e. the line segment Terminal with sequenceNumber equal to 1.""" + ac_line_segment: Optional['AcLineSegment'] = field(default=None) + """The line segment to which the cut is applied.""" _ac_line_segment: Optional['AcLineSegment'] = None @property diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index be5dfb6db..6b99da983 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -8,20 +8,16 @@ __all__ = ["EnergyConsumer"] from dataclasses import field -from typing import Optional, List, TYPE_CHECKING +from typing import Optional, List from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations, Alias +from zepben.ewb import Alias from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection +from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase from zepben.ewb.model.cim.iec61970.base.wires.phase_shunt_connection_kind import PhaseShuntConnectionKind -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass - -if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase @zb_dataclass @@ -58,6 +54,7 @@ class EnergyConsumer(EnergyConnection): phases: MridCollection[EnergyConsumerPhase] = LazyMridList( _energy_consumer_phases, "An EnergyConsumerPhase", + backfill=Backfill(EnergyConsumerPhase.energy_consumer) ) # TODO: Remove hack # Old inits used the wrong name. This allows backwards compatibility @@ -75,25 +72,9 @@ def num_phases(self): def get_phase(self, mrid: str) -> EnergyConsumerPhase: return self.phases.get_by_mrid(mrid) + @deprecated("Use obj.phases.append(phase) instead.") def add_phase(self, phase: EnergyConsumerPhase) -> EnergyConsumer: - """ - Associate an `EnergyConsumerPhase` with this `EnergyConsumer` - - `phase` the `EnergyConsumerPhase` to associate with this `EnergyConsumer`. - Returns A reference to this `EnergyConsumer` to allow fluent use. - Raises `ValueError` if another `EnergyConsumerPhase` with the same `mrid` already exists for this `EnergyConsumer`, or if `phase.energy_consumer` is not - this `EnergyConsumer`. - """ - if self._validate_reference(phase, self.get_phase, "An EnergyConsumerPhase"): - return self - - if phase.energy_consumer is None: - phase.energy_consumer = self - - require(phase.energy_consumer is self, lambda: f"{phase} `energy_consumer` property references {phase.energy_consumer}, expected {self}.") - - self._energy_consumer_phases = list() if self._energy_consumer_phases is None else self._energy_consumer_phases - self._energy_consumer_phases.append(phase) + self.phases.append(phase) return self @deprecated("Use obj.phases.remove(phase) instead.") diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index 48d1d0fc5..47c5befbb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -7,18 +7,16 @@ __all__ = ["EnergySource"] -from typing import List, Optional, Generator, TYPE_CHECKING from dataclasses import field +from typing import List, Optional + from typing_extensions import deprecated -from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require +from zepben.ewb import Alias from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList - -if TYPE_CHECKING: - from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection +from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase @zb_dataclass @@ -120,6 +118,7 @@ class EnergySource(EnergyConnection): phases: MridCollection[EnergySourcePhase] = LazyMridList( _energy_source_phases, "An EnergySourcePhase", + backfill=Backfill(EnergySourcePhase.energy_source) ) energy_source_phases = Alias(phases) @@ -135,25 +134,9 @@ def num_phases(self): def get_phase(self, mrid: str) -> EnergySourcePhase: return self.phases.get_by_mrid(mrid) + @deprecated("Use obj.phases.append(phase) instead.") def add_phase(self, phase: EnergySourcePhase) -> EnergySource: - """ - Associate an `EnergySourcePhase` with this `EnergySource` - - `phase` the `EnergySourcePhase` to associate with this `EnergySource`. - Returns A reference to this `EnergySource` to allow fluent use. - Raises `ValueError` if another `EnergySourcePhase` with the same `mrid` already exists for this `EnergySource`, or if `phase.energy_source` is not - this `EnergySource`. - """ - if self._validate_reference(phase, self.get_phase, "An EnergySourcePhase"): - return self - - if phase.energy_source is None: - phase.energy_source = self - - require(phase.energy_source is self, lambda: f"{phase} `energy_source` property references {phase.energy_source}, expected {self}.") - - self._energy_source_phases = list() if self._energy_source_phases is None else self._energy_source_phases - self._energy_source_phases.append(phase) + self.phases.append(phase) return self @deprecated("Use obj.phases.remove(phase) instead.") diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 5649efe89..5e7cf25b2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -7,18 +7,19 @@ __all__ = ["PowerElectronicsConnection"] -from typing import Optional, List, Generator, TYPE_CHECKING from dataclasses import field +from typing import Optional, List, TYPE_CHECKING + from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList -from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq -from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase +from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq +from zepben.ewb.util import require if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit - from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase @zb_dataclass @@ -426,38 +427,13 @@ def inv_var_resp_q_at_v4(self, value): phases: MridCollection[PowerElectronicsConnectionPhase] = LazyMridList( _power_electronics_connection_phases, "A PowerElectronicsConnectionPhase", + backfill=Backfill(PowerElectronicsConnectionPhase.power_electronics_connection) ) - - - - def add_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: - """ - Associate a `PowerElectronicsConnectionPhase` with this `PowerElectronicsConnection` - - `phase` the `PowerElectronicsConnectionPhase` to associate with this `PowerElectronicsConnection`. - Returns A reference to this `PowerElectronicsConnection` to allow fluent use. - Raises `ValueError` if another `PowerElectronicsConnectionPhase` with the same `mrid` already exists for this `PowerElectronicsConnection`, or if - `phase.power_electronics_connection` is not this `PowerElectronicsConnection`. - """ - if self._validate_reference(phase, self.get_phase, "A PowerElectronicsConnectionPhase"): - return self - - if phase.power_electronics_connection is None: - phase.power_electronics_connection = self - - require(phase.power_electronics_connection is self, lambda: f"{phase} `power_electronics_connection` property references {phase.power_electronics_connection}, expected {self}.") - - self._power_electronics_connection_phases = list() if self._power_electronics_connection_phases is None else self._power_electronics_connection_phases - self._power_electronics_connection_phases.append(phase) - return self - - - # region deprecated list boilerplate # region units boilerplate @@ -496,6 +472,11 @@ def num_phases(self): def get_phase(self, mrid: str) -> PowerElectronicsConnectionPhase: return self.phases.get_by_mrid(mrid) + @deprecated("Use obj.phases.append(phase) instead.") + def add_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: + self.phases.append(phase) + return self + @deprecated("Use obj.phases.remove(phase) instead.") def remove_phase(self, phase: PowerElectronicsConnectionPhase) -> PowerElectronicsConnection: self.phases.remove(phase) diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py index 3345c6914..ef5f3d102 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py @@ -5,10 +5,9 @@ __all__ = ["PowerElectronicsConnectionPhase"] +from dataclasses import field from typing import Optional, TYPE_CHECKING -from typing_extensions import deprecated - from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -21,7 +20,8 @@ class PowerElectronicsConnectionPhase(PowerSystemResource): """A single phase of a power electronics connection.""" - _power_electronics_connection: Optional['PowerElectronicsConnection'] = None + power_electronics_connection: Optional['PowerElectronicsConnection'] = None + """The power electronics connection to which the phase belongs.""" p: Optional[float] = None """Active power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index ca4215c3c..390da1060 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -11,8 +11,7 @@ from dataclasses import field from typing import List, Optional, TYPE_CHECKING -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList +from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill if sys.version_info >= (3, 13): from warnings import deprecated @@ -24,12 +23,11 @@ from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_construction_kind import TransformerConstructionKind from zepben.ewb.model.cim.iec61968.infiec61968.infassetinfo.transformer_function_kind import TransformerFunctionKind from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment -from zepben.ewb.util import require +from zepben.ewb.model.cim.iec61970.base.wires.power_transformer_end import PowerTransformerEnd if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal - from zepben.ewb.model.cim.iec61970.base.wires.power_transformer_end import PowerTransformerEnd class PowerTransformerEndList(LazyMridList['PowerTransformerEnd']): @@ -132,13 +130,16 @@ def __init__(self, *args, power_transformer_ends: List[PowerTransformerEnd] = No ends: PowerTransformerEndList = PowerTransformerEndList( _power_transformer_ends, "A PowerTransformerEnd", + backfill=Backfill(PowerTransformerEnd.power_transformer), validate=lambda self, it: self._validate_end(it), sort_by=lambda it: it.end_number ) - @deprecated("...") - def num_ends(self): - return len(self.ends) + def _validate_end(self, end: PowerTransformerEnd): + self._validate_reference_by_field(end, end.end_number, self.ends.get_by_num, "end_number") + + if end.end_number == 0: + end.end_number = self.num_ends() + 1 @property @@ -165,59 +166,47 @@ def get_base_voltage(self, terminal: Terminal = None): else: return None - @deprecated("...") + + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. + + # ends boilerplate + + @deprecated("Use `len(power_transformer.ends)` instead.") + def num_ends(self): + return len(self.ends) + + @deprecated("Use `power_transformer.ends.get_by_mrid(mrid)` instead.") def get_end_by_mrid(self, mrid: str) -> PowerTransformerEnd: return self.ends.get_by_mrid(mrid) - @deprecated("...") + @deprecated("Use `power_transformer.ends.get_by_num(end_number)` instead.") def get_end_by_num(self, end_number: int) -> PowerTransformerEnd: return self.ends.get_by_num(end_number) - @deprecated("...") + @deprecated("Use `power_transformer.ends.get_by_terminal(terminal)` instead.") def get_end_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: return self.ends.get_by_terminal(terminal) - @deprecated("...") + @deprecated("Use `power_transformer.ends.append(end)` instead.") def add_end(self, end: PowerTransformerEnd) -> PowerTransformer: - """ - Associate a `PowerTransformerEnd` with this `PowerTransformer`. If `end.end_number` == 0, the end will be assigned an end_number of - `self.num_ends() + 1`. - - `end` the `PowerTransformerEnd` to associate with this `PowerTransformer`. - Returns A reference to this `PowerTransformer` to allow fluent use. - Raises `ValueError` if another `PowerTransformerEnd` with the same `mrid` already exists for this `PowerTransformer`. - """ self.ends.append(end) return self - @deprecated("...") + @deprecated("Use `power_transformer.ends.remove(end)` instead.") def remove_end(self, end: PowerTransformerEnd) -> PowerTransformer: self.ends.remove(end) return self - @deprecated("...") + @deprecated("Use `power_transformer.ends.clear()` instead.") def clear_ends(self) -> PowerTransformer: self.ends.clear() return self - def _validate_end(self, end: PowerTransformerEnd): - """ - Validate an end against this `PowerTransformer`'s `PowerTransformerEnd`s. + # endregion - `end` The `PowerTransformerEnd` to validate. - Returns True if `end` is already associated with this `PowerTransformer`, otherwise False. - Raises `ValueError` if `end.power_transformer` is not this `PowerTransformer`, or if this `PowerTransformer` has a different `PowerTransformerEnd` - with the same mRID. - """ - self._validate_reference_by_field(end, end.end_number, self.get_end_by_num, "end_number") - - if not end.power_transformer: - end.power_transformer = self - - require( - end.power_transformer is self, - lambda: f"{end} `power_transformer` property references {end.power_transformer}, expected {str(self)}.", - ) - - if end.end_number == 0: - end.end_number = self.num_ends() + 1 + # endregion From aa82a10299045c4583b4975027e6b32386dbbacc Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Tue, 21 Jul 2026 14:04:02 +1000 Subject: [PATCH 27/54] Backfill progress Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/lazy_list.py | 1 + src/zepben/ewb/boilerplate/mrid_list.py | 23 ++++++++++++---- src/zepben/ewb/boilerplate/tst.py | 5 ++++ .../base/core/sub_geographical_region.py | 26 ++++++++++++++----- .../iec61970/base/wires/power_transformer.py | 1 + 5 files changed, 44 insertions(+), 12 deletions(-) diff --git a/src/zepben/ewb/boilerplate/lazy_list.py b/src/zepben/ewb/boilerplate/lazy_list.py index c4980e993..43b19ca5e 100644 --- a/src/zepben/ewb/boilerplate/lazy_list.py +++ b/src/zepben/ewb/boilerplate/lazy_list.py @@ -173,6 +173,7 @@ def __getitem__(self, item): return (self._get_or_empty())[item] + if __name__ == '__main__': @zb_dataclass class C(DataclassBase): diff --git a/src/zepben/ewb/boilerplate/mrid_list.py b/src/zepben/ewb/boilerplate/mrid_list.py index 178b19a6b..3b31802c0 100644 --- a/src/zepben/ewb/boilerplate/mrid_list.py +++ b/src/zepben/ewb/boilerplate/mrid_list.py @@ -5,7 +5,7 @@ from abc import abstractmethod, ABC from dataclasses import dataclass, field, Field from types import MemberDescriptorType -from typing import Any, TypeVar, Protocol +from typing import Any, TypeVar, Protocol, Callable import pytest from typing_extensions import Self @@ -59,14 +59,27 @@ def apply(self, element: S, owner: Any): name = self.backfill_prop.fget.__name__ else: name = self.backfill_prop.__name__ - - if getattr(element, name) is None: - setattr(element, name, owner) - ref = getattr(element, name) + if hasattr(self.backfill_prop, "__target"): + backing_name = self.backfill_prop.__target.__name__ + else: + backing_name = name + + if getattr(element, backing_name) is None: + setattr(element, backing_name, owner) + + ref = getattr(element, backing_name) if ref is not owner: raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") +def targets(target: Any): + if not any(isinstance(target, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): + raise TypeError(f"target parameter of the target decorator has to be an instance of dataclass Field, instead is {target}") + + def dec(func: Callable): + setattr(func, "__target", target) + return func + return dec class LazyMridList(LazyValidatedList, MridCollection[S]): def __init__(self, diff --git a/src/zepben/ewb/boilerplate/tst.py b/src/zepben/ewb/boilerplate/tst.py index 55ed4ce86..b0e8fa5e9 100644 --- a/src/zepben/ewb/boilerplate/tst.py +++ b/src/zepben/ewb/boilerplate/tst.py @@ -130,6 +130,9 @@ class C: _x: list[int] | None = field(default=None) x: ListWrapper = ListWrapper(_x, sort_by=lambda it: -it) + @property + def thing(self): + return 42 if __name__ == '__main__': c = C([1, 2, 3]) @@ -141,6 +144,8 @@ class C: print(c) print(c.x) + c.thing = 24 + # class C: # def __init__(self, x, f1, f2): # self.x = x diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index e401267ce..6b1783a9c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -7,15 +7,14 @@ __all__ = ["SubGeographicalRegion"] -from typing import Optional, List, Generator, TYPE_CHECKING from dataclasses import field +from typing import Optional, List, TYPE_CHECKING + from typing_extensions import deprecated -from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, targets +from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.substation import Substation if TYPE_CHECKING: @@ -28,8 +27,7 @@ class SubGeographicalRegion(IdentifiedObject): A subset of a geographical region of a power system network model. """ - geographical_region: Optional[GeographicalRegion] = field(default=None) - """The geographical region to which this sub-geographical region is within.""" + _geographical_region: Optional[GeographicalRegion] = field(default=None) _substations: Optional[List[Substation]] = field(default=None) @@ -39,6 +37,16 @@ def __init__(self, *args, substations: List[Substation] = None, **kwargs): for sub in substations: self.add_substation(sub) + @property + @targets(_geographical_region) + def geographical_region(self): + """The geographical region to which this sub-geographical region is within.""" + return self._geographical_region + + @geographical_region.setter + @deprecated("Geographical region is a backfill property - it should only be set by adding the sub region to the sub regions list") + def geographical_region(self, value): + self._geographical_region = value @property def geographical_region(self): @@ -92,3 +100,7 @@ def clear_substations(self) -> SubGeographicalRegion: # endregion substations boilerplate # endregion deprecated list boilerplate + +if __name__ == '__main__': + s = SubGeographicalRegion("") + s.geographical_region = GeographicalRegion("") \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index 390da1060..c1c839b9f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -11,6 +11,7 @@ from dataclasses import field from typing import List, Optional, TYPE_CHECKING +from zepben.ewb import remove_descriptor_annotations from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill if sys.version_info >= (3, 13): From 281b67661c55182fdc5f2ac3b510392d2ff7e96e Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Tue, 21 Jul 2026 16:15:26 +1000 Subject: [PATCH 28/54] Indexed lists migration Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/lazy_list.py | 160 ++++++++- .../iec61968/assetinfo/relay_info.py | 124 +++---- .../protection/protection_relay_function.py | 336 +++++------------- .../ewb/model/cim/iec61968/common/location.py | 161 ++++----- .../base/diagramlayout/diagram_object.py | 154 ++++---- 5 files changed, 418 insertions(+), 517 deletions(-) diff --git a/src/zepben/ewb/boilerplate/lazy_list.py b/src/zepben/ewb/boilerplate/lazy_list.py index 43b19ca5e..7969c2c1f 100644 --- a/src/zepben/ewb/boilerplate/lazy_list.py +++ b/src/zepben/ewb/boilerplate/lazy_list.py @@ -2,10 +2,10 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from abc import ABC +from abc import ABC, abstractmethod from collections.abc import Collection from dataclasses import field -from typing import TypeVar, Protocol, Any, Iterable +from typing import TypeVar, Protocol, Any, Iterable, Generic, Iterator, Sequence, overload, Callable from typing_extensions import deprecated, Self @@ -112,7 +112,74 @@ def __set__(self, instance, value): else: self.__get__(instance).clear() -class LazyValidatedList(_IterableWrapper, IndexedMutableCollection[T]): + +class AbstractBackedCollection(Collection[T], Generic[T], ABC): + + @abstractmethod + def _get_collection(self) -> Collection[T]: + ... + + @abstractmethod + def append(self, element: T) -> None: + ... + + def extend(self, elements: Iterable[T]) -> None: + for element in elements: + self.append(element) + + @abstractmethod + def remove(self, element: T) -> None: + ... + + @abstractmethod + def clear(self) -> None: + ... + + def __len__(self) -> int: + return len(self._get_collection()) + + def __iter__(self) -> Iterator[T]: + return iter(self._get_collection()) + + def __contains__(self, element: object) -> bool: + return element in self._get_collection() + + def for_each_indexed(self, action: Callable[[int, T], Any]): + """ + Call the `action` on each item in the list + + :param action: An action to apply to each :class:`RelaySetting` in the `thresholds` collection, taking the index of the threshold, and the threshold itself. + """ + for index, item in enumerate(self._get_collection()): + action(index, item) + + + +class AbstractBackedList( + AbstractBackedCollection[T], + Sequence[T], + Generic[T], + ABC, +): + + @abstractmethod + def _get_collection(self) -> Sequence[T]: + ... + + @overload + def __getitem__(self, index: int) -> T: + ... + + @overload + def __getitem__(self, index: slice) -> Sequence[T]: + ... + + def __getitem__(self, index: int | slice) -> T | Sequence[T]: + return self._get_collection()[index] + + +class LazyValidatedList(_IterableWrapper, AbstractBackedList[T]): + def __init__(self, private_field, validate=None, @@ -124,7 +191,7 @@ def __init__(self, def _get(self): return getattr(self.instance, self.backing_name) - def _get_or_empty(self) -> list[T]: + def _get_collection(self) -> list[T]: return getattr(self.instance, self.backing_name) or [] # clearIfEmpty is inlined for performance @@ -144,16 +211,16 @@ def append(self, item): existing.sort(key=self.sort_by) def __len__(self): - return len(self._get_or_empty()) + return len(self._get_collection()) def __iter__(self): - return iter(self._get_or_empty()) + return iter(self._get_collection()) def __contains__(self, item): - return item in (self._get_or_empty()) + return item in (self._get_collection()) def remove(self, item): - existing = self._get_or_empty() + existing = self._get_collection() existing.remove(item) if not existing: self.clear() @@ -164,31 +231,92 @@ def clear(self): def __repr__(self): if self.instance is None: return self.__class__.__repr__() - return str(self._get_or_empty()) + return str(self._get_collection()) def __str__(self): return self.__repr__() def __getitem__(self, item): - return (self._get_or_empty())[item] + return (self._get_collection())[item] +class LazyIndexedList(LazyValidatedList[T]): + + def __init__( + self, + private_field, + element_description: str + ): + super().__init__(private_field) + self.element_description = element_description + + def insert(self, index: int, item: T) -> None: + """ + Insert ``item`` at ``index``. + """ + size = len(self) + + if not 0 <= index <= size: + raise ValueError( + f"Unable to add {self.element_description} to " + f"{self.instance}. " + f"Sequence number {index} is invalid. " + f"Expected a value between 0 and {size}. " + "Make sure you are adding the items in order and there are " + "no gaps in the numbering." + ) + + existing = getattr(self.instance, self.backing_name) + + if existing is None: + existing = [item] + setattr(self.instance, self.backing_name, existing) + else: + existing.insert(index, item) + + if self.sort_by is not None: + existing.sort(key=self.sort_by) + + def append(self, item: T) -> None: + self.insert(len(self), item) + + def pop(self, index: int = -1) -> T: + """ + Remove and return the item at ``index``. + + Uses normal Python list semantics, including support for negative + indexes and raising ``IndexError`` when the index is invalid. + """ + existing = getattr(self.instance, self.backing_name) + + if existing is None: + raise IndexError("pop from empty list") + + item = existing.pop(index) + + if not existing: + self.clear() + + return item if __name__ == '__main__': @zb_dataclass class C(DataclassBase): _xs: list[int] | None = field(default=None) - xs: IndexedMutableCollection[int] = LazyValidatedList( + xs: LazyIndexedList[int] = LazyIndexedList( _xs, - validate=lambda self, it: print("validate", it), - sort_by=lambda it: -it + "Thing" ) c = C(xs=[24, 42]) - print(c._xs) + print(c._xs, c.xs) c.xs.clear() - print(c._xs) + print(c._xs, c.xs) c.xs.append(1) c.xs.append(2) c.xs.append(3) - print(c._xs) + print(c._xs, c.xs) + c.xs.insert(2, 42) + print(c._xs, c.xs) + c.xs.pop(2) + print(c._xs, c.xs) diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index 1936a7f02..a772d4373 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -7,8 +7,12 @@ __all__ = ["RelayInfo"] +from dataclasses import field from typing import Optional, List, Generator, Callable, Any +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.util import ngen, nlen, safe_remove, require @@ -26,20 +30,12 @@ class RelayInfo(AssetInfo): reclose_fast: Optional[bool] = None """True if reclose_delays are associated with a fast Curve, false otherwise.""" - _reclose_delays: Optional[List[float]] = None + _reclose_delays: Optional[List[float]] = field(default=None) - def __init__(self, *args, reclose_delays: Optional[List[float]] = None, **kwargs): - super(RelayInfo, self).__init__(*args, **kwargs) - if reclose_delays: - for index, delay in enumerate(reclose_delays): - self.add_delay(delay, index) - - @property - def reclose_delays(self) -> Generator[float, None, None]: - """ - The reclose delays for this curve and relay type. The index of the list is the reclose step, and the value is the overall delay time. - """ - return ngen(self._reclose_delays) + reclose_delays: LazyIndexedList[float] = LazyIndexedList( + _reclose_delays, + "float" + ) def set_delays(self, delays: List[float]) -> RelayInfo: """ @@ -48,83 +44,53 @@ def set_delays(self, delays: List[float]) -> RelayInfo: :param delays: The delays to set. The provided list will be copied. :return: A reference to this :class:`RelayInfo` to allow fluent use. """ - self._reclose_delays = delays.copy() + self.reclose_delays.clear() + self.reclose_delays.extend(delays) return self - def num_delays(self) -> int: - """ - Get the number of reclose delays for this :class:`RelayInfo` - """ - return nlen(self._reclose_delays) + # region deprecated methods - def get_delay(self, index: int) -> float: - """ - Get the reclose delay at the specified index, if it exists. Otherwise, this returns + # region reclose_delays boilerplate - :param index: The index of the delay to retrieve. - :return: The reclose delay at `index` if it exists, otherwise None. - """ - if self._reclose_delays: - return self._reclose_delays[index] - else: - raise IndexError(index) - - def for_each_delay(self, action: Callable[[int, float], Any]): - """ - Call the `action` on each delay in the `reclose_delays` collection - - :param action: An action to apply to each delay in the `reclose_delays` collection, taking the index of the delay, and the delay itself. - """ - for index, point in enumerate(self._reclose_delays): - action(index, point) - - def add_delay(self, delay: float, index: int = None) -> RelayInfo: - """ - Add a reclose delay. + @deprecated("Use len(reclose_delays) instead.") + def num_delays(self) -> int: + return len(self.reclose_delays) - :param delay: The delay in seconds to add. - :param index: The index into the list to add the delay at. Defaults to the end of the list. - :return: A reference to this :class:`RelayInfo` to allow fluent use. - """ - if index is None: - index = self.num_delays() - require(0 <= index <= self.num_delays(), - lambda: f"Unable to add float to {str(self)}. Index number {index} " - f"is invalid. Expected a value between 0 and {self.num_delays()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._reclose_delays = list() if self._reclose_delays is None else self._reclose_delays - self._reclose_delays.insert(index, delay) + @deprecated("Use reclose_delays[index] instead.") + def get_delay(self, index: int) -> float: + return self.reclose_delays[index] + + @deprecated("Use reclose_delays.for_each_indexed(action) instead.") + def for_each_delay( + self, + action: Callable[[int, float], Any], + ) -> None: + self.reclose_delays.for_each_indexed(action) + + @deprecated("Use reclose_delays.append(delay)") + def add_delay( + self, + delay: float, + index: int | None = None, + ) -> RelayInfo: + if index is None: index = len(self.reclose_delays) + self.reclose_delays.insert(index, delay) return self + @deprecated("Use reclose_delays.remove(delay) instead.") def remove_delay(self, delay: float) -> RelayInfo: - """ - Remove a delay from the list. - - :param delay: The delay to remove. - :return: A reference to this :class:`RelayInfo` to allow fluent use. - """ - self._reclose_delays = safe_remove(self._reclose_delays, delay) + self.reclose_delays.remove(delay) return self + @deprecated("Use reclose_delays.pop(index) instead.") def remove_delay_at(self, index: int) -> float: - """ - Remove a delay from the list. - - :param index: The index of the delay to remove. - :return: The delay that was removed, or `None` if no delay was present at `index`. - :raises IndexError: If `sequence_number` is out of range. - """ - if self._reclose_delays: - delay = self._reclose_delays.pop(index) - self._reclose_delays = self._reclose_delays if self._reclose_delays else None - return delay - raise IndexError(index) + return self.reclose_delays.pop(index) + @deprecated("Use reclose_delays.clear() instead.") def clear_delays(self) -> RelayInfo: - """ - Clear all reclose delays. - - :return: A reference to this :class:`RelayInfo` to allow fluent use. - """ - self._reclose_delays = None + self.reclose_delays.clear() return self + + # endregion + + # endregion \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 639328c83..c971cc5a0 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -11,6 +11,8 @@ import warnings from typing import Optional, List, Generator, Iterable, Callable, TYPE_CHECKING, Any from abc import ABCMeta + +from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList if sys.version_info >= (3, 13): from warnings import deprecated else: @@ -68,41 +70,10 @@ class ProtectionRelayFunction(PowerSystemResource, metaclass=ABCMeta): _schemes: Optional[List[ProtectionRelayScheme]] = field(default=None) - _time_limits: Optional[List[float]] = None + _time_limits: Optional[List[float]] = field(default=None) - _thresholds: Optional[List[RelaySetting]] = None + _thresholds: Optional[List[RelaySetting]] = field(default=None) - def __init__( - self, - *args, - sensors: Iterable[Sensor] = None, - protected_switches: Iterable[ProtectedSwitch] = None, - schemes: Iterable[ProtectionRelayScheme] = None, - time_limits: Iterable[float] = None, - thresholds: Iterable[RelaySetting] = None, - relay_info: RelayInfo | None = None, - **kwargs - ): - super(ProtectionRelayFunction, self).__init__(*args, **kwargs) - - if sensors is not None: - for sensor in sensors: - self.add_sensor(sensor) - if protected_switches is not None: - for protected_switch in protected_switches: - self.add_protected_switch(protected_switch) - if schemes is not None: - for scheme in schemes: - self.add_scheme(scheme) - if time_limits is not None: - for time_limit in time_limits: - self.add_time_limit(time_limit) - if thresholds is not None: - for threshold in thresholds: - self.add_threshold(threshold) - if relay_info is not None: - warnings.warn("relay_info is deprecated, use asset_info instead.") - self.asset_info = relay_info @property @deprecated("use asset_info instead.") @@ -115,247 +86,123 @@ def relay_info(self): def relay_info(self, relay_info: Optional[RelayInfo]): self.asset_info = relay_info - @property - def thresholds(self) -> Generator[RelaySetting, None, None]: - """ - [ZBEX] Yields all the thresholds[:class:`RelaySettings`] for this :class:`ProtectionRelayFunction`. The order of thresholds corresponds to the order of time limits. - - :return: A generator that iterates over all thresholds[:class:`RelaySettings`] for this relay function. - """ - return ngen(self._thresholds) - - @property - def time_limits(self) -> Generator[float, None, None]: - """ - [ZBEX] Yields all the time limits (in seconds) for this relay function. Order of entries corresponds to the order of entries in thresholds. + time_limits: LazyIndexedList[float] = LazyIndexedList( + _time_limits, + "float" + ) - :return: A generator that iterates over all time limits for this relay function. - """ - return ngen(self._time_limits) + thresholds: LazyIndexedList[RelaySetting] = LazyIndexedList( + _thresholds, + "RelaySetting" + ) - @property - def sensors(self) -> Generator[Sensor, None, None]: - """ - [ZBEX] Yields all the :class:`Sensors` for this relay function. + sensors: MridCollection[Sensor] = LazyMridList( + _sensors, + "A Sensor", + ) - :return: A generator that iterates over all :class:`Sensors` for this relay function. - """ - return ngen(self._sensors) + protected_switches: MridCollection[ProtectedSwitch] = LazyMridList( + _protected_switches, + "A ProtectedSwitch", + ) - @property - def protected_switches(self) -> Generator[ProtectedSwitch, None, None]: - """ - [ZBEX] Yields the :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. + schemes: MridCollection[ProtectionRelayScheme] = LazyMridList( + _schemes, + "A ProtectionRelayScheme", + ) - :return: A generator that iterates over all :class:`ProtectedSwitches` operated by this :class:`ProtectionRelayFunction`. - """ - return ngen(self._protected_switches) - @property - def schemes(self) -> Generator[ProtectionRelayScheme, None, None]: - """ - [ZBEX] Yields the :class:`ProtectionRelaySchemes` this :class:`ProtectionRelayFunction` operates under. + # region deprecated list boilerplate - :return: A generator that iterates over all :class:`ProtectionRelaySchemes` this :class:`ProtectionRelayFunction` operates under. - """ - return ngen(self._schemes) + # region thresholds boilerplate + @deprecated("Use thresholds.for_each_indexed(action)") def for_each_threshold(self, action: Callable[[int, RelaySetting], Any]): - """ - Call the `action` on each :class:`RelaySetting` in the `thresholds` collection - - :param action: An action to apply to each :class:`RelaySetting` in the `thresholds` collection, taking the index of the threshold, and the threshold itself. - """ - for index, point in enumerate(self.thresholds): - action(index, point) - - def add_threshold(self, threshold: RelaySetting, sequence_number: int = None) -> ProtectionRelayFunction: - """ - Add a threshold[:class:`RelaySetting`] to this :class:`ProtectionRelayFunction`'s list of thresholds. - - :param threshold: The threshold[:class:`RelaySetting`] to add to this :class:`ProtectionRelayFunction`. - :param sequence_number: The sequence number of the `threshold` being added. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if sequence_number is None: - sequence_number = self.num_thresholds() - require(0 <= sequence_number <= self.num_thresholds(), - lambda: f"Unable to add RelaySetting to {str(self)}. Sequence number {sequence_number} " - f"is invalid. Expected a value between 0 and {self.num_thresholds()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._thresholds = list() if self._thresholds is None else self._thresholds - self._thresholds.insert(sequence_number, threshold) + self.thresholds.for_each_indexed(action) + + @deprecated("Use thresholds.append(threshold)") + def add_threshold( + self, + threshold: RelaySetting, + sequence_number: int = None, + ) -> ProtectionRelayFunction: + if sequence_number is None: sequence_number = len(self.thresholds) + self.thresholds.insert(sequence_number, threshold) return self + @deprecated("Use len(thresholds) instead.") def num_thresholds(self) -> int: - """ - Get the number of thresholds for this :class:`ProtectionRelayFunction`. - - :return: The number of thresholds for this `ProtectionRelayFunction`. - """ - return nlen(self._thresholds) + return len(self.thresholds) + @deprecated("Use thresholds[sequence_number] instead.") def get_threshold(self, sequence_number: int) -> RelaySetting: - """ - Get the threshold[:class:`RelaySetting`] for this :class:`ProtectionRelayFunction` by its `sequence_number`. - - :param sequence_number: The sequence_number of the threshold :class:`RelaySetting` for this :class:`ProtectionRelayFunction`. - :returns: The threshold[:class:`RelaySetting`] for this :class:`ProtectionRelayFunction` with sequence number `sequence_number` - :raises IndexError: if no :class:`RelaySetting` was found with sequence_number `sequence_number`. - """ - if self._thresholds is not None: - return self._thresholds[sequence_number] - else: - raise IndexError(sequence_number) - - def remove_threshold(self, threshold: RelaySetting) -> ProtectionRelayFunction: - """ - Removes a threshold[:class:`RelaySetting`] from this :class:`ProtectionRelayFunction`. - - :param threshold: The threshold[:class:`RelaySetting`] to disassociate from this :class:`ProtectionRelayFunction`. - :returns: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._thresholds = safe_remove(self._thresholds, threshold) - return self + return self.thresholds[sequence_number] - def remove_threshold_at(self, sequence_number: int) -> RelaySetting: - """ - Removes a threshold[:class:`RelaySetting`] from this :class:`ProtectionRelayFunction`. + @deprecated("Use thresholds.remove(threshold) instead.") + def remove_threshold( + self, + threshold: RelaySetting, + ) -> ProtectionRelayFunction: + self.thresholds.remove(threshold) + return self - :param sequence_number: The sequence_number of the threshold[:class:`RelaySetting`] to disassociate from this :class:`ProtectionRelayFunction`. - :returns: A reference to removed threshold[:class:`RelaySetting`]. - :raises IndexError: If `sequence_number` is out of range. - """ - threshold = self.get_threshold(sequence_number) - self._thresholds = safe_remove(self._thresholds, threshold) - return threshold + @deprecated("Use thresholds.pop(sequence_number) instead.") + def remove_threshold_at( + self, + sequence_number: int, + ) -> RelaySetting: + return self.thresholds.pop(sequence_number) + @deprecated("Use thresholds.clear() instead.") def clear_thresholds(self) -> ProtectionRelayFunction: - """ - Removes all thresholds from this :class:`ProtectionRelayFunction`. - - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._thresholds = None + self.thresholds.clear() return self + # endregion + + # region time_limits boilerplate + @deprecated("Use time_limits.for_each_indexed(action)") def for_each_time_limit(self, action: Callable[[int, float], Any]): - """ - Call the `action` on each time limit in the `time_limits` collection - - :param action: An action to apply to each time limit in the `time_limits` collection, taking the index of the limit, and the limit itself. - """ - for index, limit in enumerate(self.time_limits): - action(index, limit) - - def add_time_limit(self, time_limit: float, index: int = None) -> ProtectionRelayFunction: - """ - Add a time limit. - - :param time_limit: The time limit in seconds to add to this :class:`ProtectionRelayFunction`. - :param index: The index into the list to add the time limit at. Defaults to the end of the list. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - if index is None: - index = self.num_time_limits() - require(0 <= index <= self.num_time_limits(), - lambda: f"Unable to add float to {str(self)}. Sequence number {index} " - f"is invalid. Expected a value between 0 and {self.num_time_limits()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._time_limits = list() if self._time_limits is None else self._time_limits - self._time_limits.insert(index, time_limit) - return self + self.time_limits.for_each_indexed(action) - def num_time_limits(self) -> int: - return nlen(self._time_limits) - - def get_time_limit(self, index: int): - """ - Get the time limit for this :class:`ProtectionRelayFunction` by its `index`. - - :param index: The index of the desired time limit. - :returns: The time limit with the specified `index` if it exists. - :raises IndexError: if no time limit was found with provided index. - """ - if self._time_limits is not None: - return self._time_limits[index] - else: - raise IndexError(index) - - def remove_time_limit(self, time_limit: float) -> ProtectionRelayFunction: - """ - Remove a time limit from the list. - - :param time_limit: The time limit to remove. - :returns: A reference to this `ProtectionRelayFunction` to allow fluent use. - """ - self._time_limits = safe_remove(self._time_limits, time_limit) + @deprecated( + "Use time_limits.append(time_limit)") + def add_time_limit( + self, + time_limit: float, + index: int = None, + ) -> ProtectionRelayFunction: + if index is None: index = len(self.time_limits) + self.time_limits.insert(index, time_limit) return self - def remove_time_limit_at(self, index: int) -> float: - """ - Remove a time limit from the list. - - :param index: The time limit to remove. - :returns: The time limit that was removed, or `None` if no time limit was present at `index`. - :raises IndexError: If `sequence_number` is out of range. - """ - if self._time_limits: - limit = self._time_limits.pop(index) - self._time_limits = self._time_limits if self._time_limits else None - return limit - raise IndexError(index) + @deprecated("Use len(time_limits) instead.") + def num_time_limits(self) -> int: + return len(self.time_limits) - def clear_time_limits(self) -> ProtectionRelayFunction: - """ - Removes all time limits from this :class:`ProtectionRelayFunction`. + @deprecated("Use time_limits[index] instead.") + def get_time_limit(self, index: int) -> float: + return self.time_limits[index] - :returns: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._time_limits = None + @deprecated("Use time_limits.remove(time_limit) instead.") + def remove_time_limit( + self, + time_limit: float, + ) -> ProtectionRelayFunction: + self.time_limits.remove(time_limit) return self - sensors: MridCollection[Sensor] = LazyMridList( - _sensors, - "A Sensor", - ) - - - - - - - protected_switches: MridCollection[ProtectedSwitch] = LazyMridList( - _protected_switches, - "A ProtectedSwitch", - ) - - - + @deprecated("Use time_limits.pop(index) instead.") + def remove_time_limit_at(self, index: int) -> float: + return self.time_limits.pop(index) - def remove_protected_switch(self, protected_switch: Optional[ProtectedSwitch]) -> ProtectionRelayFunction: - """ - Disassociate this :class:`ProtectionRelayFunction` from a :class:`ProtectedSwitch`. - - :param protected_switch: The :class:`ProtectedSwitch` to disassociate from this :class:`ProtectionRelayFunction`. - :raises ValueError: If protected_switch was not associated with this :class:`ProtectionRelayFunction`. - :return: A reference to this :class:`ProtectionRelayFunction` for fluent use. - """ - self._sensors = safe_remove(self._protected_switches, protected_switch) + @deprecated("Use time_limits.clear() instead.") + def clear_time_limits(self) -> ProtectionRelayFunction: + self.time_limits.clear() return self + # endregion - schemes: MridCollection[ProtectionRelayScheme] = LazyMridList( - _schemes, - "A ProtectionRelayScheme", - ) - - - - - - - # region deprecated list boilerplate # region sensors boilerplate @deprecated("Use len(obj.sensors) instead.") @@ -398,6 +245,11 @@ def add_protected_switch(self, protected_switch: ProtectedSwitch) -> ProtectionR self.protected_switches.append(protected_switch) return self + @deprecated("Use obj.protected_switches.remove(protected_switch) instead.") + def remove_protected_switch(self, protected_switch: Optional[ProtectedSwitch]) -> ProtectionRelayFunction: + self.protected_switches.remove(protected_switch) + return self + @deprecated("Use obj.protected_switches.clear() instead.") def clear_protected_switches(self) -> ProtectionRelayFunction: self.protected_switches.clear() diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index cceb97065..3cc31a458 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -7,13 +7,17 @@ __all__ = ["Location"] -from typing import List, Optional, Generator, Callable, Any +from dataclasses import field +from typing import List, Optional, Callable, Any +from typing_extensions import deprecated + +from zepben.ewb import Alias +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import require, nlen, ngen, safe_remove -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass @@ -25,107 +29,84 @@ class Location(IdentifiedObject): main_address: Optional[StreetAddress] = None """Main address of the location.""" - _position_points: Optional[List[PositionPoint]] = None - - def __init__(self, *args, position_points: List[PositionPoint] = None, **kwargs): - """ - `position_points` A list of `PositionPoint`s to associate with this `Location`. - """ - super(Location, self).__init__(*args, **kwargs) - if position_points: - for point in position_points: - self.add_point(point) - - @property - def points(self) -> Generator[PositionPoint, None, None]: - """ - Returns Generator over the `PositionPoint`s of this `Location`. - """ - for point in ngen(self._position_points): - yield point - - def for_each_point(self, action: Callable[[int, PositionPoint], Any]): - """ - Call the `action` on each :class:`PositionPoint` in the `points` collection - - :param action: An action to apply to each :class:`PositionPoint` in the `points` collection, taking the index of the point, and the point itself. - """ - for index, point in enumerate(self.points): - action(index, point) + _position_points: Optional[List[PositionPoint]] = field(default=None) + + points: LazyIndexedList[PositionPoint] = LazyIndexedList( + _position_points, + "PositionPoint", + ) + position_points = Alias(points) + + + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. + + # region points boilerplate + + @deprecated("Use points.for_each_indexed(action) instead.") + def for_each_point( + self, + action: Callable[[int, PositionPoint], Any], + ): + self.points.for_each_indexed(action) + @deprecated("Use len(points) instead.") def num_points(self): - """ - Returns The number of `PositionPoint`s in this `Location` - """ - return nlen(self._position_points) + return len(self.points) + @deprecated("Use points[sequence_number] instead.") def get_point(self, sequence_number: int) -> PositionPoint: - """ - Get the `sequence_number` `PositionPoint` for this `Location`. + return self.points[sequence_number] - `sequence_number` The sequence number of the `PositionPoint` to get. - Returns The `PositionPoint` identified by `sequence_number` - Raises IndexError if this `Location` didn't contain `sequence_number` points. - """ - return self._position_points[sequence_number] - - def __getitem__(self, item): - return self.get_point(item) + @deprecated("Use points[item] instead.") + def __getitem__(self, item: int) -> PositionPoint: + return self.points[item] + @deprecated("Use points.append(point) instead.") def add_point(self, point: PositionPoint) -> Location: - """ - Associate a `PositionPoint` with this `Location`, assigning it a sequence_number of `num_points`. - `point` The `PositionPoint` to associate with this `Location`. - Returns A reference to this `Location` to allow fluent use. - """ - return self.insert_point(point) - - def insert_point(self, point: PositionPoint, sequence_number: int = None) -> Location: - """ - Associate a `PositionPoint` with this `Location` - - `point` The `PositionPoint` to associate with this `Location`. - `sequence_number` The sequence number of the `PositionPoint`. - Returns A reference to this `Location` to allow fluent use. - Raises `ValueError` if `sequence_number` < 0 or > `num_points()`. - """ - if sequence_number is None: - sequence_number = self.num_points() - require(0 <= sequence_number <= self.num_points(), - lambda: f"Unable to add PositionPoint to {str(self)}. Sequence number {sequence_number} " - f"is invalid. Expected a value between 0 and {self.num_points()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._position_points = list() if self._position_points is None else self._position_points - self._position_points.insert(sequence_number, point) + self.points.append(point) return self - def __setitem__(self, key, value): - return self.insert_point(value, key) + @deprecated("Use points.insert(sequence_number, point)") + def insert_point( + self, + point: PositionPoint, + sequence_number: int | None = None, + ) -> Location: + if sequence_number is None: sequence_number = len(self.points) + self.points.insert(sequence_number, point) - def remove_point(self, point: PositionPoint) -> Location: - """ - Remove a `PositionPoint` from this `Location` - `point` The `PositionPoint` to remove. - Raises `ValueError` if `point` was not part of this `Location` - Returns A reference to this `Location` to allow fluent use. - """ - self._position_points = safe_remove(self._position_points, point) return self - def remove_point_by_sequence_number(self, sequence_number: int) -> PositionPoint: - """ - Remove a :class:`PositionPoint` from this :class:`Location` by its sequence number. + @deprecated("Use points.insert(key, value) instead.") + def __setitem__( + self, + key: int, + value: PositionPoint, + ) -> None: + self.points.insert(key, value) - NOTE: This will update the sequence numbers of all items located after the removed sequence number. + @deprecated("Use points.remove(point) instead.") + def remove_point(self, point: PositionPoint) -> Location: + self.points.remove(point) + return self - :param sequence_number: The sequence number of the `PositionPoint` to remove. - :return: The :class:`PositionPoint` that was removed, or null if there was no :class:`PositionPoint` for the given `sequenceNumber`. - :raises IndexError: If no :class:`PositionPoint` with the specified `sequence_number` was not associated with this :class:`Location`. - """ - point = self.get_point(sequence_number) - self._position_points = safe_remove(self._position_points, point) - return point + @deprecated("Use points.pop(sequence_number) instead.") + def remove_point_by_sequence_number( + self, + sequence_number: int, + ) -> PositionPoint: + return self.points.pop(sequence_number) + @deprecated("Use points.clear() instead.") def clear_points(self) -> Location: - self._position_points = None + self.points.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index c4f5fdfe1..f79ce5672 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -7,10 +7,13 @@ __all__ = ["DiagramObject"] +from dataclasses import field from typing import Optional, List, Generator, Callable, TYPE_CHECKING, Any from typing_extensions import deprecated +from zepben.ewb import Alias +from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint from zepben.ewb.util import nlen, ngen, require, safe_remove @@ -38,15 +41,8 @@ class DiagramObject(IdentifiedObject): rotation: float = 0.0 """Sets the angle of rotation of the diagram object. Zero degrees is pointing to the top of the diagram. Rotation is clockwise.""" - _diagram_object_points: Optional[List[DiagramObjectPoint]] = None + _diagram_object_points: Optional[List[DiagramObjectPoint]] = field(default=None) - def __init__(self, *args, diagram: Diagram = None, diagram_object_points: List[DiagramObjectPoint] = None, **kwargs): - super(DiagramObject, self).__init__(*args, **kwargs) - if diagram: - self.diagram = diagram - if diagram_object_points: - for point in diagram_object_points: - self.add_point(point) @property def diagram(self): @@ -61,105 +57,83 @@ def diagram(self, diag): else: raise ValueError(f"diagram for {str(self)} has already been set to {self._diagram}, cannot reset this field to {diag}") - @property - def points(self) -> Generator[DiagramObjectPoint, None, None]: - """ - The `DiagramObjectPoint`s for this `DiagramObject`. - """ - return ngen(self._diagram_object_points) + points: LazyIndexedList[DiagramObjectPoint] = LazyIndexedList( + _diagram_object_points, + "DiagramObjectPoint", + ) + diagram_object_points = Alias(points) + + + # region deprecated list boilerplate + # + # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) + # This boilerplate exists solely to enable backwards compatibility. + # It will be removed eventually. + # Every single method simply forwards the call to the corresponding list. - def for_each_point(self, action: Callable[[int, DiagramObjectPoint], Any]): - """ - Call the `action` on each :class:`DiagramObjectPoint` in the `points` collection + # region points boilerplate - :param action: An action to apply to each :class:`DiagramObjectPoint` in the `points` collection, taking the index of the point, and the point itself. - """ - for index, point in enumerate(self.points): - action(index, point) + @deprecated("Use points.for_each_indexed(action) instead.") + def for_each_point( + self, + action: Callable[[int, DiagramObjectPoint], Any], + ): + self.points.for_each_indexed(action) + @deprecated("Use len(points) instead.") def num_points(self): - """ - Returns the number of `DiagramObjectPoint`s associated with this `DiagramObject` - """ - return nlen(self._diagram_object_points) + return len(self.points) + @deprecated("Use points[sequence_number] instead.") def get_point(self, sequence_number: int) -> DiagramObjectPoint: - """ - Get the `DiagramObjectPoint` for this `DiagramObject` represented by `sequence_number` . - A diagram object can have 0 or more points to reflect its layout position, routing (for polylines) or boundary (for polygons). - Index in the underlying points collection corresponds to the sequence number - - `sequence_number` The sequence number of the `DiagramObjectPoint` to get. - Returns The `DiagramObjectPoint` identified by `sequence_number` - Raises IndexError if this `DiagramObject` didn't contain `sequence_number` points. - """ - if self._diagram_object_points is not None: - return self._diagram_object_points[sequence_number] - else: - raise IndexError(sequence_number) + return self.points[sequence_number] + @deprecated("Use points[item] instead.") def __getitem__(self, item: int) -> DiagramObjectPoint: - return self.get_point(item) + return self.points[item] + @deprecated("Use points.append(point) instead.") def add_point(self, point: DiagramObjectPoint) -> DiagramObject: - """ - Associate a `DiagramObjectPoint` with this `DiagramObject`, assigning it a sequence_number of `num_points`. - `point` The `DiagramObjectPoint` to associate with this `DiagramObject`. - Returns A reference to this `DiagramObject` to allow fluent use. - """ - return self.insert_point(point) - - def insert_point(self, point: DiagramObjectPoint, sequence_number: int = None) -> DiagramObject: - """ - Associate a `DiagramObjectPoint` with this `DiagramObject` - - `point` The `DiagramObjectPoint` to associate with this `DiagramObject`. - `sequence_number` The sequence number of the `DiagramObjectPoint`. - Returns A reference to this `DiagramObject` to allow fluent use. - Raises `ValueError` if `sequence_number` < 0 or > `num_points()`. - """ - if sequence_number is None: - sequence_number = self.num_points() - require(0 <= sequence_number <= self.num_points(), - lambda: f"Unable to add DiagramObjectPoint to {str(self)}. Sequence number {sequence_number}" - f" is invalid. Expected a value between 0 and {self.num_points()}. Make sure you are " - f"adding the items in order and there are no gaps in the numbering.") - self._diagram_object_points = list() if self._diagram_object_points is None else self._diagram_object_points - self._diagram_object_points.insert(sequence_number, point) + self.points.append(point) return self - def __setitem__(self, key, value): - self.insert_point(value, key) + @deprecated("Use points.insert(sequence_number, point)") + def insert_point( + self, + point: DiagramObjectPoint, + sequence_number: int | None = None, + ) -> DiagramObject: + if sequence_number is None: sequence_number = len(self.points) + self.points.insert(sequence_number, point) - def remove_point(self, point: DiagramObjectPoint) -> DiagramObject: - """ - Disassociate `point` from this `DiagramObject` - - `point` The `DiagramObjectPoint` to disassociate from this `DiagramObject`. - Returns A reference to this `DiagramObject` to allow fluent use. - Raises `ValueError` if `point` was not associated with this `DiagramObject`. - """ - self._diagram_object_points = safe_remove(self._diagram_object_points, point) return self - def remove_point_by_sequence_number(self, sequence_number: int) -> DiagramObjectPoint: - """ - Remove a :class:`DiagramObjectPoint` from this :class:`DiagramObject` by its sequence number. + @deprecated("Use points.insert(key, value) instead.") + def __setitem__( + self, + key: int, + value: DiagramObjectPoint, + ) -> None: + self.points.insert(key, value) - NOTE: This will update the sequence numbers of all items located after the removed sequence number. + @deprecated("Use points.remove(point) instead.") + def remove_point(self, point: DiagramObjectPoint) -> DiagramObject: + self.points.remove(point) + return self - :param sequence_number: The sequence number of the `DiagramObjectPoint` to remove. - :return: The :class:`DiagramObjectPoint` that was removed, or null if there was no :class:`DiagramObjectPoint` for the given `sequenceNumber`. - :raises IndexError: If no :class:`DiagramObjectPoint` with the specified `sequence_number` was not associated with this :class:`DiagramObject`. - """ - point = self.get_point(sequence_number) - self._diagram_object_points = safe_remove(self._diagram_object_points, point) - return point + @deprecated("Use points.pop(sequence_number) instead.") + def remove_point_by_sequence_number( + self, + sequence_number: int, + ) -> DiagramObjectPoint: + return self.points.pop(sequence_number) + @deprecated("Use points.clear() instead.") def clear_points(self) -> DiagramObject: - """ - Clear all points. - Returns A reference to this `DiagramObject` to allow fluent use. - """ - self._diagram_object_points = None + self.points.clear() return self + + # endregion + + # endregion + From cbe03c3155069723ee48753df92ef2fc634d39e7 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Tue, 21 Jul 2026 17:26:27 +1000 Subject: [PATCH 29/54] Remove inits; migrate contactdetails, usagepoint, connectivitynode, conductingequipment Signed-off-by: Boris Filin --- src/zepben/ewb/__init__.py | 2 +- .../iec61968/assetinfo/relay_info.py | 5 +- .../iec61968/common/contact_details.py | 140 +++++++-------- .../cim/iec61968/metering/usage_point.py | 66 +++---- .../base/core/conducting_equipment.py | 162 +++++++----------- .../iec61970/base/core/connectivity_node.py | 75 +++----- .../ewb/model/cim/iec61970/base/core/curve.py | 9 - .../iec61970/base/core/geographical_region.py | 7 - .../base/core/sub_geographical_region.py | 5 - .../model/cim/iec61970/base/core/terminal.py | 15 +- .../wires/power_electronics_connection.py | 81 +-------- .../iec61970/base/wires/power_transformer.py | 10 +- .../base/wires/power_transformer_end.py | 131 +++++++++----- .../iec61970/base/wires/shunt_compensator.py | 5 - 14 files changed, 270 insertions(+), 443 deletions(-) diff --git a/src/zepben/ewb/__init__.py b/src/zepben/ewb/__init__.py index bb5bd2339..114dcc8e3 100644 --- a/src/zepben/ewb/__init__.py +++ b/src/zepben/ewb/__init__.py @@ -127,6 +127,7 @@ from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import * from zepben.ewb.model.cim.iec61970.base.core.base_voltage import * +from zepben.ewb.model.cim.iec61970.base.core.terminal import * from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import * from zepben.ewb.model.cim.iec61970.base.core.connectivity_node import * from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import * @@ -144,7 +145,6 @@ from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import * from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import * from zepben.ewb.model.cim.iec61970.base.core.substation import * -from zepben.ewb.model.cim.iec61970.base.core.terminal import * from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram import * from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import * diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index a772d4373..7c02259f2 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -8,15 +8,14 @@ __all__ = ["RelayInfo"] from dataclasses import field -from typing import Optional, List, Generator, Callable, Any +from typing import Optional, List, Callable, Any from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo -from zepben.ewb.util import ngen, nlen, safe_remove, require -from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py index c7cbe845b..b0fe95a38 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -5,11 +5,13 @@ __all__ = ["ContactDetails"] +from dataclasses import field from typing import Generator, Any from typing_extensions import deprecated from zepben.ewb import zb_dataclass +from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress @@ -63,41 +65,31 @@ def id(self) -> str: business_name: str | None = None """[ZBEX] The business name of this contact.""" - _phone_numbers: list[TelephoneNumber] | None = None + _phone_numbers: list[TelephoneNumber] | None = field(default=None) - _electronic_addresses: list[ElectronicAddress] | None = None + phone_numbers: LazyValidatedList[TelephoneNumber] = LazyValidatedList(_phone_numbers) + """[ZBEX] Phone numbers.""" - def __init__(self, id: str|None=None, *args, phone_numbers: list[TelephoneNumber] = None, electronic_addresses: list[ElectronicAddress] = None, **kwargs): + _electronic_addresses: list[ElectronicAddress] | None = field(default=None) + + electronic_addresses: LazyValidatedList[ElectronicAddress] = LazyValidatedList(_electronic_addresses) + """[ZBEX] Electronic addresses.""" + + def __init__(self, id: str|None=None, *args, **kwargs): if id is not None: if "mrid" in kwargs: raise TypeError("ContactDetails.id is an alias for mrid. Do not pass both to the constructor!") kwargs["mrid"] = id super(ContactDetails, self).__init__(*args, **kwargs) - for number in phone_numbers or []: - self.add_phone_number(number) - - for email in electronic_addresses or []: - self.add_electronic_address(email) def __str__(self): - return f"ContactDetails({self.id})" + return f"ContactDetails({self.mrid})" def __hash__(self): # noinspection PyUnresolvedReferences return hash((type(self), *(getattr(self, s) for s in self.__slots__))) - @zbex - @property - def phone_numbers(self) -> Generator[TelephoneNumber, None, None]: - """[ZBEX] Phone numbers.""" - return ngen(self._phone_numbers) - - @zbex - @property - def electronic_addresses(self) -> Generator[ElectronicAddress, None, None]: - """[ZBEX] Electronic addresses.""" - return ngen(self._electronic_addresses) def __eq__(self, other: Any) -> bool: # @@ -118,79 +110,65 @@ def __eq__(self, other: Any) -> bool: self._electronic_addresses == other._electronic_addresses, )) + + # region deprecated list boilerplate + + # region phone_numbers boilerplate + + @deprecated("Use len(phone_numbers) instead.") def num_phone_numbers(self) -> int: - """Get the number of entries in the ``TelephoneNumber`` collection.""" - return nlen(self._phone_numbers) - - def add_phone_number(self, phone_number: TelephoneNumber) -> "ContactDetails": - """ - Add an ``TelephoneNumber`` to this ``ContactDetails``. - - :param phone_number: The ``TelephoneNumber`` to add. - :return: This ``ContactDetails`` for fluent use. - """ - if self._phone_numbers is None: - self._phone_numbers = [] - self._phone_numbers.append(phone_number) + return len(self.phone_numbers) + + @deprecated("Use phone_numbers.append(phone_number) instead.") + def add_phone_number( + self, + phone_number: TelephoneNumber, + ) -> "ContactDetails": + self.phone_numbers.append(phone_number) return self - def remove_phone_number(self, phone_number: TelephoneNumber) -> bool: - """ - Remove an ``TelephoneNumber`` from this ``ContactDetails``. - - :param phone_number: The ``TelephoneNumber`` to remove. - :return: True if the ``TelephoneNumber`` was removed. - """ - if not self._phone_numbers: - raise KeyError(phone_number) - self._phone_numbers.remove(phone_number) - if self.num_phone_numbers == 0: - self._phone_numbers = None + @deprecated("Use phone_numbers.remove(phone_number) instead.") + def remove_phone_number( + self, + phone_number: TelephoneNumber, + ) -> bool: + self.phone_numbers.remove(phone_number) return True + @deprecated("Use phone_numbers.clear() instead.") def clear_phone_numbers(self) -> "ContactDetails": - """ - Clear all ``TelephoneNumber``'s from this ``ContactDetails``. - :return: this ``ContactDetails`` for fluent use. - """ - self._phone_numbers = None + self.phone_numbers.clear() return self + # endregion + + # region electronic_addresses boilerplate + + @deprecated("Use len(electronic_addresses) instead.") def num_electronic_addresses(self) -> int: - """Get the number of entries in the [ElectronicAddress] collection.""" - return nlen(self._electronic_addresses) - - def add_electronic_address(self, electronic_address: ElectronicAddress) -> "ContactDetails": - """ - Add an ``ElectronicAddress`` to this ``ContactDetails``. - - :param electronic_address: The ``ElectronicAddress`` to add. - :return: this ``ContactDetails`` for fluent use. - """ - if self._electronic_addresses is None: - self._electronic_addresses = [] - self._electronic_addresses.append(electronic_address) + return len(self.electronic_addresses) + + @deprecated("Use electronic_addresses.append(electronic_address) instead.") + def add_electronic_address( + self, + electronic_address: ElectronicAddress, + ) -> "ContactDetails": + self.electronic_addresses.append(electronic_address) return self - def remove_electronic_address(self, electronic_address: ElectronicAddress) -> bool: - """ - Remove an ``ElectronicAddress`` from this ``ContactDetails``. - - :param electronic_address: The ``ElectronicAddress`` to remove. - :return: True if the ``ElectronicAddress`` was removed. - """ - if not self._electronic_addresses: - raise KeyError(electronic_address) - self._electronic_addresses.remove(electronic_address) - if self.num_electronic_addresses == 0: - self._electronic_addresses = None + @deprecated("Use electronic_addresses.remove(electronic_address) instead.") + def remove_electronic_address( + self, + electronic_address: ElectronicAddress, + ) -> bool: + self.electronic_addresses.remove(electronic_address) return True + @deprecated("Use electronic_addresses.clear() instead.") def clear_electronic_addresses(self) -> "ContactDetails": - """ - Clear all ``ElectronicAddress``'s from this ``ContactDetails``. - - :return: this ``ContactDetails`` for fluent use. - """ - self._electronic_addresses = None + self.electronic_addresses.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index dc8235b9b..16fba5a6e 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -62,13 +62,7 @@ class UsagePoint(IdentifiedObject): _equipment: list[Equipment] | None = field(default=None) _end_devices: list[EndDevice] | None = field(default=None) - _contacts: list[ContactDetails] | None = None - - def __init__(self, *args, contacts: List[ContactDetails] = None, **kwargs): - super(UsagePoint, self).__init__(*args, **kwargs) - if contacts: - for c in contacts: - self.add_contact(c) + _contacts: list[ContactDetails] | None = field(default=None) end_devices: MridCollection[EndDevice] = LazyMridList( @@ -81,6 +75,10 @@ def __init__(self, *args, contacts: List[ContactDetails] = None, **kwargs): "An Equipment", ) + contacts: MridCollection[ContactDetails] = LazyMridList( + _contacts, + "A ContactDetails" + ) def num_equipment(self): """ @@ -88,51 +86,35 @@ def num_equipment(self): """ return nlen(self._equipment) - def get_equipment(self, mrid: str) -> Equipment: - """ - Get the `Equipment` for this `UsagePoint` identified by `mrid` + # region deprecated list boilerplate - `mrid` The mRID of the required `Equipment` - Returns The `Equipment` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._equipment, mrid) + # region contacts boilerplate - def add_equipment(self, equipment: Equipment) -> UsagePoint: - """ - Associate an `Equipment` with this `UsagePoint` + @deprecated("Use len(contacts) instead.") + def num_contacts(self) -> int: + return len(self.contacts) - `equipment` The `Equipment` to associate with this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if another `Equipment` with the same `mrid` already exists for this `UsagePoint`. - """ - if self._validate_reference(equipment, self.get_equipment, "An Equipment"): - return self + @deprecated("Use contacts.get_by_mrid(mrid) instead.") + def get_contact(self, mrid: str) -> ContactDetails: + return self.contacts.get_by_mrid(mrid) - self._equipment = list() if self._equipment is None else self._equipment - self._equipment.append(equipment) + @deprecated("Use contacts.append(contact) instead.") + def add_contact(self, contact: ContactDetails) -> UsagePoint: + self.contacts.append(contact) return self - def remove_equipment(self, equipment: Equipment) -> UsagePoint: - """ - Disassociate an `Equipment` from this `UsagePoint` - - `equipment` The `Equipment` to disassociate with this `UsagePoint`. - Returns A reference to this `UsagePoint` to allow fluent use. - Raises `ValueError` if `equipment` was not associated with this `UsagePoint`. - """ - self._equipment = safe_remove(self._equipment, equipment) + @deprecated("Use contacts.remove(contact) instead.") + def remove_contact(self, contact: ContactDetails) -> UsagePoint: + self.contacts.remove(contact) return self - def clear_equipment(self) -> UsagePoint: - """ - Clear all equipment. - Returns A reference to this `UsagePoint` to allow fluent use. - """ - self._equipment = None + @deprecated("Use contacts.clear() instead.") + def clear_contacts(self) -> UsagePoint: + self.contacts.clear() return self - # region deprecated list boilerplate + # endregion + # region end_devices boilerplate @deprecated("Use len(obj.end_devices) instead.") diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py index 0a6124c59..88bc67e4c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py @@ -12,15 +12,26 @@ from abc import ABCMeta from dataclasses import field +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill +from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.util import get_by_mrid, require, ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.base_voltage import BaseVoltage - from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal +class TerminalsList(LazyMridList[Terminal]): + + def get_by_sequence_number(self, sequence_number: int) -> Terminal: + term = next((it for it in self if it.sequence_number == sequence_number), None) + if term is None: + raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self.instance)}") + return term + @zb_dataclass class ConductingEquipment(Equipment, metaclass=ABCMeta): """ @@ -42,14 +53,6 @@ class ConductingEquipment(Equipment, metaclass=ABCMeta): _terminals: List[Terminal] = field(default_factory=list) max_terminals = int(sys.maxsize) - def __init__(self, *args, terminals: List[Terminal] = None, **kwargs): - super(ConductingEquipment, self).__init__(*args, **kwargs) - if terminals: - for term in terminals: - if term.conducting_equipment is None: - term.conducting_equipment = self - self.add_terminal(term) - # pylint: disable=unused-argument def get_base_voltage(self, terminal: Terminal = None): """ @@ -70,13 +73,13 @@ def base_voltage_value(self) -> int: """ return self.base_voltage.nominal_voltage if self.base_voltage and self.base_voltage.nominal_voltage else 0 - @property - def terminals(self) -> Generator[Terminal, None, None]: - """ - `ConductingEquipment` have `Terminal`s that may be connected to other `ConductingEquipment` - `Terminal`s via `ConnectivityNode`s. - """ - return ngen(self._terminals) + terminals: TerminalsList = TerminalsList( + _terminals, + "A Terminal", + backfill=Backfill(Terminal.conducting_equipment), + validate=lambda self, it: self._validate_terminal(it), + sort_by=lambda it: it.sequence_number + ) def __repr__(self): return (f"{super(ConductingEquipment, self).__repr__()}, in_service={self.in_service}, " @@ -92,111 +95,70 @@ def _validate_terminal(self, terminal: Terminal) -> bool: Raises `ValueError` if `Terminal`s `conducting_equipment` is not this `ConductingEquipment`, or if this `ConductingEquipment` has a different `Terminal` with the same mRID. """ - if self._validate_reference(terminal, self.get_terminal_by_mrid, "A Terminal"): - return True - if self._validate_reference_by_field(terminal, terminal.sequence_number, self.get_terminal_by_sn, "sequence_number"): return True - if not terminal.conducting_equipment: - terminal.conducting_equipment = self - - require(terminal.conducting_equipment is self, - lambda: f"Terminal {terminal} references another piece of conducting equipment {terminal.conducting_equipment}, expected {str(self)}.",) - return False - - def num_terminals(self): - """ - Get the number of `Terminal`s for this `ConductingEquipment`. - """ - return len(self._terminals) + require(self.num_terminals() < self.max_terminals, + lambda: f"Unable to add {terminal} to {str(self)}. This conducting equipment already has the maximum number of terminals ({self.max_terminals}).") - def get_terminal(self, identifier: Union[int, str]): - """ - Get the `Terminal` for this `ConductingEquipment` identified by `mrid` or `sequence_number` + if terminal.sequence_number == 0: + terminal.sequence_number = self.num_terminals() + 1 - :param identifier: the mRID of the required `Terminal`, or the `sequence_number` of the terminal in relation - to this `ConductingEquipment` - :return: The `Terminal` with the specified `mrid` if it exists + return False - Raises `KeyError` if `mrid` wasn't present. - Raises `TypeError` if the identifier wasn't a recognised type - """ - if isinstance(identifier, int): - return self.get_terminal_by_sn(identifier) - elif isinstance(identifier, str): - return self.get_terminal_by_mrid(identifier) - raise TypeError(f'`identifier` parameter not a recognised type: {type(identifier)}') - def get_terminal_by_mrid(self, mrid: str) -> Terminal: - """ - Get the `Terminal` for this `ConductingEquipment` identified by `mrid` + # region deprecated list boilerplate - :param mrid: the mRID of the required `Terminal` + # region terminals boilerplate - :return: The `Terminal` with the specified `mrid` if it exists + @deprecated("Use len(terminals) instead.") + def num_terminals(self) -> int: + return len(self.terminals) - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._terminals, mrid) + @deprecated( + "Use terminals.get_by_sequence_number(identifier) for integer identifiers " + + "or terminals.get_by_mrid(identifier) for string identifiers instead." + ) + def get_terminal(self, identifier: Union[int, str]) -> Terminal: + if isinstance(identifier, int): + return self.terminals.get_by_sequence_number(identifier) - def get_terminal_by_sn(self, sequence_number: int): - """ - Get the `Terminal` on this `ConductingEquipment` by its `sequence_number`. + if isinstance(identifier, str): + return self.terminals.get_by_mrid(identifier) - :param sequence_number: The `sequence_number` of the `Terminal` in relation to this `ConductingEquipment`. + raise TypeError( + f"`identifier` parameter not a recognised type: {type(identifier)}" + ) - :return: The `Terminal` on this `ConductingEquipment` with sequence number `sequence_number` + @deprecated("Use terminals.get_by_mrid(mrid) instead.") + def get_terminal_by_mrid(self, mrid: str) -> Terminal: + return self.terminals.get_by_mrid(mrid) - Raises IndexError if no `Terminal` was found with sequence_number `sequence_number`. - """ - for term in self._terminals: - if term.sequence_number == sequence_number: - return term - raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self)}") + @deprecated( + "Use terminals.get_by_sequence_number(sequence_number) instead." + ) + def get_terminal_by_sn(self, sequence_number: int) -> Terminal: + return self.terminals.get_by_sequence_number(sequence_number) - def __getitem__(self, item: int): - return self.get_terminal_by_sn(item) + @deprecated("Use terminals.get_by_sequence_number(item) instead.") + def __getitem__(self, item: int) -> Terminal: + return self.terminals.get_by_sequence_number(item) + @deprecated("Use terminals.append(terminal) instead.") def add_terminal(self, terminal: Terminal) -> ConductingEquipment: - """ - Associate `terminal` with this `ConductingEquipment`. If `terminal.sequence_number` == 0, the terminal will be assigned a sequence_number of - `self.num_terminals() + 1`. - - `terminal` The `Terminal` to associate with this `ConductingEquipment`. - Returns A reference to this `ConductingEquipment` to allow fluent use. - Raises `ValueError` if another `Terminal` with the same `mrid` already exists for this `ConductingEquipment`. - Raises `ValueError` if `max_terminals` has already been reached. - """ - if self._validate_terminal(terminal): - return self - - require(self.num_terminals() < self.max_terminals, - lambda: f"Unable to add {terminal} to {str(self)}. This conducting equipment already has the maximum number of terminals ({self.max_terminals}).") - - if terminal.sequence_number == 0: - terminal.sequence_number = self.num_terminals() + 1 - - self._terminals.append(terminal) - self._terminals.sort(key=lambda t: t.sequence_number) - + self.terminals.append(terminal) return self + @deprecated("Use terminals.remove(terminal) instead.") def remove_terminal(self, terminal: Terminal) -> ConductingEquipment: - """ - Disassociate `terminal` from this `ConductingEquipment` - - `terminal` the `Terminal` to disassociate from this `ConductingEquipment`. - Returns A reference to this `ConductingEquipment` to allow fluent use. - Raises `ValueError` if `terminal` was not associated with this `ConductingEquipment`. - """ - self._terminals.remove(terminal) + self.terminals.remove(terminal) return self + @deprecated("Use terminals.clear() instead.") def clear_terminals(self) -> ConductingEquipment: - """ - Clear all terminals. - Returns A reference to this `ConductingEquipment` to allow fluent use. - """ - self._terminals.clear() + self.terminals.clear() return self + + # endregion + + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index f900d551a..6085f0738 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -10,6 +10,9 @@ from typing import Generator, List, TYPE_CHECKING from dataclasses import field +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import get_by_mrid, ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -30,21 +33,15 @@ class ConnectivityNode(IdentifiedObject, WeakrefSlot): """ _terminals: List[Terminal] = field(default_factory=list) - def __init__(self, *args, terminals: List[Terminal] = None, **kwargs): - super(ConnectivityNode, self).__init__(*args, **kwargs) - if terminals: - for term in terminals: - self.add_terminal(term) + terminals: MridCollection[Terminal] = LazyMridList( + _terminals, + "A Terminal" + ) + """The `Terminal`s attached to this `ConnectivityNode`""" def __iter__(self): return iter(self._terminals) - @property - def terminals(self) -> Generator[Terminal, None, None]: - """ - The `Terminal`s attached to this `ConnectivityNode` - """ - return ngen(self._terminals) def is_switched(self): return self.get_switch() is not None @@ -59,51 +56,33 @@ def get_switch(self): pass return None - def num_terminals(self): - """ - Get the number of `Terminal`s for this `ConnectivityNode`. - """ - return len(self._terminals) - - def get_terminal(self, mrid: str) -> Terminal: - """ - Get the `Terminal` for this `ConnectivityNode` identified by `mrid` + # region deprecated list boilerplate - `mrid` The mRID of the required `Terminal` - Returns The `Terminal` with the specified `mrid` if it exists - Raises `KeyError` if `mrid` wasn't present. - """ - return get_by_mrid(self._terminals, mrid) + # region terminals boilerplate - def add_terminal(self, terminal: Terminal) -> ConnectivityNode: - """ - Associate a `terminal.Terminal` with this `ConnectivityNode` + @deprecated("Use len(terminals) instead.") + def num_terminals(self) -> int: + return len(self.terminals) - `terminal` The `Terminal` to add. Will only add to this object if it is not already associated. - Returns A reference to this `ConnectivityNode` to allow fluent use. - Raises `ValueError` if another `Terminal` with the same `mrid` already exists for this `ConnectivityNode`. - """ - if self._validate_reference(terminal, self.get_terminal, "A Terminal"): - return self + @deprecated("Use terminals.get_by_mrid(mrid) instead.") + def get_terminal(self, mrid: str) -> Terminal: + return self.terminals.get_by_mrid(mrid) - self._terminals.append(terminal) + @deprecated("Use terminals.append(terminal) instead.") + def add_terminal(self, terminal: Terminal) -> ConnectivityNode: + self.terminals.append(terminal) return self + @deprecated("Use terminals.remove(terminal) instead.") def remove_terminal(self, terminal: Terminal) -> ConnectivityNode: - """ - Disassociate `terminal` from this `ConnectivityNode`. - - `terminal` The `Terminal` to disassociate from this `ConnectivityNode`. - Returns A reference to this `ConnectivityNode` to allow fluent use. - Raises `ValueError` if `terminal` was not associated with this `ConnectivityNode`. - """ - self._terminals.remove(terminal) + self.terminals.remove(terminal) return self + @deprecated("Use terminals.clear() instead.") def clear_terminals(self) -> ConnectivityNode: - """ - Clear all terminals. - Returns A reference to this `ConnectivityNode` to allow fluent use. - """ - self._terminals.clear() + self.terminals.clear() return self + + # endregion + + # endregion \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index 821e87af8..be90f3852 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -53,15 +53,6 @@ class Curve(IdentifiedObject, metaclass=ABCMeta): _data: list[CurveData] | None = field(default=None) - def __init__(self, *args, data: List[CurveData] = None, **kwargs): - """ - `data` A list of `CurveData`s to associate with this `Curve`. - """ - super(Curve, self).__init__(*args, **kwargs) - if data: - for curve_data in data: - self.add_curve_data(curve_data) - data: CurveDataList[CurveData] = CurveDataList( _data, validate=lambda self, it: self._validate_data(it), diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index 923ca240e..b5efdd541 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -25,13 +25,6 @@ class GeographicalRegion(IdentifiedObject): """ _sub_geographical_regions: Optional[List[SubGeographicalRegion]] = field(default=None) - def __init__(self, *args, sub_geographical_regions: List[SubGeographicalRegion] = None, **kwargs): - super(GeographicalRegion, self).__init__(*args, **kwargs) - if sub_geographical_regions: - for sgr in sub_geographical_regions: - self.add_sub_geographical_region(sgr) - - sub_geographical_regions: MridCollection[SubGeographicalRegion] = LazyMridList( _sub_geographical_regions, "A SubGeographicalRegion", diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index 6b1783a9c..b90c911cf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -31,11 +31,6 @@ class SubGeographicalRegion(IdentifiedObject): _substations: Optional[List[Substation]] = field(default=None) - def __init__(self, *args, substations: List[Substation] = None, **kwargs): - super(SubGeographicalRegion, self).__init__(*args, **kwargs) - if substations: - for sub in substations: - self.add_substation(sub) @property @targets(_geographical_region) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py index 79668113d..aab8434ad 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py @@ -16,7 +16,6 @@ from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import AcDcTerminal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode -from zepben.ewb.model.cim.iec61970.base.wires.busbar_section import BusbarSection from zepben.ewb.services.network.tracing.feeder.feeder_direction import FeederDirection from zepben.ewb.services.network.tracing.phases.phase_status import PhaseStatus from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -32,7 +31,7 @@ class Terminal(AcDcTerminal): An AC electrical connection point to a piece of conducting equipment. Terminals are connected at physical connection points called connectivity nodes. """ - _conducting_equipment: Optional[ConductingEquipment] = None + _conducting_equipment: Optional['ConductingEquipment'] = None """The conducting equipment of the terminal. Conducting equipment have terminals that may be connected to other conducting equipment terminals via connectivity nodes.""" @@ -58,21 +57,12 @@ class Terminal(AcDcTerminal): _normal_phases: PhaseStatus = None _current_phases: PhaseStatus = None - def __init__(self, *args, conducting_equipment: ConductingEquipment = None, connectivity_node: ConnectivityNode = None, **kwargs): + def __init__(self, *args, **kwargs): super(Terminal, self).__init__(*args, **kwargs) self._normal_phases = PhaseStatus(self) - self._current_phases = PhaseStatus(self) - if conducting_equipment: - self.conducting_equipment = conducting_equipment - - # We set the connectivity node to itself if the name parameter is not used to make sure the positional argument is wrapped in a reference. - if connectivity_node: - self.connectivity_node = connectivity_node - else: - self.connectivity_node = self._cn @property def normal_phases(self) -> PhaseStatus: @@ -174,6 +164,7 @@ def is_feeder_head_terminal(self): return True def has_connected_busbars(self): + from zepben.ewb.model.cim.iec61970.base.wires.busbar_section import BusbarSection try: return any(it != self and isinstance(it.conducting_equipment, BusbarSection) for it in self.connectivity_node.terminals) except AttributeError: diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 5e7cf25b2..006c5aaaf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -12,6 +12,7 @@ from typing_extensions import deprecated +from zepben.ewb import Alias from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase @@ -146,83 +147,6 @@ class PowerElectronicsConnection(RegulatingCondEq): _power_electronics_connection_phases: Optional[List[PowerElectronicsConnectionPhase]] = field(default=None) """The individual units models for the power electronics connection.""" - def __init__(self, - *args, - power_electronics_units: List[PowerElectronicsUnit] = None, - power_electronics_connection_phases: List[PowerElectronicsConnectionPhase] = None, - inv_watt_resp_v1=None, - inv_watt_resp_v2=None, - inv_watt_resp_v3=None, - inv_watt_resp_v4=None, - inv_watt_resp_p_at_v1=None, - inv_watt_resp_p_at_v2=None, - inv_watt_resp_p_at_v3=None, - inv_watt_resp_p_at_v4=None, - inv_var_resp_v1=None, - inv_var_resp_v2=None, - inv_var_resp_v3=None, - inv_var_resp_v4=None, - inv_var_resp_q_at_v1=None, - inv_var_resp_q_at_v2=None, - inv_var_resp_q_at_v3=None, - inv_var_resp_q_at_v4=None, - **kwargs): - super(PowerElectronicsConnection, self).__init__(*args, **kwargs) - if power_electronics_units: - for unit in power_electronics_units: - self.add_unit(unit) - - if power_electronics_connection_phases: - for phase in power_electronics_connection_phases: - self.add_phase(phase) - - if inv_watt_resp_v1 is not None: - self.inv_watt_resp_v1 = inv_watt_resp_v1 - - if inv_watt_resp_v2 is not None: - self.inv_watt_resp_v2 = inv_watt_resp_v2 - - if inv_watt_resp_v3 is not None: - self.inv_watt_resp_v3 = inv_watt_resp_v3 - - if inv_watt_resp_v4 is not None: - self.inv_watt_resp_v4 = inv_watt_resp_v4 - - if inv_watt_resp_p_at_v1 is not None: - self.inv_watt_resp_p_at_v1 = inv_watt_resp_p_at_v1 - - if inv_watt_resp_p_at_v2 is not None: - self.inv_watt_resp_p_at_v2 = inv_watt_resp_p_at_v2 - - if inv_watt_resp_p_at_v3 is not None: - self.inv_watt_resp_p_at_v3 = inv_watt_resp_p_at_v3 - - if inv_watt_resp_p_at_v4 is not None: - self.inv_watt_resp_p_at_v4 = inv_watt_resp_p_at_v4 - - if inv_var_resp_v1 is not None: - self.inv_var_resp_v1 = inv_var_resp_v1 - - if inv_var_resp_v2 is not None: - self.inv_var_resp_v2 = inv_var_resp_v2 - - if inv_var_resp_v3 is not None: - self.inv_var_resp_v3 = inv_var_resp_v3 - - if inv_var_resp_v4 is not None: - self.inv_var_resp_v4 = inv_var_resp_v4 - - if inv_var_resp_q_at_v1 is not None: - self.inv_var_resp_q_at_v1 = inv_var_resp_q_at_v1 - - if inv_var_resp_q_at_v2 is not None: - self.inv_var_resp_q_at_v2 = inv_var_resp_q_at_v2 - - if inv_var_resp_q_at_v3 is not None: - self.inv_var_resp_q_at_v3 = inv_var_resp_q_at_v3 - - if inv_var_resp_q_at_v4 is not None: - self.inv_var_resp_q_at_v4 = inv_var_resp_q_at_v4 @property def inv_watt_resp_v1(self): @@ -423,13 +347,14 @@ def inv_var_resp_q_at_v4(self, value): _power_electronics_units, "A PowerElectronicsUnit", ) + power_electronics_units = Alias(units) phases: MridCollection[PowerElectronicsConnectionPhase] = LazyMridList( _power_electronics_connection_phases, "A PowerElectronicsConnectionPhase", backfill=Backfill(PowerElectronicsConnectionPhase.power_electronics_connection) ) - + power_electronics_connection_phases = Alias(phases) diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index c1c839b9f..c4a5ce1fe 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -11,7 +11,7 @@ from dataclasses import field from typing import List, Optional, TYPE_CHECKING -from zepben.ewb import remove_descriptor_annotations +from zepben.ewb import remove_descriptor_annotations, Alias from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill if sys.version_info >= (3, 13): @@ -120,13 +120,6 @@ class PowerTransformer(ConductingEquipment): The function of this transformer. """ - def __init__(self, *args, power_transformer_ends: List[PowerTransformerEnd] = None, **kwargs): - super(PowerTransformer, self).__init__(*args, **kwargs) - if power_transformer_ends: - for end in power_transformer_ends: - if end.power_transformer is None: - end.power_transformer = self - self.add_end(end) ends: PowerTransformerEndList = PowerTransformerEndList( _power_transformer_ends, @@ -135,6 +128,7 @@ def __init__(self, *args, power_transformer_ends: List[PowerTransformerEnd] = No validate=lambda self, it: self._validate_end(it), sort_by=lambda it: it.end_number ) + power_transformer_ends = Alias(ends) def _validate_end(self, end: PowerTransformerEnd): self._validate_reference_by_field(end, end.end_number, self.ends.get_by_num, "end_number") diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index aa503e247..804f29b7a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -8,8 +8,12 @@ __all__ = ["PowerTransformerEnd"] import warnings +from dataclasses import field from typing import Optional, List, Generator, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd @@ -22,6 +26,24 @@ from zepben.ewb.model.cim.iec61970.base.wires.power_transformer import PowerTransformer +class TransformerEndRatedSList(LazyValidatedList[TransformerEndRatedS]): + + def get_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS | None: + return next((rating for rating in self if rating.cooling_type == cooling_type), None) + + def remove_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS | None: + rating = self.get_by_cooling_type(cooling_type) + if rating is not None: + self.remove(rating) + return rating + + @zb_dataclass class PowerTransformerEnd(TransformerEnd): """ @@ -80,16 +102,14 @@ class PowerTransformerEnd(TransformerEnd): secondary side end of a transformer with vector group code of 'Dyn11', specify the connection kind as wye with neutral and specify the phase angle of the clock as 11. The clock value of the transformer end number specified as 1, is assumed to be zero.""" - _s_ratings: Optional[List[TransformerEndRatedS]] = None + _s_ratings: Optional[List[TransformerEndRatedS]] = field(default=None) """ Backing list for storing transformer ratings. Placed here to not mess with __init__ param order. Must always be placed at the end. Should not be used directly, instead use add_rating and get_rating functions. """ - def __init__(self, *args, power_transformer: PowerTransformer = None, rated_s: int = None, ratings: list[TransformerEndRatedS] = None, **kwargs): + def __init__(self, *args, rated_s: int = None, **kwargs): super(PowerTransformerEnd, self).__init__(*args, **kwargs) - if power_transformer: - self.power_transformer = power_transformer if self._s_ratings: raise ValueError("Do not directly set s_ratings through the constructor. You have one more constructor parameter than expected.") if rated_s and self._rated_s: @@ -104,9 +124,6 @@ def __init__(self, *args, power_transformer: PowerTransformer = None, rated_s: i if self._rated_s is not None: self.rated_s = self._rated_s self._rated_s = None - if ratings: - for rating in ratings: - self.add_rating(rating.rated_s, rating.cooling_type) @property def power_transformer(self): @@ -145,9 +162,15 @@ def rated_s(self, rated_s: Optional[int]): if rated_s is not None: self.add_transformer_end_rated_s(TransformerEndRatedS(TransformerCoolingType.UNKNOWN, rated_s)) - @property - def s_ratings(self) -> Generator[TransformerEndRatedS, None, None]: - return ngen(self._s_ratings) + s_ratings: TransformerEndRatedSList = TransformerEndRatedSList( + _s_ratings, + validate=lambda self, it: self._validate_rating(it), + sort_by=lambda it: -it.rated_s + ) + + def _validate_rating(self, rating: TransformerEndRatedS): + if any(it.cooling_type == rating.cooling_type for it in self.s_ratings): + raise ValueError(f"A rating for coolingType {rating.cooling_type.name} already exists, please remove it first.") def resistance_reactance(self): """ @@ -166,48 +189,68 @@ def resistance_reactance(self): else None ) - def num_ratings(self) -> int: - return nlen(self._s_ratings) - - def get_rating(self, cooling_type: TransformerCoolingType) -> TransformerEndRatedS: - if self._s_ratings: - for s_rating in self._s_ratings: - if s_rating.cooling_type == cooling_type: - return s_rating - raise KeyError(cooling_type) + # region deprecated list methods - def add_rating(self, rated_s: int, cooling_type: TransformerCoolingType = TransformerCoolingType.UNKNOWN) -> PowerTransformerEnd: - self._s_ratings = self._s_ratings if self._s_ratings else list() + # region s_ratings boilerplate - for s_rating in self._s_ratings: - if s_rating.cooling_type == cooling_type: - raise ValueError(f"A rating for coolingType {cooling_type.name} already exists, please remove it first.") - - self._s_ratings.append(TransformerEndRatedS(cooling_type, rated_s)) - - def sort_by_rated_s(t: TransformerEndRatedS) -> int: - return t.rated_s + @deprecated("Use len(s_ratings) instead.") + def num_ratings(self) -> int: + return len(self.s_ratings) + + @deprecated("Use s_ratings.get_by_cooling_type(cooling_type) instead.") + def get_rating( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS: + rating = self.s_ratings.get_by_cooling_type(cooling_type) + + if rating is None: + raise KeyError(cooling_type) + + return rating + + @deprecated("Use s_ratings.append(TransformerEndRatedS(cooling_type, rated_s)) instead.") + def add_rating( + self, + rated_s: int, + cooling_type: TransformerCoolingType = TransformerCoolingType.UNKNOWN, + ) -> PowerTransformerEnd: + self.s_ratings.append(TransformerEndRatedS(cooling_type, rated_s)) + return self - self._s_ratings.sort(key=sort_by_rated_s, reverse=True) + @deprecated("Use s_ratings.append(transformer_end_rated_s) instead.") + def add_transformer_end_rated_s( + self, + transformer_end_rated_s: TransformerEndRatedS, + ) -> PowerTransformerEnd: + self.s_ratings.append(transformer_end_rated_s) + return self + @deprecated("Use s_ratings.remove(transformer_end_rated_s) instead.") + def remove_rating( + self, + transformer_end_rated_s: TransformerEndRatedS, + ) -> PowerTransformerEnd: + self.s_ratings.remove(transformer_end_rated_s) return self - def add_transformer_end_rated_s(self, transformer_end_rated_s: TransformerEndRatedS) -> PowerTransformerEnd: - return self.add_rating(transformer_end_rated_s.rated_s, transformer_end_rated_s.cooling_type) + @deprecated("Use s_ratings.remove_by_cooling_type(cooling_type) instead.") + def remove_rating_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS: + rating = self.s_ratings.remove_by_cooling_type(cooling_type) - def remove_rating(self, transformer_end_rated_s: TransformerEndRatedS) -> PowerTransformerEnd: - self._s_ratings = safe_remove(self._s_ratings, transformer_end_rated_s) - return self + if rating is None: + raise IndexError(cooling_type) - def remove_rating_by_cooling_type(self, cooling_type: TransformerCoolingType) -> TransformerEndRatedS: - if self._s_ratings: - for transformer_end_rated_s in self._s_ratings: - if transformer_end_rated_s.cooling_type == cooling_type: - self._s_ratings.remove(transformer_end_rated_s) - self._s_ratings = self._s_ratings if self._s_ratings else None - return transformer_end_rated_s - raise IndexError(cooling_type) + return rating + @deprecated("Use s_ratings.clear() instead.") def clear_ratings(self) -> PowerTransformerEnd: - self._s_ratings = None + self.s_ratings.clear() return self + + # endregion + + # endregion \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py b/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py index 504ead31f..12e93c6c8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/shunt_compensator.py @@ -57,11 +57,6 @@ class ShuntCompensator(RegulatingCondEq, metaclass=ABCMeta): _grounding_terminal: 'Terminal | None' = None sections: Optional[float] = None - def __init__(self, *args, grounding_terminal = None, **kwargs): - super(ShuntCompensator, self).__init__(*args, **kwargs) - if grounding_terminal is not None: - self.grounding_terminal = grounding_terminal - @property @deprecated("use asset_info instead.") def shunt_compensator_info(self) -> Optional['ShuntCompensatorInfo']: From 48a9f8b67f7ac77bb14f2c77ac1754bf53fc158f Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 27 Jul 2026 12:13:06 +1000 Subject: [PATCH 30/54] Backfill finished Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/mrid_list.py | 11 ++-- .../model/cim/iec61970/base/core/feeder.py | 13 ++--- .../base/core/sub_geographical_region.py | 34 +++---------- .../cim/iec61970/base/core/substation.py | 50 ++++++++----------- .../model/cim/iec61970/base/core/terminal.py | 6 ++- .../base/diagramlayout/diagram_object.py | 25 +++++----- .../base/wires/ac_line_segment_phase.py | 10 ++-- .../model/cim/iec61970/base/wires/clamp.py | 18 ++++--- .../ewb/model/cim/iec61970/base/wires/cut.py | 16 +++--- .../base/wires/energy_consumer_phase.py | 15 +++--- .../base/wires/energy_source_phase.py | 18 ++++--- .../power_electronics_connection_phase.py | 16 +++--- .../base/wires/power_transformer_end.py | 8 ++- 13 files changed, 113 insertions(+), 127 deletions(-) diff --git a/src/zepben/ewb/boilerplate/mrid_list.py b/src/zepben/ewb/boilerplate/mrid_list.py index 3b31802c0..195254139 100644 --- a/src/zepben/ewb/boilerplate/mrid_list.py +++ b/src/zepben/ewb/boilerplate/mrid_list.py @@ -49,16 +49,11 @@ def _can_add_by_mrid(self, element: S) -> bool: class Backfill: - def __init__(self, backfill_prop: Any): - if not any(isinstance(backfill_prop, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): - raise TypeError(f"backfill_prop parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {backfill_prop}") + def __init__(self, backfill_prop: property): self.backfill_prop = backfill_prop def apply(self, element: S, owner: Any): - if isinstance(self.backfill_prop, property): - name = self.backfill_prop.fget.__name__ - else: - name = self.backfill_prop.__name__ + name = self.backfill_prop.fget.__name__ if hasattr(self.backfill_prop, "__target"): backing_name = self.backfill_prop.__target.__name__ @@ -72,7 +67,7 @@ def apply(self, element: S, owner: Any): if ref is not owner: raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") -def targets(target: Any): +def internal(target: Any): if not any(isinstance(target, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): raise TypeError(f"target parameter of the target decorator has to be an instance of dataclass Field, instead is {target}") diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 83a62f353..aa312b5f4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb import get_by_mrid, remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, internal from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer @@ -38,8 +38,7 @@ class Feeder(EquipmentContainer): _normal_head_terminal: Terminal | None = None """The normal head terminal or terminals of the feeder.""" - normal_energizing_substation: Substation | None = field(default=None) - """The substation that normally energizes the feeder. Also used for naming purposes.""" + _normal_energizing_substation: Substation | None = field(default=None) _current_equipment_by_id: Dict[str, Equipment] | None = field(default=None) """The equipment contained in this feeder in the current state of the network.""" @@ -96,6 +95,7 @@ def normal_head_terminal(self, term: Optional[Terminal]): ) """[ZBEX]""" @property + @internal(_normal_energizing_substation) def normal_energizing_substation(self): """The substation that normally energizes the feeder. Also used for naming purposes.""" return self._normal_energizing_substation @@ -105,13 +105,6 @@ def normal_energizing_substation(self): def normal_energizing_substation(self, value): self._normal_energizing_substation = value - @property - def current_equipment(self) -> Generator[Equipment, None, None]: - """ - Contained `Equipment` using the current state of the network. - """ - return ngen(self._current_equipment) - # region deprecated list boilerplate # region current_equipment boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index b90c911cf..448a12ba2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, targets +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.substation import Substation @@ -31,40 +31,22 @@ class SubGeographicalRegion(IdentifiedObject): _substations: Optional[List[Substation]] = field(default=None) - - @property - @targets(_geographical_region) - def geographical_region(self): - """The geographical region to which this sub-geographical region is within.""" - return self._geographical_region - - @geographical_region.setter - @deprecated("Geographical region is a backfill property - it should only be set by adding the sub region to the sub regions list") - def geographical_region(self, value): - self._geographical_region = value - @property + @internal(_geographical_region) def geographical_region(self): """The geographical region to which this sub-geographical region is within.""" return self._geographical_region - @geographical_region.setter - @deprecated("geographical_region should never be set directly - it is automatically set when adding it to the `sub_geographical_regions` list") - def geographical_region(self, value): - self._geographical_region = value - - def num_substations(self) -> int: - """ - Returns The number of `Substation`s associated with this `SubGeographicalRegion` - """ - return nlen(self._substations) - substations: MridCollection[Substation] = LazyMridList( _substations, "A Substation", backfill=Backfill(Substation.sub_geographical_region) ) + @geographical_region.setter + @deprecated("Geographical region is a backfill property - it should only be set by adding the sub region to the sub regions list") + def geographical_region(self, value): + self._geographical_region = value # region deprecated list boilerplate # region substations boilerplate @@ -95,7 +77,3 @@ def clear_substations(self) -> SubGeographicalRegion: # endregion substations boilerplate # endregion deprecated list boilerplate - -if __name__ == '__main__': - s = SubGeographicalRegion("") - s.geographical_region = GeographicalRegion("") \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index d34be25fb..3904988b4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -15,7 +15,7 @@ from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder if TYPE_CHECKING: @@ -31,15 +31,31 @@ class Substation(EquipmentContainer): is passed for the purposes of switching or modifying its characteristics. """ - sub_geographical_region: Optional[SubGeographicalRegion] = field(default=None) - """The SubGeographicalRegion containing the substation.""" + _sub_geographical_region: Optional[SubGeographicalRegion] = field(default=None) _normal_energized_feeders: Optional[List[Feeder]] = field(default=None) + feeders: MridCollection[Feeder] = LazyMridList( + _normal_energized_feeders, + "A Feeder", + backfill=Backfill(Feeder.normal_energizing_substation) + ) + normal_energized_feeders = Alias(feeders) + _loops: Optional[List[Loop]] = field(default=None) + loops: MridCollection[Loop] = LazyMridList( + _loops, + "A Loop", + ) + _energized_loops: Optional[List[Loop]] = field(default=None) + energized_loops: MridCollection[Loop] = LazyMridList( + _energized_loops, + "A Loop", + ) + _circuits: Optional[List[Circuit]] = field(default=None) circuits: MridCollection[Circuit] = LazyMridList( @@ -48,40 +64,18 @@ class Substation(EquipmentContainer): ) @property + @internal(_sub_geographical_region) def sub_geographical_region(self): """The SubGeographicalRegion containing the substation.""" return self._sub_geographical_region + + @sub_geographical_region.setter @deprecated("sub_geographical_region should never be set directly - it is automatically set when adding it to the `substations` list") def sub_geographical_region(self, value): self._sub_geographical_region = value - @property - def circuits(self) -> Generator[Circuit, None, None]: - """ - The `Circuit`s originating from this substation. - """ - return ngen(self._circuits) - - loops: MridCollection[Loop] = LazyMridList( - _loops, - "A Loop", - ) - - energized_loops: MridCollection[Loop] = LazyMridList( - _energized_loops, - "A Loop", - ) - - feeders: MridCollection[Feeder] = LazyMridList( - _normal_energized_feeders, - "A Feeder", - backfill=Backfill(Feeder.normal_energizing_substation) - ) - normal_energized_feeders = Alias(feeders) - - # region deprecated list boilerplate # region circuits boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py index aab8434ad..0db98ffce 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py @@ -7,12 +7,14 @@ __all__ = ["Terminal"] +from dataclasses import field from typing import Optional, Generator from typing import TYPE_CHECKING from weakref import ref, ReferenceType from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import AcDcTerminal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode @@ -31,7 +33,7 @@ class Terminal(AcDcTerminal): An AC electrical connection point to a piece of conducting equipment. Terminals are connected at physical connection points called connectivity nodes. """ - _conducting_equipment: Optional['ConductingEquipment'] = None + _conducting_equipment: Optional['ConductingEquipment'] = field(default=None) """The conducting equipment of the terminal. Conducting equipment have terminals that may be connected to other conducting equipment terminals via connectivity nodes.""" @@ -75,6 +77,7 @@ def current_phases(self) -> PhaseStatus: return self._current_phases @property + @internal(_conducting_equipment) def conducting_equipment(self): """ The conducting equipment of the terminal. Conducting equipment have terminals that may be connected to other conducting equipment terminals via @@ -162,6 +165,7 @@ def is_feeder_head_terminal(self): for feeder in filter(lambda c: isinstance(c, Feeder), self.conducting_equipment.containers): if feeder.normal_head_terminal == self: return True + return False def has_connected_busbars(self): from zepben.ewb.model.cim.iec61970.base.wires.busbar_section import BusbarSection diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index f79ce5672..8f7339f76 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -14,6 +14,7 @@ from zepben.ewb import Alias from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint from zepben.ewb.util import nlen, ngen, require, safe_remove @@ -30,7 +31,13 @@ class DiagramObject(IdentifiedObject): analog values, breakers, disconnectors, power transformers, and transmission lines. """ - _diagram: Optional[Diagram] = None + _diagram: Optional[Diagram] = field(default=None) + + @property + @internal(_diagram) + def diagram(self): + """A diagram object is part of a diagram.""" + return self._diagram identified_object_mrid: Optional[str] = None """The domain object to which this diagram object is associated.""" @@ -43,11 +50,12 @@ class DiagramObject(IdentifiedObject): _diagram_object_points: Optional[List[DiagramObjectPoint]] = field(default=None) + points: LazyIndexedList[DiagramObjectPoint] = LazyIndexedList( + _diagram_object_points, + "DiagramObjectPoint", + ) + diagram_object_points = Alias(points) - @property - def diagram(self): - """A diagram object is part of a diagram.""" - return self._diagram @diagram.setter @deprecated("diagram should never be set directly - it is automatically set when adding it to the `diagram_objects` list") @@ -57,13 +65,6 @@ def diagram(self, diag): else: raise ValueError(f"diagram for {str(self)} has already been set to {self._diagram}, cannot reset this field to {diag}") - points: LazyIndexedList[DiagramObjectPoint] = LazyIndexedList( - _diagram_object_points, - "DiagramObjectPoint", - ) - diagram_object_points = Alias(points) - - # region deprecated list boilerplate # # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py index 929c6e1f3..b9148bd87 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py @@ -7,10 +7,12 @@ __all__ = ['AcLineSegmentPhase'] +from dataclasses import field from typing import TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo @@ -35,12 +37,15 @@ class AcLineSegmentPhase(PowerSystemResource): phase: SinglePhaseKind = SinglePhaseKind.X sequence_number: int | None = None - _ac_line_segment: AcLineSegment | None = None + _ac_line_segment: AcLineSegment | None = field(default=None) @property + @internal(_ac_line_segment) def ac_line_segment(self) -> 'AcLineSegment | None': return self._ac_line_segment + asset_info: WireInfo | None = None + @ac_line_segment.setter @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `phases` list") def ac_line_segment(self, ac_line_segment: 'AcLineSegment') -> None: @@ -48,6 +53,3 @@ def ac_line_segment(self, ac_line_segment: 'AcLineSegment') -> None: self._ac_line_segment = ac_line_segment else: raise ValueError(f"ac_line_segment has already been set to {self._ac_line_segment}. Cannot set this field again") - - - asset_info: WireInfo | None = None diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index 355dd6288..73b667165 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -8,6 +8,9 @@ from dataclasses import field from typing import Optional, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -25,21 +28,22 @@ class Clamp(ConductingEquipment): length_from_terminal_1: Optional[float] = None """The length to the place where the clamp is located starting from side one of the line segment, i.e. the line segment terminal with sequence number equal to 1.""" - ac_line_segment: Optional['AcLineSegment'] = field(default=None) + _ac_line_segment: Optional['AcLineSegment'] = field(default=None) """The line segment to which the clamp is connected.""" - max_terminals = 1 - @property + @internal(_ac_line_segment) def ac_line_segment(self) -> Optional['AcLineSegment']: """The line segment to which the clamp is connected.""" return self._ac_line_segment - @ac_line_segment.setter - @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `clamps` list") - def ac_line_segment(self, value): - self._ac_line_segment = value + max_terminals = 1 @property def length_from_t1_or_0(self) -> float: return self.length_from_terminal_1 or 0.0 + + @ac_line_segment.setter + @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `clamps` list") + def ac_line_segment(self, value): + self._ac_line_segment = value diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index d8dc18b72..0bf40b4cb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -8,6 +8,9 @@ from dataclasses import field from typing import Optional, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -31,20 +34,19 @@ class Cut(Switch): length_from_terminal_1: Optional[float] = None """The length to the place where the cut is located starting from side one of the cut line segment, i.e. the line segment Terminal with sequenceNumber equal to 1.""" - ac_line_segment: Optional['AcLineSegment'] = field(default=None) - """The line segment to which the cut is applied.""" - _ac_line_segment: Optional['AcLineSegment'] = None + _ac_line_segment: Optional['AcLineSegment'] = field(default=None) @property + @internal(_ac_line_segment) def ac_line_segment(self) -> Optional['AcLineSegment']: """The line segment to which the cut is applied.""" return self._ac_line_segment + @property + def length_from_t1_or_0(self) -> float: + return self.length_from_terminal_1 or 0.0 + @ac_line_segment.setter @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `cuts` list") def ac_line_segment(self, value): self._ac_line_segment = value - - @property - def length_from_t1_or_0(self) -> float: - return self.length_from_terminal_1 or 0.0 diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py index 304091e80..534558673 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py @@ -5,10 +5,12 @@ __all__ = ["EnergyConsumerPhase"] +from dataclasses import field from typing import Optional, TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -21,7 +23,13 @@ class EnergyConsumerPhase(PowerSystemResource): """A single phase of an energy consumer.""" - _energy_consumer: Optional['EnergyConsumer'] = None + _energy_consumer: Optional['EnergyConsumer'] = field(default=None) + + @property + @internal(_energy_consumer) + def energy_consumer(self): + """The `EnergyConsumer` that has this phase.""" + return self._energy_consumer phase: SinglePhaseKind = SinglePhaseKind.X """Phase of this energy consumer component. If the energy consumer is wye connected, the connection is from the indicated phase to the central ground or @@ -42,11 +50,6 @@ class EnergyConsumerPhase(PowerSystemResource): q_fixed: Optional[float] = None """Reactive power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" - @property - def energy_consumer(self): - """The `EnergyConsumer` that has this phase.""" - return self._energy_consumer - @energy_consumer.setter @deprecated("energy_consumer should never be set directly - it is automatically set when adding it to the `phases` list") def energy_consumer(self, ec): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py index 4b2fcfe5a..847ca36da 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py @@ -5,10 +5,12 @@ __all__ = ["EnergySourcePhase"] +from dataclasses import field from typing import Optional, TYPE_CHECKING from typing_extensions import deprecated +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -23,21 +25,21 @@ class EnergySourcePhase(PowerSystemResource): A single phase of an energy source. """ - _energy_source: Optional['EnergySource'] = None - """The `zepben.ewb.model.cim.iec61970.wires.EnergySource` with this `EnergySourcePhase`""" - - phase: SinglePhaseKind = SinglePhaseKind.NONE - """A `zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind.SinglePhaseKind` Phase of this energy source component. If the energy source is wye connected, - the connection is from the indicated phase to the central ground or neutral point. If the energy source is delta connected, the phase indicates an energy - source connected from the indicated phase to the next logical non-neutral phase.""" + _energy_source: Optional['EnergySource'] = field(default=None) @property + @internal(_energy_source) def energy_source(self): """The `EnergySource` with this `EnergySourcePhase`""" return self._energy_source + phase: SinglePhaseKind = SinglePhaseKind.NONE + """A `zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind.SinglePhaseKind` Phase of this energy source component. If the energy source is wye connected, + the connection is from the indicated phase to the central ground or neutral point. If the energy source is delta connected, the phase indicates an energy + source connected from the indicated phase to the next logical non-neutral phase.""" + @energy_source.setter - @deprecated("energy_sounrce should never be set directly - it is automatically set when adding it to the `phases` list") + @deprecated("energy_source should never be set directly - it is automatically set when adding it to the `phases` list") def energy_source(self, es): if self._energy_source is None or self._energy_source is es: self._energy_source = es diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py index ef5f3d102..aed773497 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py @@ -8,6 +8,9 @@ from dataclasses import field from typing import Optional, TYPE_CHECKING +from typing_extensions import deprecated + +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -20,9 +23,15 @@ class PowerElectronicsConnectionPhase(PowerSystemResource): """A single phase of a power electronics connection.""" - power_electronics_connection: Optional['PowerElectronicsConnection'] = None + _power_electronics_connection: Optional['PowerElectronicsConnection'] = field(default=None) """The power electronics connection to which the phase belongs.""" + @property + @internal(_power_electronics_connection) + def power_electronics_connection(self): + """The power electronics connection to which the phase belongs.""" + return self._power_electronics_connection + p: Optional[float] = None """Active power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" @@ -36,11 +45,6 @@ class PowerElectronicsConnectionPhase(PowerSystemResource): q: Optional[float] = None """Reactive power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" - @property - def power_electronics_connection(self): - """The power electronics connection to which the phase belongs.""" - return self._power_electronics_connection - @power_electronics_connection.setter @deprecated("power_electronics_connection should never be set directly - it is automatically set when adding it to the `phases` list") def power_electronics_connection(self, value): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index 804f29b7a..87bf23c7b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -13,7 +13,9 @@ from typing_extensions import deprecated +from zepben.ewb import Alias from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList +from zepben.ewb.dataclass_descriptors.mrid_list import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd @@ -62,7 +64,7 @@ class PowerTransformerEnd(TransformerEnd): Instead use the TransformerMeshImpedance or split the transformer into multiple PowerTransformers. """ - _power_transformer: Optional[PowerTransformer] = None + _power_transformer: Optional[PowerTransformer] = field(default=None) """The power transformer of this power transformer end.""" _rated_s: Optional[int] = None @@ -110,7 +112,7 @@ class PowerTransformerEnd(TransformerEnd): def __init__(self, *args, rated_s: int = None, **kwargs): super(PowerTransformerEnd, self).__init__(*args, **kwargs) - if self._s_ratings: + if "_s_ratings" in kwargs: raise ValueError("Do not directly set s_ratings through the constructor. You have one more constructor parameter than expected.") if rated_s and self._rated_s: raise ValueError(f"Cannot specify both rated_s and _rated_s properties when constructing {self}. Check your constructor parameters.") @@ -126,6 +128,7 @@ def __init__(self, *args, rated_s: int = None, **kwargs): self._rated_s = None @property + @internal(_power_transformer) def power_transformer(self): """The power transformer of this power transformer end.""" return self._power_transformer @@ -167,6 +170,7 @@ def rated_s(self, rated_s: Optional[int]): validate=lambda self, it: self._validate_rating(it), sort_by=lambda it: -it.rated_s ) + ratings = Alias(s_ratings) def _validate_rating(self, rating: TransformerEndRatedS): if any(it.cooling_type == rating.cooling_type for it in self.s_ratings): From fdd247baf6758ff6cb7f7f7fbfe6fabd6f758b8d Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 27 Jul 2026 14:37:06 +1000 Subject: [PATCH 31/54] Rearrange boilerplate; Fix tests to assume list-like object instead of a generator Signed-off-by: Boris Filin --- src/zepben/ewb/__init__.py | 2 +- src/zepben/ewb/boilerplate/backfill.py | 4 + .../ewb/boilerplate/collections/__init__.py | 4 + .../ewb/boilerplate/collections/base.py | 73 ++++ .../ewb/boilerplate/collections/lazy_list.py | 164 +++++++++ .../{ => collections}/mrid_list.py | 15 +- .../boilerplate/{ => collections}/mrid_map.py | 10 +- .../ewb/boilerplate/collections/wrapper.py | 80 +++++ ...backed_descriptor.py => descriptor_fix.py} | 0 src/zepben/ewb/boilerplate/lazy_list.py | 322 ------------------ src/zepben/ewb/boilerplate/tst.py | 167 --------- .../iec61968/assetinfo/relay_info.py | 4 +- .../iec61968/common/contact_details.py | 5 +- .../extensions/iec61970/base/feeder/loop.py | 8 +- .../iec61970/base/feeder/lv_feeder.py | 10 +- .../iec61970/base/feeder/lv_substation.py | 5 +- .../protection/protection_relay_function.py | 9 +- .../protection/protection_relay_scheme.py | 8 +- .../protection/protection_relay_system.py | 8 +- .../assetinfo/power_transformer_info.py | 8 +- .../assetinfo/transformer_tank_info.py | 8 +- .../ewb/model/cim/iec61968/assets/asset.py | 8 +- .../ewb/model/cim/iec61968/common/location.py | 4 +- .../model/cim/iec61968/customers/customer.py | 9 +- .../iec61968/customers/customer_agreement.py | 8 +- .../iec61968/customers/pricing_structure.py | 8 +- .../iec61968/infiec61968/infassets/pole.py | 8 +- .../model/cim/iec61968/metering/end_device.py | 8 +- .../cim/iec61968/metering/usage_point.py | 9 +- .../operations/operational_restriction.py | 8 +- .../base/auxiliaryequipment/sensor.py | 8 +- .../base/core/conducting_equipment.py | 6 +- .../iec61970/base/core/connectivity_node.py | 5 +- .../ewb/model/cim/iec61970/base/core/curve.py | 8 +- .../model/cim/iec61970/base/core/equipment.py | 8 +- .../iec61970/base/core/equipment_container.py | 9 +- .../model/cim/iec61970/base/core/feeder.py | 9 +- .../iec61970/base/core/geographical_region.py | 4 +- .../ewb/model/cim/iec61970/base/core/name.py | 2 +- .../model/cim/iec61970/base/core/name_type.py | 4 +- .../base/core/power_system_resource.py | 9 +- .../base/core/sub_geographical_region.py | 4 +- .../cim/iec61970/base/core/substation.py | 9 +- .../model/cim/iec61970/base/core/terminal.py | 2 +- .../iec61970/base/diagramlayout/diagram.py | 6 +- .../base/diagramlayout/diagram_object.py | 7 +- .../generation/production/battery_unit.py | 8 +- .../iec61970/base/wires/ac_line_segment.py | 4 +- .../base/wires/ac_line_segment_phase.py | 2 +- .../model/cim/iec61970/base/wires/clamp.py | 2 +- .../ewb/model/cim/iec61970/base/wires/cut.py | 2 +- .../iec61970/base/wires/energy_consumer.py | 4 +- .../base/wires/energy_consumer_phase.py | 2 +- .../cim/iec61970/base/wires/energy_source.py | 4 +- .../base/wires/energy_source_phase.py | 2 +- .../base/wires/per_length_phase_impedance.py | 6 +- .../wires/power_electronics_connection.py | 4 +- .../power_electronics_connection_phase.py | 2 +- .../iec61970/base/wires/power_transformer.py | 4 +- .../base/wires/power_transformer_end.py | 7 +- .../iec61970/base/wires/protected_switch.py | 8 +- .../iec61970/base/wires/regulating_control.py | 8 +- .../base/wires/synchronous_machine.py | 8 +- .../iec61970/infiec61970/feeder/circuit.py | 8 +- test/busbranch/test_bus_branch.py | 8 +- .../test_basic_descriptor.py | 4 +- .../test_dataclass_base.py | 2 +- .../common/service_comparator_validator.py | 18 +- .../tracing/test_assign_to_lv_feeders.py | 2 +- 69 files changed, 496 insertions(+), 707 deletions(-) create mode 100644 src/zepben/ewb/boilerplate/backfill.py create mode 100644 src/zepben/ewb/boilerplate/collections/__init__.py create mode 100644 src/zepben/ewb/boilerplate/collections/base.py create mode 100644 src/zepben/ewb/boilerplate/collections/lazy_list.py rename src/zepben/ewb/boilerplate/{ => collections}/mrid_list.py (91%) rename src/zepben/ewb/boilerplate/{ => collections}/mrid_map.py (93%) create mode 100644 src/zepben/ewb/boilerplate/collections/wrapper.py rename src/zepben/ewb/boilerplate/{backed_descriptor.py => descriptor_fix.py} (100%) delete mode 100644 src/zepben/ewb/boilerplate/lazy_list.py delete mode 100644 src/zepben/ewb/boilerplate/tst.py diff --git a/src/zepben/ewb/__init__.py b/src/zepben/ewb/__init__.py index 114dcc8e3..c3ea06945 100644 --- a/src/zepben/ewb/__init__.py +++ b/src/zepben/ewb/__init__.py @@ -14,7 +14,7 @@ from zepben.ewb.auth.common.auth_exception import * from zepben.ewb.auth.common.auth_method import * from zepben.ewb.boilerplate.dataclass_base import * -from zepben.ewb.boilerplate.backed_descriptor import * +from zepben.ewb.boilerplate.descriptor_fix import * from zepben.ewb.util import * ############# diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py new file mode 100644 index 000000000..35e3122c5 --- /dev/null +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -0,0 +1,4 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/src/zepben/ewb/boilerplate/collections/__init__.py b/src/zepben/ewb/boilerplate/collections/__init__.py new file mode 100644 index 000000000..35e3122c5 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/src/zepben/ewb/boilerplate/collections/base.py b/src/zepben/ewb/boilerplate/collections/base.py new file mode 100644 index 000000000..fcd9c9524 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/base.py @@ -0,0 +1,73 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC, abstractmethod +from typing import Collection, Protocol, Iterable, Generic, Iterator, Callable, Any, Sequence, overload, TypeVar + + +T = TypeVar("T") + + +class AbstractBackedCollection(Collection[T], Generic[T], ABC): + + @abstractmethod + def _get_collection(self) -> Collection[T]: + ... + + @abstractmethod + def append(self, element: T) -> None: + ... + + def extend(self, elements: Iterable[T]) -> None: + for element in elements: + self.append(element) + + @abstractmethod + def remove(self, element: T) -> None: + ... + + @abstractmethod + def clear(self) -> None: + ... + + def __len__(self) -> int: + return len(self._get_collection()) + + def __iter__(self) -> Iterator[T]: + return iter(self._get_collection()) + + def __contains__(self, element: object) -> bool: + return element in self._get_collection() + + def for_each_indexed(self, action: Callable[[int, T], Any]): + """ + Call the `action` on each item in the list + + :param action: An action to apply to each :class:`RelaySetting` in the `thresholds` collection, taking the index of the threshold, and the threshold itself. + """ + for index, item in enumerate(self._get_collection()): + action(index, item) + + +class AbstractBackedList( + AbstractBackedCollection[T], + Sequence[T], + Generic[T], + ABC, +): + + @abstractmethod + def _get_collection(self) -> Sequence[T]: + ... + + @overload + def __getitem__(self, index: int) -> T: + ... + + @overload + def __getitem__(self, index: slice) -> Sequence[T]: + ... + + def __getitem__(self, index: int | slice) -> T | Sequence[T]: + return self._get_collection()[index] diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py new file mode 100644 index 000000000..b7a28dcb5 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -0,0 +1,164 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from dataclasses import field +from typing import TypeVar, Protocol + +from zepben.ewb import DataclassBase, zb_dataclass +from zepben.ewb.boilerplate.collections.base import AbstractBackedList +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper + + +# from descriptor_fix import BackedDescriptor, remove_descriptor_annotations + + +class HasMrid(Protocol): + mrid: str + +T = TypeVar("T") +S = TypeVar("S", bound=HasMrid) + + +class LazyValidatedList(_IterableWrapper, AbstractBackedList[T]): + + def __init__(self, + private_field, + validate=None, + sort_by=None): + super().__init__(private_field) + self.validate = validate + self.sort_by = sort_by + + def _get(self): + return getattr(self.instance, self.backing_name) + + def _get_collection(self) -> list[T]: + return getattr(self.instance, self.backing_name) or [] + + # clearIfEmpty is inlined for performance + + def append(self, item): + if self.validate is not None: + self.validate(self.instance, item) + + existing = getattr(self.instance, self.backing_name) + if existing is None: + existing = [item] + setattr(self.instance, self.backing_name, existing) + else: + existing.append(item) + + if self.sort_by is not None: + existing.sort(key=self.sort_by) + + def __len__(self): + return len(self._get_collection()) + + def __iter__(self): + return iter(self._get_collection()) + + def __contains__(self, item): + return item in (self._get_collection()) + + def remove(self, item): + existing = self._get_collection() + existing.remove(item) + if not existing: + self.clear() + + def clear(self): + setattr(self.instance, self.backing_name, None) + + def __repr__(self): + if self.instance is None: + return self.__class__.__repr__() + return str(self._get_collection()) + + def __str__(self): + return self.__repr__() + + def __getitem__(self, item): + return (self._get_collection())[item] + + +class LazyIndexedList(LazyValidatedList[T]): + + def __init__( + self, + private_field, + element_description: str + ): + super().__init__(private_field) + self.element_description = element_description + + def insert(self, index: int, item: T) -> None: + """ + Insert ``item`` at ``index``. + """ + size = len(self) + + if not 0 <= index <= size: + raise ValueError( + f"Unable to add {self.element_description} to " + f"{self.instance}. " + f"Sequence number {index} is invalid. " + f"Expected a value between 0 and {size}. " + "Make sure you are adding the items in order and there are " + "no gaps in the numbering." + ) + + existing = getattr(self.instance, self.backing_name) + + if existing is None: + existing = [item] + setattr(self.instance, self.backing_name, existing) + else: + existing.insert(index, item) + + if self.sort_by is not None: + existing.sort(key=self.sort_by) + + def append(self, item: T) -> None: + self.insert(len(self), item) + + def pop(self, index: int = -1) -> T: + """ + Remove and return the item at ``index``. + + Uses normal Python list semantics, including support for negative + indexes and raising ``IndexError`` when the index is invalid. + """ + existing = getattr(self.instance, self.backing_name) + + if existing is None: + raise IndexError("pop from empty list") + + item = existing.pop(index) + + if not existing: + self.clear() + + return item + +if __name__ == '__main__': + @zb_dataclass + class C(DataclassBase): + _xs: list[int] | None = field(default=None) + xs: LazyIndexedList[int] = LazyIndexedList( + _xs, + "Thing" + ) + + c = C(xs=[24, 42]) + print(c._xs, c.xs) + c.xs.clear() + print(c._xs, c.xs) + c.xs.append(1) + c.xs.append(2) + c.xs.append(3) + print(c._xs, c.xs) + c.xs.insert(2, 42) + print(c._xs, c.xs) + c.xs.pop(2) + print(c._xs, c.xs) diff --git a/src/zepben/ewb/boilerplate/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py similarity index 91% rename from src/zepben/ewb/boilerplate/mrid_list.py rename to src/zepben/ewb/boilerplate/collections/mrid_list.py index 195254139..29b2f691e 100644 --- a/src/zepben/ewb/boilerplate/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -5,13 +5,15 @@ from abc import abstractmethod, ABC from dataclasses import dataclass, field, Field from types import MemberDescriptorType -from typing import Any, TypeVar, Protocol, Callable +from typing import Any, TypeVar, Protocol, Callable, Sequence import pytest from typing_extensions import Self -from zepben.ewb import remove_descriptor_annotations, BackedDescriptor, require -from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList, MutableCollection, IndexedMutableCollection, _IterableWrapper +from zepben.ewb import BackedDescriptor +from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper +from zepben.ewb.boilerplate.collections.base import AbstractBackedCollection, AbstractBackedList class HasMrid(Protocol): @@ -20,7 +22,7 @@ class HasMrid(Protocol): T = TypeVar("T") S = TypeVar("S", bound=HasMrid) -class MridCollection(MutableCollection[S], ABC): +class MridCollection(AbstractBackedCollection[S], ABC): instance: Any element_description: str @@ -104,7 +106,7 @@ def append(self, item: T): super().append(item) -class MridList(_IterableWrapper, IndexedMutableCollection[S], MridCollection[S]): +class MridList(_IterableWrapper, AbstractBackedList[S], MridCollection[S]): def __init__(self, private_field, element_description: str, @@ -126,6 +128,9 @@ def __get__(self, instance, owner=None) -> Self: def __post_init__(self): self.backing_list = getattr(self.instance, self.backing_name) + def _get_collection(self) -> Sequence[T]: + return self.backing_list + def _safe_get_by_mrid(self, mrid: str) -> S | None: found = next((element for element in self.backing_list if element.mrid == mrid), None) return found diff --git a/src/zepben/ewb/boilerplate/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py similarity index 93% rename from src/zepben/ewb/boilerplate/mrid_map.py rename to src/zepben/ewb/boilerplate/collections/mrid_map.py index 0f71c474c..ac5af32aa 100644 --- a/src/zepben/ewb/boilerplate/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -3,13 +3,12 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import dataclass, field -from typing import Protocol, TypeVar +from typing import Protocol, TypeVar, Collection import pytest -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.lazy_list import _IterableWrapper -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, Backfill +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, Backfill class HasMrid(Protocol): @@ -20,6 +19,9 @@ class HasMrid(Protocol): class LazyMridMap(_IterableWrapper[S], MridCollection[S]): + def _get_collection(self) -> Collection[T]: + pass + def __init__( self, private_field, diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py new file mode 100644 index 000000000..9913f8df8 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -0,0 +1,80 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC +from typing import Any +from typing_extensions import Self, deprecated + +from zepben.ewb import BackedDescriptor, resolve_default +from zepben.ewb.boilerplate.collections.base import AbstractBackedList + + +class _Wrapper(BackedDescriptor): + + instance: Any + backing_name: Any + + def __new__(cls, *args: Any, **kwargs: Any) -> Self: + """ + This __new__ stores the init arguments, and otherwise acts as default. + We need this to efficiently and truly re-create the wrapper and bind it to an instance upon a get call. + """ + obj = super().__new__(cls) + obj._init_args = args + obj._init_kwargs = kwargs.copy() + return obj + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.instance = None + self.backing_name = None + + def __get__(self, instance, _=None) -> Self: + """ + This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. + From there on, the wrapper functions as a lazy list. + """ + if instance is None: + return self + obj = type(self)(*self._init_args, **self._init_kwargs) + obj.instance = instance + obj.backing_name = self.private_field.name + return obj + + +class _WrapperFgetFix(_Wrapper): + """ + This class exists to fix the tests that rely on the old lists being @property. + TODO: Remove in a separate PR fixing tests + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + def fget(instance): + return self.__get__(instance) + self.fget = fget + + def __set_name__(self, owner, name): + super().__set_name__(owner, name) + self.fget.__name__ = name + self.fget.__qualname__ = f"{owner.__qualname__}.{name}" + + @deprecated("Lists are no longer a property") + def fget(self, instance): ... + + +class _IterableWrapper(_WrapperFgetFix, AbstractBackedList, ABC): + """ + This class allows us to assign lists at init time to avoid special-case handling. + """ + def __set__(self, instance, value): + if instance is None: + return + if not hasattr(instance, self.backing_name): + resolve_default(instance, self.private_field) + elif getattr(instance, self.backing_name): + raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") + if value is not None: + self.__get__(instance).extend(value) + else: + self.__get__(instance).clear() diff --git a/src/zepben/ewb/boilerplate/backed_descriptor.py b/src/zepben/ewb/boilerplate/descriptor_fix.py similarity index 100% rename from src/zepben/ewb/boilerplate/backed_descriptor.py rename to src/zepben/ewb/boilerplate/descriptor_fix.py diff --git a/src/zepben/ewb/boilerplate/lazy_list.py b/src/zepben/ewb/boilerplate/lazy_list.py deleted file mode 100644 index 7969c2c1f..000000000 --- a/src/zepben/ewb/boilerplate/lazy_list.py +++ /dev/null @@ -1,322 +0,0 @@ -# Copyright 2026 Zeppelin Bend Pty Ltd -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -from abc import ABC, abstractmethod -from collections.abc import Collection -from dataclasses import field -from typing import TypeVar, Protocol, Any, Iterable, Generic, Iterator, Sequence, overload, Callable - -from typing_extensions import deprecated, Self - -from zepben.ewb import BackedDescriptor, remove_descriptor_annotations, resolve_default, DataclassBase, zb_dataclass - - -# from descriptor_fix import BackedDescriptor, remove_descriptor_annotations - - -class HasMrid(Protocol): - mrid: str - -T = TypeVar("T") -S = TypeVar("S", bound=HasMrid) - -class MutableCollection(Collection[T], Protocol[T]): - def append(self, item: T) -> None: ... - - def remove(self, item: T) -> None: ... - - def clear(self) -> None: ... - - def extend(self, items: Collection[T]) -> None: - for item in items: - self.append(item) - - def __next__(self): - # TODO: Remove test hack in separate PR - return next(iter(self)) - - def __eq__(self, other): - return isinstance(other, Iterable) and all(a == b for a, b in zip(self, other)) - - -class IndexedMutableCollection(MutableCollection, ABC): - def __getitem__(self, idx: int) -> T: ... - - - -class _Wrapper(BackedDescriptor): - - instance: Any - backing_name: Any - - def __new__(cls, *args: Any, **kwargs: Any) -> Self: - """ - This __new__ stores the init arguments, and otherwise acts as default. - We need this to efficiently and truly re-create the wrapper and bind it to an instance upon a get call. - """ - obj = super().__new__(cls) - obj._init_args = args - obj._init_kwargs = kwargs.copy() - return obj - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.instance = None - self.backing_name = None - - def __get__(self, instance, _=None) -> Self: - """ - This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. - From there on, the wrapper functions as a lazy list. - """ - if instance is None: - return self - obj = type(self)(*self._init_args, **self._init_kwargs) - obj.instance = instance - obj.backing_name = self.private_field.name - return obj - -class _WrapperFgetFix(_Wrapper): - """ - This class exists to fix the tests that rely on the old lists being @property. - TODO: Remove in a separate PR fixing tests - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - def fget(instance): - return self.__get__(instance) - self.fget = fget - - def __set_name__(self, owner, name): - super().__set_name__(owner, name) - self.fget.__name__ = name - self.fget.__qualname__ = f"{owner.__qualname__}.{name}" - - @deprecated("Lists are no longer a property") - def fget(self, instance): ... - -class _IterableWrapper(_WrapperFgetFix, IndexedMutableCollection, ABC): - """ - This class allows us to assign lists at init time to avoid special-case handling. - """ - def __set__(self, instance, value): - if instance is None: - return - if not hasattr(instance, self.backing_name): - resolve_default(instance, self.private_field) - elif getattr(instance, self.backing_name): - raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") - if value is not None: - self.__get__(instance).extend(value) - else: - self.__get__(instance).clear() - - -class AbstractBackedCollection(Collection[T], Generic[T], ABC): - - @abstractmethod - def _get_collection(self) -> Collection[T]: - ... - - @abstractmethod - def append(self, element: T) -> None: - ... - - def extend(self, elements: Iterable[T]) -> None: - for element in elements: - self.append(element) - - @abstractmethod - def remove(self, element: T) -> None: - ... - - @abstractmethod - def clear(self) -> None: - ... - - def __len__(self) -> int: - return len(self._get_collection()) - - def __iter__(self) -> Iterator[T]: - return iter(self._get_collection()) - - def __contains__(self, element: object) -> bool: - return element in self._get_collection() - - def for_each_indexed(self, action: Callable[[int, T], Any]): - """ - Call the `action` on each item in the list - - :param action: An action to apply to each :class:`RelaySetting` in the `thresholds` collection, taking the index of the threshold, and the threshold itself. - """ - for index, item in enumerate(self._get_collection()): - action(index, item) - - - -class AbstractBackedList( - AbstractBackedCollection[T], - Sequence[T], - Generic[T], - ABC, -): - - @abstractmethod - def _get_collection(self) -> Sequence[T]: - ... - - @overload - def __getitem__(self, index: int) -> T: - ... - - @overload - def __getitem__(self, index: slice) -> Sequence[T]: - ... - - def __getitem__(self, index: int | slice) -> T | Sequence[T]: - return self._get_collection()[index] - - -class LazyValidatedList(_IterableWrapper, AbstractBackedList[T]): - - def __init__(self, - private_field, - validate=None, - sort_by=None): - super().__init__(private_field) - self.validate = validate - self.sort_by = sort_by - - def _get(self): - return getattr(self.instance, self.backing_name) - - def _get_collection(self) -> list[T]: - return getattr(self.instance, self.backing_name) or [] - - # clearIfEmpty is inlined for performance - - def append(self, item): - if self.validate is not None: - self.validate(self.instance, item) - - existing = getattr(self.instance, self.backing_name) - if existing is None: - existing = [item] - setattr(self.instance, self.backing_name, existing) - else: - existing.append(item) - - if self.sort_by is not None: - existing.sort(key=self.sort_by) - - def __len__(self): - return len(self._get_collection()) - - def __iter__(self): - return iter(self._get_collection()) - - def __contains__(self, item): - return item in (self._get_collection()) - - def remove(self, item): - existing = self._get_collection() - existing.remove(item) - if not existing: - self.clear() - - def clear(self): - setattr(self.instance, self.backing_name, None) - - def __repr__(self): - if self.instance is None: - return self.__class__.__repr__() - return str(self._get_collection()) - - def __str__(self): - return self.__repr__() - - def __getitem__(self, item): - return (self._get_collection())[item] - - -class LazyIndexedList(LazyValidatedList[T]): - - def __init__( - self, - private_field, - element_description: str - ): - super().__init__(private_field) - self.element_description = element_description - - def insert(self, index: int, item: T) -> None: - """ - Insert ``item`` at ``index``. - """ - size = len(self) - - if not 0 <= index <= size: - raise ValueError( - f"Unable to add {self.element_description} to " - f"{self.instance}. " - f"Sequence number {index} is invalid. " - f"Expected a value between 0 and {size}. " - "Make sure you are adding the items in order and there are " - "no gaps in the numbering." - ) - - existing = getattr(self.instance, self.backing_name) - - if existing is None: - existing = [item] - setattr(self.instance, self.backing_name, existing) - else: - existing.insert(index, item) - - if self.sort_by is not None: - existing.sort(key=self.sort_by) - - def append(self, item: T) -> None: - self.insert(len(self), item) - - def pop(self, index: int = -1) -> T: - """ - Remove and return the item at ``index``. - - Uses normal Python list semantics, including support for negative - indexes and raising ``IndexError`` when the index is invalid. - """ - existing = getattr(self.instance, self.backing_name) - - if existing is None: - raise IndexError("pop from empty list") - - item = existing.pop(index) - - if not existing: - self.clear() - - return item - -if __name__ == '__main__': - @zb_dataclass - class C(DataclassBase): - _xs: list[int] | None = field(default=None) - xs: LazyIndexedList[int] = LazyIndexedList( - _xs, - "Thing" - ) - - c = C(xs=[24, 42]) - print(c._xs, c.xs) - c.xs.clear() - print(c._xs, c.xs) - c.xs.append(1) - c.xs.append(2) - c.xs.append(3) - print(c._xs, c.xs) - c.xs.insert(2, 42) - print(c._xs, c.xs) - c.xs.pop(2) - print(c._xs, c.xs) diff --git a/src/zepben/ewb/boilerplate/tst.py b/src/zepben/ewb/boilerplate/tst.py deleted file mode 100644 index b0e8fa5e9..000000000 --- a/src/zepben/ewb/boilerplate/tst.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright 2026 Zeppelin Bend Pty Ltd -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -from __future__ import annotations - -import copy -import time -from abc import abstractmethod, ABC -from dataclasses import dataclass, field -from typing import Any -from typing_extensions import Self, deprecated -from zepben.ewb import remove_descriptor_annotations, BackedDescriptor, resolve_default - - -class _Wrapper(BackedDescriptor): - - instance: Any - backing_name: Any - - def __new__(cls, *args: Any, **kwargs: Any) -> Self: - """ - This __new__ stores the init arguments, and otherwise acts as default. - We need this to efficiently and truly re-create the wrapper and bind it to an instance upon a get call. - """ - obj = super().__new__(cls) - obj._init_args = args - obj._init_kwargs = kwargs.copy() - return obj - - def __get__(self, instance, _=None) -> Self: - """ - This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. - From there on, the wrapper functions as a lazy list. - """ - obj = type(self)(*self._init_args, **self._init_kwargs) - obj.instance = instance - obj.backing_name = self.private_field.name - return obj - -class _WrapperFgetFix(_Wrapper): - """ - This class exists to fix the tests that rely on the old lists being @property. - TODO: Remove in a separate PR fixing tests - """ - def __set_name__(self, owner, name): - super().__set_name__(owner, name) - - def fget(instance): - return self.__get__(instance) - - fget.__name__ = name - fget.__qualname__ = f"{owner.__qualname__}.{name}" - - self.fget = fget - - @deprecated("Lists are no longer a property") - def fget(self, instance): - # TODO: Remove test hack in separate PR - raise NotImplementedError() - -class _IterableWrapper(_WrapperFgetFix, ABC): - """ - This class allows us to assign lists at init time to avoid special-case handling. - """ - def __set__(self, instance, value): - if instance is None: - return - if not hasattr(instance, self.backing_name): - resolve_default(instance, self.private_field) - elif getattr(instance, self.backing_name): - raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") - if value is not None: - self.__get__(instance).extend(value) - else: - self.__get__(instance).clear() - - @abstractmethod - def extend(self, items): ... - - @abstractmethod - def clear(self): ... - - -class ListWrapper(_IterableWrapper): - - def clear(self): - self._set(None) - - def extend(self, items): - for item in items: - self.append(item) - - def __init__( - self, - private_field: Any, - validate = None, - sort_by = None - ): - super().__init__(private_field) - self.validate = validate - self.sort_by = sort_by - - def _get(self): - if self.instance is None: - return self - return getattr(self.instance, self.backing_name) - - def _set(self, value): - if self.instance is None: - return - setattr(self.instance, self.backing_name, value) - - def __str__(self): - return str(self._get()) - - def append(self, item): - l = self._get() - if l is None: - self._set([item]) - else: - l.append(item) - - - - -@dataclass -class C: - _x: list[int] | None = field(default=None) - x: ListWrapper = ListWrapper(_x, sort_by=lambda it: -it) - - @property - def thing(self): - return 42 - -if __name__ == '__main__': - c = C([1, 2, 3]) - print(c) - print(c.x) - print(c.x.sort_by) - c.x.clear() - c.x.append(42) - print(c) - print(c.x) - - c.thing = 24 - - # class C: - # def __init__(self, x, f1, f2): - # self.x = x - # self.y = x + x - # self.f1 = f1 - # self.f2 = f2 - # - # N = 1_000_000 - # t = time.time() - # for _ in range(N): - # tmp = C("blahblahblah", lambda : 42, lambda : 24) - # print(time.time() - t) - # - # c = C("blahblahblah", lambda : 42, lambda : 24) - # t = time.time() - # for _ in range(N): - # tmp = copy.copy(c) - # print(time.time() - t) - ... \ No newline at end of file diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index 7c02259f2..b107ac078 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -12,8 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py index b0fe95a38..31e5336ad 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -6,19 +6,18 @@ __all__ = ["ContactDetails"] from dataclasses import field -from typing import Generator, Any +from typing import Any from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61968.common.telephone_number import TelephoneNumber from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable -from zepben.ewb.util import ngen, nlen @zbex diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py index 79f3e77fc..d9356a515 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py @@ -7,16 +7,14 @@ __all__ = ["Loop"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import safe_remove, ngen, nlen, get_by_mrid -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.substation import Substation diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py index bf95e8f33..679214809 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py @@ -9,24 +9,20 @@ import typing from dataclasses import field -from typing import Generator, Optional, Dict, List +from typing import Optional, Dict -from pygments.lexer import default from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection -from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer -from zepben.ewb.util import safe_remove_by_id, nlen, ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if typing.TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal - from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_substation import LvSubstation @zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index 4bd63d40e..f4502f637 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -12,9 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb import ngen, nlen, safe_remove_by_id, get_by_mrid, remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection -from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index c971cc5a0..7bce7fcd8 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -8,11 +8,10 @@ __all__ = ["ProtectionRelayFunction"] import sys -import warnings -from typing import Optional, List, Generator, Iterable, Callable, TYPE_CHECKING, Any +from typing import Optional, List, Callable, TYPE_CHECKING, Any from abc import ABCMeta -from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList if sys.version_info >= (3, 13): from warnings import deprecated else: @@ -24,9 +23,7 @@ from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource -from zepben.ewb.util import require, nlen, ngen, safe_remove, get_by_mrid -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61968.assetinfo.relay_info import RelayInfo diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py index dbeb9cf07..d6a4737d6 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py @@ -7,16 +7,14 @@ __all__ = ["ProtectionRelayScheme"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import ngen, get_by_mrid, nlen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_system import ProtectionRelaySystem diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py index 49276fd6d..c3a48512f 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py @@ -7,17 +7,15 @@ __all__ = ["ProtectionRelaySystem"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment -from zepben.ewb.util import ngen, get_by_mrid, nlen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_scheme import ProtectionRelayScheme diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py index e966a3e14..b894424cf 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py @@ -7,16 +7,14 @@ __all__ = ["PowerTransformerInfo"] -from typing import List, Optional, Generator, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.transformer_tank_info import TransformerTankInfo diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py index a0e0c406f..be905cde1 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py @@ -7,16 +7,14 @@ __all__ = ["TransformerTankInfo"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance -from zepben.ewb.util import nlen, ngen, safe_remove, get_by_mrid -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset.py b/src/zepben/ewb/model/cim/iec61968/assets/asset.py index afb538c2c..f6b3880e9 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset.py @@ -7,16 +7,14 @@ __all__ = ["Asset"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset_organisation_role import AssetOrganisationRole diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index 3cc31a458..e6d22987d 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -13,8 +13,8 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index 9787f551d..ed32f5557 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -7,16 +7,15 @@ __all__ = ["Customer"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.organisation_role import OrganisationRole from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb import Alias +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer_agreement import CustomerAgreement diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py index 9d6b0a4e0..83e7a12d2 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py @@ -7,15 +7,13 @@ __all__ = ["CustomerAgreement"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.agreement import Agreement -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer import Customer diff --git a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py index be237a622..dfe79e4d2 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py @@ -7,15 +7,13 @@ __all__ = ["PricingStructure"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.document import Document -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.tariff import Tariff diff --git a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py index a3ad34d3a..92da48fd8 100644 --- a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py +++ b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py @@ -7,15 +7,13 @@ __all__ = ["Pole"] -from typing import List, Optional, Generator, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.structure import Structure -from zepben.ewb.util import get_by_mrid, ngen, nlen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.streetlight import Streetlight diff --git a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py index bc4e3daff..c0c3158e4 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py @@ -7,16 +7,14 @@ __all__ = ["EndDevice"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.assets.asset_container import AssetContainer -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index 16fba5a6e..594a5613e 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -7,17 +7,16 @@ __all__ = ["UsagePoint"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61968.common.contact_details import ContactDetails from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.util import nlen +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location diff --git a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py index 5b79d57ee..481c6af89 100644 --- a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py +++ b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py @@ -7,15 +7,13 @@ __all__ = ["OperationalRestriction"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61968.common.document import Document -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py index e8370543a..8ba98f005 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py @@ -7,16 +7,14 @@ __all__ = ["Sensor"] -from typing import Generator, Optional, List, TYPE_CHECKING, Iterable +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment -from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py index 88bc67e4c..32b6422a4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py @@ -8,16 +8,16 @@ __all__ = ['ConductingEquipment'] import sys -from typing import List, Optional, Generator, TYPE_CHECKING, Union +from typing import List, Optional, TYPE_CHECKING, Union from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment -from zepben.ewb.util import get_by_mrid, require, ngen +from zepben.ewb.util import require from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index 6085f0738..a41e2d4b6 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -7,14 +7,13 @@ __all__ = ["ConnectivityNode"] -from typing import Generator, List, TYPE_CHECKING +from typing import List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import get_by_mrid, ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index be90f3852..0e1fa4039 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -6,16 +6,16 @@ __all__ = ["Curve"] from dataclasses import field -from typing import Optional, List, Generator +from typing import Optional from abc import ABCMeta from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations, zb_dataclass -from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList +from zepben.ewb import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList from zepben.ewb.model.cim.iec61970.base.core.curve_data import CurveData from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import require, ngen, nlen, safe_remove +from zepben.ewb.util import require class CurveDataList(LazyValidatedList[CurveData]): diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index fd41ad360..604f838ec 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -19,10 +19,10 @@ from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.core.substation import Substation from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.util import ngen +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb import Alias +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.metering.usage_point import UsagePoint diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py index 2a2dadeeb..bad482aaa 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py @@ -8,16 +8,15 @@ __all__ = ['EquipmentContainer'] from dataclasses import field -from typing import Optional, Dict, Generator, List, TYPE_CHECKING, TypeVar, Iterable, Type +from typing import Dict, Generator, TYPE_CHECKING, TypeVar, Iterable, Type from abc import ABCMeta from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection -from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap +from zepben.ewb import Alias +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import ConnectivityNodeContainer -from zepben.ewb.util import nlen, ngen, safe_remove_by_id from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index aa312b5f4..30c9c0e3f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -8,16 +8,13 @@ __all__ = ["Feeder"] from dataclasses import field -from typing import Optional, Dict, List, Generator, TYPE_CHECKING +from typing import Optional, Dict, TYPE_CHECKING from typing_extensions import deprecated -from zepben.ewb import get_by_mrid, remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, internal -from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap -from zepben.ewb.model.cim.extensions.zbex import zbex +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, internal +from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer -from zepben.ewb.util import ngen, nlen, safe_remove_by_id from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index b5efdd541..ecbbcf664 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -12,8 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name.py b/src/zepben/ewb/model/cim/iec61970/base/core/name.py index d457580f6..123b7cffc 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Optional -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py index a15d9a1b0..447c1f2c4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py @@ -12,8 +12,8 @@ from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable from zepben.ewb.model.cim.iec61970.base.core.name import Name -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.descriptor_fix import BackedDescriptor +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.descriptor_fix import BackedDescriptor if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py index 50331de93..3c04f3f14 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py @@ -7,16 +7,15 @@ __all__ = ['PowerSystemResource'] -from typing import Optional, TYPE_CHECKING, List, Generator, Iterable +from typing import Optional, TYPE_CHECKING, List from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject -from zepben.ewb.util import get_by_mrid, nlen, ngen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.util import nlen +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset import Asset diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index 448a12ba2..da176f013 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -12,8 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, internal +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.substation import Substation diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index 3904988b4..c57f8efa0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -7,15 +7,14 @@ __all__ = ["Substation"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer -from zepben.ewb.util import nlen, get_by_mrid, ngen, safe_remove, require -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill, internal +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb import Alias +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py index 0db98ffce..768bec06a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py @@ -14,7 +14,7 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import AcDcTerminal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index 0767d2244..f89d525cb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -12,9 +12,9 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, Backfill -from zepben.ewb.dataclass_descriptors.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, Backfill +from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_style import DiagramStyle diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 8f7339f76..2edd8537d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -8,16 +8,15 @@ __all__ = ["DiagramObject"] from dataclasses import field -from typing import Optional, List, Generator, Callable, TYPE_CHECKING, Any +from typing import Optional, List, Callable, TYPE_CHECKING, Any from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.lazy_list import LazyIndexedList -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint -from zepben.ewb.util import nlen, ngen, require, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py index 1dd0e62ed..060279fe3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py +++ b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py @@ -7,17 +7,15 @@ __all__ = ["BatteryUnit"] -from typing import List, Optional, Generator, TYPE_CHECKING +from typing import List, Optional, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control_mode import BatteryControlMode from zepben.ewb.model.cim.iec61970.base.generation.production.battery_state_kind import BatteryStateKind from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit -from zepben.ewb.util import nlen, ngen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control import BatteryControl diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index b0a5eae4a..e3b871db7 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -12,8 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp from zepben.ewb.model.cim.iec61970.base.wires.conductor import Conductor diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py index b9148bd87..16ac46c33 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py @@ -12,7 +12,7 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index 73b667165..404993bce 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index 0bf40b4cb..fb9912df1 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 6b99da983..0c95462fb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -13,8 +13,8 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase from zepben.ewb.model.cim.iec61970.base.wires.phase_shunt_connection_kind import PhaseShuntConnectionKind diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py index 534558673..cee56b5a2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index 47c5befbb..abb919e4e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -13,8 +13,8 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py index 847ca36da..46358a03b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py index 7b7709f66..9bae87c2d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py @@ -12,12 +12,12 @@ from typing_extensions import deprecated -from zepben.ewb import remove_descriptor_annotations, zb_dataclass -from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList +from zepben.ewb import zb_dataclass +from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.phase_impedance_data import PhaseImpedanceData from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind -from zepben.ewb.util import require, ngen, nlen, safe_remove, none +from zepben.ewb.util import require, none class PhaseImpedanceDataList(LazyValidatedList): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 006c5aaaf..0eed0a1e2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -13,8 +13,8 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq from zepben.ewb.util import require diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py index aed773497..87eed05ef 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index c4a5ce1fe..fddaa03d4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -11,8 +11,8 @@ from dataclasses import field from typing import List, Optional, TYPE_CHECKING -from zepben.ewb import remove_descriptor_annotations, Alias -from zepben.ewb.dataclass_descriptors.mrid_list import LazyMridList, Backfill +from zepben.ewb import Alias +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList, Backfill if sys.version_info >= (3, 13): from warnings import deprecated diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index 87bf23c7b..4786d7c3a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -9,19 +9,18 @@ import warnings from dataclasses import field -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.dataclass_descriptors.lazy_list import LazyValidatedList -from zepben.ewb.dataclass_descriptors.mrid_list import internal +from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.mrid_list import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd from zepben.ewb.model.cim.iec61970.base.wires.winding_connection import WindingConnection from zepben.ewb.model.resistance_reactance import ResistanceReactance -from zepben.ewb.util import ngen, nlen, safe_remove from zepben.ewb.boilerplate.dataclass_base import zb_dataclass if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py index f1f0c046b..65b11195c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py @@ -7,16 +7,14 @@ __all__ = ["ProtectedSwitch"] -from typing import Optional, List, Generator, TYPE_CHECKING, Iterable +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch -from zepben.ewb.util import get_by_mrid, ngen, nlen, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py index 4a11127a4..76ca8cf20 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py @@ -7,7 +7,7 @@ __all__ = ["RegulatingControl"] -from typing import Optional, List, Generator, Iterable, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from abc import ABCMeta from dataclasses import field from typing_extensions import deprecated @@ -15,10 +15,8 @@ from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.regulating_control_mode_kind import RegulatingControlModeKind -from zepben.ewb.util import nlen, get_by_mrid, safe_remove, ngen -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py index 602212359..eeaf4a685 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py @@ -5,16 +5,14 @@ __all__ = ["SynchronousMachine"] -from typing import Optional, List, Generator, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.rotating_machine import RotatingMachine from zepben.ewb.model.cim.iec61970.base.wires.synchronous_machine_kind import SynchronousMachineKind -from zepben.ewb.util import ngen, nlen, get_by_mrid, safe_remove -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.reactive_capability_curve import ReactiveCapabilityCurve diff --git a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py index c2ae3ff63..2481e8117 100644 --- a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py +++ b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py @@ -7,15 +7,13 @@ __all__ = ["Circuit"] -from typing import Optional, Generator, List, TYPE_CHECKING +from typing import Optional, List, TYPE_CHECKING from dataclasses import field from typing_extensions import deprecated from zepben.ewb.model.cim.iec61970.base.wires.line import Line -from zepben.ewb.util import ngen, get_by_mrid, safe_remove, nlen -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass -from zepben.ewb import remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop diff --git a/test/busbranch/test_bus_branch.py b/test/busbranch/test_bus_branch.py index cffa0cf6c..3e84e30c9 100644 --- a/test/busbranch/test_bus_branch.py +++ b/test/busbranch/test_bus_branch.py @@ -309,15 +309,15 @@ async def test_switches_excluded_when_getting_voltage(): def _get_expected(nb_network, line, pt, es, ec, pec, eb, ec_eb1, ec_eb2): # -- Bus - exp_bb0 = (create_terminal_based_id({next(es.terminals), get_term(pt, 1)}), + exp_bb0 = (create_terminal_based_id({next(iter(es.terminals)), get_term(pt, 1)}), (20000, frozenset(), frozenset({get_term(es, 1), get_term(pt, 1)}), frozenset(), nb_network)) exp_bb1 = (create_terminal_based_id({get_term(pt, 2), get_term(line, 1)}), (400, frozenset(), frozenset({get_term(line, 1), get_term(pt, 2)}), frozenset(), nb_network)) - exp_bb2 = (create_terminal_based_id({get_term(line, 2), next(ec.terminals), next(pec.terminals), get_term(eb, 1)}), + exp_bb2 = (create_terminal_based_id({get_term(line, 2), next(iter(ec.terminals)), next(iter(pec.terminals)), get_term(eb, 1)}), (400, frozenset(), frozenset({get_term(ec, 1), get_term(line, 2), get_term(pec, 1), get_term(eb, 1)}), frozenset(), nb_network)) - exp_bb3 = (create_terminal_based_id({get_term(eb, 2), next(ec_eb1.terminals), next(ec_eb2.terminals)}), - (400, frozenset(), frozenset({get_term(eb, 2), next(ec_eb1.terminals), next(ec_eb2.terminals)}), frozenset(), nb_network)) + exp_bb3 = (create_terminal_based_id({get_term(eb, 2), next(iter(ec_eb1.terminals)), next(iter(ec_eb2.terminals))}), + (400, frozenset(), frozenset({get_term(eb, 2), next(iter(ec_eb1.terminals)), next(iter(ec_eb2.terminals))}), frozenset(), nb_network)) # -- Branch exp_branch = (f"tb_{line.mrid}", ((exp_bb1[1], exp_bb2[1]), 100, frozenset({line}), frozenset({*line.terminals}), frozenset(), nb_network)) diff --git a/test/dataclass_descriptors/test_basic_descriptor.py b/test/dataclass_descriptors/test_basic_descriptor.py index 6941d9682..f0b9b1caa 100644 --- a/test/dataclass_descriptors/test_basic_descriptor.py +++ b/test/dataclass_descriptors/test_basic_descriptor.py @@ -4,9 +4,9 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import field -from zepben.ewb.dataclass_descriptors.dataclass_base import DataclassBase +from zepben.ewb.boilerplate.dataclass_base import DataclassBase from zepben.ewb import zb_dataclass, remove_descriptor_annotations -from zepben.ewb.dataclass_descriptors.descriptor_fix import BackedDescriptor +from zepben.ewb.boilerplate.descriptor_fix import BackedDescriptor @zb_dataclass diff --git a/test/dataclass_descriptors/test_dataclass_base.py b/test/dataclass_descriptors/test_dataclass_base.py index f35c26ee5..ebc954a1b 100644 --- a/test/dataclass_descriptors/test_dataclass_base.py +++ b/test/dataclass_descriptors/test_dataclass_base.py @@ -7,7 +7,7 @@ import pytest -from zepben.ewb.dataclass_descriptors.dataclass_base import zb_dataclass, DataclassBase +from zepben.ewb.boilerplate.dataclass_base import zb_dataclass, DataclassBase @zb_dataclass diff --git a/test/services/common/service_comparator_validator.py b/test/services/common/service_comparator_validator.py index b0530a169..c9fc79d0a 100644 --- a/test/services/common/service_comparator_validator.py +++ b/test/services/common/service_comparator_validator.py @@ -182,7 +182,7 @@ def validate_collection( in_source, target_empty, { - _prop_name(prop): CollectionDifference(missing_from_target=[next(_get_prop(in_source, prop))]) + _prop_name(prop): CollectionDifference(missing_from_target=[next(iter(_get_prop(in_source, prop)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -191,7 +191,7 @@ def validate_collection( source_empty, in_target, { - _prop_name(prop): CollectionDifference(missing_from_source=[next(_get_prop(in_target, prop))]) + _prop_name(prop): CollectionDifference(missing_from_source=[next(iter(_get_prop(in_target, prop)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -201,8 +201,8 @@ def validate_collection( in_target_difference, { _prop_name(prop): CollectionDifference( - missing_from_source=[next(_get_prop(in_target_difference, prop))], - missing_from_target=[next(_get_prop(in_source, prop))], + missing_from_source=[next(iter(_get_prop(in_target_difference, prop)))], + missing_from_target=[next(iter(_get_prop(in_source, prop)))], ) }, ) @@ -235,7 +235,7 @@ def validate_name_collection( in_source, target_empty, { - _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_target=[next(_get_prop(in_source, IdentifiedObject.names))]) + _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_target=[next(iter(_get_prop(in_source, IdentifiedObject.names)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -244,7 +244,7 @@ def validate_name_collection( source_empty, in_target, { - _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_source=[next(_get_prop(in_target, IdentifiedObject.names))]) + _prop_name(IdentifiedObject.names): CollectionDifference(missing_from_source=[next(iter(_get_prop(in_target, IdentifiedObject.names)))]) }, ) self._validate_expected(diff, options, options_stop_compare, expected_differences=expected_differences) @@ -254,8 +254,8 @@ def validate_name_collection( in_target_difference, { _prop_name(IdentifiedObject.names): CollectionDifference( - missing_from_source=[next(_get_prop(in_target_difference, IdentifiedObject.names))], - missing_from_target=[next(_get_prop(in_source, IdentifiedObject.names))], + missing_from_source=[next(iter(_get_prop(in_target_difference, IdentifiedObject.names)))], + missing_from_target=[next(iter(_get_prop(in_source, IdentifiedObject.names)))], ) }, ) @@ -286,7 +286,7 @@ def validate_indexed_collection( self.validate_compare(in_source, in_target, options=options, options_stop_compare=options_stop_compare) def get_item(obj) -> Optional[R]: - return next(_get_prop(obj, prop), None) + return next(iter(_get_prop(obj, prop)), None) diff = ObjectDifference( in_source, diff --git a/test/services/network/tracing/test_assign_to_lv_feeders.py b/test/services/network/tracing/test_assign_to_lv_feeders.py index 94bcd234d..9ee758731 100644 --- a/test/services/network/tracing/test_assign_to_lv_feeders.py +++ b/test/services/network/tracing/test_assign_to_lv_feeders.py @@ -391,7 +391,7 @@ async def run_with_operators(operators: Type[NetworkStateOperators]): operators.associate_energizing_feeder(back_feed, lv_feeder) await Tracing.assign_equipment_to_lv_feeders().run( - next(b7.terminals), + next(iter(b7.terminals)), network.lv_feeder_start_points, terminal_to_aux_equipment = dict(), lv_feeders_to_assign=[lv_feeder], From c8611c37d9e453b8256f93c173e599613e716e71 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 27 Jul 2026 15:19:34 +1000 Subject: [PATCH 32/54] Move backfill; Fix base to use zb dataclass; Fix iterable wrapper to not rely on abstractbackedlist Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/backfill.py | 42 ++++++++ ...base.py => abstract_backed_collections.py} | 2 +- .../ewb/boilerplate/collections/lazy_list.py | 37 +------ .../ewb/boilerplate/collections/mrid_list.py | 101 +++--------------- .../ewb/boilerplate/collections/mrid_map.py | 53 +-------- .../ewb/boilerplate/collections/wrapper.py | 4 +- src/zepben/ewb/boilerplate/dataclass_base.py | 101 +++++++++--------- src/zepben/ewb/boilerplate/descriptor_fix.py | 3 - .../base/core/conducting_equipment.py | 3 +- .../model/cim/iec61970/base/core/feeder.py | 3 +- .../iec61970/base/core/geographical_region.py | 3 +- .../base/core/sub_geographical_region.py | 3 +- .../cim/iec61970/base/core/substation.py | 3 +- .../model/cim/iec61970/base/core/terminal.py | 2 +- .../iec61970/base/diagramlayout/diagram.py | 3 +- .../base/diagramlayout/diagram_object.py | 2 +- .../iec61970/base/wires/ac_line_segment.py | 3 +- .../base/wires/ac_line_segment_phase.py | 2 +- .../model/cim/iec61970/base/wires/clamp.py | 2 +- .../ewb/model/cim/iec61970/base/wires/cut.py | 2 +- .../iec61970/base/wires/energy_consumer.py | 3 +- .../base/wires/energy_consumer_phase.py | 2 +- .../cim/iec61970/base/wires/energy_source.py | 3 +- .../base/wires/energy_source_phase.py | 2 +- .../wires/power_electronics_connection.py | 3 +- .../power_electronics_connection_phase.py | 2 +- .../iec61970/base/wires/power_transformer.py | 3 +- .../base/wires/power_transformer_end.py | 2 +- 28 files changed, 143 insertions(+), 251 deletions(-) rename src/zepben/ewb/boilerplate/collections/{base.py => abstract_backed_collections.py} (94%) diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py index 35e3122c5..ec5599d1a 100644 --- a/src/zepben/ewb/boilerplate/backfill.py +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -2,3 +2,45 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from dataclasses import Field +from types import MemberDescriptorType +from typing import Any, Callable, TypeVar, Protocol + +from zepben.ewb import BackedDescriptor + + +class HasMrid(Protocol): + mrid: str + + +S = TypeVar("S", bound=HasMrid) + + +class Backfill: + def __init__(self, backfill_prop: property): + self.backfill_prop = backfill_prop + + def apply(self, element: S, owner: Any): + name = self.backfill_prop.fget.__name__ + + if hasattr(self.backfill_prop, "__target"): + backing_name = self.backfill_prop.__target.__name__ + else: + backing_name = name + + if getattr(element, backing_name) is None: + setattr(element, backing_name, owner) + + ref = getattr(element, backing_name) + if ref is not owner: + raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") + + +def internal(target: Any): + if not any(isinstance(target, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): + raise TypeError(f"target parameter of the target decorator has to be an instance of dataclass Field, instead is {target}") + + def dec(func: Callable): + setattr(func, "__target", target) + return func + return dec diff --git a/src/zepben/ewb/boilerplate/collections/base.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py similarity index 94% rename from src/zepben/ewb/boilerplate/collections/base.py rename to src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py index fcd9c9524..f8c1c8862 100644 --- a/src/zepben/ewb/boilerplate/collections/base.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py @@ -3,7 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import ABC, abstractmethod -from typing import Collection, Protocol, Iterable, Generic, Iterator, Callable, Any, Sequence, overload, TypeVar +from typing import Collection, Iterable, Generic, Iterator, Callable, Any, Sequence, overload, TypeVar T = TypeVar("T") diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index b7a28dcb5..ed5df35ec 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -2,22 +2,13 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from dataclasses import field -from typing import TypeVar, Protocol +from typing import TypeVar -from zepben.ewb import DataclassBase, zb_dataclass -from zepben.ewb.boilerplate.collections.base import AbstractBackedList +from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -# from descriptor_fix import BackedDescriptor, remove_descriptor_annotations - - -class HasMrid(Protocol): - mrid: str - T = TypeVar("T") -S = TypeVar("S", bound=HasMrid) class LazyValidatedList(_IterableWrapper, AbstractBackedList[T]): @@ -36,8 +27,6 @@ def _get(self): def _get_collection(self) -> list[T]: return getattr(self.instance, self.backing_name) or [] - # clearIfEmpty is inlined for performance - def append(self, item): if self.validate is not None: self.validate(self.instance, item) @@ -140,25 +129,3 @@ def pop(self, index: int = -1) -> T: self.clear() return item - -if __name__ == '__main__': - @zb_dataclass - class C(DataclassBase): - _xs: list[int] | None = field(default=None) - xs: LazyIndexedList[int] = LazyIndexedList( - _xs, - "Thing" - ) - - c = C(xs=[24, 42]) - print(c._xs, c.xs) - c.xs.clear() - print(c._xs, c.xs) - c.xs.append(1) - c.xs.append(2) - c.xs.append(3) - print(c._xs, c.xs) - c.xs.insert(2, 42) - print(c._xs, c.xs) - c.xs.pop(2) - print(c._xs, c.xs) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 29b2f691e..d5f6f5040 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -3,25 +3,23 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import abstractmethod, ABC -from dataclasses import dataclass, field, Field -from types import MemberDescriptorType -from typing import Any, TypeVar, Protocol, Callable, Sequence +from typing import Any, TypeVar, Protocol, Sequence -import pytest from typing_extensions import Self -from zepben.ewb import BackedDescriptor +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedCollection, AbstractBackedList from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -from zepben.ewb.boilerplate.collections.base import AbstractBackedCollection, AbstractBackedList class HasMrid(Protocol): mrid: str -T = TypeVar("T") + S = TypeVar("S", bound=HasMrid) + class MridCollection(AbstractBackedCollection[S], ABC): instance: Any element_description: str @@ -30,6 +28,11 @@ class MridCollection(AbstractBackedCollection[S], ABC): def _safe_get_by_mrid(self, mrid: str) -> S | None: ... def get_by_mrid(self, mrid: str) -> S: + """ + Get an element matching given ``mrid`` + + raises KeyError if one is not present + """ res = self._safe_get_by_mrid(mrid) if res is None: raise KeyError @@ -50,34 +53,6 @@ def _can_add_by_mrid(self, element: S) -> bool: return False -class Backfill: - def __init__(self, backfill_prop: property): - self.backfill_prop = backfill_prop - - def apply(self, element: S, owner: Any): - name = self.backfill_prop.fget.__name__ - - if hasattr(self.backfill_prop, "__target"): - backing_name = self.backfill_prop.__target.__name__ - else: - backing_name = name - - if getattr(element, backing_name) is None: - setattr(element, backing_name, owner) - - ref = getattr(element, backing_name) - if ref is not owner: - raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") - -def internal(target: Any): - if not any(isinstance(target, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): - raise TypeError(f"target parameter of the target decorator has to be an instance of dataclass Field, instead is {target}") - - def dec(func: Callable): - setattr(func, "__target", target) - return func - return dec - class LazyMridList(LazyValidatedList, MridCollection[S]): def __init__(self, private_field, @@ -96,7 +71,7 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: found = next((element for element in existing if element.mrid == mrid), None) return found - def append(self, item: T): + def append(self, item: S): if not self._can_add_by_mrid(item): return @@ -128,7 +103,7 @@ def __get__(self, instance, owner=None) -> Self: def __post_init__(self): self.backing_list = getattr(self.instance, self.backing_name) - def _get_collection(self) -> Sequence[T]: + def _get_collection(self) -> Sequence[S]: return self.backing_list def _safe_get_by_mrid(self, mrid: str) -> S | None: @@ -170,55 +145,3 @@ def __str__(self): def __getitem__(self, item): return self.backing_list[item] - - - - - - -if __name__ == '__main__': - @dataclass(slots=True) - class Ido: - mrid: str - n: int = 42 - - @dataclass(slots=True) - class A: - # _x: list[Ido] = field(default_factory=list) - _x: list[Ido] = field(default=None) - # x: MridCollection[Ido] = MridList( - x: MridCollection[Ido] = LazyMridList( - _x, - "A Thingo", - validate=(lambda self, it: self.validate_x(it)), - sort_by=(lambda it: it.n), - ) - - def validate_x(self, item): - print(f"validating {item}") - - def __hash__(self): - return object.__hash__(self) - - o1 = Ido("1") - o2 = Ido("2", 24) - oX = Ido("2", 32) - - a = A() - print(a, a.x) - a.x.append(o1) - print(a, a.x) - a.x.append(o2) - print(a, a.x) - for t in a.x: - print(t) - a.x.remove(o1) - print(a, a.x) - a.x.remove(o2) - print(a, a.x) - - a.x.append(o2) - print(a, a.x) - with pytest.raises(ValueError): - a.x.append(oX) - print(a, a.x) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py index ac5af32aa..f4fadd1d9 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -2,13 +2,11 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from dataclasses import dataclass, field from typing import Protocol, TypeVar, Collection -import pytest - +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, Backfill class HasMrid(Protocol): @@ -103,50 +101,3 @@ def __str__(self): def __getitem__(self, item): return (self._get_or_empty())[item] - - -if __name__ == '__main__': - @dataclass(slots=True) - class Ido: - mrid: str - n: int = 42 - - @dataclass(slots=True) - class A: - _x: list[Ido] | None = field(default=None) - x: MridCollection[Ido] = LazyMridMap( - _x, - "A Thingo", - validate=(lambda self, it: self.validate_x(it)), - # cached=False - ) - - def validate_x(self, item): - print(f"{self} validating {item}") - - - def __hash__(self): - return object.__hash__(self) - - o1 = Ido("1") - o2 = Ido("2", 24) - oX = Ido("2", 32) - - a = A() - print(a, a.x) - a.x.append(o1) - print(a, a.x) - a.x.append(o2) - print(a, a.x) - for t in a.x: - print(t) - a.x.remove(o1) - print(a, a.x) - a.x.remove(o2) - print(a, a.x) - - a.x.append(o2) - print(a, a.x) - with pytest.raises(ValueError): - a.x.append(oX) - print(a, a.x) diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py index 9913f8df8..786141d14 100644 --- a/src/zepben/ewb/boilerplate/collections/wrapper.py +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -7,7 +7,7 @@ from typing_extensions import Self, deprecated from zepben.ewb import BackedDescriptor, resolve_default -from zepben.ewb.boilerplate.collections.base import AbstractBackedList +from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList, AbstractBackedCollection class _Wrapper(BackedDescriptor): @@ -63,7 +63,7 @@ def __set_name__(self, owner, name): def fget(self, instance): ... -class _IterableWrapper(_WrapperFgetFix, AbstractBackedList, ABC): +class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection, ABC): """ This class allows us to assign lists at init time to avoid special-case handling. """ diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index ceed79f33..7f49170ef 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -13,56 +13,6 @@ def _is_set(obj: object, name: str) -> bool: return False return True -@dataclass(slots=True) -class DataclassBase: - """ - Instantiate the default fields and interpret the kwargs like ``@dataclass`` does - - This class serves as a base class for mostly init-less dataclasses used in CIM, - allowing custom inits to not break the entire inheritance tree. - It fills fields with default values, and treats kwargs the same way @dataclass does. - - For more motivation, refer to `MANIFESTO.md` - """ - def __init__(self, **kwargs) -> None: - - # Manually assign defaults in dataclass fields - for f in fields(type(self)): - # We cannot just check kwargs because fields could be set in subclass __init__'s - if _is_set(self, f.name) or f.name in kwargs: - continue - - resolve_default(self, f) - - # Assign all of the kwargs manually. - # str-based setattr triggers descriptors, allowing us to intercept __set__. - for attr, value in kwargs.items(): - setattr(self, attr, value) - - - # Currently post init implementation requires a lot of extra logic, - # which would mess with readability. If you require it - add it. - if callable(getattr(self, "__post_init__", None)): - raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") - -def resolve_default(instance, field: Field): - # Set field defaults - if field.default is not MISSING: - setattr(instance, field.name, field.default) - elif field.default_factory is not MISSING: - setattr(instance, field.name, field.default_factory()) - - # Ignore custom descriptors - elif hasattr(field, '__get__'): - return - - # Mimic default Python missing arg error - else: - raise TypeError( - f"Missing required field {field.name!r} " - f"for {type(instance).__name__}" - ) - T = TypeVar("T", bound=type) def remove_descriptor_annotations(cls: T) -> T: @@ -143,3 +93,54 @@ def zb_dataclass(cls: type[object]): """ cls = remove_descriptor_annotations(cls) return dataclass(init=False, eq=False, slots=True, repr=False)(cls) + + +@zb_dataclass +class DataclassBase: + """ + Instantiate the default fields and interpret the kwargs like ``@dataclass`` does + + This class serves as a base class for mostly init-less dataclasses used in CIM, + allowing custom inits to not break the entire inheritance tree. + It fills fields with default values, and treats kwargs the same way @dataclass does. + + For more motivation, refer to `MANIFESTO.md` + """ + def __init__(self, **kwargs) -> None: + + # Manually assign defaults in dataclass fields + for f in fields(type(self)): + # We cannot just check kwargs because fields could be set in subclass __init__'s + if _is_set(self, f.name) or f.name in kwargs: + continue + + resolve_default(self, f) + + # Assign all of the kwargs manually. + # str-based setattr triggers descriptors, allowing us to intercept __set__. + for attr, value in kwargs.items(): + setattr(self, attr, value) + + + # Currently post init implementation requires a lot of extra logic, + # which would mess with readability. If you require it - add it. + if callable(getattr(self, "__post_init__", None)): + raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") + +def resolve_default(instance, field: Field): + # Set field defaults + if field.default is not MISSING: + setattr(instance, field.name, field.default) + elif field.default_factory is not MISSING: + setattr(instance, field.name, field.default_factory()) + + # Ignore custom descriptors + elif hasattr(field, '__get__'): + return + + # Mimic default Python missing arg error + else: + raise TypeError( + f"Missing required field {field.name!r} " + f"for {type(instance).__name__}" + ) diff --git a/src/zepben/ewb/boilerplate/descriptor_fix.py b/src/zepben/ewb/boilerplate/descriptor_fix.py index 6b4afbb91..ae8c2cc3d 100644 --- a/src/zepben/ewb/boilerplate/descriptor_fix.py +++ b/src/zepben/ewb/boilerplate/descriptor_fix.py @@ -10,9 +10,6 @@ from __future__ import annotations from dataclasses import Field -from typing import TypeVar - - class BackedDescriptor: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py index 32b6422a4..8ed0a6b7c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py @@ -14,7 +14,8 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.util import require diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 30c9c0e3f..01ce4ed0b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -12,7 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, internal +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index ecbbcf664..7400e2db9 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index da176f013..ed7a7b3ff 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill, internal +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.substation import Substation diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index c57f8efa0..1d63b2e95 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill, internal +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py index 768bec06a..ea1d8392a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/terminal.py @@ -14,7 +14,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.ac_dc_terminal import AcDcTerminal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index f89d525cb..cf0a02a99 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 2edd8537d..4ea2e9683 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -14,7 +14,7 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index e3b871db7..6fa190c15 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp from zepben.ewb.model.cim.iec61970.base.wires.conductor import Conductor diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py index 16ac46c33..61fb22ee1 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py @@ -12,7 +12,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.model.cim.iec61968.assetinfo.wire_info import WireInfo diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index 404993bce..6c07993e3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.conducting_equipment import ConductingEquipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index fb9912df1..7836d745e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 0c95462fb..45bd5933c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase from zepben.ewb.model.cim.iec61970.base.wires.phase_shunt_connection_kind import PhaseShuntConnectionKind diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py index cee56b5a2..fa1748731 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index abb919e4e..77bcbd1f5 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py index 46358a03b..149166903 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 0eed0a1e2..9adcfb80e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq from zepben.ewb.util import require diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py index 87eed05ef..6bdd66733 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py @@ -10,7 +10,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index fddaa03d4..3a1ff4b1d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -12,7 +12,8 @@ from typing import List, Optional, TYPE_CHECKING from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList, Backfill +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.backfill import Backfill if sys.version_info >= (3, 13): from warnings import deprecated diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index 4786d7c3a..c64d2fb03 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -15,7 +15,7 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList -from zepben.ewb.boilerplate.collections.mrid_list import internal +from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd From 1a224f42a19d0c211bd6855519fb0c889fc57d56 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 27 Jul 2026 16:24:40 +1000 Subject: [PATCH 33/54] More cleanup Signed-off-by: Boris Filin --- src/zepben/ewb/__init__.py | 2 +- ...descriptor_fix.py => backed_descriptor.py} | 5 +- src/zepben/ewb/boilerplate/backfill.py | 35 +++++----- .../ewb/boilerplate/collections/lazy_list.py | 23 ++----- .../collections/mrid_collection.py | 48 ++++++++++++++ .../ewb/boilerplate/collections/mrid_list.py | 66 +++---------------- .../ewb/boilerplate/collections/mrid_map.py | 34 ++-------- src/zepben/ewb/boilerplate/dataclass_base.py | 49 +++++++------- .../extensions/iec61970/base/feeder/loop.py | 3 +- .../iec61970/base/feeder/lv_feeder.py | 2 +- .../iec61970/base/feeder/lv_substation.py | 2 +- .../protection/protection_relay_function.py | 3 +- .../protection/protection_relay_scheme.py | 3 +- .../protection/protection_relay_system.py | 3 +- .../assetinfo/power_transformer_info.py | 3 +- .../assetinfo/transformer_tank_info.py | 3 +- .../ewb/model/cim/iec61968/assets/asset.py | 3 +- .../model/cim/iec61968/customers/customer.py | 3 +- .../iec61968/customers/customer_agreement.py | 3 +- .../iec61968/customers/pricing_structure.py | 3 +- .../iec61968/infiec61968/infassets/pole.py | 3 +- .../model/cim/iec61968/metering/end_device.py | 3 +- .../cim/iec61968/metering/usage_point.py | 3 +- .../operations/operational_restriction.py | 3 +- .../base/auxiliaryequipment/sensor.py | 3 +- .../iec61970/base/core/connectivity_node.py | 3 +- .../model/cim/iec61970/base/core/equipment.py | 3 +- .../iec61970/base/core/equipment_container.py | 2 +- .../model/cim/iec61970/base/core/feeder.py | 2 +- .../iec61970/base/core/geographical_region.py | 3 +- .../model/cim/iec61970/base/core/name_type.py | 2 +- .../base/core/power_system_resource.py | 3 +- .../base/core/sub_geographical_region.py | 3 +- .../cim/iec61970/base/core/substation.py | 3 +- .../iec61970/base/diagramlayout/diagram.py | 2 +- .../iec61970/base/wires/ac_line_segment.py | 3 +- .../iec61970/base/wires/energy_consumer.py | 3 +- .../cim/iec61970/base/wires/energy_source.py | 3 +- .../wires/power_electronics_connection.py | 3 +- .../iec61970/base/wires/protected_switch.py | 3 +- .../iec61970/base/wires/regulating_control.py | 3 +- .../base/wires/synchronous_machine.py | 3 +- .../iec61970/infiec61970/feeder/circuit.py | 3 +- .../test_basic_descriptor.py | 2 +- 44 files changed, 178 insertions(+), 185 deletions(-) rename src/zepben/ewb/boilerplate/{descriptor_fix.py => backed_descriptor.py} (91%) create mode 100644 src/zepben/ewb/boilerplate/collections/mrid_collection.py diff --git a/src/zepben/ewb/__init__.py b/src/zepben/ewb/__init__.py index c3ea06945..114dcc8e3 100644 --- a/src/zepben/ewb/__init__.py +++ b/src/zepben/ewb/__init__.py @@ -14,7 +14,7 @@ from zepben.ewb.auth.common.auth_exception import * from zepben.ewb.auth.common.auth_method import * from zepben.ewb.boilerplate.dataclass_base import * -from zepben.ewb.boilerplate.descriptor_fix import * +from zepben.ewb.boilerplate.backed_descriptor import * from zepben.ewb.util import * ############# diff --git a/src/zepben/ewb/boilerplate/descriptor_fix.py b/src/zepben/ewb/boilerplate/backed_descriptor.py similarity index 91% rename from src/zepben/ewb/boilerplate/descriptor_fix.py rename to src/zepben/ewb/boilerplate/backed_descriptor.py index ae8c2cc3d..4aab5fd79 100644 --- a/src/zepben/ewb/boilerplate/descriptor_fix.py +++ b/src/zepben/ewb/boilerplate/backed_descriptor.py @@ -34,12 +34,11 @@ def __set_name__(self, owner, name): self.backing_name = self.private_field.name def __get__(self, instance, _): + if instance is None: + return self return getattr(instance, self.backing_name) def __set__(self, instance, value): return setattr(instance, self.backing_name, value) Alias = BackedDescriptor - -# Alias for dataclass with params. Makes it easier to edit params for all of CIM at once. -# zb_dataclass = dataclass(init=False, eq=False, slots=True, repr=False) diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py index ec5599d1a..7b063ea9b 100644 --- a/src/zepben/ewb/boilerplate/backfill.py +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -4,29 +4,31 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import Field from types import MemberDescriptorType -from typing import Any, Callable, TypeVar, Protocol +from typing import Any, Callable, TypeVar from zepben.ewb import BackedDescriptor +from zepben.ewb.boilerplate.collections.mrid_collection import S -class HasMrid(Protocol): - mrid: str - - -S = TypeVar("S", bound=HasMrid) +F = TypeVar("F", bound=Callable[..., Any]) class Backfill: - def __init__(self, backfill_prop: property): + def __init__(self, backfill_prop: property) -> None: self.backfill_prop = backfill_prop - def apply(self, element: S, owner: Any): + def apply(self, element: S, owner: Any) -> None: name = self.backfill_prop.fget.__name__ - if hasattr(self.backfill_prop, "__target"): - backing_name = self.backfill_prop.__target.__name__ - else: - backing_name = name + target = getattr(self.backfill_prop.fget, "_internal_target", None) + backing_name = name if target is None else ( + getattr(target, "name", None) + or getattr(target, "__name__", None) + or getattr(target.fget, "__name__", None) + ) + + if backing_name is None: + raise TypeError(f"Cannot determine backing name for {target!r}") if getattr(element, backing_name) is None: setattr(element, backing_name, owner) @@ -36,11 +38,14 @@ def apply(self, element: S, owner: Any): raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") -def internal(target: Any): +def internal(target: Any) -> Callable[[F], F]: if not any(isinstance(target, cls) for cls in (Field, MemberDescriptorType, BackedDescriptor, property)): - raise TypeError(f"target parameter of the target decorator has to be an instance of dataclass Field, instead is {target}") + raise TypeError(f"target parameter of the target decorator has to be an instance of property or dataclass Field, instead is {target}") + + if isinstance(target, property) and target.fget is None: + raise TypeError(f"Cannot backfill a property without a getter: {target}") def dec(func: Callable): - setattr(func, "__target", target) + setattr(func, "_internal_target", target) return func return dec diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index ed5df35ec..15744b1ef 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -41,15 +41,6 @@ def append(self, item): if self.sort_by is not None: existing.sort(key=self.sort_by) - def __len__(self): - return len(self._get_collection()) - - def __iter__(self): - return iter(self._get_collection()) - - def __contains__(self, item): - return item in (self._get_collection()) - def remove(self, item): existing = self._get_collection() existing.remove(item) @@ -64,21 +55,16 @@ def __repr__(self): return self.__class__.__repr__() return str(self._get_collection()) - def __str__(self): - return self.__repr__() - - def __getitem__(self, item): - return (self._get_collection())[item] - class LazyIndexedList(LazyValidatedList[T]): def __init__( self, private_field, - element_description: str + element_description: str, + validate = None, ): - super().__init__(private_field) + super().__init__(private_field, validate=validate) self.element_description = element_description def insert(self, index: int, item: T) -> None: @@ -97,6 +83,9 @@ def insert(self, index: int, item: T) -> None: "no gaps in the numbering." ) + if self.validate is not None: + self.validate(self.instance, item) + existing = getattr(self.instance, self.backing_name) if existing is None: diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py new file mode 100644 index 000000000..f08f7b0df --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -0,0 +1,48 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC, abstractmethod +from typing import Any, Protocol, TypeVar + +from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedCollection + + +class HasMrid(Protocol): + mrid: str + + +S = TypeVar("S", bound=HasMrid) + + +class MridCollection(AbstractBackedCollection[S], ABC): + instance: Any + element_description: str + + @abstractmethod + def _safe_get_by_mrid(self, mrid: str) -> S | None: ... + + def get_by_mrid(self, mrid: str) -> S: + """ + Get an element matching given ``mrid`` + + raises KeyError if one is not present + """ + res = self._safe_get_by_mrid(mrid) + if res is None: + raise KeyError(mrid) + return res + + def _can_add_by_mrid(self, element: S) -> bool: + existing = self._safe_get_by_mrid(element.mrid) + + if existing is None: + return True + + if existing is not element: + raise ValueError( + f"{self.element_description} with mRID {element.mrid} " + f"already exists in {self.instance}." + ) + + return False diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index d5f6f5040..8409d2aac 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -2,57 +2,17 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from abc import abstractmethod, ABC -from typing import Any, TypeVar, Protocol, Sequence +from typing import TypeVar, Protocol, Sequence from typing_extensions import Self from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedCollection, AbstractBackedList +from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class HasMrid(Protocol): - mrid: str - - -S = TypeVar("S", bound=HasMrid) - - -class MridCollection(AbstractBackedCollection[S], ABC): - instance: Any - element_description: str - - @abstractmethod - def _safe_get_by_mrid(self, mrid: str) -> S | None: ... - - def get_by_mrid(self, mrid: str) -> S: - """ - Get an element matching given ``mrid`` - - raises KeyError if one is not present - """ - res = self._safe_get_by_mrid(mrid) - if res is None: - raise KeyError - return res - - def _can_add_by_mrid(self, element: S) -> bool: - existing = self._safe_get_by_mrid(element.mrid) - - if existing is None: - return True - - if existing is not element: - raise ValueError( - f"{self.element_description} with mRID {element.mrid} " - f"already exists in {self.instance}." - ) - - return False - - class LazyMridList(LazyValidatedList, MridCollection[S]): def __init__(self, private_field, @@ -85,10 +45,12 @@ class MridList(_IterableWrapper, AbstractBackedList[S], MridCollection[S]): def __init__(self, private_field, element_description: str, + backfill: Backfill = None, validate=None, sort_by=None): super().__init__(private_field) self.element_description = element_description + self.backfill = backfill self.validate = validate self.sort_by = sort_by self.backing_list = None @@ -114,6 +76,9 @@ def append(self, item: S): if not self._can_add_by_mrid(item): return + if self.backfill is not None: + self.backfill.apply(item, self.instance) + if self.validate is not None: self.validate(self.instance, item) @@ -122,15 +87,6 @@ def append(self, item: S): if self.sort_by is not None: self.backing_list.sort(key=self.sort_by) - def __len__(self): - return len(self.backing_list) - - def __iter__(self): - return iter(self.backing_list) - - def __contains__(self, item): - return item in self.backing_list - def remove(self, item): self.backing_list.remove(item) @@ -139,9 +95,3 @@ def clear(self): def __repr__(self): return str(self.backing_list) - - def __str__(self): - return self.__repr__() - - def __getitem__(self, item): - return self.backing_list[item] diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py index f4fadd1d9..26752d683 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -5,21 +5,14 @@ from typing import Protocol, TypeVar, Collection from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class HasMrid(Protocol): - mrid: str - T = TypeVar("T") -S = TypeVar("S", bound=HasMrid) class LazyMridMap(_IterableWrapper[S], MridCollection[S]): - def _get_collection(self) -> Collection[T]: - pass - def __init__( self, private_field, @@ -37,26 +30,15 @@ def _get(self): def _get_or_empty(self) -> dict[str, S]: return getattr(self.instance, self.backing_name) or {} + def _get_collection(self) -> list[S]: + return list(self._get_or_empty().values()) + def _safe_get_by_mrid(self, mrid: str) -> S | None: return self._get_or_empty().get(mrid, None) def get_by_mrid(self, mrid: str) -> S: return self._get_or_empty()[mrid] - def _can_add_by_mrid(self, element: S) -> bool: - existing = self._safe_get_by_mrid(element.mrid) - - if existing is None: - return True - - if existing is not element: - raise ValueError( - f"{self.element_description} with mRID {element.mrid} " - f"already exists in {self.instance}." - ) - - return False - def append(self, element: S): if not self._can_add_by_mrid(element): return @@ -77,11 +59,8 @@ def append(self, element: S): def __len__(self): return len(self._get_or_empty()) - def __iter__(self): - return iter(self._get_or_empty().values()) - def __contains__(self, item: S): - return self._get_or_empty().get(item.mrid, None) == item + return self._get_or_empty().get(getattr(item, "mrid", None), None) == item def remove(self, item): existing = self._get_or_empty() @@ -96,8 +75,5 @@ def clear(self): def __repr__(self): return str(self._get_or_empty()) - def __str__(self): - return self.__repr__() - def __getitem__(self, item): return (self._get_or_empty())[item] diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index 7f49170ef..020a5dc10 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -3,7 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from dataclasses import dataclass, fields, MISSING, Field -from typing import TypeVar +from typing import TypeVar, dataclass_transform def _is_set(obj: object, name: str) -> bool: @@ -85,7 +85,6 @@ class MyClass: return cls - def zb_dataclass(cls: type[object]): """ Shorthand alias for ``@dataclass(init=False,eq=False,slots=True,repr=False)`` @@ -94,6 +93,24 @@ def zb_dataclass(cls: type[object]): cls = remove_descriptor_annotations(cls) return dataclass(init=False, eq=False, slots=True, repr=False)(cls) +def resolve_default(instance, field: Field): + # Set field defaults + if field.default is not MISSING: + setattr(instance, field.name, field.default) + elif field.default_factory is not MISSING: + setattr(instance, field.name, field.default_factory()) + + # Ignore custom descriptors + elif hasattr(field, '__get__') and not isinstance(field, Field): + return + + # Mimic default Python missing arg error + else: + raise TypeError( + f"Missing required field {field.name!r} " + f"for {type(instance).__name__}" + ) + @zb_dataclass class DataclassBase: @@ -107,6 +124,10 @@ class DataclassBase: For more motivation, refer to `MANIFESTO.md` """ def __init__(self, **kwargs) -> None: + # Currently post init implementation requires a lot of extra logic, + # which would mess with readability. If you require it - add it. + if callable(getattr(self, "__post_init__", None)): + raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") # Manually assign defaults in dataclass fields for f in fields(type(self)): @@ -120,27 +141,3 @@ def __init__(self, **kwargs) -> None: # str-based setattr triggers descriptors, allowing us to intercept __set__. for attr, value in kwargs.items(): setattr(self, attr, value) - - - # Currently post init implementation requires a lot of extra logic, - # which would mess with readability. If you require it - add it. - if callable(getattr(self, "__post_init__", None)): - raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") - -def resolve_default(instance, field: Field): - # Set field defaults - if field.default is not MISSING: - setattr(instance, field.name, field.default) - elif field.default_factory is not MISSING: - setattr(instance, field.name, field.default_factory()) - - # Ignore custom descriptors - elif hasattr(field, '__get__'): - return - - # Mimic default Python missing arg error - else: - raise TypeError( - f"Missing required field {field.name!r} " - f"for {type(instance).__name__}" - ) diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py index d9356a515..2659d0d43 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.substation import Substation diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py index 679214809..4ac09a559 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index f4502f637..8d7ef7724 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -12,7 +12,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 7bce7fcd8..e099d658d 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -23,7 +23,8 @@ from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61968.assetinfo.relay_info import RelayInfo diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py index d6a4737d6..e99991643 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_system import ProtectionRelaySystem diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py index c3a48512f..c62658d45 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py @@ -15,7 +15,8 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_scheme import ProtectionRelayScheme diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py index b894424cf..3dfafdd20 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.transformer_tank_info import TransformerTankInfo diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py index be905cde1..a5653106e 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assetinfo.power_transformer_info import PowerTransformerInfo diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset.py b/src/zepben/ewb/model/cim/iec61968/assets/asset.py index f6b3880e9..b28e0a2eb 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset_organisation_role import AssetOrganisationRole diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index ed32f5557..1e66805ac 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -15,7 +15,8 @@ from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer_agreement import CustomerAgreement diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py index 83e7a12d2..1d2ae9700 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py @@ -13,7 +13,8 @@ from zepben.ewb.model.cim.iec61968.common.agreement import Agreement from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.customer import Customer diff --git a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py index dfe79e4d2..c8d60f991 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py @@ -13,7 +13,8 @@ from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.customers.tariff import Tariff diff --git a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py index 92da48fd8..451f7f25c 100644 --- a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py +++ b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py @@ -13,7 +13,8 @@ from zepben.ewb.model.cim.iec61968.assets.structure import Structure from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.streetlight import Streetlight diff --git a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py index c0c3158e4..877a7ac53 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61968.assets.asset_container import AssetContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index 594a5613e..625c6a0d1 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -16,7 +16,8 @@ from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.util import nlen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.common.location import Location diff --git a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py index 481c6af89..e0f0a8145 100644 --- a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py +++ b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py @@ -13,7 +13,8 @@ from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py index 8ba98f005..99094aa19 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index a41e2d4b6..a103ed3f3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -12,7 +12,8 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index 604f838ec..423cb884d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -22,7 +22,8 @@ from zepben.ewb.util import ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.metering.usage_point import UsagePoint diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py index bad482aaa..d4c2bcc53 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py @@ -14,7 +14,7 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import ConnectivityNodeContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 01ce4ed0b..7c5f87bcc 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -12,7 +12,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index 7400e2db9..9a44f3aed 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.sub_geographical_region import SubGeographicalRegion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py index 447c1f2c4..84cdd64ef 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/name_type.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable from zepben.ewb.model.cim.iec61970.base.core.name import Name from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.descriptor_fix import BackedDescriptor +from zepben.ewb.boilerplate.backed_descriptor import BackedDescriptor if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py index 3c04f3f14..8482bba2c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py @@ -15,7 +15,8 @@ from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import nlen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61968.assets.asset import Asset diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index ed7a7b3ff..258c08430 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.core.substation import Substation diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index 1d63b2e95..83d037ff8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index cf0a02a99..eb62a1e33 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index 6fa190c15..a19f232ab 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -13,7 +13,8 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase from zepben.ewb.model.cim.iec61970.base.wires.clamp import Clamp diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 45bd5933c..8bb47d7d8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_consumer_phase import EnergyConsumerPhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index 77bcbd1f5..491371a53 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection from zepben.ewb.model.cim.iec61970.base.wires.energy_source_phase import EnergySourcePhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 9adcfb80e..00e52dcbf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -14,7 +14,8 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase from zepben.ewb.model.cim.iec61970.base.wires.regulating_cond_eq import RegulatingCondEq diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py index 65b11195c..bb5091b87 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py @@ -14,7 +14,8 @@ from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_relay_function import ProtectionRelayFunction diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py index 76ca8cf20..5bfa3848c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py @@ -16,7 +16,8 @@ from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.regulating_control_mode_kind import RegulatingControlModeKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py index eeaf4a685..f98cfc0a7 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py @@ -12,7 +12,8 @@ from zepben.ewb.model.cim.iec61970.base.wires.rotating_machine import RotatingMachine from zepben.ewb.model.cim.iec61970.base.wires.synchronous_machine_kind import SynchronousMachineKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.reactive_capability_curve import ReactiveCapabilityCurve diff --git a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py index 2481e8117..f0f1fd83e 100644 --- a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py +++ b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py @@ -13,7 +13,8 @@ from zepben.ewb.model.cim.iec61970.base.wires.line import Line from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import MridCollection, LazyMridList +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.feeder.loop import Loop diff --git a/test/dataclass_descriptors/test_basic_descriptor.py b/test/dataclass_descriptors/test_basic_descriptor.py index f0b9b1caa..8d6c756ed 100644 --- a/test/dataclass_descriptors/test_basic_descriptor.py +++ b/test/dataclass_descriptors/test_basic_descriptor.py @@ -6,7 +6,7 @@ from zepben.ewb.boilerplate.dataclass_base import DataclassBase from zepben.ewb import zb_dataclass, remove_descriptor_annotations -from zepben.ewb.boilerplate.descriptor_fix import BackedDescriptor +from zepben.ewb.boilerplate.backed_descriptor import BackedDescriptor @zb_dataclass From 40277b11de662b1227c9eb4bc5ad671fca7f65ff Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 27 Jul 2026 17:07:02 +1000 Subject: [PATCH 34/54] Yet more cleanup Signed-off-by: Boris Filin --- .../ewb/boilerplate/backed_descriptor.py | 7 +-- src/zepben/ewb/boilerplate/backfill.py | 6 +- ...tions.py => abstract_backed_collection.py} | 25 +------- .../collections/abstract_backed_list.py | 31 +++++++++ .../collections/lazy_collection.py | 60 ++++++++++++++++++ .../ewb/boilerplate/collections/lazy_list.py | 63 ++----------------- .../collections/mrid_collection.py | 2 +- .../ewb/boilerplate/collections/mrid_list.py | 54 +++++++++------- .../ewb/boilerplate/collections/mrid_map.py | 34 +++++----- .../ewb/boilerplate/collections/wrapper.py | 15 +++-- src/zepben/ewb/boilerplate/dataclass_base.py | 32 +++++++--- .../iec61968/assetinfo/relay_info.py | 4 +- .../iec61968/common/contact_details.py | 7 ++- .../protection/protection_relay_function.py | 6 +- .../ewb/model/cim/iec61968/common/location.py | 4 +- .../ewb/model/cim/iec61970/base/core/curve.py | 4 +- .../base/diagramlayout/diagram_object.py | 4 +- .../base/wires/per_length_phase_impedance.py | 4 +- .../base/wires/power_transformer_end.py | 4 +- 19 files changed, 205 insertions(+), 161 deletions(-) rename src/zepben/ewb/boilerplate/collections/{abstract_backed_collections.py => abstract_backed_collection.py} (72%) create mode 100644 src/zepben/ewb/boilerplate/collections/abstract_backed_list.py create mode 100644 src/zepben/ewb/boilerplate/collections/lazy_collection.py diff --git a/src/zepben/ewb/boilerplate/backed_descriptor.py b/src/zepben/ewb/boilerplate/backed_descriptor.py index 4aab5fd79..4c63fe961 100644 --- a/src/zepben/ewb/boilerplate/backed_descriptor.py +++ b/src/zepben/ewb/boilerplate/backed_descriptor.py @@ -2,10 +2,6 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -# Copyright 2026 Zeppelin Bend Pty Ltd -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. from __future__ import annotations @@ -39,6 +35,9 @@ def __get__(self, instance, _): return getattr(instance, self.backing_name) def __set__(self, instance, value): + if self.backing_name is None: + raise ValueError(f"Descriptor {self} is not yet aware of the supporting field - `__set__` cannot be called") return setattr(instance, self.backing_name, value) + Alias = BackedDescriptor diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py index 7b063ea9b..27df77c3f 100644 --- a/src/zepben/ewb/boilerplate/backfill.py +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -2,6 +2,7 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. + from dataclasses import Field from types import MemberDescriptorType from typing import Any, Callable, TypeVar @@ -15,6 +16,9 @@ class Backfill: def __init__(self, backfill_prop: property) -> None: + if backfill_prop.fget is None: + raise TypeError(f"Cannot backfill a property without a getter: {backfill_prop!r}") + self.backfill_prop = backfill_prop def apply(self, element: S, owner: Any) -> None: @@ -24,7 +28,7 @@ def apply(self, element: S, owner: Any) -> None: backing_name = name if target is None else ( getattr(target, "name", None) or getattr(target, "__name__", None) - or getattr(target.fget, "__name__", None) + or getattr(getattr(target, "fget", None), "__name__", None) ) if backing_name is None: diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py similarity index 72% rename from src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py rename to src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py index f8c1c8862..859216aa8 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_collections.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -3,7 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import ABC, abstractmethod -from typing import Collection, Iterable, Generic, Iterator, Callable, Any, Sequence, overload, TypeVar +from typing import Collection, Iterable, Generic, Iterator, Callable, TypeVar T = TypeVar("T") @@ -40,7 +40,7 @@ def __iter__(self) -> Iterator[T]: def __contains__(self, element: object) -> bool: return element in self._get_collection() - def for_each_indexed(self, action: Callable[[int, T], Any]): + def for_each_indexed(self, action: Callable[[int, T], object]) -> None: """ Call the `action` on each item in the list @@ -50,24 +50,3 @@ def for_each_indexed(self, action: Callable[[int, T], Any]): action(index, item) -class AbstractBackedList( - AbstractBackedCollection[T], - Sequence[T], - Generic[T], - ABC, -): - - @abstractmethod - def _get_collection(self) -> Sequence[T]: - ... - - @overload - def __getitem__(self, index: int) -> T: - ... - - @overload - def __getitem__(self, index: slice) -> Sequence[T]: - ... - - def __getitem__(self, index: int | slice) -> T | Sequence[T]: - return self._get_collection()[index] diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py new file mode 100644 index 000000000..0e60de702 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py @@ -0,0 +1,31 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC, abstractmethod +from typing import Sequence, Generic, overload + +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection, T + + +class AbstractBackedList( + AbstractBackedCollection[T], + Sequence[T], + Generic[T], + ABC, +): + + @abstractmethod + def _get_collection(self) -> Sequence[T]: + ... + + @overload + def __getitem__(self, index: int) -> T: + ... + + @overload + def __getitem__(self, index: slice) -> Sequence[T]: + ... + + def __getitem__(self, index: int | slice) -> T | Sequence[T]: + return self._get_collection()[index] diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py new file mode 100644 index 000000000..b7fe49595 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_collection.py @@ -0,0 +1,60 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import TypeVar + +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper + + +T = TypeVar("T") + + +class LazyCollection(_IterableWrapper[T], AbstractBackedList[T]): + + def __init__( + self, + private_field, + validate=None, + sort_by=None + ) -> None: + super().__init__(private_field) + self.validate = validate + self.sort_by = sort_by + + def _get(self) -> list[T] | None: + return getattr(self.instance, self.backing_name) + + def _get_collection(self) -> list[T]: + return getattr(self.instance, self.backing_name) or [] + + def append(self, item: T) -> None: + if self.validate is not None: + self.validate(self.instance, item) + + existing = getattr(self.instance, self.backing_name) + if existing is None: + existing = [item] + setattr(self.instance, self.backing_name, existing) + else: + existing.append(item) + + if self.sort_by is not None: + existing.sort(key=self.sort_by) + + def remove(self, item: T) -> None: + existing = self._get_collection() + existing.remove(item) + if not existing: + self.clear() + + def clear(self) -> None: + setattr(self.instance, self.backing_name, None) + + def __repr__(self) -> str: + if self.instance is None: + return object.__repr__(self) + return repr(self._get_collection()) + + diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index 15744b1ef..14b54afcd 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -2,69 +2,19 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from typing import TypeVar -from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList -from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection, T -T = TypeVar("T") - - -class LazyValidatedList(_IterableWrapper, AbstractBackedList[T]): - - def __init__(self, - private_field, - validate=None, - sort_by=None): - super().__init__(private_field) - self.validate = validate - self.sort_by = sort_by - - def _get(self): - return getattr(self.instance, self.backing_name) - - def _get_collection(self) -> list[T]: - return getattr(self.instance, self.backing_name) or [] - - def append(self, item): - if self.validate is not None: - self.validate(self.instance, item) - - existing = getattr(self.instance, self.backing_name) - if existing is None: - existing = [item] - setattr(self.instance, self.backing_name, existing) - else: - existing.append(item) - - if self.sort_by is not None: - existing.sort(key=self.sort_by) - - def remove(self, item): - existing = self._get_collection() - existing.remove(item) - if not existing: - self.clear() - - def clear(self): - setattr(self.instance, self.backing_name, None) - - def __repr__(self): - if self.instance is None: - return self.__class__.__repr__() - return str(self._get_collection()) - - -class LazyIndexedList(LazyValidatedList[T]): +class LazyList(LazyCollection[T]): def __init__( self, private_field, element_description: str, - validate = None, - ): - super().__init__(private_field, validate=validate) + validate=None, + ) -> None: + super().__init__(private_field, validate=validate, sort_by=None) self.element_description = element_description def insert(self, index: int, item: T) -> None: @@ -94,9 +44,6 @@ def insert(self, index: int, item: T) -> None: else: existing.insert(index, item) - if self.sort_by is not None: - existing.sort(key=self.sort_by) - def append(self, item: T) -> None: self.insert(len(self), item) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py index f08f7b0df..a5f2d43de 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_collection.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -5,7 +5,7 @@ from abc import ABC, abstractmethod from typing import Any, Protocol, TypeVar -from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedCollection +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection class HasMrid(Protocol): diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 8409d2aac..260e6e2ff 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -2,24 +2,26 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from typing import TypeVar, Protocol, Sequence +from typing import Sequence from typing_extensions import Self from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList -from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class LazyMridList(LazyValidatedList, MridCollection[S]): - def __init__(self, - private_field, - element_description: str, - backfill: Backfill = None, - validate=None, - sort_by=None): +class LazyMridList(LazyCollection[S], MridCollection[S]): + def __init__( + self, + private_field, + element_description: str, + backfill: Backfill = None, + validate=None, + sort_by=None + ) -> None: super().__init__(private_field, validate, sort_by) self.element_description = element_description self.backfill = backfill @@ -31,7 +33,7 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: found = next((element for element in existing if element.mrid == mrid), None) return found - def append(self, item: S): + def append(self, item: S) -> None: if not self._can_add_by_mrid(item): return @@ -41,13 +43,15 @@ def append(self, item: S): super().append(item) -class MridList(_IterableWrapper, AbstractBackedList[S], MridCollection[S]): - def __init__(self, - private_field, - element_description: str, - backfill: Backfill = None, - validate=None, - sort_by=None): +class MridList(_IterableWrapper[S], AbstractBackedList[S], MridCollection[S]): + def __init__( + self, + private_field, + element_description: str, + backfill: Backfill = None, + validate=None, + sort_by=None + ) -> None: super().__init__(private_field) self.element_description = element_description self.backfill = backfill @@ -62,7 +66,7 @@ def __get__(self, instance, owner=None) -> Self: obj.__post_init__() return obj - def __post_init__(self): + def __post_init__(self) -> None: self.backing_list = getattr(self.instance, self.backing_name) def _get_collection(self) -> Sequence[S]: @@ -72,7 +76,7 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: found = next((element for element in self.backing_list if element.mrid == mrid), None) return found - def append(self, item: S): + def append(self, item: S) -> None: if not self._can_add_by_mrid(item): return @@ -87,11 +91,13 @@ def append(self, item: S): if self.sort_by is not None: self.backing_list.sort(key=self.sort_by) - def remove(self, item): + def remove(self, item: S) -> None: self.backing_list.remove(item) - def clear(self): + def clear(self) -> None: self.backing_list.clear() - def __repr__(self): - return str(self.backing_list) + def __repr__(self) -> str: + if self.instance is None: + return object.__repr__(self) + return repr(self.backing_list) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py index 26752d683..9d1672c58 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -2,36 +2,36 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from typing import Protocol, TypeVar, Collection +from typing import TypeVar, ValuesView from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper - T = TypeVar("T") -class LazyMridMap(_IterableWrapper[S], MridCollection[S]): +class LazyMridMap(_IterableWrapper[T], MridCollection[S]): def __init__( self, private_field, element_description: str, backfill: Backfill = None, - validate=None): + validate=None + ) -> None: super().__init__(private_field) self.element_description = element_description self.backfill = backfill self.validate = validate - def _get(self): + def _get(self) -> dict[str, S] | None: return getattr(self.instance, self.backing_name) def _get_or_empty(self) -> dict[str, S]: return getattr(self.instance, self.backing_name) or {} - def _get_collection(self) -> list[S]: - return list(self._get_or_empty().values()) + def _get_collection(self) -> ValuesView[S]: + return self._get_or_empty().values() def _safe_get_by_mrid(self, mrid: str) -> S | None: return self._get_or_empty().get(mrid, None) @@ -39,7 +39,7 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: def get_by_mrid(self, mrid: str) -> S: return self._get_or_empty()[mrid] - def append(self, element: S): + def append(self, element: S) -> None: if not self._can_add_by_mrid(element): return @@ -56,24 +56,26 @@ def append(self, element: S): else: existing[element.mrid] = element - def __len__(self): + def __len__(self) -> int: return len(self._get_or_empty()) - def __contains__(self, item: S): - return self._get_or_empty().get(getattr(item, "mrid", None), None) == item + def __contains__(self, item: S) -> bool: + return self._get_or_empty().get(getattr(item, "mrid", None)) == item - def remove(self, item): + def remove(self, item) -> None: existing = self._get_or_empty() del existing[item.mrid] if not existing: self.clear() - def clear(self): + def clear(self) -> None: setattr(self.instance, self.backing_name, None) - def __repr__(self): - return str(self._get_or_empty()) + def __repr__(self) -> str: + if self.instance is None: + return object.__repr__(self) + return repr(self._get_or_empty()) - def __getitem__(self, item): + def __getitem__(self, item) -> S: return (self._get_or_empty())[item] diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py index 786141d14..d697e1a23 100644 --- a/src/zepben/ewb/boilerplate/collections/wrapper.py +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -2,12 +2,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. + from abc import ABC -from typing import Any +from typing import Any, TypeVar + from typing_extensions import Self, deprecated from zepben.ewb import BackedDescriptor, resolve_default -from zepben.ewb.boilerplate.collections.abstract_backed_collections import AbstractBackedList, AbstractBackedCollection +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection class _Wrapper(BackedDescriptor): @@ -25,7 +27,7 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self: obj._init_kwargs = kwargs.copy() return obj - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.instance = None self.backing_name = None @@ -48,7 +50,7 @@ class _WrapperFgetFix(_Wrapper): This class exists to fix the tests that rely on the old lists being @property. TODO: Remove in a separate PR fixing tests """ - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) def fget(instance): return self.__get__(instance) @@ -62,12 +64,13 @@ def __set_name__(self, owner, name): @deprecated("Lists are no longer a property") def fget(self, instance): ... +T = TypeVar("T") -class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection, ABC): +class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection[T], ABC): """ This class allows us to assign lists at init time to avoid special-case handling. """ - def __set__(self, instance, value): + def __set__(self, instance, value) -> None: if instance is None: return if not hasattr(instance, self.backing_name): diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index 020a5dc10..ace55ca85 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -2,8 +2,14 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. + from dataclasses import dataclass, fields, MISSING, Field -from typing import TypeVar, dataclass_transform +from typing import TypeVar, cast + +from typing_extensions import dataclass_transform + + +T = TypeVar("T") def _is_set(obj: object, name: str) -> bool: @@ -13,9 +19,7 @@ def _is_set(obj: object, name: str) -> bool: return False return True -T = TypeVar("T", bound=type) - -def remove_descriptor_annotations(cls: T) -> T: +def remove_descriptor_annotations(cls: type[T]) -> T: """ Remove annotations for class attributes that are data descriptors. @@ -85,13 +89,23 @@ class MyClass: return cls -def zb_dataclass(cls: type[object]): +@dataclass_transform(eq_default=False, order_default=False) +def zb_dataclass(cls: type[T]) -> type[T]: """ - Shorthand alias for ``@dataclass(init=False,eq=False,slots=True,repr=False)`` + Shorthand alias for ``@dataclass(init=False, eq=False, slots=True, repr=False)`` Allows us to modify dataclass parameters for all of CIM from a single reference point """ cls = remove_descriptor_annotations(cls) - return dataclass(init=False, eq=False, slots=True, repr=False)(cls) + # The cast is purely for type checkers to be aware of the true class of cls + return cast( + type[T], + dataclass( + init=False, + eq=False, + slots=True, + repr=False, + )(cls), + ) def resolve_default(instance, field: Field): # Set field defaults @@ -100,9 +114,7 @@ def resolve_default(instance, field: Field): elif field.default_factory is not MISSING: setattr(instance, field.name, field.default_factory()) - # Ignore custom descriptors - elif hasattr(field, '__get__') and not isinstance(field, Field): - return + # Note: custom descriptors are not included in `fields(cls)`, so we don't need to handle them here # Mimic default Python missing arg error else: diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index b107ac078..c7441f355 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo @@ -31,7 +31,7 @@ class RelayInfo(AssetInfo): _reclose_delays: Optional[List[float]] = field(default=None) - reclose_delays: LazyIndexedList[float] = LazyIndexedList( + reclose_delays: LazyList[float] = LazyList( _reclose_delays, "float" ) diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py index 31e5336ad..4a08b7312 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -11,7 +11,8 @@ from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress @@ -66,12 +67,12 @@ def id(self) -> str: _phone_numbers: list[TelephoneNumber] | None = field(default=None) - phone_numbers: LazyValidatedList[TelephoneNumber] = LazyValidatedList(_phone_numbers) + phone_numbers: AbstractBackedList[TelephoneNumber] = LazyCollection(_phone_numbers) """[ZBEX] Phone numbers.""" _electronic_addresses: list[ElectronicAddress] | None = field(default=None) - electronic_addresses: LazyValidatedList[ElectronicAddress] = LazyValidatedList(_electronic_addresses) + electronic_addresses: AbstractBackedList[ElectronicAddress] = LazyCollection(_electronic_addresses) """[ZBEX] Electronic addresses.""" def __init__(self, id: str|None=None, *args, **kwargs): diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index e099d658d..580691d78 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -11,7 +11,7 @@ from typing import Optional, List, Callable, TYPE_CHECKING, Any from abc import ABCMeta -from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyList if sys.version_info >= (3, 13): from warnings import deprecated else: @@ -84,12 +84,12 @@ def relay_info(self): def relay_info(self, relay_info: Optional[RelayInfo]): self.asset_info = relay_info - time_limits: LazyIndexedList[float] = LazyIndexedList( + time_limits: LazyList[float] = LazyList( _time_limits, "float" ) - thresholds: LazyIndexedList[RelaySetting] = LazyIndexedList( + thresholds: LazyList[RelaySetting] = LazyList( _thresholds, "RelaySetting" ) diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index e6d22987d..cae9e9219 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -14,7 +14,7 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject @@ -31,7 +31,7 @@ class Location(IdentifiedObject): _position_points: Optional[List[PositionPoint]] = field(default=None) - points: LazyIndexedList[PositionPoint] = LazyIndexedList( + points: LazyList[PositionPoint] = LazyList( _position_points, "PositionPoint", ) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index 0e1fa4039..73b7ed83a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -12,13 +12,13 @@ from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.model.cim.iec61970.base.core.curve_data import CurveData from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import require -class CurveDataList(LazyValidatedList[CurveData]): +class CurveDataList(LazyCollection[CurveData]): def get(self, x: float) -> CurveData: """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 4ea2e9683..d4415c9c3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.lazy_list import LazyIndexedList +from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint @@ -49,7 +49,7 @@ def diagram(self): _diagram_object_points: Optional[List[DiagramObjectPoint]] = field(default=None) - points: LazyIndexedList[DiagramObjectPoint] = LazyIndexedList( + points: LazyList[DiagramObjectPoint] = LazyList( _diagram_object_points, "DiagramObjectPoint", ) diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py index 9bae87c2d..9290d87e9 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py @@ -13,14 +13,14 @@ from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.phase_impedance_data import PhaseImpedanceData from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.util import require, none -class PhaseImpedanceDataList(LazyValidatedList): +class PhaseImpedanceDataList(LazyCollection): def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: """ Get the matrix entry for the corresponding to and from phases. diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index c64d2fb03..7bbd6306f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -14,7 +14,7 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.lazy_list import LazyValidatedList +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS @@ -27,7 +27,7 @@ from zepben.ewb.model.cim.iec61970.base.wires.power_transformer import PowerTransformer -class TransformerEndRatedSList(LazyValidatedList[TransformerEndRatedS]): +class TransformerEndRatedSList(LazyCollection[TransformerEndRatedS]): def get_by_cooling_type( self, From efa8a336b3236cef460e742baccb6343e5c1d646 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Wed, 29 Jul 2026 15:24:09 +1000 Subject: [PATCH 35/54] Move extension lists to individual files Signed-off-by: Boris Filin --- .../ewb/boilerplate/relations/__init__.py | 0 .../relations/ac_line_segment_phase_list.py | 21 ++++++++++ .../relations/battery_control_list.py | 24 +++++++++++ .../boilerplate/relations/curve_data_list.py | 38 +++++++++++++++++ .../relations/phase_impedance_data_list.py | 36 ++++++++++++++++ .../relations/power_transformer_end_list.py | 42 +++++++++++++++++++ .../boilerplate/relations/terminal_list.py | 18 ++++++++ .../relations/transformer_end_rated_s_list.py | 27 ++++++++++++ .../base/core/conducting_equipment.py | 10 +---- .../ewb/model/cim/iec61970/base/core/curve.py | 29 +------------ .../generation/production/battery_unit.py | 17 +------- .../iec61970/base/wires/ac_line_segment.py | 13 +----- .../base/wires/per_length_phase_impedance.py | 26 +----------- .../iec61970/base/wires/power_transformer.py | 32 +------------- .../base/wires/power_transformer_end.py | 20 +-------- 15 files changed, 213 insertions(+), 140 deletions(-) create mode 100644 src/zepben/ewb/boilerplate/relations/__init__.py create mode 100644 src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/battery_control_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/curve_data_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/terminal_list.py create mode 100644 src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py diff --git a/src/zepben/ewb/boilerplate/relations/__init__.py b/src/zepben/ewb/boilerplate/relations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py new file mode 100644 index 000000000..a19a660aa --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py @@ -0,0 +1,21 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import SinglePhaseKind +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList + + +class AcLineSegmentPhaseList(LazyMridList): + def get_by_phase(self, phase: SinglePhaseKind): + """ + The individual phase models for an AcLineSegment. + `phase` the phase of the required [AcLineSegmentPhase] + """ + res = next((it for it in self if it.phase == phase), None) + if res is None: + raise KeyError(phase) + return res diff --git a/src/zepben/ewb/boilerplate/relations/battery_control_list.py b/src/zepben/ewb/boilerplate/relations/battery_control_list.py new file mode 100644 index 000000000..dcdeb7439 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/battery_control_list.py @@ -0,0 +1,24 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import BatteryControlMode +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList + + +class BatteryControlList(LazyMridList): + def get_by_mode(self, control_mode: BatteryControlMode): + """ + Get the `BatteryControl` identified by its `control_mode` + + `control_mode` the `BatteryControlMode` of the desired `BatteryControl` + Returns The `BatteryControl` with the specified `control_mode` if it exists + Raises `KeyError` if a `BatteryControl` with `control_mode` wasn't present. + """ + for control in self: + if control.control_mode == control_mode: + return control + raise IndexError(f"No BatteryControl with a control_mode of {control_mode} was found in BatteryUnit {str(self)}") diff --git a/src/zepben/ewb/boilerplate/relations/curve_data_list.py b/src/zepben/ewb/boilerplate/relations/curve_data_list.py new file mode 100644 index 000000000..3ee44553f --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/curve_data_list.py @@ -0,0 +1,38 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import TYPE_CHECKING + +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection + +if TYPE_CHECKING: + from zepben.ewb import CurveData + + +class CurveDataList(LazyCollection): + + def get(self, x: float) -> 'CurveData': + """ + Get the :class:`CurveData` identified by its `x_value`. + + :param x: The X value of the required :class:`CurveData`. + :returns: The :class:`CurveData` with the specified `x` if it exists. + :raises KeyError: When no `CurveData` was found with `x`. + """ + curve_data = next((it for it in self if it.x_value == x), None) + if curve_data: + return curve_data + raise KeyError(x) + + def remove_data_at(self, x: float) -> 'CurveData': + """ + Disassociate a :class:`CurveData` from this collection based on its `x_value`. + + :param x: The :class:`CurveData` to disassociate from this :class:`Curve`. + :returns: A reference to the removed :class:`CurveData`. + :raises IndexError: If no :class:`CurveData` with a value of `x` was not associated with this :class:`Curve`. + """ + data = self.get(x) + self.remove(data) + return data diff --git a/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py new file mode 100644 index 000000000..dae48773f --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py @@ -0,0 +1,36 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +if TYPE_CHECKING: + from zepben.ewb import SinglePhaseKind, PhaseImpedanceData + + +class PhaseImpedanceDataList(LazyCollection): + def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: + """ + Get the matrix entry for the corresponding to and from phases. + + :param from_phase: The from_phase to lookup. + :param to_phase: The to_phase to lookup. + :returns: The :class:`PhaseImpedanceData` with the specified `from_phase` and `to_phase` if it exists. + :raises KeyError: When no `PhaseImpedanceData` was found with a matching `from_phase` and `to_phase`. + """ + phase_impedance_data = next((it for it in self if it.from_phase == from_phase and it.to_phase == to_phase), None) + if phase_impedance_data: + return phase_impedance_data + + raise KeyError((from_phase, to_phase)) + + @property + def diagonal(self): + """ + Get only the diagonal elements of the matrix, i.e toPhase == fromPhase. + """ + return (pid for pid in self if pid.from_phase == pid.to_phase) diff --git a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py new file mode 100644 index 000000000..9c740cf8a --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py @@ -0,0 +1,42 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +if TYPE_CHECKING: + from zepben.ewb import PowerTransformerEnd, Terminal + + +class PowerTransformerEndList(LazyMridList['PowerTransformerEnd']): + + def get_by_num(self, end_number: int) -> PowerTransformerEnd: + """ + Get the `PowerTransformerEnd` on this `PowerTransformer` by its `end_number`. + + `end_number` The `end_number` of the `PowerTransformerEnd` in relation to this `PowerTransformer`s VectorGroup. + Returns The `PowerTransformerEnd` referred to by `end_number` + Raises IndexError if no `PowerTransformerEnd` was found with end_number `end_number`. + """ + end = next((it for it in self if it.end_number == end_number), None) + if end: + return end + raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self.instance)}") + + def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: + """ + Get the `PowerTransformerEnd` on this `PowerTransformer` by its `terminal`. + + `terminal` The `terminal` to find a `PowerTransformerEnd` for. + Returns The `PowerTransformerEnd` connected to the specified `terminal` + Raises IndexError if no `PowerTransformerEnd` connected to `terminal` was found on this `PowerTransformer`. + """ + + end = next((it for it in self if it.terminal == terminal), None) + if end: + return end + raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self.instance)}") diff --git a/src/zepben/ewb/boilerplate/relations/terminal_list.py b/src/zepben/ewb/boilerplate/relations/terminal_list.py new file mode 100644 index 000000000..b46ada487 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/terminal_list.py @@ -0,0 +1,18 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import Terminal +from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList + + +class TerminalsList(LazyMridList[Terminal]): + + def get_by_sequence_number(self, sequence_number: int) -> Terminal: + term = next((it for it in self if it.sequence_number == sequence_number), None) + if term is None: + raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self.instance)}") + return term diff --git a/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py new file mode 100644 index 000000000..580015532 --- /dev/null +++ b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py @@ -0,0 +1,27 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from zepben.ewb import TransformerEndRatedS, TransformerCoolingType +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection + + +class TransformerEndRatedSList(LazyCollection[TransformerEndRatedS]): + + def get_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS | None: + return next((rating for rating in self if rating.cooling_type == cooling_type), None) + + def remove_by_cooling_type( + self, + cooling_type: TransformerCoolingType, + ) -> TransformerEndRatedS | None: + rating = self.get_by_cooling_type(cooling_type) + if rating is not None: + self.remove(rating) + return rating diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py index 8ed0a6b7c..0cb3b5d35 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/conducting_equipment.py @@ -14,10 +14,10 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment +from zepben.ewb.boilerplate.relations.terminal_list import TerminalsList from zepben.ewb.util import require from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -25,14 +25,6 @@ from zepben.ewb.model.cim.iec61970.base.core.base_voltage import BaseVoltage -class TerminalsList(LazyMridList[Terminal]): - - def get_by_sequence_number(self, sequence_number: int) -> Terminal: - term = next((it for it in self if it.sequence_number == sequence_number), None) - if term is None: - raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self.instance)}") - return term - @zb_dataclass class ConductingEquipment(Equipment, metaclass=ABCMeta): """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index 73b7ed83a..d918383f5 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -12,39 +12,12 @@ from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.model.cim.iec61970.base.core.curve_data import CurveData from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject +from zepben.ewb.boilerplate.relations.curve_data_list import CurveDataList from zepben.ewb.util import require -class CurveDataList(LazyCollection[CurveData]): - - def get(self, x: float) -> CurveData: - """ - Get the :class:`CurveData` identified by its `x_value`. - - :param x: The X value of the required :class:`CurveData`. - :returns: The :class:`CurveData` with the specified `x` if it exists. - :raises KeyError: When no `CurveData` was found with `x`. - """ - curve_data = next((it for it in self if it.x_value == x), None) - if curve_data: - return curve_data - raise KeyError(x) - - def remove_data_at(self, x: float) -> CurveData: - """ - Disassociate a :class:`CurveData` from this collection based on its `x_value`. - - :param x: The :class:`CurveData` to disassociate from this :class:`Curve`. - :returns: A reference to the removed :class:`CurveData`. - :raises IndexError: If no :class:`CurveData` with a value of `x` was not associated with this :class:`Curve`. - """ - data = self.get(x) - self.remove(data) - return data - @zb_dataclass class Curve(IdentifiedObject, metaclass=ABCMeta): """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py index 060279fe3..8fde00398 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py +++ b/src/zepben/ewb/model/cim/iec61970/base/generation/production/battery_unit.py @@ -15,27 +15,12 @@ from zepben.ewb.model.cim.iec61970.base.generation.production.battery_state_kind import BatteryStateKind from zepben.ewb.model.cim.iec61970.base.generation.production.power_electronics_unit import PowerElectronicsUnit from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.relations.battery_control_list import BatteryControlList if TYPE_CHECKING: from zepben.ewb.model.cim.extensions.iec61970.base.wires.battery_control import BatteryControl -class BatteryControlList(LazyMridList): - def get_by_mode(self, control_mode: BatteryControlMode): - """ - Get the `BatteryControl` identified by its `control_mode` - - `control_mode` the `BatteryControlMode` of the desired `BatteryControl` - Returns The `BatteryControl` with the specified `control_mode` if it exists - Raises `KeyError` if a `BatteryControl` with `control_mode` wasn't present. - """ - for control in self: - if control.control_mode == control_mode: - return control - raise IndexError(f"No BatteryControl with a control_mode of {control_mode} was found in BatteryUnit {str(self)}") - - @zb_dataclass class BatteryUnit(PowerElectronicsUnit): """An electrochemical energy storage device.""" diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index a19f232ab..4e4320a21 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -21,24 +21,13 @@ from zepben.ewb.model.cim.iec61970.base.wires.conductor import Conductor from zepben.ewb.model.cim.iec61970.base.wires.cut import Cut from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind +from zepben.ewb.boilerplate.relations.ac_line_segment_phase_list import AcLineSegmentPhaseList if TYPE_CHECKING: from zepben.ewb.model.cim.iec61970.base.wires.per_length_phase_impedance import PerLengthPhaseImpedance from zepben.ewb.model.cim.iec61970.base.wires.per_length_sequence_impedance import PerLengthSequenceImpedance -class AcLineSegmentPhaseList(LazyMridList): - def get_by_phase(self, phase: SinglePhaseKind): - """ - The individual phase models for an AcLineSegment. - `phase` the phase of the required [AcLineSegmentPhase] - """ - res = next((it for it in self if it.phase == phase), None) - if res is None: - raise KeyError(phase) - return res - - @zb_dataclass class AcLineSegment(Conductor): """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py index 9290d87e9..ddff061e4 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py @@ -13,37 +13,13 @@ from typing_extensions import deprecated from zepben.ewb import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.model.cim.iec61970.base.wires.per_length_impedance import PerLengthImpedance from zepben.ewb.model.cim.iec61970.base.wires.phase_impedance_data import PhaseImpedanceData from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind +from zepben.ewb.boilerplate.relations.phase_impedance_data_list import PhaseImpedanceDataList from zepben.ewb.util import require, none -class PhaseImpedanceDataList(LazyCollection): - def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: - """ - Get the matrix entry for the corresponding to and from phases. - - :param from_phase: The from_phase to lookup. - :param to_phase: The to_phase to lookup. - :returns: The :class:`PhaseImpedanceData` with the specified `from_phase` and `to_phase` if it exists. - :raises KeyError: When no `PhaseImpedanceData` was found with a matching `from_phase` and `to_phase`. - """ - phase_impedance_data = next((it for it in self if it.from_phase == from_phase and it.to_phase == to_phase), None) - if phase_impedance_data: - return phase_impedance_data - - raise KeyError((from_phase, to_phase)) - - @property - def diagonal(self): - """ - Get only the diagonal elements of the matrix, i.e toPhase == fromPhase. - """ - return (pid for pid in self if pid.from_phase == pid.to_phase) - - @zb_dataclass class PerLengthPhaseImpedance(PerLengthImpedance): """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index 3a1ff4b1d..2473e4883 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -12,8 +12,8 @@ from typing import List, Optional, TYPE_CHECKING from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.relations.power_transformer_end_list import PowerTransformerEndList if sys.version_info >= (3, 13): from warnings import deprecated @@ -32,36 +32,6 @@ from zepben.ewb.model.cim.iec61970.base.core.terminal import Terminal -class PowerTransformerEndList(LazyMridList['PowerTransformerEnd']): - - def get_by_num(self, end_number: int) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `end_number`. - - `end_number` The `end_number` of the `PowerTransformerEnd` in relation to this `PowerTransformer`s VectorGroup. - Returns The `PowerTransformerEnd` referred to by `end_number` - Raises IndexError if no `PowerTransformerEnd` was found with end_number `end_number`. - """ - end = next((it for it in self if it.end_number == end_number), None) - if end: - return end - raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self.instance)}") - - def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `terminal`. - - `terminal` The `terminal` to find a `PowerTransformerEnd` for. - Returns The `PowerTransformerEnd` connected to the specified `terminal` - Raises IndexError if no `PowerTransformerEnd` connected to `terminal` was found on this `PowerTransformer`. - """ - - end = next((it for it in self if it.terminal == terminal), None) - if end: - return end - raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self.instance)}") - - @zb_dataclass class PowerTransformer(ConductingEquipment): """ diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index 7bbd6306f..a93cc67f7 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -14,12 +14,12 @@ from typing_extensions import deprecated from zepben.ewb import Alias -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS from zepben.ewb.model.cim.iec61970.base.wires.transformer_end import TransformerEnd from zepben.ewb.model.cim.iec61970.base.wires.winding_connection import WindingConnection +from zepben.ewb.boilerplate.relations.transformer_end_rated_s_list import TransformerEndRatedSList from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -27,24 +27,6 @@ from zepben.ewb.model.cim.iec61970.base.wires.power_transformer import PowerTransformer -class TransformerEndRatedSList(LazyCollection[TransformerEndRatedS]): - - def get_by_cooling_type( - self, - cooling_type: TransformerCoolingType, - ) -> TransformerEndRatedS | None: - return next((rating for rating in self if rating.cooling_type == cooling_type), None) - - def remove_by_cooling_type( - self, - cooling_type: TransformerCoolingType, - ) -> TransformerEndRatedS | None: - rating = self.get_by_cooling_type(cooling_type) - if rating is not None: - self.remove(rating) - return rating - - @zb_dataclass class PowerTransformerEnd(TransformerEnd): """ From 6625adde4ddb15d782f9546f09ee9279c7715dbe Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Wed, 29 Jul 2026 15:46:07 +1000 Subject: [PATCH 36/54] Move unnecessary aliases to custom inits Signed-off-by: Boris Filin --- .../collections/abstract_backed_collection.py | 4 ++-- .../ewb/model/cim/iec61968/common/location.py | 5 ++++- .../ewb/model/cim/iec61968/customers/customer.py | 5 ++++- .../ewb/model/cim/iec61970/base/core/equipment.py | 4 +++- .../ewb/model/cim/iec61970/base/core/substation.py | 5 ++++- .../iec61970/base/diagramlayout/diagram_object.py | 4 +++- .../cim/iec61970/base/wires/energy_consumer.py | 8 +++++--- .../model/cim/iec61970/base/wires/energy_source.py | 5 ++++- .../base/wires/power_electronics_connection.py | 13 ++++++++++--- .../cim/iec61970/base/wires/power_transformer.py | 5 ++++- .../iec61970/base/wires/power_transformer_end.py | 4 ++-- 11 files changed, 45 insertions(+), 17 deletions(-) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py index 859216aa8..8bbb19688 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -19,8 +19,8 @@ def _get_collection(self) -> Collection[T]: def append(self, element: T) -> None: ... - def extend(self, elements: Iterable[T]) -> None: - for element in elements: + def extend(self, elements: Iterable[T] | None) -> None: + for element in elements or []: self.append(element) @abstractmethod diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index cae9e9219..0e38da570 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -35,7 +35,10 @@ class Location(IdentifiedObject): _position_points, "PositionPoint", ) - position_points = Alias(points) + + def __init__(self, *args, position_points=None, **kwargs): + super(Location, self).__init__(*args, **kwargs) + self.points.extend(position_points) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index 1e66805ac..b4f7be250 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -40,7 +40,10 @@ class Customer(OrganisationRole): _customer_agreements, "A CustomerAgreement", ) - customer_agreements = Alias(agreements) + + def __init__(self, *args, customer_agreements=None, **kwargs): + super(Customer, self).__init__(*args, **kwargs) + self.agreements.extend(customer_agreements) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index 423cb884d..bff37c204 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -56,7 +56,6 @@ class Equipment(PowerSystemResource, metaclass=ABCMeta): _equipment_containers, "An EquipmentContainer", ) - equipment_containers = Alias(containers) current_containers: MridCollection[EquipmentContainer] = LazyMridList( _current_containers, @@ -73,6 +72,9 @@ class Equipment(PowerSystemResource, metaclass=ABCMeta): "An OperationalRestriction", ) + def __init__(self, *args, equipment_containers=None, **kwargs): + super(Equipment, self).__init__(*args, **kwargs) + self.containers.extend(equipment_containers) @property def sites(self) -> Generator['Site', None, None]: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index 83d037ff8..db3a11d05 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -41,7 +41,6 @@ class Substation(EquipmentContainer): "A Feeder", backfill=Backfill(Feeder.normal_energizing_substation) ) - normal_energized_feeders = Alias(feeders) _loops: Optional[List[Loop]] = field(default=None) @@ -64,6 +63,10 @@ class Substation(EquipmentContainer): "A Circuit", ) + def __init__(self, *args, normal_energized_feeders=None, **kwargs): + super(Substation, self).__init__(*args, **kwargs) + self.feeders.extend(normal_energized_feeders) + @property @internal(_sub_geographical_region) def sub_geographical_region(self): diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index d4415c9c3..6390f2e6f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -53,8 +53,10 @@ def diagram(self): _diagram_object_points, "DiagramObjectPoint", ) - diagram_object_points = Alias(points) + def __init__(self, *args, diagram_object_points=None, **kwargs): + super(DiagramObject, self).__init__(*args, **kwargs) + self.points.extend(diagram_object_points) @diagram.setter @deprecated("diagram should never be set directly - it is automatically set when adding it to the `diagram_objects` list") diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 8bb47d7d8..0377b2b02 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -58,9 +58,11 @@ class EnergyConsumer(EnergyConnection): "An EnergyConsumerPhase", backfill=Backfill(EnergyConsumerPhase.energy_consumer) ) - # TODO: Remove hack - # Old inits used the wrong name. This allows backwards compatibility - energy_consumer_phases: None = Alias(phases) + + + def __init__(self, *args, energy_consumer_phases=None, **kwargs): + super(EnergyConsumer, self).__init__(*args, **kwargs) + self.phases.extend(energy_consumer_phases) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index 491371a53..933408cd8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -122,7 +122,10 @@ class EnergySource(EnergyConnection): "An EnergySourcePhase", backfill=Backfill(EnergySourcePhase.energy_source) ) - energy_source_phases = Alias(phases) + + def __init__(self, *args, energy_source_phases=None, **kwargs): + super(EnergySource, self).__init__(*args, **kwargs) + self.phases.extend(energy_source_phases) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 00e52dcbf..9165bab93 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -349,16 +349,23 @@ def inv_var_resp_q_at_v4(self, value): _power_electronics_units, "A PowerElectronicsUnit", ) - power_electronics_units = Alias(units) phases: MridCollection[PowerElectronicsConnectionPhase] = LazyMridList( _power_electronics_connection_phases, "A PowerElectronicsConnectionPhase", backfill=Backfill(PowerElectronicsConnectionPhase.power_electronics_connection) ) - power_electronics_connection_phases = Alias(phases) - + def __init__( + self, + *args, + power_electronics_units=None, + power_electronics_connection_phases=None, + **kwargs + ): + super(PowerElectronicsConnection, self).__init__(*args, **kwargs) + self.units.extend(power_electronics_units) + self.phases.extend(power_electronics_connection_phases) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index 2473e4883..f05bde4a0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -99,7 +99,10 @@ class PowerTransformer(ConductingEquipment): validate=lambda self, it: self._validate_end(it), sort_by=lambda it: it.end_number ) - power_transformer_ends = Alias(ends) + + def __init__(self, *args, power_transformer_ends=None, **kwargs): + super(PowerTransformer, self).__init__(*args, **kwargs) + self.ends.extend(power_transformer_ends) def _validate_end(self, end: PowerTransformerEnd): self._validate_reference_by_field(end, end.end_number, self.ends.get_by_num, "end_number") diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index a93cc67f7..5bc3609a5 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -91,8 +91,9 @@ class PowerTransformerEnd(TransformerEnd): Should not be used directly, instead use add_rating and get_rating functions. """ - def __init__(self, *args, rated_s: int = None, **kwargs): + def __init__(self, *args, rated_s: int = None, ratings=None, **kwargs): super(PowerTransformerEnd, self).__init__(*args, **kwargs) + self.s_ratings.extend(ratings) if "_s_ratings" in kwargs: raise ValueError("Do not directly set s_ratings through the constructor. You have one more constructor parameter than expected.") if rated_s and self._rated_s: @@ -151,7 +152,6 @@ def rated_s(self, rated_s: Optional[int]): validate=lambda self, it: self._validate_rating(it), sort_by=lambda it: -it.rated_s ) - ratings = Alias(s_ratings) def _validate_rating(self, rating: TransformerEndRatedS): if any(it.cooling_type == rating.cooling_type for it in self.s_ratings): From 6a4fdffc803e374d10f105a6b16867267e765147 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Wed, 29 Jul 2026 17:16:19 +1000 Subject: [PATCH 37/54] Make list fields private; add a test; supposedly fix license issue Signed-off-by: Boris Filin --- .../ewb/boilerplate/backed_descriptor.py | 12 +- .../collections/lazy_collection.py | 14 +- .../ewb/boilerplate/collections/lazy_list.py | 10 +- .../collections/mrid_collection.py | 2 +- .../ewb/boilerplate/collections/mrid_list.py | 26 +- .../ewb/boilerplate/collections/mrid_map.py | 16 +- .../ewb/boilerplate/collections/wrapper.py | 16 +- src/zepben/ewb/boilerplate/dataclass_base.py | 1 - .../ewb/boilerplate/relations/__init__.py | 4 + .../relations/power_transformer_end_list.py | 4 +- .../boilerplate/relations/terminal_list.py | 2 +- .../iec61970/base/feeder/lv_substation.py | 10 +- test/boilerplate/test_mrid_list.py | 339 ++++++++++++++++++ 13 files changed, 401 insertions(+), 55 deletions(-) create mode 100644 test/boilerplate/test_mrid_list.py diff --git a/src/zepben/ewb/boilerplate/backed_descriptor.py b/src/zepben/ewb/boilerplate/backed_descriptor.py index 4c63fe961..9a52eb6f3 100644 --- a/src/zepben/ewb/boilerplate/backed_descriptor.py +++ b/src/zepben/ewb/boilerplate/backed_descriptor.py @@ -19,25 +19,25 @@ def __init__(self, private_field): if not isinstance(private_field, Field) and not isinstance(private_field, BackedDescriptor): raise TypeError(f"private_field parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {private_field}") self.private_field: Field | 'BackedDescriptor' = private_field - self.backing_name: str | None = None + self._backing_name: str | None = None self.name = None def __set_name__(self, owner, name): if name is None: return self.__name__ = self.name = name - if not self.backing_name: - self.backing_name = self.private_field.name + if not self._backing_name: + self._backing_name = self.private_field.name def __get__(self, instance, _): if instance is None: return self - return getattr(instance, self.backing_name) + return getattr(instance, self._backing_name) def __set__(self, instance, value): - if self.backing_name is None: + if self._backing_name is None: raise ValueError(f"Descriptor {self} is not yet aware of the supporting field - `__set__` cannot be called") - return setattr(instance, self.backing_name, value) + return setattr(instance, self._backing_name, value) Alias = BackedDescriptor diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py index b7fe49595..860427abd 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_collection.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_collection.py @@ -24,19 +24,19 @@ def __init__( self.sort_by = sort_by def _get(self) -> list[T] | None: - return getattr(self.instance, self.backing_name) + return getattr(self._instance, self._backing_name) def _get_collection(self) -> list[T]: - return getattr(self.instance, self.backing_name) or [] + return getattr(self._instance, self._backing_name) or [] def append(self, item: T) -> None: if self.validate is not None: - self.validate(self.instance, item) + self.validate(self._instance, item) - existing = getattr(self.instance, self.backing_name) + existing = getattr(self._instance, self._backing_name) if existing is None: existing = [item] - setattr(self.instance, self.backing_name, existing) + setattr(self._instance, self._backing_name, existing) else: existing.append(item) @@ -50,10 +50,10 @@ def remove(self, item: T) -> None: self.clear() def clear(self) -> None: - setattr(self.instance, self.backing_name, None) + setattr(self._instance, self._backing_name, None) def __repr__(self) -> str: - if self.instance is None: + if self._instance is None: return object.__repr__(self) return repr(self._get_collection()) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index 14b54afcd..ef466361f 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -26,7 +26,7 @@ def insert(self, index: int, item: T) -> None: if not 0 <= index <= size: raise ValueError( f"Unable to add {self.element_description} to " - f"{self.instance}. " + f"{self._instance}. " f"Sequence number {index} is invalid. " f"Expected a value between 0 and {size}. " "Make sure you are adding the items in order and there are " @@ -34,13 +34,13 @@ def insert(self, index: int, item: T) -> None: ) if self.validate is not None: - self.validate(self.instance, item) + self.validate(self._instance, item) - existing = getattr(self.instance, self.backing_name) + existing = getattr(self._instance, self._backing_name) if existing is None: existing = [item] - setattr(self.instance, self.backing_name, existing) + setattr(self._instance, self._backing_name, existing) else: existing.insert(index, item) @@ -54,7 +54,7 @@ def pop(self, index: int = -1) -> T: Uses normal Python list semantics, including support for negative indexes and raising ``IndexError`` when the index is invalid. """ - existing = getattr(self.instance, self.backing_name) + existing = getattr(self._instance, self._backing_name) if existing is None: raise IndexError("pop from empty list") diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py index a5f2d43de..2b7524ce1 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_collection.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -42,7 +42,7 @@ def _can_add_by_mrid(self, element: S) -> bool: if existing is not element: raise ValueError( f"{self.element_description} with mRID {element.mrid} " - f"already exists in {self.instance}." + f"already exists in {self._instance}." ) return False diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 260e6e2ff..25efba636 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -38,7 +38,7 @@ def append(self, item: S) -> None: return if self.backfill is not None: - self.backfill.apply(item, self.instance) + self.backfill.apply(item, self._instance) super().append(item) @@ -57,7 +57,7 @@ def __init__( self.backfill = backfill self.validate = validate self.sort_by = sort_by - self.backing_list = None + self._backing_list = None def __get__(self, instance, owner=None) -> Self: obj = super().__get__(instance, owner) @@ -67,13 +67,13 @@ def __get__(self, instance, owner=None) -> Self: return obj def __post_init__(self) -> None: - self.backing_list = getattr(self.instance, self.backing_name) + self._backing_list = getattr(self._instance, self._backing_name) def _get_collection(self) -> Sequence[S]: - return self.backing_list + return self._backing_list def _safe_get_by_mrid(self, mrid: str) -> S | None: - found = next((element for element in self.backing_list if element.mrid == mrid), None) + found = next((element for element in self._backing_list if element.mrid == mrid), None) return found def append(self, item: S) -> None: @@ -81,23 +81,23 @@ def append(self, item: S) -> None: return if self.backfill is not None: - self.backfill.apply(item, self.instance) + self.backfill.apply(item, self._instance) if self.validate is not None: - self.validate(self.instance, item) + self.validate(self._instance, item) - self.backing_list.append(item) + self._backing_list.append(item) if self.sort_by is not None: - self.backing_list.sort(key=self.sort_by) + self._backing_list.sort(key=self.sort_by) def remove(self, item: S) -> None: - self.backing_list.remove(item) + self._backing_list.remove(item) def clear(self) -> None: - self.backing_list.clear() + self._backing_list.clear() def __repr__(self) -> str: - if self.instance is None: + if self._instance is None: return object.__repr__(self) - return repr(self.backing_list) + return repr(self._backing_list) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py index 9d1672c58..45ee6c87a 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -25,10 +25,10 @@ def __init__( self.validate = validate def _get(self) -> dict[str, S] | None: - return getattr(self.instance, self.backing_name) + return getattr(self._instance, self._backing_name) def _get_or_empty(self) -> dict[str, S]: - return getattr(self.instance, self.backing_name) or {} + return getattr(self._instance, self._backing_name) or {} def _get_collection(self) -> ValuesView[S]: return self._get_or_empty().values() @@ -44,15 +44,15 @@ def append(self, element: S) -> None: return if self.backfill is not None: - self.backfill.apply(element, self.instance) + self.backfill.apply(element, self._instance) if self.validate is not None: - self.validate(self.instance, element) + self.validate(self._instance, element) - existing = getattr(self.instance, self.backing_name) + existing = getattr(self._instance, self._backing_name) if existing is None: existing = {element.mrid: element} - setattr(self.instance, self.backing_name, existing) + setattr(self._instance, self._backing_name, existing) else: existing[element.mrid] = element @@ -70,10 +70,10 @@ def remove(self, item) -> None: self.clear() def clear(self) -> None: - setattr(self.instance, self.backing_name, None) + setattr(self._instance, self._backing_name, None) def __repr__(self) -> str: - if self.instance is None: + if self._instance is None: return object.__repr__(self) return repr(self._get_or_empty()) diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py index d697e1a23..3ec410014 100644 --- a/src/zepben/ewb/boilerplate/collections/wrapper.py +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -14,8 +14,8 @@ class _Wrapper(BackedDescriptor): - instance: Any - backing_name: Any + _instance: Any + _backing_name: Any def __new__(cls, *args: Any, **kwargs: Any) -> Self: """ @@ -29,8 +29,8 @@ def __new__(cls, *args: Any, **kwargs: Any) -> Self: def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self.instance = None - self.backing_name = None + self._instance = None + self._backing_name = None def __get__(self, instance, _=None) -> Self: """ @@ -40,8 +40,8 @@ def __get__(self, instance, _=None) -> Self: if instance is None: return self obj = type(self)(*self._init_args, **self._init_kwargs) - obj.instance = instance - obj.backing_name = self.private_field.name + obj._instance = instance + obj._backing_name = self.private_field.name return obj @@ -73,9 +73,9 @@ class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection[T], ABC): def __set__(self, instance, value) -> None: if instance is None: return - if not hasattr(instance, self.backing_name): + if not hasattr(instance, self._backing_name): resolve_default(instance, self.private_field) - elif getattr(instance, self.backing_name): + elif getattr(instance, self._backing_name): raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") if value is not None: self.__get__(instance).extend(value) diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index ace55ca85..74a276f5a 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -89,7 +89,6 @@ class MyClass: return cls -@dataclass_transform(eq_default=False, order_default=False) def zb_dataclass(cls: type[T]) -> type[T]: """ Shorthand alias for ``@dataclass(init=False, eq=False, slots=True, repr=False)`` diff --git a/src/zepben/ewb/boilerplate/relations/__init__.py b/src/zepben/ewb/boilerplate/relations/__init__.py index e69de29bb..35e3122c5 100644 --- a/src/zepben/ewb/boilerplate/relations/__init__.py +++ b/src/zepben/ewb/boilerplate/relations/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. diff --git a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py index 9c740cf8a..fd5e23859 100644 --- a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py +++ b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py @@ -25,7 +25,7 @@ def get_by_num(self, end_number: int) -> PowerTransformerEnd: end = next((it for it in self if it.end_number == end_number), None) if end: return end - raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self.instance)}") + raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self._instance)}") def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: """ @@ -39,4 +39,4 @@ def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: end = next((it for it in self if it.terminal == terminal), None) if end: return end - raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self.instance)}") + raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self._instance)}") diff --git a/src/zepben/ewb/boilerplate/relations/terminal_list.py b/src/zepben/ewb/boilerplate/relations/terminal_list.py index b46ada487..5292a9d90 100644 --- a/src/zepben/ewb/boilerplate/relations/terminal_list.py +++ b/src/zepben/ewb/boilerplate/relations/terminal_list.py @@ -14,5 +14,5 @@ class TerminalsList(LazyMridList[Terminal]): def get_by_sequence_number(self, sequence_number: int) -> Terminal: term = next((it for it in self if it.sequence_number == sequence_number), None) if term is None: - raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self.instance)}") + raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self._instance)}") return term diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index 8d7ef7724..6949c7d2d 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -57,9 +57,13 @@ class LvSubstation(EquipmentContainer): ) """[ZBEX] the ``LvFeeders`` that are normally energized by this ``LvSubstation``.""" - def num_normal_energized_lv_feeders(self) -> int: - """Get the number of entries in the normal ``LvFeeder`` collection.""" - return nlen(self._normal_energized_lv_feeders_by_id) + def normal_energized_lv_switch_feeders(self) -> Generator[LvFeeder, None, None]: + """ + Retrieves all normally energized LvFeeders that represent low voltage network connected below a switch on the edge of this LvSubstation. This is all + LvFeeders in the normalEnergizedLvFeeders that has a normalHeadTerminal attached to a Switch. + """ + # NOTE: import exists here due to a circular import problem + from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch for lv_feeder in self.normal_energized_lv_feeders: if (it := lv_feeder.normal_head_terminal) is not None and isinstance(it.conducting_equipment, Switch): diff --git a/test/boilerplate/test_mrid_list.py b/test/boilerplate/test_mrid_list.py new file mode 100644 index 000000000..6fb5c120f --- /dev/null +++ b/test/boilerplate/test_mrid_list.py @@ -0,0 +1,339 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Callable + +import pytest + +from zepben.ewb.boilerplate.collections.mrid_list import MridList +from zepben.ewb.model.cim.iec61970.base.core.feeder import Feeder + + +class CallbackBackfill: + def __init__( + self, + callback: Callable[[Owner, Feeder], None], + ): + self._callback = callback + + def apply(self, item: Feeder, owner: Owner): + self._callback(owner, item) + + +def record_backfill(owner: Owner, item: Feeder): + owner._events.append(("backfill", item.mrid)) + + +def reject_backfill(_owner: Owner, _item: Feeder): + raise ValueError("backfill failed") + + +def record_validation(owner: Owner, item: Feeder): + owner._events.append(("validate", item.mrid)) + + +def reject_validation(_owner: Owner, _item: Feeder): + raise ValueError("validation failed") + + +def validate_for_combined_test(owner: Owner, item: Feeder): + record_validation(owner, item) + + if item.mrid == "invalid": + raise ValueError("invalid feeder") + + +@dataclass +class Owner: + _feeders: list[Feeder] = field( + default_factory=list, + repr=False, + ) + _events: list[tuple[str, str]] = field( + default_factory=list, + repr=False, + ) + + feeders = MridList( + _feeders, + "Feeder", + ) + backfilled_feeders = MridList( + _feeders, + "Feeder", + backfill=CallbackBackfill(record_backfill), + ) + rejecting_backfill_feeders = MridList( + _feeders, + "Feeder", + backfill=CallbackBackfill(reject_backfill), + ) + validated_feeders = MridList( + _feeders, + "Feeder", + validate=record_validation, + ) + rejecting_validation_feeders = MridList( + _feeders, + "Feeder", + validate=reject_validation, + ) + backfilled_validated_feeders = MridList( + _feeders, + "Feeder", + backfill=CallbackBackfill(record_backfill), + validate=record_validation, + ) + sorted_feeders = MridList( + _feeders, + "Feeder", + sort_by=lambda feeder: feeder.mrid, + ) + fully_configured_feeders = MridList( + _feeders, + "Feeder", + backfill=CallbackBackfill(record_backfill), + validate=validate_for_combined_test, + sort_by=lambda feeder: feeder.mrid, + ) + + +@pytest.fixture +def owner() -> Owner: + owner = Owner() + + # All wrappers intentionally point at this same non-null backing list. + # Reset it and the callback log for each test. + owner._feeders.clear() + owner._events.clear() + + return owner + + +def test_append_adds_item_to_owner_backing_list(owner: Owner): + feeder = Feeder("feeder") + + owner.feeders.append(feeder) + + assert owner._feeders == [feeder] + + +def test_appending_same_instance_twice_only_adds_it_once( + owner: Owner, +): + feeder = Feeder("feeder") + + owner.feeders.append(feeder) + owner.feeders.append(feeder) + + assert owner._feeders == [feeder] + + +def test_appending_different_instance_with_same_mrid_raises( + owner: Owner, +): + existing = Feeder("duplicate") + owner.feeders.append(existing) + + with pytest.raises( + ValueError, + match=r"Feeder with mRID duplicate already exists", + ): + owner.feeders.append(Feeder("duplicate")) + + assert owner._feeders == [existing] + + +def test_rejected_duplicate_does_not_run_backfill_or_validation( + owner: Owner, +): + feeder = Feeder("feeder") + owner.backfilled_validated_feeders.append(feeder) + owner._events.clear() + + owner.backfilled_validated_feeders.append(feeder) + + assert owner._feeders == [feeder] + assert owner._events == [] + + +def test_append_applies_backfill(owner: Owner): + feeder = Feeder("feeder") + + owner.backfilled_feeders.append(feeder) + + assert owner._events == [("backfill", "feeder")] + assert owner._feeders == [feeder] + + +def test_backfill_failure_does_not_append_item(owner: Owner): + with pytest.raises(ValueError, match="backfill failed"): + owner.rejecting_backfill_feeders.append(Feeder("feeder")) + + assert owner._feeders == [] + + +def test_append_runs_validation(owner: Owner): + feeder = Feeder("feeder") + + owner.validated_feeders.append(feeder) + + assert owner._events == [("validate", "feeder")] + assert owner._feeders == [feeder] + + +def test_validation_failure_does_not_append_item(owner: Owner): + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_feeders.append(Feeder("feeder")) + + assert owner._feeders == [] + + +def test_append_runs_backfill_before_validation(owner: Owner): + feeder = Feeder("feeder") + + owner.backfilled_validated_feeders.append(feeder) + + assert owner._events == [ + ("backfill", "feeder"), + ("validate", "feeder"), + ] + assert owner._feeders == [feeder] + + +def test_append_sorts_owner_backing_list(owner: Owner): + second = Feeder("second") + first = Feeder("first") + + owner.sorted_feeders.append(second) + owner.sorted_feeders.append(first) + + assert owner._feeders == [first, second] + + +def test_remove_removes_item_from_owner_backing_list( + owner: Owner, +): + retained = Feeder("retained") + removed = Feeder("removed") + owner._feeders.extend([retained, removed]) + + owner.feeders.remove(removed) + + assert owner._feeders == [retained] + + +def test_clear_empties_owner_backing_list(owner: Owner): + owner._feeders.extend( + [ + Feeder("first"), + Feeder("second"), + ] + ) + + owner.feeders.clear() + + assert owner._feeders == [] + + +def test_repr_matches_owner_backing_list_repr(owner: Owner): + owner._feeders.extend( + [ + Feeder("first"), + Feeder("second"), + ] + ) + + assert repr(owner.feeders) == repr(owner._feeders) + + + + +def test_mrid_list_all_functionality_together(owner: Owner): + third = Feeder("third") + first = Feeder("first") + second = Feeder("second") + + owner.fully_configured_feeders.extend([third, first, second]) + + assert owner._feeders == [first, second, third] + assert len(owner.fully_configured_feeders) == 3 + assert list(owner.fully_configured_feeders) == [ + first, + second, + third, + ] + assert next(iter(owner.fully_configured_feeders)) is first + assert owner.fully_configured_feeders[0] is first + assert list(owner.fully_configured_feeders[1:]) == [ + second, + third, + ] + assert second in owner.fully_configured_feeders + assert ( + owner.fully_configured_feeders.get_by_mrid("second") + is second + ) + assert repr(owner.fully_configured_feeders) == repr( + owner._feeders + ) + + with pytest.raises(KeyError, match="missing"): + owner.fully_configured_feeders.get_by_mrid("missing") + + indexed: list[tuple[int, Feeder]] = [] + owner.fully_configured_feeders.for_each_indexed( + lambda index, item: indexed.append((index, item)) + ) + assert indexed == [ + (0, first), + (1, second), + (2, third), + ] + + events_before_duplicate = list(owner._events) + owner.fully_configured_feeders.append(first) + + assert owner._feeders == [first, second, third] + assert owner._events == events_before_duplicate + + with pytest.raises( + ValueError, + match=r"Feeder with mRID second already exists", + ): + owner.fully_configured_feeders.append(Feeder("second")) + + assert owner._feeders == [first, second, third] + assert owner._events == events_before_duplicate + + with pytest.raises(ValueError, match="invalid feeder"): + owner.fully_configured_feeders.append(Feeder("invalid")) + + assert owner._feeders == [first, second, third] + assert owner._events[-2:] == [ + ("backfill", "invalid"), + ("validate", "invalid"), + ] + + owner.fully_configured_feeders.remove(second) + + assert owner._feeders == [first, third] + + owner.fully_configured_feeders.clear() + + assert len(owner.fully_configured_feeders) == 0 + assert list(owner.fully_configured_feeders) == [] + assert owner._feeders == [] + assert owner._events[:6] == [ + ("backfill", "third"), + ("validate", "third"), + ("backfill", "first"), + ("validate", "first"), + ("backfill", "second"), + ("validate", "second"), + ] From 728205e497aecacd01688a449774b72802925b0e Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Wed, 29 Jul 2026 17:16:28 +1000 Subject: [PATCH 38/54] Make list fields private; add a test; supposedly fix license issue Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/zepben/ewb/boilerplate/__init__.py b/src/zepben/ewb/boilerplate/__init__.py index 7ee764916..35e3122c5 100644 --- a/src/zepben/ewb/boilerplate/__init__.py +++ b/src/zepben/ewb/boilerplate/__init__.py @@ -2,4 +2,3 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. - From 305125c6e6619d919c440eacf39ae0bd76aba94e Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 11:41:46 +1000 Subject: [PATCH 39/54] Test Signed-off-by: Boris Filin --- test/boilerplate/test_mrid_list.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/boilerplate/test_mrid_list.py b/test/boilerplate/test_mrid_list.py index 6fb5c120f..e33b2f371 100644 --- a/test/boilerplate/test_mrid_list.py +++ b/test/boilerplate/test_mrid_list.py @@ -252,6 +252,8 @@ def test_repr_matches_owner_backing_list_repr(owner: Owner): assert repr(owner.feeders) == repr(owner._feeders) +def test_class_field_repr_uses_descriptor_repr() -> None: + assert repr(Owner.feeders) == object.__repr__(Owner.feeders) def test_mrid_list_all_functionality_together(owner: Owner): From 31d116f0cfba151c82d017508c206ccb4e088df7 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 12:36:01 +1000 Subject: [PATCH 40/54] Reorder things to minimise diff Signed-off-by: Boris Filin --- .../collections/lazy_collection.py | 2 +- .../ewb/boilerplate/collections/lazy_list.py | 2 +- .../ewb/boilerplate/collections/mrid_list.py | 6 +-- .../ewb/boilerplate/collections/mrid_map.py | 6 +-- .../iec61968/common/contact_details.py | 12 +++--- .../iec61970/base/feeder/lv_feeder.py | 35 ++++++++-------- .../iec61970/base/feeder/lv_substation.py | 13 +++--- .../protection/protection_relay_function.py | 10 ++--- .../ewb/model/cim/iec61968/common/location.py | 9 ++-- .../model/cim/iec61968/customers/customer.py | 9 ++-- .../iec61970/base/core/connectivity_node.py | 7 ++-- .../model/cim/iec61970/base/core/equipment.py | 39 +++++++++-------- .../model/cim/iec61970/base/core/feeder.py | 23 +++++----- .../base/core/power_system_resource.py | 11 +++-- .../base/core/sub_geographical_region.py | 10 ++--- .../cim/iec61970/base/core/substation.py | 42 +++++++++---------- .../base/diagramlayout/diagram_object.py | 22 +++++----- .../base/wires/ac_line_segment_phase.py | 4 +- .../model/cim/iec61970/base/wires/clamp.py | 11 ++--- .../ewb/model/cim/iec61970/base/wires/cut.py | 9 ++-- .../iec61970/base/wires/energy_consumer.py | 9 ++-- .../base/wires/energy_consumer_phase.py | 12 +++--- .../cim/iec61970/base/wires/energy_source.py | 9 ++-- .../base/wires/energy_source_phase.py | 10 ++--- .../wires/power_electronics_connection.py | 22 +++++----- .../power_electronics_connection_phase.py | 12 +++--- .../iec61970/base/wires/power_transformer.py | 20 ++++----- 27 files changed, 181 insertions(+), 195 deletions(-) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py index 860427abd..12478c4fe 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_collection.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_collection.py @@ -15,7 +15,7 @@ class LazyCollection(_IterableWrapper[T], AbstractBackedList[T]): def __init__( self, - private_field, + private_field: list[T] | None, validate=None, sort_by=None ) -> None: diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index ef466361f..7a37afcb9 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -10,7 +10,7 @@ class LazyList(LazyCollection[T]): def __init__( self, - private_field, + private_field: list[T] | None, element_description: str, validate=None, ) -> None: diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 25efba636..2d45ffcd7 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -16,9 +16,9 @@ class LazyMridList(LazyCollection[S], MridCollection[S]): def __init__( self, - private_field, + private_field: list[S] | None, element_description: str, - backfill: Backfill = None, + backfill: Backfill | None = None, validate=None, sort_by=None ) -> None: @@ -48,7 +48,7 @@ def __init__( self, private_field, element_description: str, - backfill: Backfill = None, + backfill: Backfill | None = None, validate=None, sort_by=None ) -> None: diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/mrid_map.py index 45ee6c87a..011b07637 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_map.py @@ -11,12 +11,12 @@ T = TypeVar("T") -class LazyMridMap(_IterableWrapper[T], MridCollection[S]): +class LazyMridMap(_IterableWrapper[S], MridCollection[S]): def __init__( self, - private_field, + private_field: list[S] | None, element_description: str, - backfill: Backfill = None, + backfill: Backfill | None = None, validate=None ) -> None: super().__init__(private_field) diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py index 4a08b7312..93ef787f3 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -67,14 +67,8 @@ def id(self) -> str: _phone_numbers: list[TelephoneNumber] | None = field(default=None) - phone_numbers: AbstractBackedList[TelephoneNumber] = LazyCollection(_phone_numbers) - """[ZBEX] Phone numbers.""" - _electronic_addresses: list[ElectronicAddress] | None = field(default=None) - electronic_addresses: AbstractBackedList[ElectronicAddress] = LazyCollection(_electronic_addresses) - """[ZBEX] Electronic addresses.""" - def __init__(self, id: str|None=None, *args, **kwargs): if id is not None: if "mrid" in kwargs: @@ -82,7 +76,6 @@ def __init__(self, id: str|None=None, *args, **kwargs): kwargs["mrid"] = id super(ContactDetails, self).__init__(*args, **kwargs) - def __str__(self): return f"ContactDetails({self.mrid})" @@ -90,6 +83,11 @@ def __hash__(self): # noinspection PyUnresolvedReferences return hash((type(self), *(getattr(self, s) for s in self.__slots__))) + phone_numbers: AbstractBackedList[TelephoneNumber] = LazyCollection(_phone_numbers) + """[ZBEX] Phone numbers.""" + + electronic_addresses: AbstractBackedList[ElectronicAddress] = LazyCollection(_electronic_addresses) + """[ZBEX] Electronic addresses.""" def __eq__(self, other: Any) -> bool: # diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py index 4ac09a559..384e29e13 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py @@ -55,24 +55,6 @@ class LvFeeder(EquipmentContainer): normal_energizing_lv_substation: 'LvSubstation | None' = None """[ZBEX] The normally energizing LvSubstation for this LvFeeder""" - current_equipment: MridCollection[Equipment] = LazyMridMap( - _current_equipment_by_id, - "A current Equipment", - ) - """Contained `Equipment` using the current state of the network.""" - - normal_energizing_feeders: MridCollection[Feeder] = LazyMridMap( - _normal_energizing_feeders_by_id, - "A Feeder", - ) - """[ZBEX] The HV/MV feeders that normally energize this LV feeder.""" - - current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( - _current_energizing_feeders_by_id, - "A Feeder", - ) - """[ZBEX] The HV/MV feeders that currently energize this LV feeder.""" - @property def normal_head_terminal(self) -> Optional[Terminal]: """ @@ -87,6 +69,23 @@ def normal_head_terminal(self, term: Optional[Terminal]): else: raise ValueError(f"normal_head_terminal for {str(self)} has already been set to {self._normal_head_terminal}, cannot reset this field to {term}") + normal_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _normal_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that normally energize this LV feeder.""" + + current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _current_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that currently energize this LV feeder.""" + + current_equipment: MridCollection[Equipment] = LazyMridMap( + _current_equipment_by_id, + "A current Equipment", + ) + """Contained `Equipment` using the current state of the network.""" # region deprecated list boilerplate # region current_equipment boilerplate diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index 6949c7d2d..2dfd1a0c1 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -44,19 +44,18 @@ class LvSubstation(EquipmentContainer): ) """[ZBEX] The HV/MV feeders that normally energize this ``LvSubstation``.""" - current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( - _current_energizing_feeders_by_id, - "A Feeder", - ) - """[ZBEX] The HV/MV feeders that currently energize this LV substation.""" - - normal_energized_lv_feeders: MridCollection[LvFeeder] = LazyMridMap( _normal_energized_lv_feeders_by_id, "An LvFeeder", ) """[ZBEX] the ``LvFeeders`` that are normally energized by this ``LvSubstation``.""" + current_energizing_feeders: MridCollection[Feeder] = LazyMridMap( + _current_energizing_feeders_by_id, + "A Feeder", + ) + """[ZBEX] The HV/MV feeders that currently energize this LV substation.""" + def normal_energized_lv_switch_feeders(self) -> Generator[LvFeeder, None, None]: """ Retrieves all normally energized LvFeeders that represent low voltage network connected below a switch on the edge of this LvSubstation. This is all diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 580691d78..92be720c4 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -84,16 +84,16 @@ def relay_info(self): def relay_info(self, relay_info: Optional[RelayInfo]): self.asset_info = relay_info - time_limits: LazyList[float] = LazyList( - _time_limits, - "float" - ) - thresholds: LazyList[RelaySetting] = LazyList( _thresholds, "RelaySetting" ) + time_limits: LazyList[float] = LazyList( + _time_limits, + "float" + ) + sensors: MridCollection[Sensor] = LazyMridList( _sensors, "A Sensor", diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index 0e38da570..52d1686b0 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -31,15 +31,14 @@ class Location(IdentifiedObject): _position_points: Optional[List[PositionPoint]] = field(default=None) - points: LazyList[PositionPoint] = LazyList( - _position_points, - "PositionPoint", - ) - def __init__(self, *args, position_points=None, **kwargs): super(Location, self).__init__(*args, **kwargs) self.points.extend(position_points) + points: LazyList[PositionPoint] = LazyList( + _position_points, + "PositionPoint", + ) # region deprecated list boilerplate # diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index b4f7be250..ef3bc8537 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -36,15 +36,14 @@ class Customer(OrganisationRole): _customer_agreements: Optional[List[CustomerAgreement]] = field(default=None) - agreements: MridCollection[CustomerAgreement] = LazyMridList( - _customer_agreements, - "A CustomerAgreement", - ) - def __init__(self, *args, customer_agreements=None, **kwargs): super(Customer, self).__init__(*args, **kwargs) self.agreements.extend(customer_agreements) + agreements: MridCollection[CustomerAgreement] = LazyMridList( + _customer_agreements, + "A CustomerAgreement", + ) # region deprecated list boilerplate # region agreements boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index a103ed3f3..12dc7dd18 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -33,16 +33,15 @@ class ConnectivityNode(IdentifiedObject, WeakrefSlot): """ _terminals: List[Terminal] = field(default_factory=list) + def __iter__(self): + return iter(self._terminals) + terminals: MridCollection[Terminal] = LazyMridList( _terminals, "A Terminal" ) """The `Terminal`s attached to this `ConnectivityNode`""" - def __iter__(self): - return iter(self._terminals) - - def is_switched(self): return self.get_switch() is not None diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index bff37c204..5223d6307 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -50,27 +50,8 @@ class Equipment(PowerSystemResource, metaclass=ABCMeta): _usage_points: Optional[List[UsagePoint]] = field(default=None) _equipment_containers: Optional[List[EquipmentContainer]] = field(default=None) _operational_restrictions: Optional[List[OperationalRestriction]] = field(default=None) - _current_containers: Optional[List[EquipmentContainer]] = field(default=None) - - containers: MridCollection[EquipmentContainer] = LazyMridList( - _equipment_containers, - "An EquipmentContainer", - ) - - current_containers: MridCollection[EquipmentContainer] = LazyMridList( - _current_containers, - "A current EquipmentContainer", - ) - - usage_points: MridCollection[UsagePoint] = LazyMridList( - _usage_points, - "A UsagePoint", - ) - operational_restrictions: MridCollection[OperationalRestriction] = LazyMridList( - _operational_restrictions, - "An OperationalRestriction", - ) + _current_containers: Optional[List[EquipmentContainer]] = field(default=None) def __init__(self, *args, equipment_containers=None, **kwargs): super(Equipment, self).__init__(*args, **kwargs) @@ -143,6 +124,10 @@ def current_lv_feeders(self) -> Generator[LvFeeder, None, None]: """ return ngen(_of_type(self._current_containers, LvFeeder)) + containers: MridCollection[EquipmentContainer] = LazyMridList( + _equipment_containers, + "An EquipmentContainer", + ) def num_sites(self) -> int: """ @@ -150,6 +135,20 @@ def num_sites(self) -> int: """ return len(list(self.sites)) + current_containers: MridCollection[EquipmentContainer] = LazyMridList( + _current_containers, + "A current EquipmentContainer", + ) + + usage_points: MridCollection[UsagePoint] = LazyMridList( + _usage_points, + "A UsagePoint", + ) + + operational_restrictions: MridCollection[OperationalRestriction] = LazyMridList( + _operational_restrictions, + "An OperationalRestriction", + ) # region deprecated list boilerplate # region containers boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 7c5f87bcc..69352f970 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -63,6 +63,17 @@ def normal_head_terminal(self, term: Optional[Terminal]): else: raise ValueError(f"Feeder {self.mrid} has equipment assigned to it. Cannot update normalHeadTerminal on a feeder with equipment assigned.") + @property + @internal(_normal_energizing_substation) + def normal_energizing_substation(self): + """The substation that normally energizes the feeder. Also used for naming purposes.""" + return self._normal_energizing_substation + + @normal_energizing_substation.setter + @deprecated("normal_energizing_substation should never be set directly - it is automatically set when adding it to the `feeders` list") + def normal_energizing_substation(self, value): + self._normal_energizing_substation = value + current_equipment: MridCollection[Equipment] = LazyMridMap( _current_equipment_by_id, "A current Equipment", @@ -92,18 +103,6 @@ def normal_head_terminal(self, term: Optional[Terminal]): "An LvSubstation", ) """[ZBEX]""" - @property - @internal(_normal_energizing_substation) - def normal_energizing_substation(self): - """The substation that normally energizes the feeder. Also used for naming purposes.""" - return self._normal_energizing_substation - - @normal_energizing_substation.setter - @deprecated("normal_energizing_substation should never be set directly - it is automatically set when adding it to the `feeders` list") - def normal_energizing_substation(self, value): - self._normal_energizing_substation = value - - # region deprecated list boilerplate # region current_equipment boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py index 8482bba2c..4baa1d964 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py @@ -44,12 +44,6 @@ class PowerSystemResource(IdentifiedObject, metaclass=ABCMeta): _assets: Optional[List[Asset]] = field(default=None) - assets: MridCollection[Asset] = LazyMridList( - _assets, - "An Asset", - ) - - @property def has_controls(self) -> bool: """ @@ -57,6 +51,11 @@ def has_controls(self) -> bool: """ return nlen(self.num_controls) > 0 + assets: MridCollection[Asset] = LazyMridList( + _assets, + "An Asset", + ) + # region deprecated list boilerplate # region assets boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index 258c08430..118037b74 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -39,17 +39,17 @@ def geographical_region(self): """The geographical region to which this sub-geographical region is within.""" return self._geographical_region + @geographical_region.setter + @deprecated("Geographical region is a backfill property - it should only be set by adding the sub region to the sub regions list") + def geographical_region(self, value): + self._geographical_region = value + substations: MridCollection[Substation] = LazyMridList( _substations, "A Substation", backfill=Backfill(Substation.sub_geographical_region) ) - @geographical_region.setter - @deprecated("Geographical region is a backfill property - it should only be set by adding the sub region to the sub regions list") - def geographical_region(self, value): - self._geographical_region = value - # region deprecated list boilerplate # region substations boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index db3a11d05..587d03f2d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -36,33 +36,12 @@ class Substation(EquipmentContainer): _normal_energized_feeders: Optional[List[Feeder]] = field(default=None) - feeders: MridCollection[Feeder] = LazyMridList( - _normal_energized_feeders, - "A Feeder", - backfill=Backfill(Feeder.normal_energizing_substation) - ) - _loops: Optional[List[Loop]] = field(default=None) - loops: MridCollection[Loop] = LazyMridList( - _loops, - "A Loop", - ) - _energized_loops: Optional[List[Loop]] = field(default=None) - energized_loops: MridCollection[Loop] = LazyMridList( - _energized_loops, - "A Loop", - ) - _circuits: Optional[List[Circuit]] = field(default=None) - circuits: MridCollection[Circuit] = LazyMridList( - _circuits, - "A Circuit", - ) - def __init__(self, *args, normal_energized_feeders=None, **kwargs): super(Substation, self).__init__(*args, **kwargs) self.feeders.extend(normal_energized_feeders) @@ -80,6 +59,27 @@ def sub_geographical_region(self): def sub_geographical_region(self, value): self._sub_geographical_region = value + circuits: MridCollection[Circuit] = LazyMridList( + _circuits, + "A Circuit", + ) + + loops: MridCollection[Loop] = LazyMridList( + _loops, + "A Loop", + ) + + energized_loops: MridCollection[Loop] = LazyMridList( + _energized_loops, + "A Loop", + ) + + feeders: MridCollection[Feeder] = LazyMridList( + _normal_energized_feeders, + "A Feeder", + backfill=Backfill(Feeder.normal_energizing_substation) + ) + # region deprecated list boilerplate # region circuits boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 6390f2e6f..d360c603e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -32,12 +32,6 @@ class DiagramObject(IdentifiedObject): _diagram: Optional[Diagram] = field(default=None) - @property - @internal(_diagram) - def diagram(self): - """A diagram object is part of a diagram.""" - return self._diagram - identified_object_mrid: Optional[str] = None """The domain object to which this diagram object is associated.""" @@ -49,15 +43,16 @@ def diagram(self): _diagram_object_points: Optional[List[DiagramObjectPoint]] = field(default=None) - points: LazyList[DiagramObjectPoint] = LazyList( - _diagram_object_points, - "DiagramObjectPoint", - ) - def __init__(self, *args, diagram_object_points=None, **kwargs): super(DiagramObject, self).__init__(*args, **kwargs) self.points.extend(diagram_object_points) + @property + @internal(_diagram) + def diagram(self): + """A diagram object is part of a diagram.""" + return self._diagram + @diagram.setter @deprecated("diagram should never be set directly - it is automatically set when adding it to the `diagram_objects` list") def diagram(self, diag): @@ -66,6 +61,11 @@ def diagram(self, diag): else: raise ValueError(f"diagram for {str(self)} has already been set to {self._diagram}, cannot reset this field to {diag}") + points: LazyList[DiagramObjectPoint] = LazyList( + _diagram_object_points, + "DiagramObjectPoint", + ) + # region deprecated list boilerplate # # ("region/endregion" is an IntelliJ feature letting you hide the entire thing) diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py index 61fb22ee1..e5b0caedd 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment_phase.py @@ -44,8 +44,6 @@ class AcLineSegmentPhase(PowerSystemResource): def ac_line_segment(self) -> 'AcLineSegment | None': return self._ac_line_segment - asset_info: WireInfo | None = None - @ac_line_segment.setter @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `phases` list") def ac_line_segment(self, ac_line_segment: 'AcLineSegment') -> None: @@ -53,3 +51,5 @@ def ac_line_segment(self, ac_line_segment: 'AcLineSegment') -> None: self._ac_line_segment = ac_line_segment else: raise ValueError(f"ac_line_segment has already been set to {self._ac_line_segment}. Cannot set this field again") + + asset_info: WireInfo | None = None diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index 6c07993e3..eb56b633c 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -31,19 +31,20 @@ class Clamp(ConductingEquipment): _ac_line_segment: Optional['AcLineSegment'] = field(default=None) """The line segment to which the clamp is connected.""" + max_terminals = 1 + @property @internal(_ac_line_segment) def ac_line_segment(self) -> Optional['AcLineSegment']: """The line segment to which the clamp is connected.""" return self._ac_line_segment - max_terminals = 1 + @ac_line_segment.setter + @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `clamps` list") + def ac_line_segment(self, value): + self._ac_line_segment = value @property def length_from_t1_or_0(self) -> float: return self.length_from_terminal_1 or 0.0 - @ac_line_segment.setter - @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `clamps` list") - def ac_line_segment(self, value): - self._ac_line_segment = value diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index 7836d745e..0f1658bcd 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -42,11 +42,12 @@ def ac_line_segment(self) -> Optional['AcLineSegment']: """The line segment to which the cut is applied.""" return self._ac_line_segment - @property - def length_from_t1_or_0(self) -> float: - return self.length_from_terminal_1 or 0.0 - @ac_line_segment.setter @deprecated("ac_line_segment should never be set directly - it is automatically set when adding it to the `cuts` list") def ac_line_segment(self, value): self._ac_line_segment = value + + @property + def length_from_t1_or_0(self) -> float: + return self.length_from_terminal_1 or 0.0 + diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index 0377b2b02..fb0c0e2f3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -53,6 +53,10 @@ class EnergyConsumer(EnergyConnection): q_fixed: Optional[float] = None """Power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" + def __init__(self, *args, energy_consumer_phases=None, **kwargs): + super(EnergyConsumer, self).__init__(*args, **kwargs) + self.phases.extend(energy_consumer_phases) + phases: MridCollection[EnergyConsumerPhase] = LazyMridList( _energy_consumer_phases, "An EnergyConsumerPhase", @@ -60,11 +64,6 @@ class EnergyConsumer(EnergyConnection): ) - def __init__(self, *args, energy_consumer_phases=None, **kwargs): - super(EnergyConsumer, self).__init__(*args, **kwargs) - self.phases.extend(energy_consumer_phases) - - # region deprecated list boilerplate # region phases boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py index fa1748731..96985a9d8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer_phase.py @@ -25,12 +25,6 @@ class EnergyConsumerPhase(PowerSystemResource): _energy_consumer: Optional['EnergyConsumer'] = field(default=None) - @property - @internal(_energy_consumer) - def energy_consumer(self): - """The `EnergyConsumer` that has this phase.""" - return self._energy_consumer - phase: SinglePhaseKind = SinglePhaseKind.X """Phase of this energy consumer component. If the energy consumer is wye connected, the connection is from the indicated phase to the central ground or neutral point. If the energy consumer is delta connected, the phase indicates an energy consumer connected from the indicated phase to the next @@ -50,6 +44,12 @@ def energy_consumer(self): q_fixed: Optional[float] = None """Reactive power of the load that is a fixed quantity. Load sign convention is used, i.e. positive sign means flow out from a node.""" + @property + @internal(_energy_consumer) + def energy_consumer(self): + """The `EnergyConsumer` that has this phase.""" + return self._energy_consumer + @energy_consumer.setter @deprecated("energy_consumer should never be set directly - it is automatically set when adding it to the `phases` list") def energy_consumer(self, ec): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index 933408cd8..f0b1fd2a8 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -117,17 +117,16 @@ class EnergySource(EnergyConnection): x0_max: Optional[float] = None """Maximum zero sequence Thevenin reactance.""" + def __init__(self, *args, energy_source_phases=None, **kwargs): + super(EnergySource, self).__init__(*args, **kwargs) + self.phases.extend(energy_source_phases) + phases: MridCollection[EnergySourcePhase] = LazyMridList( _energy_source_phases, "An EnergySourcePhase", backfill=Backfill(EnergySourcePhase.energy_source) ) - def __init__(self, *args, energy_source_phases=None, **kwargs): - super(EnergySource, self).__init__(*args, **kwargs) - self.phases.extend(energy_source_phases) - - # region deprecated list boilerplate # region phases boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py index 149166903..5a0f096df 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source_phase.py @@ -27,17 +27,17 @@ class EnergySourcePhase(PowerSystemResource): _energy_source: Optional['EnergySource'] = field(default=None) + phase: SinglePhaseKind = SinglePhaseKind.NONE + """A `zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind.SinglePhaseKind` Phase of this energy source component. If the energy source is wye connected, + the connection is from the indicated phase to the central ground or neutral point. If the energy source is delta connected, the phase indicates an energy + source connected from the indicated phase to the next logical non-neutral phase.""" + @property @internal(_energy_source) def energy_source(self): """The `EnergySource` with this `EnergySourcePhase`""" return self._energy_source - phase: SinglePhaseKind = SinglePhaseKind.NONE - """A `zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind.SinglePhaseKind` Phase of this energy source component. If the energy source is wye connected, - the connection is from the indicated phase to the central ground or neutral point. If the energy source is delta connected, the phase indicates an energy - source connected from the indicated phase to the next logical non-neutral phase.""" - @energy_source.setter @deprecated("energy_source should never be set directly - it is automatically set when adding it to the `phases` list") def energy_source(self, es): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 9165bab93..19143bd57 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -149,6 +149,16 @@ class PowerElectronicsConnection(RegulatingCondEq): _power_electronics_connection_phases: Optional[List[PowerElectronicsConnectionPhase]] = field(default=None) """The individual units models for the power electronics connection.""" + def __init__( + self, + *args, + power_electronics_units=None, + power_electronics_connection_phases=None, + **kwargs + ): + super(PowerElectronicsConnection, self).__init__(*args, **kwargs) + self.units.extend(power_electronics_units) + self.phases.extend(power_electronics_connection_phases) @property def inv_watt_resp_v1(self): @@ -356,18 +366,6 @@ def inv_var_resp_q_at_v4(self, value): backfill=Backfill(PowerElectronicsConnectionPhase.power_electronics_connection) ) - def __init__( - self, - *args, - power_electronics_units=None, - power_electronics_connection_phases=None, - **kwargs - ): - super(PowerElectronicsConnection, self).__init__(*args, **kwargs) - self.units.extend(power_electronics_units) - self.phases.extend(power_electronics_connection_phases) - - # region deprecated list boilerplate # region units boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py index 6bdd66733..95a27a895 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection_phase.py @@ -26,12 +26,6 @@ class PowerElectronicsConnectionPhase(PowerSystemResource): _power_electronics_connection: Optional['PowerElectronicsConnection'] = field(default=None) """The power electronics connection to which the phase belongs.""" - @property - @internal(_power_electronics_connection) - def power_electronics_connection(self): - """The power electronics connection to which the phase belongs.""" - return self._power_electronics_connection - p: Optional[float] = None """Active power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" @@ -45,6 +39,12 @@ def power_electronics_connection(self): q: Optional[float] = None """Reactive power injection. Load sign convention is used, i.e. positive sign means flow into the equipment from the network.""" + @property + @internal(_power_electronics_connection) + def power_electronics_connection(self): + """The power electronics connection to which the phase belongs.""" + return self._power_electronics_connection + @power_electronics_connection.setter @deprecated("power_electronics_connection should never be set directly - it is automatically set when adding it to the `phases` list") def power_electronics_connection(self, value): diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index f05bde4a0..b8a65d7d9 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -91,6 +91,9 @@ class PowerTransformer(ConductingEquipment): The function of this transformer. """ + def __init__(self, *args, power_transformer_ends=None, **kwargs): + super(PowerTransformer, self).__init__(*args, **kwargs) + self.ends.extend(power_transformer_ends) ends: PowerTransformerEndList = PowerTransformerEndList( _power_transformer_ends, @@ -100,17 +103,6 @@ class PowerTransformer(ConductingEquipment): sort_by=lambda it: it.end_number ) - def __init__(self, *args, power_transformer_ends=None, **kwargs): - super(PowerTransformer, self).__init__(*args, **kwargs) - self.ends.extend(power_transformer_ends) - - def _validate_end(self, end: PowerTransformerEnd): - self._validate_reference_by_field(end, end.end_number, self.ends.get_by_num, "end_number") - - if end.end_number == 0: - end.end_number = self.num_ends() + 1 - - @property @deprecated("use asset_info instead.") def power_transformer_info(self) -> Optional[PowerTransformerInfo]: @@ -135,6 +127,12 @@ def get_base_voltage(self, terminal: Terminal = None): else: return None + def _validate_end(self, end: PowerTransformerEnd): + self._validate_reference_by_field(end, end.end_number, self.ends.get_by_num, "end_number") + + if end.end_number == 0: + end.end_number = self.num_ends() + 1 + # region deprecated list boilerplate # From 85b8113198afad07210d280fb559790e1cd9f700 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 13:08:53 +1000 Subject: [PATCH 41/54] ruff fix and newlines Signed-off-by: Boris Filin --- .../ewb/boilerplate/collections/abstract_backed_collection.py | 2 -- src/zepben/ewb/boilerplate/collections/lazy_collection.py | 2 -- src/zepben/ewb/boilerplate/dataclass_base.py | 1 - .../ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py | 2 +- src/zepben/ewb/model/cim/iec61968/common/location.py | 1 - src/zepben/ewb/model/cim/iec61968/customers/customer.py | 1 - .../ewb/model/cim/iec61968/metering/controlled_appliance.py | 2 +- .../ewb/model/cim/iec61970/base/core/connectivity_node.py | 2 +- src/zepben/ewb/model/cim/iec61970/base/core/curve.py | 2 +- src/zepben/ewb/model/cim/iec61970/base/core/equipment.py | 1 - src/zepben/ewb/model/cim/iec61970/base/core/feeder.py | 1 - src/zepben/ewb/model/cim/iec61970/base/core/substation.py | 1 - .../ewb/model/cim/iec61970/base/diagramlayout/diagram.py | 2 +- .../model/cim/iec61970/base/diagramlayout/diagram_object.py | 2 -- src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py | 1 - src/zepben/ewb/model/cim/iec61970/base/wires/cut.py | 1 - .../ewb/model/cim/iec61970/base/wires/energy_consumer.py | 1 - src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py | 1 - .../cim/iec61970/base/wires/per_length_phase_impedance.py | 2 +- .../cim/iec61970/base/wires/power_electronics_connection.py | 1 - .../ewb/model/cim/iec61970/base/wires/power_transformer.py | 1 - .../ewb/model/cim/iec61970/base/wires/power_transformer_end.py | 3 +-- test/boilerplate/test_mrid_list.py | 2 +- test/dataclass_descriptors/test_basic_descriptor.py | 2 +- .../networktrace/test_network_trace_step_path_provider.py | 1 - 25 files changed, 9 insertions(+), 29 deletions(-) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py index 8bbb19688..4235a3532 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -48,5 +48,3 @@ def for_each_indexed(self, action: Callable[[int, T], object]) -> None: """ for index, item in enumerate(self._get_collection()): action(index, item) - - diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py index 12478c4fe..90808ddde 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_collection.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_collection.py @@ -56,5 +56,3 @@ def __repr__(self) -> str: if self._instance is None: return object.__repr__(self) return repr(self._get_collection()) - - diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index 74a276f5a..82bb2a2b3 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -6,7 +6,6 @@ from dataclasses import dataclass, fields, MISSING, Field from typing import TypeVar, cast -from typing_extensions import dataclass_transform T = TypeVar("T") diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index c7441f355..b9dcbe8c7 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -92,4 +92,4 @@ def clear_delays(self) -> RelayInfo: # endregion - # endregion \ No newline at end of file + # endregion diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index 52d1686b0..be4bc42c2 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -12,7 +12,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index ef3bc8537..05a80ade8 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -14,7 +14,6 @@ from zepben.ewb.model.cim.iec61968.common.organisation_role import OrganisationRole from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection diff --git a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py index 91db333b4..9bef5881f 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/controlled_appliance.py @@ -9,7 +9,7 @@ from dataclasses import dataclass from enum import Enum -from typing import List, Union +from typing import List from zepben.ewb import unique diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index 12dc7dd18..e732fe838 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -84,4 +84,4 @@ def clear_terminals(self) -> ConnectivityNode: # endregion - # endregion \ No newline at end of file + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py index d918383f5..7e2a6765d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/curve.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/curve.py @@ -90,4 +90,4 @@ def clear_data(self) -> "Curve": # endregion - # endregion \ No newline at end of file + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index 5223d6307..ee38a743e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -21,7 +21,6 @@ from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site from zepben.ewb.util import ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 69352f970..5e4bae259 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -235,4 +235,3 @@ def clear_current_energized_lv_substations(self) -> Feeder: # endregion # endregion - diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index 587d03f2d..b2bf02a2f 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -13,7 +13,6 @@ from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill, internal diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index eb62a1e33..1548a2a90 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -72,4 +72,4 @@ def clear_diagram_objects(self) -> Diagram: return self # endregion - # endregion \ No newline at end of file + # endregion diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index d360c603e..8440b2c94 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -12,7 +12,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject @@ -138,4 +137,3 @@ def clear_points(self) -> DiagramObject: # endregion # endregion - diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py index eb56b633c..afd6f37ae 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/clamp.py @@ -47,4 +47,3 @@ def ac_line_segment(self, value): @property def length_from_t1_or_0(self) -> float: return self.length_from_terminal_1 or 0.0 - diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py index 0f1658bcd..d588a0e13 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/cut.py @@ -50,4 +50,3 @@ def ac_line_segment(self, value): @property def length_from_t1_or_0(self) -> float: return self.length_from_terminal_1 or 0.0 - diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index fb0c0e2f3..ab4329fc5 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -12,7 +12,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index f0b1fd2a8..d775c3ac2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -12,7 +12,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py index ddff061e4..906b58e87 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/per_length_phase_impedance.py @@ -71,4 +71,4 @@ def clear_data(self) -> PerLengthPhaseImpedance: # endregion data boilerplate - # endregion deprecated list boilerplate \ No newline at end of file + # endregion deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 19143bd57..0d3659fe2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -12,7 +12,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py index b8a65d7d9..dc59d9edf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer.py @@ -11,7 +11,6 @@ from dataclasses import field from typing import List, Optional, TYPE_CHECKING -from zepben.ewb import Alias from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.boilerplate.relations.power_transformer_end_list import PowerTransformerEndList diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py index 5bc3609a5..792790068 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_transformer_end.py @@ -13,7 +13,6 @@ from typing_extensions import deprecated -from zepben.ewb import Alias from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_cooling_type import TransformerCoolingType from zepben.ewb.model.cim.extensions.iec61970.base.wires.transformer_end_rated_s import TransformerEndRatedS @@ -238,4 +237,4 @@ def clear_ratings(self) -> PowerTransformerEnd: # endregion - # endregion \ No newline at end of file + # endregion diff --git a/test/boilerplate/test_mrid_list.py b/test/boilerplate/test_mrid_list.py index e33b2f371..d9457f1fb 100644 --- a/test/boilerplate/test_mrid_list.py +++ b/test/boilerplate/test_mrid_list.py @@ -6,7 +6,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Any, Callable +from typing import Callable import pytest diff --git a/test/dataclass_descriptors/test_basic_descriptor.py b/test/dataclass_descriptors/test_basic_descriptor.py index 8d6c756ed..a8166f7da 100644 --- a/test/dataclass_descriptors/test_basic_descriptor.py +++ b/test/dataclass_descriptors/test_basic_descriptor.py @@ -5,7 +5,7 @@ from dataclasses import field from zepben.ewb.boilerplate.dataclass_base import DataclassBase -from zepben.ewb import zb_dataclass, remove_descriptor_annotations +from zepben.ewb import zb_dataclass from zepben.ewb.boilerplate.backed_descriptor import BackedDescriptor diff --git a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py index 8c943ad74..726cba3cc 100644 --- a/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py +++ b/test/services/network/tracing/networktrace/test_network_trace_step_path_provider.py @@ -12,7 +12,6 @@ from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.model.cim.iec61970.base.wires.single_phase_kind import SinglePhaseKind from zepben.ewb.services.network.network_service import NetworkService -from zepben.ewb import NetworkStateOperators, TestNetworkBuilder, NetworkTraceStep, Terminal, NominalPhasePath, Breaker, AcLineSegment, Clamp, Cut from zepben.ewb.services.network.tracing.networktrace.network_trace_step_path_provider import NetworkTraceStepPathProvider From 516eea4084578ff507de707ec50767bf1fd74425 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 14:04:31 +1000 Subject: [PATCH 42/54] Comments on boilerplate; Match boilerplate file structure to JVM Signed-off-by: Boris Filin --- .../collections/abstract_backed_collection.py | 22 ++++---- .../collections/abstract_backed_list.py | 11 ++-- .../collections/lazy_collection.py | 29 +++++++++++ .../ewb/boilerplate/collections/lazy_list.py | 23 ++++++++- .../boilerplate/collections/lazy_mrid_list.py | 51 +++++++++++++++++++ .../{mrid_map.py => lazy_mrid_map.py} | 38 +++++++++++--- .../collections/mrid_collection.py | 8 ++- .../ewb/boilerplate/collections/mrid_list.py | 38 +++----------- .../ewb/boilerplate/collections/wrapper.py | 31 ++++++++++- .../relations/ac_line_segment_phase_list.py | 2 +- .../relations/battery_control_list.py | 2 +- .../relations/power_transformer_end_list.py | 2 +- .../boilerplate/relations/terminal_list.py | 2 +- .../extensions/iec61970/base/feeder/loop.py | 2 +- .../iec61970/base/feeder/lv_feeder.py | 2 +- .../iec61970/base/feeder/lv_substation.py | 2 +- .../protection/protection_relay_function.py | 2 +- .../protection/protection_relay_scheme.py | 2 +- .../protection/protection_relay_system.py | 2 +- .../assetinfo/power_transformer_info.py | 2 +- .../assetinfo/transformer_tank_info.py | 2 +- .../ewb/model/cim/iec61968/assets/asset.py | 2 +- .../model/cim/iec61968/customers/customer.py | 2 +- .../iec61968/customers/customer_agreement.py | 2 +- .../iec61968/customers/pricing_structure.py | 2 +- .../iec61968/infiec61968/infassets/pole.py | 2 +- .../model/cim/iec61968/metering/end_device.py | 2 +- .../cim/iec61968/metering/usage_point.py | 2 +- .../operations/operational_restriction.py | 2 +- .../base/auxiliaryequipment/sensor.py | 2 +- .../iec61970/base/core/connectivity_node.py | 2 +- .../model/cim/iec61970/base/core/equipment.py | 2 +- .../iec61970/base/core/equipment_container.py | 2 +- .../model/cim/iec61970/base/core/feeder.py | 2 +- .../iec61970/base/core/geographical_region.py | 2 +- .../base/core/power_system_resource.py | 2 +- .../base/core/sub_geographical_region.py | 2 +- .../cim/iec61970/base/core/substation.py | 2 +- .../iec61970/base/diagramlayout/diagram.py | 2 +- .../iec61970/base/wires/ac_line_segment.py | 2 +- .../iec61970/base/wires/energy_consumer.py | 2 +- .../cim/iec61970/base/wires/energy_source.py | 2 +- .../wires/power_electronics_connection.py | 2 +- .../iec61970/base/wires/protected_switch.py | 2 +- .../iec61970/base/wires/regulating_control.py | 2 +- .../base/wires/synchronous_machine.py | 2 +- .../iec61970/infiec61970/feeder/circuit.py | 2 +- 47 files changed, 234 insertions(+), 93 deletions(-) create mode 100644 src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py rename src/zepben/ewb/boilerplate/collections/{mrid_map.py => lazy_mrid_map.py} (67%) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py index 4235a3532..148c8fb36 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -16,19 +16,23 @@ def _get_collection(self) -> Collection[T]: ... @abstractmethod - def append(self, element: T) -> None: + def append(self, item: T, /) -> None: + """Append an item to the collection.""" ... - def extend(self, elements: Iterable[T] | None) -> None: - for element in elements or []: + def extend(self, items: Iterable[T] | None, /) -> None: + """Append each item to the collection.""" + for element in items or []: self.append(element) @abstractmethod - def remove(self, element: T) -> None: + def remove(self, item: T, /) -> None: + """Remove an item from the collection.""" ... @abstractmethod def clear(self) -> None: + """Remove all items from the collection.""" ... def __len__(self) -> int: @@ -37,14 +41,10 @@ def __len__(self) -> int: def __iter__(self) -> Iterator[T]: return iter(self._get_collection()) - def __contains__(self, element: object) -> bool: - return element in self._get_collection() + def __contains__(self, item: object) -> bool: + return item in self._get_collection() def for_each_indexed(self, action: Callable[[int, T], object]) -> None: - """ - Call the `action` on each item in the list - - :param action: An action to apply to each :class:`RelaySetting` in the `thresholds` collection, taking the index of the threshold, and the threshold itself. - """ + """Call the `action` on each item in the list.""" for index, item in enumerate(self._get_collection()): action(index, item) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py index 0e60de702..3caa14d52 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py @@ -24,8 +24,13 @@ def __getitem__(self, index: int) -> T: ... @overload - def __getitem__(self, index: slice) -> Sequence[T]: - ... + def __getitem__(self, index: int) -> T: ... + + @overload + def __getitem__(self, index: slice) -> Sequence[T]: ... def __getitem__(self, index: int | slice) -> T | Sequence[T]: - return self._get_collection()[index] + collection = self._get_collection() + if isinstance(index, slice): + return collection[index] + return collection[index] diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py index 90808ddde..bc2267843 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_collection.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_collection.py @@ -12,7 +12,31 @@ class LazyCollection(_IterableWrapper[T], AbstractBackedList[T]): + """ + Concrete collection wrapper that treats its backing field as a nullable + list. + A backing value of ``None`` is exposed as an empty collection. The backing + list is created when the first item is appended and reset to ``None`` when + the last item is removed or the collection is cleared. + + For example:: + + class Container: + _items = field(default=None) + items = LazyCollection(_items) + + container = Container() + + assert list(container.items) == [] + assert container._items is None + + container.items.append("value") + assert container._items == ["value"] + + container.items.clear() + assert container._items is None + """ def __init__( self, private_field: list[T] | None, @@ -30,6 +54,11 @@ def _get_collection(self) -> list[T]: return getattr(self._instance, self._backing_name) or [] def append(self, item: T) -> None: + """ + Append an item to the collection. + Run optional validation. + Sort the collection if key lambda is provided. + """ if self.validate is not None: self.validate(self._instance, item) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index 7a37afcb9..6296f9cd7 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -7,7 +7,21 @@ class LazyList(LazyCollection[T]): + """ + Lazy collection with list-style index-based insertion and deletion. + It retains the nullable backing-list behaviour of ``LazyCollection``, + creating the backing list when an item is inserted and resetting it to + ``None`` when the final item is deleted. + + For example:: + + container.items.insert(0, "value") + assert container._items == ["value"] + + del container.items[0] + assert container._items is None + """ def __init__( self, private_field: list[T] | None, @@ -19,7 +33,9 @@ def __init__( def insert(self, index: int, item: T) -> None: """ - Insert ``item`` at ``index``. + Insert an item into the collection at a given index. + Check for mRID collisions and run optional validation. + Sort the collection if key lambda is provided. """ size = len(self) @@ -45,6 +61,11 @@ def insert(self, index: int, item: T) -> None: existing.insert(index, item) def append(self, item: T) -> None: + """ + Append an item to the collection. + Run optional validation. + Sort the collection if key lambda is provided. + """ self.insert(len(self), item) def pop(self, index: int = -1) -> T: diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py new file mode 100644 index 000000000..d6d313f62 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py @@ -0,0 +1,51 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from zepben.ewb.boilerplate.backfill import Backfill +from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.mrid_collection import S, MridCollection + + +class LazyMridList(LazyCollection[S], MridCollection[S]): + """ + Nullable list implementation of :class:`MridCollection`. + + Inherits mRID lookup and uniqueness semantics from ``MridCollection`` and + lazy backing-list behavior from ``LazyCollection``. + """ + def __init__( + self, + private_field: list[S] | None, + element_description: str, + backfill: Backfill | None = None, + validate=None, + sort_by=None + ) -> None: + super().__init__(private_field, validate, sort_by) + self.element_description = element_description + self.backfill = backfill + + def _safe_get_by_mrid(self, mrid: str) -> S | None: + existing = self._get() + if existing is None: + return None + found = next((element for element in existing if element.mrid == mrid), None) + return found + + def append(self, item: S) -> None: + """ + Append an item to the collection. + Check for mRID collisions with existing items. + Optionally fill the backref field on the added item. + Run optional validation. + Sort the collection if key lambda is provided. + """ + if not self._can_add_by_mrid(item): + return + + if self.backfill is not None: + self.backfill.apply(item, self._instance) + + super().append(item) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_map.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py similarity index 67% rename from src/zepben/ewb/boilerplate/collections/mrid_map.py rename to src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py index 011b07637..c2d32a0d0 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py @@ -12,6 +12,24 @@ class LazyMridMap(_IterableWrapper[S], MridCollection[S]): + """ + Lazy mRID collection backed by a nullable dictionary. + + Items are stored by their ``mrid`` while iteration exposes the dictionary + values. A backing value of ``None`` is treated as an empty collection. The + dictionary is created when the first item is appended and reset to ``None`` + when the collection becomes empty. + + For example:: + + container.items.append(item) + + assert container._items == {item.mrid: item} + assert container.items.get_by_mrid(item.mrid) is item + + container.items.remove(item) + assert container._items is None + """ def __init__( self, private_field: list[S] | None, @@ -39,27 +57,33 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: def get_by_mrid(self, mrid: str) -> S: return self._get_or_empty()[mrid] - def append(self, element: S) -> None: - if not self._can_add_by_mrid(element): + def append(self, item: S) -> None: + """ + Add an item to the collection. + Check for mRID collisions with existing items. + Optionally fill the backref field on the added item. + Run optional validation. + """ + if not self._can_add_by_mrid(item): return if self.backfill is not None: - self.backfill.apply(element, self._instance) + self.backfill.apply(item, self._instance) if self.validate is not None: - self.validate(self._instance, element) + self.validate(self._instance, item) existing = getattr(self._instance, self._backing_name) if existing is None: - existing = {element.mrid: element} + existing = {item.mrid: item} setattr(self._instance, self._backing_name, existing) else: - existing[element.mrid] = element + existing[item.mrid] = item def __len__(self) -> int: return len(self._get_or_empty()) - def __contains__(self, item: S) -> bool: + def __contains__(self, item: object) -> bool: return self._get_or_empty().get(getattr(item, "mrid", None)) == item def remove(self, item) -> None: diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py index 2b7524ce1..867fe1b48 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_collection.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -16,7 +16,13 @@ class HasMrid(Protocol): class MridCollection(AbstractBackedCollection[S], ABC): - instance: Any + """ + Collection of objects identified by a unique ``mrid``. + + Provides lookup by mRID and rejects distinct objects with duplicate mRIDs. + """ + + _instance: Any element_description: str @abstractmethod diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 2d45ffcd7..81ed03f28 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -8,41 +8,10 @@ from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class LazyMridList(LazyCollection[S], MridCollection[S]): - def __init__( - self, - private_field: list[S] | None, - element_description: str, - backfill: Backfill | None = None, - validate=None, - sort_by=None - ) -> None: - super().__init__(private_field, validate, sort_by) - self.element_description = element_description - self.backfill = backfill - - def _safe_get_by_mrid(self, mrid: str) -> S | None: - existing = self._get() - if existing is None: - return None - found = next((element for element in existing if element.mrid == mrid), None) - return found - - def append(self, item: S) -> None: - if not self._can_add_by_mrid(item): - return - - if self.backfill is not None: - self.backfill.apply(item, self._instance) - - super().append(item) - - class MridList(_IterableWrapper[S], AbstractBackedList[S], MridCollection[S]): def __init__( self, @@ -77,6 +46,13 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: return found def append(self, item: S) -> None: + """ + Append an item to the collection. + Check for mRID collisions with existing items. + Optionally fill the backref field on the added item. + Run optional validation. + Sort the collection if key lambda is provided. + """ if not self._can_add_by_mrid(item): return diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py index 3ec410014..40bdcf683 100644 --- a/src/zepben/ewb/boilerplate/collections/wrapper.py +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -3,7 +3,10 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from __future__ import annotations + from abc import ABC +from dataclasses import Field from typing import Any, TypeVar from typing_extensions import Self, deprecated @@ -13,6 +16,29 @@ class _Wrapper(BackedDescriptor): + """ + Descriptor base for collection views backed by another dataclass field. + + Access through an instance creates a short-lived wrapper bound to that + instance and its backing field. For example:: + + class Container: + _items = field(default=None) + items = LazyCollection(_items) + + container = Container() + + # Calls Container.items.__get__(container, Container), returning a + # wrapper whose _instance is container and _backing_name is "_items". + bound_items = container.items + bound_items.append("value") + + assert container._items == ["value"] + + Access through the class returns the original shared descriptor:: + + assert isinstance(Container.items, LazyCollection) + """ _instance: Any _backing_name: Any @@ -48,6 +74,7 @@ def __get__(self, instance, _=None) -> Self: class _WrapperFgetFix(_Wrapper): """ This class exists to fix the tests that rely on the old lists being @property. + It implements a minimalistic callable fget with a name. TODO: Remove in a separate PR fixing tests """ def __init__(self, *args, **kwargs) -> None: @@ -73,7 +100,7 @@ class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection[T], ABC): def __set__(self, instance, value) -> None: if instance is None: return - if not hasattr(instance, self._backing_name): + if not hasattr(instance, self._backing_name) and isinstance(self.private_field, Field): resolve_default(instance, self.private_field) elif getattr(instance, self._backing_name): raise ValueError(f"Cannot assign list {self.__name__} for {instance}: currently non-empty") @@ -81,3 +108,5 @@ def __set__(self, instance, value) -> None: self.__get__(instance).extend(value) else: self.__get__(instance).clear() + + diff --git a/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py index a19a660aa..0fa921cd1 100644 --- a/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py +++ b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py @@ -6,7 +6,7 @@ from __future__ import annotations from zepben.ewb import SinglePhaseKind -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList class AcLineSegmentPhaseList(LazyMridList): diff --git a/src/zepben/ewb/boilerplate/relations/battery_control_list.py b/src/zepben/ewb/boilerplate/relations/battery_control_list.py index dcdeb7439..233a59867 100644 --- a/src/zepben/ewb/boilerplate/relations/battery_control_list.py +++ b/src/zepben/ewb/boilerplate/relations/battery_control_list.py @@ -6,7 +6,7 @@ from __future__ import annotations from zepben.ewb import BatteryControlMode -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList class BatteryControlList(LazyMridList): diff --git a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py index fd5e23859..ede88484f 100644 --- a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py +++ b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList if TYPE_CHECKING: from zepben.ewb import PowerTransformerEnd, Terminal diff --git a/src/zepben/ewb/boilerplate/relations/terminal_list.py b/src/zepben/ewb/boilerplate/relations/terminal_list.py index 5292a9d90..3242be310 100644 --- a/src/zepben/ewb/boilerplate/relations/terminal_list.py +++ b/src/zepben/ewb/boilerplate/relations/terminal_list.py @@ -6,7 +6,7 @@ from __future__ import annotations from zepben.ewb import Terminal -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList class TerminalsList(LazyMridList[Terminal]): diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py index 2659d0d43..828b1c269 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py index 384e29e13..deab4947b 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_feeder.py @@ -14,7 +14,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection -from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py index 2dfd1a0c1..c18713852 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/lv_substation.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection -from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.model.cim.extensions.iec61970.base.feeder.lv_feeder import LvFeeder diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 92be720c4..8e1751acc 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -23,7 +23,7 @@ from zepben.ewb.model.cim.extensions.iec61970.base.protection.protection_kind import ProtectionKind from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py index e99991643..01252e8f9 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_scheme.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py index c62658d45..ec0f4fda5 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_system.py @@ -15,7 +15,7 @@ from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61970.base.core.equipment import Equipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py index 3dfafdd20..04d5077c2 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/power_transformer_info.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py index a5653106e..4f29eb367 100644 --- a/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py +++ b/src/zepben/ewb/model/cim/iec61968/assetinfo/transformer_tank_info.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo from zepben.ewb.model.resistance_reactance import ResistanceReactance from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset.py b/src/zepben/ewb/model/cim/iec61968/assets/asset.py index b28e0a2eb..ea8989474 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer.py b/src/zepben/ewb/model/cim/iec61968/customers/customer.py index 05a80ade8..90c2a0a4e 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61968.common.organisation_role import OrganisationRole from zepben.ewb.model.cim.iec61968.customers.customer_kind import CustomerKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py index 1d2ae9700..538806a6b 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/customer_agreement.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61968.common.agreement import Agreement from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py index c8d60f991..d0c353800 100644 --- a/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py +++ b/src/zepben/ewb/model/cim/iec61968/customers/pricing_structure.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py index 451f7f25c..7c13ee54c 100644 --- a/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py +++ b/src/zepben/ewb/model/cim/iec61968/infiec61968/infassets/pole.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61968.assets.structure import Structure from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py index 877a7ac53..ed4f559a2 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/end_device.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/end_device.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61968.assets.asset_container import AssetContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py index 625c6a0d1..5f6d3f612 100644 --- a/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py +++ b/src/zepben/ewb/model/cim/iec61968/metering/usage_point.py @@ -16,7 +16,7 @@ from zepben.ewb.model.cim.iec61970.base.core.phase_code import PhaseCode from zepben.ewb.util import nlen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py index e0f0a8145..2d31b9262 100644 --- a/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py +++ b/src/zepben/ewb/model/cim/iec61968/operations/operational_restriction.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61968.common.document import Document from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py index 99094aa19..03266cdb2 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py +++ b/src/zepben/ewb/model/cim/iec61970/base/auxiliaryequipment/sensor.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61970.base.auxiliaryequipment.auxiliary_equipment import AuxiliaryEquipment from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index e732fe838..493e42c4d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -12,7 +12,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py index ee38a743e..679013e06 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment.py @@ -21,7 +21,7 @@ from zepben.ewb.model.cim.extensions.iec61970.base.core.site import Site from zepben.ewb.util import ngen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py index d4c2bcc53..e979566fe 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/equipment_container.py @@ -15,7 +15,7 @@ from zepben.ewb import Alias from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection -from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.connectivity_node_container import ConnectivityNodeContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py index 5e4bae259..6415fec39 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/feeder.py @@ -14,7 +14,7 @@ from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import internal -from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py index 9a44f3aed..3ab1de3f0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/geographical_region.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py index 4baa1d964..56a48491d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/power_system_resource.py @@ -15,7 +15,7 @@ from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.util import nlen from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py index 118037b74..042cdcf2a 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/sub_geographical_region.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill, internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py index b2bf02a2f..56e68b7e3 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/substation.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/substation.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61970.base.core.equipment_container import EquipmentContainer from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill, internal diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py index 1548a2a90..e627bae01 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram.py @@ -15,7 +15,7 @@ from zepben.ewb.boilerplate.dataclass_base import zb_dataclass from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object import DiagramObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_style import DiagramStyle diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py index 4e4320a21..f88c7b2d0 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/ac_line_segment.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.ac_line_segment_phase import AcLineSegmentPhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py index ab4329fc5..70711e37b 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_consumer.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py index d775c3ac2..08f11d428 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/energy_source.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.energy_connection import EnergyConnection diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py index 0d3659fe2..caf4b3ebb 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/power_electronics_connection.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.backfill import Backfill from zepben.ewb.model.cim.iec61970.base.wires.power_electronics_connection_phase import PowerElectronicsConnectionPhase diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py index bb5091b87..03f914bac 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/protected_switch.py @@ -14,7 +14,7 @@ from zepben.ewb.model.cim.iec61970.base.wires.switch import Switch from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py index 5bfa3848c..bc6eac26e 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/regulating_control.py @@ -16,7 +16,7 @@ from zepben.ewb.model.cim.iec61970.base.core.power_system_resource import PowerSystemResource from zepben.ewb.model.cim.iec61970.base.wires.regulating_control_mode_kind import RegulatingControlModeKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py index f98cfc0a7..5465514e1 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py +++ b/src/zepben/ewb/model/cim/iec61970/base/wires/synchronous_machine.py @@ -12,7 +12,7 @@ from zepben.ewb.model.cim.iec61970.base.wires.rotating_machine import RotatingMachine from zepben.ewb.model.cim.iec61970.base.wires.synchronous_machine_kind import SynchronousMachineKind from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: diff --git a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py index f0f1fd83e..5fda60a1b 100644 --- a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py +++ b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py @@ -13,7 +13,7 @@ from zepben.ewb.model.cim.iec61970.base.wires.line import Line from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection if TYPE_CHECKING: From 82b44f06d5df427b1015d0854a61e4f1cee8e2c3 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 15:06:50 +1000 Subject: [PATCH 43/54] Update MANIFESTO.md; Rename lists to match JVM Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/MANIFESTO.md | 36 +++++-- .../collections/lazy_collection.py | 87 ---------------- .../collections/lazy_index_list.py | 88 +++++++++++++++++ .../ewb/boilerplate/collections/lazy_list.py | 99 +++++++++---------- .../boilerplate/collections/lazy_mrid_list.py | 4 +- .../ewb/boilerplate/collections/mrid_list.py | 2 +- .../boilerplate/relations/curve_data_list.py | 4 +- .../relations/phase_impedance_data_list.py | 4 +- .../relations/transformer_end_rated_s_list.py | 4 +- .../iec61968/assetinfo/relay_info.py | 4 +- .../iec61968/common/contact_details.py | 6 +- .../protection/protection_relay_function.py | 6 +- .../ewb/model/cim/iec61968/common/location.py | 4 +- .../iec61970/base/core/connectivity_node.py | 3 +- .../base/diagramlayout/diagram_object.py | 4 +- 15 files changed, 190 insertions(+), 165 deletions(-) delete mode 100644 src/zepben/ewb/boilerplate/collections/lazy_collection.py create mode 100644 src/zepben/ewb/boilerplate/collections/lazy_index_list.py diff --git a/src/zepben/ewb/boilerplate/MANIFESTO.md b/src/zepben/ewb/boilerplate/MANIFESTO.md index 53220a622..ea136d722 100644 --- a/src/zepben/ewb/boilerplate/MANIFESTO.md +++ b/src/zepben/ewb/boilerplate/MANIFESTO.md @@ -6,13 +6,13 @@ Yes, this is not your usual manifest We have removed the dataclassy library from the SDK, migrating to using the actual Python dataclass decorator. This was motivated by lack of ongoing support for dataclassy, as well as no-one on the team wanting to deal with its mess of magic code. -Our Python version of CIM has some very specific requirements leading to some unorthodox decisions (eg custom inits). Whatever your "why does it work this way" is, it has likely been considered, and many are provided below. +Our Python version of CIM has some very specific requirements leading to some unorthodox decisions (eg custom inits). Whatever your "why does it work this way" is, it has likely been considered, and many are answered below. This document is to serve as a guide in case you need to modify the SDK and are unfamiliar with how things are implemented around here. The following are some major decisions we have made and how they influence your potential contribution. ## Slots -Due to the immense size of grid models we are dealing with, every byte of memory matters, lest you fry your RAM with 120% load. By default, Python stores its values as a dictionary, which takes up hella space. Slots exist to fix that - a `__slots__` class variable describes the fixed memory layout of a class, making it rigid and a lot more compact. Every single one of our CIM classes uses slots, and all the new ones should too. `@dataclass` with slots enabled saves us specifying them explicitly at the cost of transparency - every field of such a class is a descriptor, and overwrites don't always work the way you expect them to. (For example, `@property` overwrite of a static field in a superclass is completely ignored by Python). +Due to the immense size of grid models we are dealing with, every byte of memory matters, lest you fry your RAM with 120% load. By default, Python stores instance values as a dictionary, which takes up hella space. Slots exist to fix that - a `__slots__` class variable describes the fixed attribute layout of a class, and through some dark magic (static instance-hashed attribute dictionaries) removes a lot of the memory overhead. Every single one of our CIM classes uses slots, and all the new ones should too. `@dataclass` with slots enabled saves us specifying them explicitly at the cost of transparency - every field of such a class is a descriptor, and overwrites don't always work the way you expect them to. (For example, `@property` overwrite of a static field in a superclass is completely ignored by Python). Our chosen approach is delegating as much functionality as we can to the native Python `@dataclass`, which lets you rely on its documentation and AI's knowledge of its quirks. It might be weird, but it is weird in consistent ways. No more reading a random biologist's library to figure out why your ConnectivityNode just did a barrel roll! @@ -20,7 +20,7 @@ Our chosen approach is delegating as much functionality as we can to the native We have chosen to use dataclasses simply because we have a lot of attributes, and any non-dataclass Python init requires a ridiculous amount of code duplication. This allows us to make use of its slot construction, as well as default values for fields that don't need to be passed in the init. -Unfortunately, dataclass generates custom `__init__` functions for the classes, but ONLY if you don't have an existing init already. That's why slightly tweaking class instantiation is quite complicated (requiring a custom decorator to rebuild the class from the original and the dataclass combined - I tried this). Instead, we went for a "clean slate" base class init approach, which, while stripping some of the dataclass functionality, is a lot more transparent when it explodes in your face. +Unfortunately, dataclass generates custom `__init__` functions for the classes, but ONLY if you don't have an existing init already. That's why slightly tweaking class instantiation is quite complicated (requiring a custom decorator to rebuild the class from the original and the dataclass combined - I tried this). Instead, we went for a "clean slate" base class init approach (explained below), which, while stripping some of the dataclass functionality, is a lot more transparent when it explodes in your face. ## Instantiation @@ -44,10 +44,34 @@ While it would be possible to bring args back if all subclass inits are removed ## Eq, Hash, Str, Repr -CIM classes need to be homogenous in their behaviour. Normally, dataclasses define these four methods on subclass level, but we need them to be propagated all the way up to `Identifiable` (or any overriding children such as `NameType`), where `__eq__` and `__hash__` both asses mrid equality, and `__str__` and `__rer__` represent the class as `{}` eg `Terminal{terminal_mrid}`. +CIM classes need to be homogenous in their behaviour. Normally, dataclasses define these four methods on subclass level, but we need them to be propagated all the way up to `Identifiable` (or any overriding children such as `NameType`), where `__eq__` and `__hash__` both asses memory reference equality, and `__str__` and `__repr__` represent the class as `{}` eg `Terminal{terminal_mrid}`. ## Descriptors -Properties are bulky and limited in functionality. There is a more general python concept called a Descriptor. This is any class implementing `__get__` and/or (usually and) `__set__` methods, which overwrite accessing a class member provided they are attached to the class and not its instance. This allows us to, for example, implement internally nullable fields. +Properties are bulky and limited in functionality. There is a more general python concept called a Descriptor. This is any class implementing `__get__` and/or (usually and) `__set__` methods, which overwrite accessing a class member (provided they are attached to the class and not its instance). This allows us to, for example, implement internally nullable fields. -These fields technically require no space in the object, since they access other values internally, but `@dataclass` assigns a slot to anything with a type. Obviously, if you want type hinting on member access, descriptor fields need to be typed. `@remove_descriptor_annotations` decorator takes care of that by stripping the types off of anything defining the aforementioned methods before it gets passed to a dataclass. Since this happens at runtime, IDE type-checking still gets the types, and `@dataclass` is no longer confused about which fields should be created. +These fields technically require no space in the object, since they access other values internally, but `@dataclass` assigns a slot to anything with a type. Obviously, if you want type hinting on member access, descriptor fields need to be typed. `@remove_descriptor_annotations` decorator (ordinarily part of `@zb_dataclass`) takes care of that by stripping the types off of anything defining the aforementioned methods before it gets passed to a dataclass. Since this happens at runtime, IDE type-checking still gets the types, and `@dataclass` is no longer confused about which fields should be created. + +## Lists + +Since CIM is UML-based, there are a lot of one-to-many and many-to-many class relationships, represented with lists. Most such relationships are usually empty. Python empty list is 56 bytes, whereas `None` is 16, so we want the lists to be nullable. + +Consequently, we have created a hierarchy of nullable descriptors backed by private fields. Each descriptor implements a list-like interface, allowing the user to interact with the relations normally, blissfully unaware of the nullable insanity going on under the hood. + +Due to the high diversity of the relationship behaviours, there is a lot of optional functionality shared by some these lists: + +- *mRID collision checks*: for mrid collections, we perform an mRID lookup when an item is added. If another item comes back, we check if the identities match. If they do, we disregard the addition. If they don't, we throw an appropriate error. +- *backfill*: some items contained in lists link back to the owner item. In that case, we perform a backfill to do this automatically. We also ensure that the item is not already linked to another owner. +- *validation*: some lists are create with validation lambdas, calling a validator function on the owner itself. This is useful when additional validation is required, eg sequence number matching. +- *sorting*: passing a sorting lambda to ordered collections automatically re-sorts the list after an item is added. + +Here are the concrete implementations of the lists that are used in the SDK: + +- `LazyList`: A nullable list of non-`Identifiable` objects that has validation and sorting built in. +- `LazyIndexList`: `LazyList` with index-based insertion and deletion, for strictly ordered relationships such as diagram points. + +The remaining collections all implement `MridCollection` - an interface that declares functionality of interaction with a collection of `Identifiable` objects, mainly focusing on `get_by_mrid`, `append`, `remove`, and `clear` methods. This allows us to have a common way of interacting with inter-object relationships, while hiding the underlying implementation (eg `list` vs `dict`) + +- `LazyMridList`: A `LazyList` with an mrid check and backfill added. `O(N)` mRID lookup. +- `MridList`: An mRID collection backed by a non-nullable list. Useful for relationships that are most likely to be filled (eg `ConnectivityNode.terminals`). `O(N)` mRID lookup. +- `LazyMridMap`: A nullable map of Identifiable objects keyed on their mRID. Supports mRID checks and `O(1)` mRID lookup. Does not support sorting (duh). Useful fot large-scale collections (for which the lookup speedup is significant) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_collection.py b/src/zepben/ewb/boilerplate/collections/lazy_collection.py deleted file mode 100644 index bc2267843..000000000 --- a/src/zepben/ewb/boilerplate/collections/lazy_collection.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright 2026 Zeppelin Bend Pty Ltd -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -from typing import TypeVar - -from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList -from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper - - -T = TypeVar("T") - - -class LazyCollection(_IterableWrapper[T], AbstractBackedList[T]): - """ - Concrete collection wrapper that treats its backing field as a nullable - list. - - A backing value of ``None`` is exposed as an empty collection. The backing - list is created when the first item is appended and reset to ``None`` when - the last item is removed or the collection is cleared. - - For example:: - - class Container: - _items = field(default=None) - items = LazyCollection(_items) - - container = Container() - - assert list(container.items) == [] - assert container._items is None - - container.items.append("value") - assert container._items == ["value"] - - container.items.clear() - assert container._items is None - """ - def __init__( - self, - private_field: list[T] | None, - validate=None, - sort_by=None - ) -> None: - super().__init__(private_field) - self.validate = validate - self.sort_by = sort_by - - def _get(self) -> list[T] | None: - return getattr(self._instance, self._backing_name) - - def _get_collection(self) -> list[T]: - return getattr(self._instance, self._backing_name) or [] - - def append(self, item: T) -> None: - """ - Append an item to the collection. - Run optional validation. - Sort the collection if key lambda is provided. - """ - if self.validate is not None: - self.validate(self._instance, item) - - existing = getattr(self._instance, self._backing_name) - if existing is None: - existing = [item] - setattr(self._instance, self._backing_name, existing) - else: - existing.append(item) - - if self.sort_by is not None: - existing.sort(key=self.sort_by) - - def remove(self, item: T) -> None: - existing = self._get_collection() - existing.remove(item) - if not existing: - self.clear() - - def clear(self) -> None: - setattr(self._instance, self._backing_name, None) - - def __repr__(self) -> str: - if self._instance is None: - return object.__repr__(self) - return repr(self._get_collection()) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py new file mode 100644 index 000000000..e310a6c0b --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py @@ -0,0 +1,88 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from zepben.ewb.boilerplate.collections.lazy_list import LazyList, T + + +class LazyIndexList(LazyList[T]): + """ + Lazy collection with list-style index-based insertion and deletion. + + It retains the nullable backing-list behaviour of ``LazyCollection``, + creating the backing list when an item is inserted and resetting it to + ``None`` when the final item is deleted. + + For example:: + + container.items.insert(0, "value") + assert container._items == ["value"] + + del container.items[0] + assert container._items is None + """ + def __init__( + self, + private_field: list[T] | None, + element_description: str, + validate=None, + ) -> None: + super().__init__(private_field, validate=validate, sort_by=None) + self.element_description = element_description + + def insert(self, index: int, item: T) -> None: + """ + Insert an item into the collection at a given index. + Check for mRID collisions and run optional validation. + Sort the collection if key lambda is provided. + """ + size = len(self) + + if not 0 <= index <= size: + raise ValueError( + f"Unable to add {self.element_description} to " + f"{self._instance}. " + f"Sequence number {index} is invalid. " + f"Expected a value between 0 and {size}. " + "Make sure you are adding the items in order and there are " + "no gaps in the numbering." + ) + + if self.validate is not None: + self.validate(self._instance, item) + + existing = getattr(self._instance, self._backing_name) + + if existing is None: + existing = [item] + setattr(self._instance, self._backing_name, existing) + else: + existing.insert(index, item) + + def append(self, item: T) -> None: + """ + Append an item to the collection. + Run optional validation. + Sort the collection if key lambda is provided. + """ + self.insert(len(self), item) + + def pop(self, index: int = -1) -> T: + """ + Remove and return the item at ``index``. + + Uses normal Python list semantics, including support for negative + indexes and raising ``IndexError`` when the index is invalid. + """ + existing = getattr(self._instance, self._backing_name) + + if existing is None: + raise IndexError("pop from empty list") + + item = existing.pop(index) + + if not existing: + self.clear() + + return item diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index 6296f9cd7..e40248e95 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -2,63 +2,56 @@ # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from typing import TypeVar -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection, T +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class LazyList(LazyCollection[T]): +T = TypeVar("T") + + +class LazyList(_IterableWrapper[T], AbstractBackedList[T]): """ - Lazy collection with list-style index-based insertion and deletion. + Concrete collection wrapper that treats its backing field as a nullable + list. - It retains the nullable backing-list behaviour of ``LazyCollection``, - creating the backing list when an item is inserted and resetting it to - ``None`` when the final item is deleted. + A backing value of ``None`` is exposed as an empty collection. The backing + list is created when the first item is appended and reset to ``None`` when + the last item is removed or the collection is cleared. For example:: - container.items.insert(0, "value") + class Container: + _items = field(default=None) + items = LazyCollection(_items) + + container = Container() + + assert list(container.items) == [] + assert container._items is None + + container.items.append("value") assert container._items == ["value"] - del container.items[0] + container.items.clear() assert container._items is None """ def __init__( self, private_field: list[T] | None, - element_description: str, validate=None, + sort_by=None ) -> None: - super().__init__(private_field, validate=validate, sort_by=None) - self.element_description = element_description - - def insert(self, index: int, item: T) -> None: - """ - Insert an item into the collection at a given index. - Check for mRID collisions and run optional validation. - Sort the collection if key lambda is provided. - """ - size = len(self) - - if not 0 <= index <= size: - raise ValueError( - f"Unable to add {self.element_description} to " - f"{self._instance}. " - f"Sequence number {index} is invalid. " - f"Expected a value between 0 and {size}. " - "Make sure you are adding the items in order and there are " - "no gaps in the numbering." - ) + super().__init__(private_field) + self.validate = validate + self.sort_by = sort_by - if self.validate is not None: - self.validate(self._instance, item) - - existing = getattr(self._instance, self._backing_name) + def _get(self) -> list[T] | None: + return getattr(self._instance, self._backing_name) - if existing is None: - existing = [item] - setattr(self._instance, self._backing_name, existing) - else: - existing.insert(index, item) + def _get_collection(self) -> list[T]: + return getattr(self._instance, self._backing_name) or [] def append(self, item: T) -> None: """ @@ -66,23 +59,29 @@ def append(self, item: T) -> None: Run optional validation. Sort the collection if key lambda is provided. """ - self.insert(len(self), item) - - def pop(self, index: int = -1) -> T: - """ - Remove and return the item at ``index``. + if self.validate is not None: + self.validate(self._instance, item) - Uses normal Python list semantics, including support for negative - indexes and raising ``IndexError`` when the index is invalid. - """ existing = getattr(self._instance, self._backing_name) - if existing is None: - raise IndexError("pop from empty list") + existing = [item] + setattr(self._instance, self._backing_name, existing) + else: + existing.append(item) - item = existing.pop(index) + if self.sort_by is not None: + existing.sort(key=self.sort_by) + def remove(self, item: T) -> None: + existing = self._get_collection() + existing.remove(item) if not existing: self.clear() - return item + def clear(self) -> None: + setattr(self._instance, self._backing_name, None) + + def __repr__(self) -> str: + if self._instance is None: + return object.__repr__(self) + return repr(self._get_collection()) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py index d6d313f62..d33a4b726 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py @@ -4,11 +4,11 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.boilerplate.collections.mrid_collection import S, MridCollection -class LazyMridList(LazyCollection[S], MridCollection[S]): +class LazyMridList(LazyList[S], MridCollection[S]): """ Nullable list implementation of :class:`MridCollection`. diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 81ed03f28..7fac3e6e4 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -15,7 +15,7 @@ class MridList(_IterableWrapper[S], AbstractBackedList[S], MridCollection[S]): def __init__( self, - private_field, + private_field: list[S], element_description: str, backfill: Backfill | None = None, validate=None, diff --git a/src/zepben/ewb/boilerplate/relations/curve_data_list.py b/src/zepben/ewb/boilerplate/relations/curve_data_list.py index 3ee44553f..85954a1d2 100644 --- a/src/zepben/ewb/boilerplate/relations/curve_data_list.py +++ b/src/zepben/ewb/boilerplate/relations/curve_data_list.py @@ -4,13 +4,13 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from typing import TYPE_CHECKING -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.lazy_list import LazyList if TYPE_CHECKING: from zepben.ewb import CurveData -class CurveDataList(LazyCollection): +class CurveDataList(LazyList): def get(self, x: float) -> 'CurveData': """ diff --git a/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py index dae48773f..f1f2951e4 100644 --- a/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py +++ b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py @@ -7,12 +7,12 @@ from typing import TYPE_CHECKING -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.lazy_list import LazyList if TYPE_CHECKING: from zepben.ewb import SinglePhaseKind, PhaseImpedanceData -class PhaseImpedanceDataList(LazyCollection): +class PhaseImpedanceDataList(LazyList): def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: """ Get the matrix entry for the corresponding to and from phases. diff --git a/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py index 580015532..f62647491 100644 --- a/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py +++ b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py @@ -6,10 +6,10 @@ from __future__ import annotations from zepben.ewb import TransformerEndRatedS, TransformerCoolingType -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.lazy_list import LazyList -class TransformerEndRatedSList(LazyCollection[TransformerEndRatedS]): +class TransformerEndRatedSList(LazyList[TransformerEndRatedS]): def get_by_cooling_type( self, diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index b9dcbe8c7..cf500fa23 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyList +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.assets.asset_info import AssetInfo @@ -31,7 +31,7 @@ class RelayInfo(AssetInfo): _reclose_delays: Optional[List[float]] = field(default=None) - reclose_delays: LazyList[float] = LazyList( + reclose_delays: LazyIndexList[float] = LazyIndexList( _reclose_delays, "float" ) diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py index 93ef787f3..4d03083a8 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/common/contact_details.py @@ -12,7 +12,7 @@ from zepben.ewb import zb_dataclass from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList -from zepben.ewb.boilerplate.collections.lazy_collection import LazyCollection +from zepben.ewb.boilerplate.collections.lazy_list import LazyList from zepben.ewb.model.cim.extensions.iec61968.common.contact_method_type import ContactMethodType from zepben.ewb.model.cim.extensions.zbex import zbex from zepben.ewb.model.cim.iec61968.common.electronic_address import ElectronicAddress @@ -83,10 +83,10 @@ def __hash__(self): # noinspection PyUnresolvedReferences return hash((type(self), *(getattr(self, s) for s in self.__slots__))) - phone_numbers: AbstractBackedList[TelephoneNumber] = LazyCollection(_phone_numbers) + phone_numbers: AbstractBackedList[TelephoneNumber] = LazyList(_phone_numbers) """[ZBEX] Phone numbers.""" - electronic_addresses: AbstractBackedList[ElectronicAddress] = LazyCollection(_electronic_addresses) + electronic_addresses: AbstractBackedList[ElectronicAddress] = LazyList(_electronic_addresses) """[ZBEX] Electronic addresses.""" def __eq__(self, other: Any) -> bool: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index 8e1751acc..b92f7fe12 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -11,7 +11,7 @@ from typing import Optional, List, Callable, TYPE_CHECKING, Any from abc import ABCMeta -from zepben.ewb.boilerplate.collections.lazy_list import LazyList +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList if sys.version_info >= (3, 13): from warnings import deprecated else: @@ -84,12 +84,12 @@ def relay_info(self): def relay_info(self, relay_info: Optional[RelayInfo]): self.asset_info = relay_info - thresholds: LazyList[RelaySetting] = LazyList( + thresholds: LazyIndexList[RelaySetting] = LazyIndexList( _thresholds, "RelaySetting" ) - time_limits: LazyList[float] = LazyList( + time_limits: LazyIndexList[float] = LazyIndexList( _time_limits, "float" ) diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index be4bc42c2..c20302742 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -13,7 +13,7 @@ from typing_extensions import deprecated from zepben.ewb.boilerplate.dataclass_base import zb_dataclass -from zepben.ewb.boilerplate.collections.lazy_list import LazyList +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList from zepben.ewb.model.cim.iec61968.common.position_point import PositionPoint from zepben.ewb.model.cim.iec61968.common.street_address import StreetAddress from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject @@ -34,7 +34,7 @@ def __init__(self, *args, position_points=None, **kwargs): super(Location, self).__init__(*args, **kwargs) self.points.extend(position_points) - points: LazyList[PositionPoint] = LazyList( + points: LazyIndexList[PositionPoint] = LazyIndexList( _position_points, "PositionPoint", ) diff --git a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py index 493e42c4d..6fcda037d 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py +++ b/src/zepben/ewb/model/cim/iec61970/base/core/connectivity_node.py @@ -14,6 +14,7 @@ from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.boilerplate.collections.mrid_list import MridList from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.boilerplate.dataclass_base import zb_dataclass @@ -36,7 +37,7 @@ class ConnectivityNode(IdentifiedObject, WeakrefSlot): def __iter__(self): return iter(self._terminals) - terminals: MridCollection[Terminal] = LazyMridList( + terminals: MridCollection[Terminal] = MridList( _terminals, "A Terminal" ) diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index 8440b2c94..a3cb235cf 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -12,7 +12,7 @@ from typing_extensions import deprecated -from zepben.ewb.boilerplate.collections.lazy_list import LazyList +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList from zepben.ewb.boilerplate.backfill import internal from zepben.ewb.model.cim.iec61970.base.core.identified_object import IdentifiedObject from zepben.ewb.model.cim.iec61970.base.diagramlayout.diagram_object_point import DiagramObjectPoint @@ -60,7 +60,7 @@ def diagram(self, diag): else: raise ValueError(f"diagram for {str(self)} has already been set to {self._diagram}, cannot reset this field to {diag}") - points: LazyList[DiagramObjectPoint] = LazyList( + points: LazyIndexList[DiagramObjectPoint] = LazyIndexList( _diagram_object_points, "DiagramObjectPoint", ) From 2d6baaa3cb9586a6b5dcd2c5ed25082aa1b61e97 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Fri, 31 Jul 2026 15:45:05 +1000 Subject: [PATCH 44/54] Documentation (and other) tweaks Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/MANIFESTO.md | 18 +++++++++--------- .../collections/abstract_backed_list.py | 6 ++---- .../boilerplate/collections/lazy_index_list.py | 9 ++++++--- .../ewb/boilerplate/collections/lazy_list.py | 2 +- .../boilerplate/collections/lazy_mrid_list.py | 2 +- .../boilerplate/collections/lazy_mrid_map.py | 2 +- .../ewb/boilerplate/collections/mrid_list.py | 10 ++++++++++ .../ewb/boilerplate/collections/wrapper.py | 6 ++---- 8 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/zepben/ewb/boilerplate/MANIFESTO.md b/src/zepben/ewb/boilerplate/MANIFESTO.md index ea136d722..571788402 100644 --- a/src/zepben/ewb/boilerplate/MANIFESTO.md +++ b/src/zepben/ewb/boilerplate/MANIFESTO.md @@ -38,13 +38,13 @@ Since `@dataclass` generates an init for every subclass without one, the `@zb_da Positional args are very hard to get consistent with the use of `@dataclass` - to know the order, you must parse the inheritance tree. `@property` values are not included in dataclass inits by default, meaning that adding them as an argument would create arbitrary ordering, and that is a readability nightmare. Moreover, we have custom inits in subclasses, making maintaining the arg order functionally impossible. -That is why, from this version onwards, any argument that is not the object's `mrid` (or equivalent identifier) must be passed as a keyword argument (`MyClass("mrid", 42)` bad, `MyClass("mrid", thing=42)` good). +That is why, from this version onwards, any argument that is not the object's `mrid` (or equivalent identifier) must be passed as a keyword argument (`MyClass("mRID", 42)` bad, `MyClass("mRID", thing=42)` good). While it would be possible to bring args back if all subclass inits are removed (probable future feature), it is a terrible idea. Do not do it unless you are very sure of yourself. ## Eq, Hash, Str, Repr -CIM classes need to be homogenous in their behaviour. Normally, dataclasses define these four methods on subclass level, but we need them to be propagated all the way up to `Identifiable` (or any overriding children such as `NameType`), where `__eq__` and `__hash__` both asses memory reference equality, and `__str__` and `__repr__` represent the class as `{}` eg `Terminal{terminal_mrid}`. +CIM classes need to be homogenous in their behaviour. Normally, dataclasses define these four methods on subclass level, but we need them to be propagated all the way up to `Identifiable` (or any overriding children such as `NameType`), where `__eq__` and `__hash__` both asses memory reference equality, and `__str__` and `__repr__` represent the class as `{}` eg `Terminal{terminal_mRID}`. ## Descriptors @@ -58,20 +58,20 @@ Since CIM is UML-based, there are a lot of one-to-many and many-to-many class re Consequently, we have created a hierarchy of nullable descriptors backed by private fields. Each descriptor implements a list-like interface, allowing the user to interact with the relations normally, blissfully unaware of the nullable insanity going on under the hood. -Due to the high diversity of the relationship behaviours, there is a lot of optional functionality shared by some these lists: +Due to the high diversity of the relationship behaviours, there is a lot of optional functionality shared by some of these lists: -- *mRID collision checks*: for mrid collections, we perform an mRID lookup when an item is added. If another item comes back, we check if the identities match. If they do, we disregard the addition. If they don't, we throw an appropriate error. +- *mRID collision checks*: for mRID collections, we perform an mRID lookup when an item is added. If another item comes back, we check if the identities match. If they do, we disregard the addition. If they don't, we throw an appropriate error. - *backfill*: some items contained in lists link back to the owner item. In that case, we perform a backfill to do this automatically. We also ensure that the item is not already linked to another owner. -- *validation*: some lists are create with validation lambdas, calling a validator function on the owner itself. This is useful when additional validation is required, eg sequence number matching. +- *validation*: some lists are created with validation lambdas, calling a validator function on the owner itself. This is useful when additional validation is required, eg sequence number matching. - *sorting*: passing a sorting lambda to ordered collections automatically re-sorts the list after an item is added. Here are the concrete implementations of the lists that are used in the SDK: -- `LazyList`: A nullable list of non-`Identifiable` objects that has validation and sorting built in. +- `LazyList`: A nullable list of objects that has validation and sorting built in. Typically used for non-`Identifiable` relationships. - `LazyIndexList`: `LazyList` with index-based insertion and deletion, for strictly ordered relationships such as diagram points. -The remaining collections all implement `MridCollection` - an interface that declares functionality of interaction with a collection of `Identifiable` objects, mainly focusing on `get_by_mrid`, `append`, `remove`, and `clear` methods. This allows us to have a common way of interacting with inter-object relationships, while hiding the underlying implementation (eg `list` vs `dict`) +The remaining collections all implement `MridCollection` - an interface that declares functionality of interaction with a collection of `Identifiable` objects, mainly focusing on `get_by_mRID`, `append`, `remove`, and `clear` methods. This allows us to have a common way of interacting with inter-object relationships, while hiding the underlying implementation (eg `list` vs `dict`) -- `LazyMridList`: A `LazyList` with an mrid check and backfill added. `O(N)` mRID lookup. +- `LazyMridList`: A `LazyList` with an mRID check and backfill added. `O(N)` mRID lookup. - `MridList`: An mRID collection backed by a non-nullable list. Useful for relationships that are most likely to be filled (eg `ConnectivityNode.terminals`). `O(N)` mRID lookup. -- `LazyMridMap`: A nullable map of Identifiable objects keyed on their mRID. Supports mRID checks and `O(1)` mRID lookup. Does not support sorting (duh). Useful fot large-scale collections (for which the lookup speedup is significant) +- `LazyMridMap`: A nullable map of Identifiable objects keyed on their mRID. Supports mRID checks and `O(1)` mRID lookup. Does not support sorting (duh). Useful for large-scale collections (for which the lookup speedup is significant) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py index 3caa14d52..49f348e0b 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py @@ -24,10 +24,8 @@ def __getitem__(self, index: int) -> T: ... @overload - def __getitem__(self, index: int) -> T: ... - - @overload - def __getitem__(self, index: slice) -> Sequence[T]: ... + def __getitem__(self, index: slice) -> Sequence[T]: + ... def __getitem__(self, index: int | slice) -> T | Sequence[T]: collection = self._get_collection() diff --git a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py index e310a6c0b..421eac1a8 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py @@ -10,7 +10,7 @@ class LazyIndexList(LazyList[T]): """ Lazy collection with list-style index-based insertion and deletion. - It retains the nullable backing-list behaviour of ``LazyCollection``, + It retains the nullable backing-list behaviour of ``LazyList``, creating the backing list when an item is inserted and resetting it to ``None`` when the final item is deleted. @@ -34,8 +34,7 @@ def __init__( def insert(self, index: int, item: T) -> None: """ Insert an item into the collection at a given index. - Check for mRID collisions and run optional validation. - Sort the collection if key lambda is provided. + Run optional validation. """ size = len(self) @@ -86,3 +85,7 @@ def pop(self, index: int = -1) -> T: self.clear() return item + + def __delitem__(self, index: int, /) -> None: + """Remove the item at the given index.""" + self.remove(self[index]) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index e40248e95..c9cf9cec2 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -24,7 +24,7 @@ class LazyList(_IterableWrapper[T], AbstractBackedList[T]): class Container: _items = field(default=None) - items = LazyCollection(_items) + items = LazyList(_items) container = Container() diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py index d33a4b726..33c9f41b0 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py @@ -13,7 +13,7 @@ class LazyMridList(LazyList[S], MridCollection[S]): Nullable list implementation of :class:`MridCollection`. Inherits mRID lookup and uniqueness semantics from ``MridCollection`` and - lazy backing-list behavior from ``LazyCollection``. + lazy backing-list behavior from ``LazyList``. """ def __init__( self, diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py index c2d32a0d0..a5e5b36eb 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py @@ -32,7 +32,7 @@ class LazyMridMap(_IterableWrapper[S], MridCollection[S]): """ def __init__( self, - private_field: list[S] | None, + private_field: dict[str, S] | None, element_description: str, backfill: Backfill | None = None, validate=None diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 7fac3e6e4..b1764cead 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -13,6 +13,16 @@ class MridList(_IterableWrapper[S], AbstractBackedList[S], MridCollection[S]): + """ + mRID collection backed by a non-nullable list. + + Items are kept in insertion order and retrieved by mRID using a linear search. + Appending enforces mRID uniqueness and can optionally apply backfill, + validation, and sorting. + + Unlike ``LazyMridList``, clearing the collection leaves an empty backing list + rather than resetting the backing field to ``None``. + """ def __init__( self, private_field: list[S], diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py index 40bdcf683..a388be93b 100644 --- a/src/zepben/ewb/boilerplate/collections/wrapper.py +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -24,7 +24,7 @@ class _Wrapper(BackedDescriptor): class Container: _items = field(default=None) - items = LazyCollection(_items) + items = LazyList(_items) container = Container() @@ -37,7 +37,7 @@ class Container: Access through the class returns the original shared descriptor:: - assert isinstance(Container.items, LazyCollection) + assert isinstance(Container.items, LazyList) """ _instance: Any @@ -108,5 +108,3 @@ def __set__(self, instance, value) -> None: self.__get__(instance).extend(value) else: self.__get__(instance).clear() - - From 2b2995d99c409d40282a8565638a64b9dff82a00 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 3 Aug 2026 12:11:29 +1000 Subject: [PATCH 45/54] Changelog and tests; backfill clear Signed-off-by: Boris Filin Signed-off-by: Boris Filin --- changelog.md | 1 + src/zepben/ewb/boilerplate/backfill.py | 12 +- .../test_abstract_backed_collection.py | 109 ++++++++ test/boilerplate/test_abstract_backed_list.py | 58 +++++ test/boilerplate/test_backfill.py | 92 +++++++ test/boilerplate/test_lazy_index_list.py | 136 ++++++++++ test/boilerplate/test_lazy_list.py | 146 +++++++++++ test/boilerplate/test_lazy_mrid_list.py | 88 +++++++ test/boilerplate/test_lazy_mrid_map.py | 175 +++++++++++++ test/boilerplate/test_mrid_collection.py | 86 +++++++ test/boilerplate/test_mrid_list.py | 238 ++++-------------- test/boilerplate/test_wrapper.py | 116 +++++++++ 12 files changed, 1067 insertions(+), 190 deletions(-) create mode 100644 test/boilerplate/test_abstract_backed_collection.py create mode 100644 test/boilerplate/test_abstract_backed_list.py create mode 100644 test/boilerplate/test_backfill.py create mode 100644 test/boilerplate/test_lazy_index_list.py create mode 100644 test/boilerplate/test_lazy_list.py create mode 100644 test/boilerplate/test_lazy_mrid_list.py create mode 100644 test/boilerplate/test_lazy_mrid_map.py create mode 100644 test/boilerplate/test_mrid_collection.py create mode 100644 test/boilerplate/test_wrapper.py diff --git a/changelog.md b/changelog.md index ee265bd01..a52f8189b 100644 --- a/changelog.md +++ b/changelog.md @@ -23,6 +23,7 @@ * Added `skip_install = true` to the lint environment to avoid installing package dependencies that aren't needed for linting. * Added E402 noqa comments for intentional mid-file imports used to avoid circular dependency issues in `dataclassy/dataclass.py`, `context_value_computer.py`, `queue_condition.py`, `direction_logger.py`, and `test_network_trace.py`. * Switched from using `dataclassy` to native dataclasses +* Every UML relationship is now a custom list wrapper that takes no memory and has a reference to the backing field - usage interface is nearly identical to lists ### Fixes * added python3.14 to compatibility list. diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py index 27df77c3f..c0cca6157 100644 --- a/src/zepben/ewb/boilerplate/backfill.py +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -21,7 +21,7 @@ def __init__(self, backfill_prop: property) -> None: self.backfill_prop = backfill_prop - def apply(self, element: S, owner: Any) -> None: + def _get_backing_name(self) -> str: name = self.backfill_prop.fget.__name__ target = getattr(self.backfill_prop.fget, "_internal_target", None) @@ -33,13 +33,21 @@ def apply(self, element: S, owner: Any) -> None: if backing_name is None: raise TypeError(f"Cannot determine backing name for {target!r}") + return backing_name + + def apply(self, element: S, owner: Any) -> None: + backing_name = self._get_backing_name() if getattr(element, backing_name) is None: setattr(element, backing_name, owner) ref = getattr(element, backing_name) if ref is not owner: - raise ValueError(f"{element} `{name}` property references {ref}, expected {owner}.") + raise ValueError(f"{element} `{self.backfill_prop.fget.__name__}` property references {ref}, expected {owner}.") + + def clear(self, element: S) -> None: + backing_name = self._get_backing_name() + setattr(element, backing_name, None) def internal(target: Any) -> Callable[[F], F]: diff --git a/test/boilerplate/test_abstract_backed_collection.py b/test/boilerplate/test_abstract_backed_collection.py new file mode 100644 index 000000000..35306120f --- /dev/null +++ b/test/boilerplate/test_abstract_backed_collection.py @@ -0,0 +1,109 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection + + +class BackedCollection(AbstractBackedCollection[str]): + def __init__(self, items: list[str] | None = None): + self.items = items or [] + + def _get_collection(self) -> list[str]: + return self.items + + def append(self, item: str) -> None: + self.items.append(item) + + def remove(self, item: str) -> None: + self.items.remove(item) + + def clear(self) -> None: + self.items.clear() + + +def test_extend_appends_each_item_in_order(): + collection = BackedCollection(["first"]) + + collection.extend(["second", "third"]) + + assert collection.items == ["first", "second", "third"] + + +def test_extend_accepts_single_pass_iterable(): + collection = BackedCollection() + + collection.extend(item for item in ["first", "second"]) + + assert collection.items == ["first", "second"] + + +def test_extend_empty_iterable_does_nothing(): + collection = BackedCollection(["item"]) + + collection.extend([]) + + assert collection.items == ["item"] + + +def test_extend_none_does_nothing(): + collection = BackedCollection(["item"]) + + collection.extend(None) + + assert collection.items == ["item"] + + +def test_len_returns_backing_collection_size(): + collection = BackedCollection(["first", "second"]) + + assert len(collection) == 2 + + +def test_len_returns_zero_for_empty_backing_collection(): + assert len(BackedCollection()) == 0 + + +def test_iter_returns_items_in_backing_collection_order(): + collection = BackedCollection(["first", "second"]) + + assert list(collection) == ["first", "second"] + + +def test_iter_returns_no_items_for_empty_backing_collection(): + assert list(BackedCollection()) == [] + + +def test_contains_finds_item_in_backing_collection(): + collection = BackedCollection(["first", "second"]) + + assert "second" in collection + + +def test_contains_rejects_item_missing_from_backing_collection(): + collection = BackedCollection(["first", "second"]) + + assert "missing" not in collection + + +def test_for_each_indexed_visits_each_item_with_its_index(): + collection = BackedCollection(["first", "second"]) + visited: list[tuple[int, str]] = [] + + collection.for_each_indexed( + lambda index, item: visited.append((index, item)) + ) + + assert visited == [(0, "first"), (1, "second")] + + +def test_for_each_indexed_does_not_call_action_for_empty_collection(): + collection = BackedCollection() + visited: list[tuple[int, str]] = [] + + collection.for_each_indexed( + lambda index, item: visited.append((index, item)) + ) + + assert visited == [] diff --git a/test/boilerplate/test_abstract_backed_list.py b/test/boilerplate/test_abstract_backed_list.py new file mode 100644 index 000000000..ea086979d --- /dev/null +++ b/test/boilerplate/test_abstract_backed_list.py @@ -0,0 +1,58 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import pytest + +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList + + +class BackedList(AbstractBackedList[str]): + def __init__(self, items: list[str]): + self.items = items + + def _get_collection(self) -> list[str]: + return self.items + + def append(self, item: str) -> None: + self.items.append(item) + + def remove(self, item: str) -> None: + self.items.remove(item) + + def clear(self) -> None: + self.items.clear() + + +def test_getitem_returns_item_at_index(): + collection = BackedList(["first", "second"]) + + assert collection[1] == "second" + + +def test_getitem_supports_negative_index(): + collection = BackedList(["first", "second"]) + + assert collection[-1] == "second" + + +def test_getitem_raises_for_index_out_of_range(): + collection = BackedList(["item"]) + + with pytest.raises(IndexError): + collection[1] + + +def test_getitem_returns_backing_list_slice(): + collection = BackedList(["first", "second", "third"]) + + assert collection[1:] == ["second", "third"] + + +def test_getitem_supports_slice_step(): + collection = BackedList( + ["first", "second", "third", "fourth"] + ) + + assert collection[::2] == ["first", "third"] diff --git a/test/boilerplate/test_backfill.py b/test/boilerplate/test_backfill.py new file mode 100644 index 000000000..a57820c5a --- /dev/null +++ b/test/boilerplate/test_backfill.py @@ -0,0 +1,92 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.backed_descriptor import Alias +from zepben.ewb.boilerplate.backfill import Backfill, internal + + +@dataclass +class Container: + """Container exposing each supported kind of internal back-reference.""" + + field_backing: object | None = field(default=None) + descriptor_backing: object | None = field(default=None) + property_backing: object | None = field(default=None) + + descriptor = Alias(descriptor_backing) + + @property + @internal(field_backing) + def field_parent(self): + return self.field_backing + + @property + @internal(descriptor) + def descriptor_parent(self): + return self.descriptor + + @property + def internal_parent(self): + return self.property_backing + + @internal_parent.setter + def internal_parent(self, value): + self.property_backing = value + + @property + @internal(internal_parent) + def property_parent(self): + return self.internal_parent + + +# @dataclass populates Field.name after the automatic __set_name__ call. +Container.descriptor.__set_name__(Container, "descriptor") + + +@pytest.mark.parametrize( + ("property_name", "backing_name"), + [ + ("field_parent", "field_backing"), + ("descriptor_parent", "descriptor_backing"), + ("property_parent", "property_backing"), + ], +) +def test_apply_sets_back_reference( + property_name: str, + backing_name: str, +): + container = Container() + owner = object() + + Backfill(getattr(Container, property_name)).apply(container, owner) + + assert getattr(container, property_name) is owner + assert getattr(container, backing_name) is owner + + +@pytest.mark.parametrize( + ("property_name", "backing_name"), + [ + ("field_parent", "field_backing"), + ("descriptor_parent", "descriptor_backing"), + ("property_parent", "property_backing"), + ], +) +def test_clear_nulls_back_reference( + property_name: str, + backing_name: str, +): + owner = object() + container = Container() + setattr(container, backing_name, owner) + + Backfill(getattr(Container, property_name)).clear(container) + + assert getattr(container, property_name) is None + assert getattr(container, backing_name) is None diff --git a/test/boilerplate/test_lazy_index_list.py b/test/boilerplate/test_lazy_index_list.py new file mode 100644 index 000000000..1cf12ba68 --- /dev/null +++ b/test/boilerplate/test_lazy_index_list.py @@ -0,0 +1,136 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_index_list import LazyIndexList + + +def record_validation(owner: Owner, item: str): + owner.events.append(item) + + +def reject_validation(_owner: Owner, _item: str): + raise ValueError("validation failed") + + +@dataclass +class Owner: + backing_items: list[str] | None = field(default=None) + events: list[str] = field(default_factory=list) + + items = LazyIndexList(backing_items, "Item") + validated_items = LazyIndexList( + backing_items, + "Item", + validate=record_validation, + ) + rejecting_items = LazyIndexList( + backing_items, + "Item", + validate=reject_validation, + ) + + +def test_insert_into_empty_creates_backing_list(): + owner = Owner() + + owner.items.insert(0, "item") + + assert owner.backing_items == ["item"] + + +def test_insert_places_item_at_requested_index(): + owner = Owner(["first", "third"]) + + owner.items.insert(1, "second") + + assert owner.backing_items == ["first", "second", "third"] + + +@pytest.mark.parametrize("index", [-1, 1]) +def test_insert_rejects_index_outside_valid_range(index: int): + owner = Owner() + + with pytest.raises( + ValueError, + match=rf"Sequence number {index} is invalid.*between 0 and 0", + ): + owner.items.insert(index, "item") + + assert owner.backing_items is None + + +def test_insert_validates_before_creating_backing_list(): + owner = Owner() + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_items.insert(0, "item") + + assert owner.backing_items is None + + +def test_insert_runs_validation(): + owner = Owner() + + owner.validated_items.insert(0, "item") + + assert owner.events == ["item"] + assert owner.backing_items == ["item"] + + +def test_append_inserts_at_end(): + owner = Owner(["first"]) + + owner.items.append("second") + + assert owner.backing_items == ["first", "second"] + + +def test_pop_returns_and_removes_requested_item(): + owner = Owner(["first", "second"]) + + popped = owner.items.pop(0) + + assert popped == "first" + assert owner.backing_items == ["second"] + + +def test_pop_uses_last_item_by_default_and_nulls_empty_backing(): + owner = Owner(["item"]) + + popped = owner.items.pop() + + assert popped == "item" + assert owner.backing_items is None + + +def test_pop_from_empty_raises(): + owner = Owner() + + with pytest.raises(IndexError, match="pop from empty list"): + owner.items.pop() + + assert owner.backing_items is None + + +def test_delete_removes_item_at_index(): + owner = Owner(["first", "second"]) + + del owner.items[0] + + assert owner.backing_items == ["second"] + + +def test_delete_last_item_nulls_backing_list(): + owner = Owner(["item"]) + + del owner.items[0] + + assert owner.backing_items is None diff --git a/test/boilerplate/test_lazy_list.py b/test/boilerplate/test_lazy_list.py new file mode 100644 index 000000000..a5c9c07e6 --- /dev/null +++ b/test/boilerplate/test_lazy_list.py @@ -0,0 +1,146 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_list import LazyList + + +def record_validation(owner: Owner, item: str): + owner.events.append(("validate", item)) + + +def reject_validation(_owner: Owner, _item: str): + raise ValueError("validation failed") + + +@dataclass +class Owner: + backing_items: list[str] | None = field( + default=None, + repr=False, + ) + events: list[tuple[str, str]] = field( + default_factory=list, + repr=False, + ) + + items = LazyList(backing_items) + validated_items = LazyList( + backing_items, + validate=record_validation, + ) + rejecting_validation_items = LazyList( + backing_items, + validate=reject_validation, + ) + sorted_items = LazyList( + backing_items, + sort_by=lambda item: item, + ) + + +def test_append_to_empty_creates_owner_backing_list(): + owner = Owner() + + owner.items.append("item") + + assert owner.backing_items == ["item"] + + +def test_append_adds_item_to_existing_owner_backing_list(): + owner = Owner() + owner.backing_items = ["first"] + + owner.items.append("second") + + assert owner.backing_items == ["first", "second"] + + +def test_append_runs_validation(): + owner = Owner() + + owner.validated_items.append("item") + + assert owner.events == [("validate", "item")] + assert owner.backing_items == ["item"] + + +def test_failed_append_to_empty_does_not_create_backing_list(): + owner = Owner() + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_items.append("item") + + assert owner.backing_items is None + + +def test_validation_failure_does_not_append_to_existing_list(): + owner = Owner() + owner.backing_items = ["existing"] + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_items.append("item") + + assert owner.backing_items == ["existing"] + + +def test_append_sorts_owner_backing_list(): + owner = Owner() + + owner.sorted_items.append("second") + owner.sorted_items.append("first") + + assert owner.backing_items == ["first", "second"] + + +def test_remove_removes_item_from_owner_backing_list(): + owner = Owner() + owner.backing_items = ["retained", "removed"] + + owner.items.remove("removed") + + assert owner.backing_items == ["retained"] + + +def test_remove_last_item_sets_owner_backing_list_to_null(): + owner = Owner() + owner.backing_items = ["item"] + + owner.items.remove("item") + + assert owner.backing_items is None + + +def test_clear_sets_owner_backing_list_to_null(): + owner = Owner() + owner.backing_items = ["first", "second"] + + owner.items.clear() + + assert owner.backing_items is None + + +def test_repr_matches_owner_backing_list_repr(): + owner = Owner() + owner.backing_items = ["first", "second"] + + assert owner.backing_items is not None + assert repr(owner.items) == repr(owner.backing_items) + + +def test_empty_repr_matches_empty_list_repr(): + owner = Owner() + + assert repr(owner.items) == repr([]) + assert owner.backing_items is None + + +def test_class_field_repr_uses_descriptor_repr() -> None: + assert repr(Owner.items) == object.__repr__(Owner.items) diff --git a/test/boilerplate/test_lazy_mrid_list.py b/test/boilerplate/test_lazy_mrid_list.py new file mode 100644 index 000000000..3874eb15b --- /dev/null +++ b/test/boilerplate/test_lazy_mrid_list.py @@ -0,0 +1,88 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList + + +@dataclass(eq=False) +class Item: + mrid: str + + +class RecordingBackfill: + def apply(self, item: Item, owner: Owner): + owner.events.append(("backfill", item.mrid)) + + +class RejectingBackfill: + def apply(self, _item: Item, _owner: Owner): + raise ValueError("backfill failed") + + +def record_validation(owner: Owner, item: Item): + owner.events.append(("validate", item.mrid)) + + +@dataclass +class Owner: + backing_items: list[Item] | None = field(default=None) + events: list[tuple[str, str]] = field(default_factory=list) + + items = LazyMridList(backing_items, "Item") + backfilled_items = LazyMridList( + backing_items, + "Item", + backfill=RecordingBackfill(), + validate=record_validation, + ) + rejecting_backfill_items = LazyMridList( + backing_items, + "Item", + backfill=RejectingBackfill(), + ) + + +def test_get_by_mrid_finds_item_in_backing_list(): + item = Item("item") + owner = Owner([item]) + + assert owner.items.get_by_mrid("item") is item + + +def test_get_by_mrid_treats_null_backing_as_empty(): + owner = Owner() + + with pytest.raises(KeyError, match="missing"): + owner.items.get_by_mrid("missing") + + assert owner.backing_items is None + + +def test_append_applies_backfill_before_superclass_validation(): + item = Item("item") + owner = Owner() + + owner.backfilled_items.append(item) + + assert owner.events == [ + ("backfill", "item"), + ("validate", "item"), + ] + assert owner.backing_items == [item] + + +def test_backfill_failure_does_not_call_superclass_append(): + owner = Owner() + + with pytest.raises(ValueError, match="backfill failed"): + owner.rejecting_backfill_items.append(Item("item")) + + assert owner.backing_items is None diff --git a/test/boilerplate/test_lazy_mrid_map.py b/test/boilerplate/test_lazy_mrid_map.py new file mode 100644 index 000000000..f6e073cfc --- /dev/null +++ b/test/boilerplate/test_lazy_mrid_map.py @@ -0,0 +1,175 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from __future__ import annotations + +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap + + +@dataclass(eq=False) +class Item: + mrid: str + + +class RecordingBackfill: + def apply(self, item: Item, owner: Owner): + owner.events.append(("backfill", item.mrid)) + + +class RejectingBackfill: + def apply(self, _item: Item, _owner: Owner): + raise ValueError("backfill failed") + + +def record_validation(owner: Owner, item: Item): + owner.events.append(("validate", item.mrid)) + + +def reject_validation(_owner: Owner, _item: Item): + raise ValueError("validation failed") + + +@dataclass +class Owner: + backing_items: dict[str, Item] | None = field(default=None) + events: list[tuple[str, str]] = field(default_factory=list) + + items = LazyMridMap(backing_items, "Item") + configured_items = LazyMridMap( + backing_items, + "Item", + backfill=RecordingBackfill(), + validate=record_validation, + ) + rejecting_backfill_items = LazyMridMap( + backing_items, + "Item", + backfill=RejectingBackfill(), + ) + rejecting_validation_items = LazyMridMap( + backing_items, + "Item", + validate=reject_validation, + ) + + +def test_append_to_empty_creates_map_keyed_by_mrid(): + item = Item("item") + owner = Owner() + + owner.items.append(item) + + assert owner.backing_items == {"item": item} + + +def test_append_to_existing_map_adds_mrid_entry(): + first = Item("first") + second = Item("second") + owner = Owner({"first": first}) + + owner.items.append(second) + + assert owner.backing_items == { + "first": first, + "second": second, + } + + +def test_append_runs_backfill_before_validation(): + item = Item("item") + owner = Owner() + + owner.configured_items.append(item) + + assert owner.events == [ + ("backfill", "item"), + ("validate", "item"), + ] + assert owner.backing_items == {"item": item} + + +def test_backfill_failure_does_not_create_map(): + owner = Owner() + + with pytest.raises(ValueError, match="backfill failed"): + owner.rejecting_backfill_items.append(Item("item")) + + assert owner.backing_items is None + + +def test_validation_failure_does_not_create_map(): + owner = Owner() + + with pytest.raises(ValueError, match="validation failed"): + owner.rejecting_validation_items.append(Item("item")) + + assert owner.backing_items is None + + +def test_lookup_and_indexing_use_mrid_keys(): + item = Item("item") + owner = Owner({"item": item}) + + assert owner.items.get_by_mrid("item") is item + assert owner.items["item"] is item + + with pytest.raises(KeyError, match="missing"): + owner.items.get_by_mrid("missing") + + with pytest.raises(KeyError, match="missing"): + owner.items["missing"] + + +def test_collection_view_exposes_map_values(): + first = Item("first") + second = Item("second") + owner = Owner({"first": first, "second": second}) + + assert len(owner.items) == 2 + assert list(owner.items) == [first, second] + assert first in owner.items + assert Item("first") not in owner.items + + +def test_remove_deletes_item_by_mrid(): + first = Item("first") + second = Item("second") + owner = Owner({"first": first, "second": second}) + + owner.items.remove(first) + + assert owner.backing_items == {"second": second} + + +def test_remove_last_item_nulls_backing_map(): + item = Item("item") + owner = Owner({"item": item}) + + owner.items.remove(item) + + assert owner.backing_items is None + + +def test_clear_nulls_backing_map(): + owner = Owner({"item": Item("item")}) + + owner.items.clear() + + assert owner.backing_items is None + + +def test_repr_matches_backing_map_repr(): + item = Item("item") + owner = Owner({"item": item}) + + assert repr(owner.items) == repr(owner.backing_items) + + +def test_class_field_repr_uses_descriptor_repr(): + assert repr(Owner.items) == object.__repr__(Owner.items) diff --git a/test/boilerplate/test_mrid_collection.py b/test/boilerplate/test_mrid_collection.py new file mode 100644 index 000000000..2ddd5d333 --- /dev/null +++ b/test/boilerplate/test_mrid_collection.py @@ -0,0 +1,86 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from dataclasses import dataclass + +import pytest + +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection + + +@dataclass +class Item: + mrid: str + + +class MridCollectionImpl(MridCollection[Item]): + def __init__(self, items: list[Item] | None = None): + self.items = items or [] + self.element_description = "Item" + self._instance = "owner" + + def _get_collection(self) -> list[Item]: + return self.items + + def _safe_get_by_mrid(self, mrid: str) -> Item | None: + return next( + (item for item in self.items if item.mrid == mrid), + None, + ) + + def append(self, item: Item) -> None: + if self._can_add_by_mrid(item): + self.items.append(item) + + def remove(self, item: Item) -> None: + self.items.remove(item) + + def clear(self) -> None: + self.items.clear() + + +def test_get_by_mrid_returns_matching_item(): + item = Item("item") + collection = MridCollectionImpl([item]) + + assert collection.get_by_mrid("item") is item + + +def test_get_by_mrid_raises_for_missing_item(): + collection = MridCollectionImpl() + + with pytest.raises(KeyError, match="missing"): + collection.get_by_mrid("missing") + + +def test_append_accepts_new_mrid(): + item = Item("item") + collection = MridCollectionImpl() + + collection.append(item) + + assert collection.items == [item] + + +def test_append_ignores_same_instance(): + item = Item("item") + collection = MridCollectionImpl([item]) + + collection.append(item) + + assert collection.items == [item] + + +def test_append_rejects_different_instance_with_same_mrid(): + existing = Item("duplicate") + collection = MridCollectionImpl([existing]) + + with pytest.raises( + ValueError, + match=r"Item with mRID duplicate already exists in owner", + ): + collection.append(Item("duplicate")) + + assert collection.items == [existing] diff --git a/test/boilerplate/test_mrid_list.py b/test/boilerplate/test_mrid_list.py index d9457f1fb..674bbe8b9 100644 --- a/test/boilerplate/test_mrid_list.py +++ b/test/boilerplate/test_mrid_list.py @@ -26,7 +26,7 @@ def apply(self, item: Feeder, owner: Owner): def record_backfill(owner: Owner, item: Feeder): - owner._events.append(("backfill", item.mrid)) + owner.events.append(("backfill", item.mrid)) def reject_backfill(_owner: Owner, _item: Feeder): @@ -34,202 +34,147 @@ def reject_backfill(_owner: Owner, _item: Feeder): def record_validation(owner: Owner, item: Feeder): - owner._events.append(("validate", item.mrid)) + owner.events.append(("validate", item.mrid)) def reject_validation(_owner: Owner, _item: Feeder): raise ValueError("validation failed") -def validate_for_combined_test(owner: Owner, item: Feeder): - record_validation(owner, item) - - if item.mrid == "invalid": - raise ValueError("invalid feeder") - - @dataclass class Owner: - _feeders: list[Feeder] = field( + backing_feeders: list[Feeder] = field( default_factory=list, repr=False, ) - _events: list[tuple[str, str]] = field( + events: list[tuple[str, str]] = field( default_factory=list, repr=False, ) feeders = MridList( - _feeders, + backing_feeders, "Feeder", ) backfilled_feeders = MridList( - _feeders, + backing_feeders, "Feeder", backfill=CallbackBackfill(record_backfill), ) rejecting_backfill_feeders = MridList( - _feeders, + backing_feeders, "Feeder", backfill=CallbackBackfill(reject_backfill), ) validated_feeders = MridList( - _feeders, + backing_feeders, "Feeder", validate=record_validation, ) rejecting_validation_feeders = MridList( - _feeders, + backing_feeders, "Feeder", validate=reject_validation, ) backfilled_validated_feeders = MridList( - _feeders, + backing_feeders, "Feeder", backfill=CallbackBackfill(record_backfill), validate=record_validation, ) sorted_feeders = MridList( - _feeders, + backing_feeders, "Feeder", sort_by=lambda feeder: feeder.mrid, ) - fully_configured_feeders = MridList( - _feeders, - "Feeder", - backfill=CallbackBackfill(record_backfill), - validate=validate_for_combined_test, - sort_by=lambda feeder: feeder.mrid, - ) -@pytest.fixture -def owner() -> Owner: +def test_append_adds_item_to_owner_backing_list(): owner = Owner() - - # All wrappers intentionally point at this same non-null backing list. - # Reset it and the callback log for each test. - owner._feeders.clear() - owner._events.clear() - - return owner - - -def test_append_adds_item_to_owner_backing_list(owner: Owner): feeder = Feeder("feeder") owner.feeders.append(feeder) - assert owner._feeders == [feeder] - - -def test_appending_same_instance_twice_only_adds_it_once( - owner: Owner, -): - feeder = Feeder("feeder") - - owner.feeders.append(feeder) - owner.feeders.append(feeder) - - assert owner._feeders == [feeder] + assert owner.backing_feeders == [feeder] -def test_appending_different_instance_with_same_mrid_raises( - owner: Owner, -): - existing = Feeder("duplicate") - owner.feeders.append(existing) - - with pytest.raises( - ValueError, - match=r"Feeder with mRID duplicate already exists", - ): - owner.feeders.append(Feeder("duplicate")) - - assert owner._feeders == [existing] - - -def test_rejected_duplicate_does_not_run_backfill_or_validation( - owner: Owner, -): - feeder = Feeder("feeder") - owner.backfilled_validated_feeders.append(feeder) - owner._events.clear() - - owner.backfilled_validated_feeders.append(feeder) - - assert owner._feeders == [feeder] - assert owner._events == [] - - -def test_append_applies_backfill(owner: Owner): +def test_append_applies_backfill(): + owner = Owner() feeder = Feeder("feeder") owner.backfilled_feeders.append(feeder) - assert owner._events == [("backfill", "feeder")] - assert owner._feeders == [feeder] + assert owner.events == [("backfill", "feeder")] + assert owner.backing_feeders == [feeder] -def test_backfill_failure_does_not_append_item(owner: Owner): +def test_backfill_failure_does_not_append_item(): + owner = Owner() + with pytest.raises(ValueError, match="backfill failed"): owner.rejecting_backfill_feeders.append(Feeder("feeder")) - assert owner._feeders == [] + assert owner.backing_feeders == [] -def test_append_runs_validation(owner: Owner): +def test_append_runs_validation(): + owner = Owner() feeder = Feeder("feeder") owner.validated_feeders.append(feeder) - assert owner._events == [("validate", "feeder")] - assert owner._feeders == [feeder] + assert owner.events == [("validate", "feeder")] + assert owner.backing_feeders == [feeder] + +def test_validation_failure_does_not_append_item(): + owner = Owner() -def test_validation_failure_does_not_append_item(owner: Owner): with pytest.raises(ValueError, match="validation failed"): owner.rejecting_validation_feeders.append(Feeder("feeder")) - assert owner._feeders == [] + assert owner.backing_feeders == [] -def test_append_runs_backfill_before_validation(owner: Owner): +def test_append_runs_backfill_before_validation(): + owner = Owner() feeder = Feeder("feeder") owner.backfilled_validated_feeders.append(feeder) - assert owner._events == [ + assert owner.events == [ ("backfill", "feeder"), ("validate", "feeder"), ] - assert owner._feeders == [feeder] + assert owner.backing_feeders == [feeder] -def test_append_sorts_owner_backing_list(owner: Owner): +def test_append_sorts_owner_backing_list(): + owner = Owner() second = Feeder("second") first = Feeder("first") owner.sorted_feeders.append(second) owner.sorted_feeders.append(first) - assert owner._feeders == [first, second] + assert owner.backing_feeders == [first, second] -def test_remove_removes_item_from_owner_backing_list( - owner: Owner, -): +def test_remove_removes_item_from_owner_backing_list(): + owner = Owner() retained = Feeder("retained") removed = Feeder("removed") - owner._feeders.extend([retained, removed]) + owner.backing_feeders.extend([retained, removed]) owner.feeders.remove(removed) - assert owner._feeders == [retained] + assert owner.backing_feeders == [retained] -def test_clear_empties_owner_backing_list(owner: Owner): - owner._feeders.extend( +def test_clear_empties_owner_backing_list(): + owner = Owner() + + owner.backing_feeders.extend( [ Feeder("first"), Feeder("second"), @@ -238,104 +183,21 @@ def test_clear_empties_owner_backing_list(owner: Owner): owner.feeders.clear() - assert owner._feeders == [] + assert owner.backing_feeders == [] + +def test_repr_matches_owner_backing_list_repr(): + owner = Owner() -def test_repr_matches_owner_backing_list_repr(owner: Owner): - owner._feeders.extend( + owner.backing_feeders.extend( [ Feeder("first"), Feeder("second"), ] ) - assert repr(owner.feeders) == repr(owner._feeders) + assert repr(owner.feeders) == repr(owner.backing_feeders) def test_class_field_repr_uses_descriptor_repr() -> None: assert repr(Owner.feeders) == object.__repr__(Owner.feeders) - - -def test_mrid_list_all_functionality_together(owner: Owner): - third = Feeder("third") - first = Feeder("first") - second = Feeder("second") - - owner.fully_configured_feeders.extend([third, first, second]) - - assert owner._feeders == [first, second, third] - assert len(owner.fully_configured_feeders) == 3 - assert list(owner.fully_configured_feeders) == [ - first, - second, - third, - ] - assert next(iter(owner.fully_configured_feeders)) is first - assert owner.fully_configured_feeders[0] is first - assert list(owner.fully_configured_feeders[1:]) == [ - second, - third, - ] - assert second in owner.fully_configured_feeders - assert ( - owner.fully_configured_feeders.get_by_mrid("second") - is second - ) - assert repr(owner.fully_configured_feeders) == repr( - owner._feeders - ) - - with pytest.raises(KeyError, match="missing"): - owner.fully_configured_feeders.get_by_mrid("missing") - - indexed: list[tuple[int, Feeder]] = [] - owner.fully_configured_feeders.for_each_indexed( - lambda index, item: indexed.append((index, item)) - ) - assert indexed == [ - (0, first), - (1, second), - (2, third), - ] - - events_before_duplicate = list(owner._events) - owner.fully_configured_feeders.append(first) - - assert owner._feeders == [first, second, third] - assert owner._events == events_before_duplicate - - with pytest.raises( - ValueError, - match=r"Feeder with mRID second already exists", - ): - owner.fully_configured_feeders.append(Feeder("second")) - - assert owner._feeders == [first, second, third] - assert owner._events == events_before_duplicate - - with pytest.raises(ValueError, match="invalid feeder"): - owner.fully_configured_feeders.append(Feeder("invalid")) - - assert owner._feeders == [first, second, third] - assert owner._events[-2:] == [ - ("backfill", "invalid"), - ("validate", "invalid"), - ] - - owner.fully_configured_feeders.remove(second) - - assert owner._feeders == [first, third] - - owner.fully_configured_feeders.clear() - - assert len(owner.fully_configured_feeders) == 0 - assert list(owner.fully_configured_feeders) == [] - assert owner._feeders == [] - assert owner._events[:6] == [ - ("backfill", "third"), - ("validate", "third"), - ("backfill", "first"), - ("validate", "first"), - ("backfill", "second"), - ("validate", "second"), - ] diff --git a/test/boilerplate/test_wrapper.py b/test/boilerplate/test_wrapper.py new file mode 100644 index 000000000..8fd3ba763 --- /dev/null +++ b/test/boilerplate/test_wrapper.py @@ -0,0 +1,116 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +from dataclasses import dataclass, field + +import pytest + +# noinspection PyProtectedMember +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper, _Wrapper + + +class RecordingWrapper(_Wrapper): + def __init__(self, private_field, marker: str): + super().__init__(private_field) + self.marker = marker + + @property + def bound_instance(self): + return self._instance + + @property + def backing_name(self): + return self._backing_name + + +class ListWrapper(_IterableWrapper[str]): + def _get_collection(self) -> list[str]: + return getattr(self._instance, self._backing_name) + + def append(self, item: str) -> None: + self._get_collection().append(item) + + def remove(self, item: str) -> None: + self._get_collection().remove(item) + + def clear(self) -> None: + self._get_collection().clear() + + @property + def bound_instance(self): + return self._instance + + +@dataclass +class Owner: + backing_items: list[str] = field(default_factory=list) + + wrapped = RecordingWrapper(backing_items, marker="marker") + items = ListWrapper(backing_items) + + +# @dataclass populates Field.name after the automatic __set_name__ call. +Owner.wrapped.__set_name__(Owner, "wrapped") +Owner.items.__set_name__(Owner, "items") + + +def test_class_access_returns_shared_descriptor(): + assert isinstance(Owner.wrapped, RecordingWrapper) + + +def test_instance_access_returns_bound_copy_with_init_arguments(): + owner = Owner() + + first = owner.wrapped + second = owner.wrapped + + assert first is not Owner.wrapped + assert first is not second + assert first.marker == "marker" + assert first.bound_instance is owner + assert first.backing_name == "backing_items" + + +def test_fget_is_named_after_descriptor_and_returns_bound_wrapper(): + owner = Owner() + + wrapper = Owner.items.fget(owner) + + assert Owner.items.fget.__name__ == "items" + assert Owner.items.fget.__qualname__ == "Owner.items" + assert wrapper.bound_instance is owner + + +def test_assigning_iterable_to_empty_backing_extends_collection(): + owner = Owner() + + owner.items = ["first", "second"] + + assert owner.backing_items == ["first", "second"] + + +def test_assigning_none_to_empty_backing_keeps_it_empty(): + owner = Owner() + + owner.items = None + + assert owner.backing_items == [] + + +def test_assigning_to_non_empty_backing_raises(): + owner = Owner(["existing"]) + + with pytest.raises(ValueError, match="currently non-empty"): + owner.items = ["new"] + + assert owner.backing_items == ["existing"] + + +def test_assignment_resolves_missing_default_before_extending(): + owner = Owner.__new__(Owner) + + Owner.items.__set__(owner, ["item"]) + + assert owner.backing_items == ["item"] From d069c21edb436cea26ada98800b25a1b2b7dc752 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Mon, 3 Aug 2026 12:16:18 +1000 Subject: [PATCH 46/54] Changelog and backref clear Signed-off-by: Boris Filin --- changelog.md | 2 ++ src/zepben/ewb/boilerplate/collections/mrid_list.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/changelog.md b/changelog.md index a52f8189b..93e266fff 100644 --- a/changelog.md +++ b/changelog.md @@ -12,6 +12,7 @@ * `EnergySource.phases` * `PowerElectronicsConnection.phases` * ContactDetails are now Identifiable and no longer have default id generation. The constructor now requires a string. The `id` field is deprecated, to be replaced with mrid. +* Removing items with backref from a containing object's list automatically clears the backref field on the item - it is not accessible after removal. ### New Features * Added a `lint` tox environment that runs `ruff check .` to enforce code quality standards. The test environments now depend on lint passing first, so CI will fail if any new lint violations are introduced. @@ -24,6 +25,7 @@ * Added E402 noqa comments for intentional mid-file imports used to avoid circular dependency issues in `dataclassy/dataclass.py`, `context_value_computer.py`, `queue_condition.py`, `direction_logger.py`, and `test_network_trace.py`. * Switched from using `dataclassy` to native dataclasses * Every UML relationship is now a custom list wrapper that takes no memory and has a reference to the backing field - usage interface is nearly identical to lists +* UML relationships can now automatically set backref values where needed (eg `Cut.ac_line_segment`). They also clear the backref field when the item is removed. ### Fixes * added python3.14 to compatibility list. diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index b1764cead..971ad9226 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -79,8 +79,13 @@ def append(self, item: S) -> None: def remove(self, item: S) -> None: self._backing_list.remove(item) + if self.backfill is not None: + self.backfill.clear(item) def clear(self) -> None: + if self.backfill is not None: + for item in self._backing_list: + self.backfill.clear(item) self._backing_list.clear() def __repr__(self) -> str: From 982e9eb59a7a73016ddc8290401e1fd0fe16b682 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Wed, 5 Aug 2026 15:01:07 +1000 Subject: [PATCH 47/54] change index list to use index error Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/collections/lazy_index_list.py | 2 +- test/boilerplate/test_lazy_index_list.py | 2 +- test/cim/private_collection_validator.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py index 421eac1a8..51a0f8937 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py @@ -39,7 +39,7 @@ def insert(self, index: int, item: T) -> None: size = len(self) if not 0 <= index <= size: - raise ValueError( + raise IndexError( f"Unable to add {self.element_description} to " f"{self._instance}. " f"Sequence number {index} is invalid. " diff --git a/test/boilerplate/test_lazy_index_list.py b/test/boilerplate/test_lazy_index_list.py index 1cf12ba68..12582a38f 100644 --- a/test/boilerplate/test_lazy_index_list.py +++ b/test/boilerplate/test_lazy_index_list.py @@ -59,7 +59,7 @@ def test_insert_rejects_index_outside_valid_range(index: int): owner = Owner() with pytest.raises( - ValueError, + IndexError, match=rf"Sequence number {index} is invalid.*between 0 and 0", ): owner.items.insert(index, "item") diff --git a/test/cim/private_collection_validator.py b/test/cim/private_collection_validator.py index faa24e468..e780da8b1 100644 --- a/test/cim/private_collection_validator.py +++ b/test/cim/private_collection_validator.py @@ -379,7 +379,7 @@ def validate_before_removal(): rf"Unable to add {other4.__class__.__name__} to {it}. \w* number 5 is invalid. Expected a value between 0 and {num(it)}. " "Make sure you are adding the items in order and there are no gaps in the numbering." ) - with pytest.raises(ValueError, match=expected_message): + with pytest.raises(IndexError, match=expected_message): add_with_index(it, other4, 5) # Python has reverse index support @@ -534,7 +534,7 @@ def _validate( # NOTE: inlining `remove_exception_types` directly into `pytest.raises` as `(ValueError, KeyError)` causes type errors (which are incorrect). # We check for both ValueError and KeyError as different underlying collections throw different exceptions. # - remove_exception_types: Tuple[Type[Exception], Type[Exception]] = (ValueError, KeyError) + remove_exception_types: Tuple[Type[Exception], Type[Exception], Type[Exception]] = (ValueError, KeyError, IndexError) with pytest.raises(remove_exception_types): remove(it, other1) From ac470183a7282db0f430f97f79fd7e0b4828e8fe Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Thu, 6 Aug 2026 11:58:44 +1000 Subject: [PATCH 48/54] HasMrid protocol is now Identifiable Signed-off-by: Boris Filin --- .../ewb/boilerplate/collections/mrid_collection.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py index 867fe1b48..eb41a0e79 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_collection.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -3,16 +3,13 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import ABC, abstractmethod -from typing import Any, Protocol, TypeVar +from typing import Any, TypeVar from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection +from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable -class HasMrid(Protocol): - mrid: str - - -S = TypeVar("S", bound=HasMrid) +S = TypeVar("S", bound=Identifiable) class MridCollection(AbstractBackedCollection[S], ABC): From 025101b1f012d6d389d674d7ddf581a248aec604 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Thu, 6 Aug 2026 14:02:30 +1000 Subject: [PATCH 49/54] Refactor Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/MANIFESTO.md | 4 +- .../collections/abstract_backed_collection.py | 36 +++++++-- .../collections/abstract_backed_list.py | 9 ++- .../collections/abstract_mrid_list.py | 40 ++++++++++ .../collections/lazy_index_list.py | 12 +-- .../ewb/boilerplate/collections/lazy_list.py | 23 ++---- .../boilerplate/collections/lazy_mrid_list.py | 55 ++++++++----- .../boilerplate/collections/lazy_mrid_map.py | 39 ++++----- .../collections/mrid_collection.py | 24 ++++++ .../ewb/boilerplate/collections/mrid_list.py | 38 +-------- test/boilerplate/test_lazy_mrid_list.py | 6 +- test/boilerplate/test_lazy_mrid_map.py | 16 +++- test/boilerplate/test_mrid_collection.py | 8 +- .../test_mrid_collection_lifecycle.py | 80 +++++++++++++++++++ test/boilerplate/test_mrid_collections.py | 15 +++- test/boilerplate/test_mrid_list.py | 13 +++ 16 files changed, 289 insertions(+), 129 deletions(-) create mode 100644 src/zepben/ewb/boilerplate/collections/abstract_mrid_list.py create mode 100644 test/boilerplate/test_mrid_collection_lifecycle.py diff --git a/src/zepben/ewb/boilerplate/MANIFESTO.md b/src/zepben/ewb/boilerplate/MANIFESTO.md index 571788402..0e3748f6d 100644 --- a/src/zepben/ewb/boilerplate/MANIFESTO.md +++ b/src/zepben/ewb/boilerplate/MANIFESTO.md @@ -70,8 +70,8 @@ Here are the concrete implementations of the lists that are used in the SDK: - `LazyList`: A nullable list of objects that has validation and sorting built in. Typically used for non-`Identifiable` relationships. - `LazyIndexList`: `LazyList` with index-based insertion and deletion, for strictly ordered relationships such as diagram points. -The remaining collections all implement `MridCollection` - an interface that declares functionality of interaction with a collection of `Identifiable` objects, mainly focusing on `get_by_mRID`, `append`, `remove`, and `clear` methods. This allows us to have a common way of interacting with inter-object relationships, while hiding the underlying implementation (eg `list` vs `dict`) +The remaining collections inherit `MridCollection`, which extends the common backed-collection lifecycle with mRID lookup, identity-based collision checks, and optional backfill. List-backed implementations share indexing and sorting through `AbstractMridList`, which inherits `MridCollection` and implements the read-only sequence interface directly. This intentionally mirrors the JVM hierarchy while keeping validation, removal cleanup, and disregarded-add behaviour in one place and hiding the underlying storage (eg `list` vs `dict`). -- `LazyMridList`: A `LazyList` with an mRID check and backfill added. `O(N)` mRID lookup. +- `LazyMridList`: A nullable `AbstractMridList` implementation with `O(N)` mRID lookup. - `MridList`: An mRID collection backed by a non-nullable list. Useful for relationships that are most likely to be filled (eg `ConnectivityNode.terminals`). `O(N)` mRID lookup. - `LazyMridMap`: A nullable map of Identifiable objects keyed on their mRID. Supports mRID checks and `O(1)` mRID lookup. Does not support sorting (duh). Useful for large-scale collections (for which the lookup speedup is significant) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py index 148c8fb36..ac4b84d85 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -3,7 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import ABC, abstractmethod -from typing import Collection, Iterable, Generic, Iterator, Callable, TypeVar +from typing import Any, Callable, Collection, Generic, Iterable, Iterator, TypeVar T = TypeVar("T") @@ -11,29 +11,49 @@ class AbstractBackedCollection(Collection[T], Generic[T], ABC): + _instance: Any + validate: Callable[[Any, T], object] | None = None + @abstractmethod def _get_collection(self) -> Collection[T]: ... - @abstractmethod + def _append_raw(self, item: T, /) -> None: + """Append without running collection lifecycle hooks.""" + self._get_collection().append(item) # type: ignore[attr-defined] + + def _post_remove(self, item: T, /) -> None: + """Run cleanup after one item has been removed.""" + + def _clear_raw(self, collection: Collection[T], /) -> None: + """Clear the backing storage without per-item cleanup.""" + collection.clear() # type: ignore[attr-defined] + + def _clear_and_copy(self, collection: Collection[T], /) -> Collection[T]: + """Clear the backing storage and return its former contents.""" + former_items = list(collection) + self._clear_raw(collection) + return former_items + def append(self, item: T, /) -> None: - """Append an item to the collection.""" - ... + """Validate and append an item to the collection.""" + if self.validate is not None: + self.validate(self._instance, item) + self._append_raw(item) def extend(self, items: Iterable[T] | None, /) -> None: """Append each item to the collection.""" for element in items or []: self.append(element) - @abstractmethod def remove(self, item: T, /) -> None: """Remove an item from the collection.""" - ... + self._get_collection().remove(item) # type: ignore[attr-defined] + self._post_remove(item) - @abstractmethod def clear(self) -> None: """Remove all items from the collection.""" - ... + self._clear_raw(self._get_collection()) def __len__(self) -> int: return len(self._get_collection()) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py index 49f348e0b..d2d5d686a 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py @@ -3,7 +3,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. from abc import ABC, abstractmethod -from typing import Sequence, Generic, overload +from typing import Any, Callable, Generic, Sequence, overload from zepben.ewb.boilerplate.collections.abstract_backed_collection import AbstractBackedCollection, T @@ -15,10 +15,17 @@ class AbstractBackedList( ABC, ): + sort_by: Callable[[T], Any] | None = None + @abstractmethod def _get_collection(self) -> Sequence[T]: ... + def append(self, item: T, /) -> None: + super().append(item) + if self.sort_by is not None: + self._get_collection().sort(key=self.sort_by) # type: ignore[attr-defined] + @overload def __getitem__(self, index: int) -> T: ... diff --git a/src/zepben/ewb/boilerplate/collections/abstract_mrid_list.py b/src/zepben/ewb/boilerplate/collections/abstract_mrid_list.py new file mode 100644 index 000000000..b46655824 --- /dev/null +++ b/src/zepben/ewb/boilerplate/collections/abstract_mrid_list.py @@ -0,0 +1,40 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from abc import ABC, abstractmethod +from typing import Any, Callable, Sequence, overload + +from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S + + +class AbstractMridList(MridCollection[S], Sequence[S], ABC): + """ + Common list behaviour for mRID-addressable collections. + + NOTE: This can be simplified by inheriting from AbstractBackedList. + But that cannot be done on the Kotlin side, so for better parity, + this class duplicates some of the functionality from that class + """ + + sort_by: Callable[[S], Any] | None = None + + @abstractmethod + def _get_collection(self) -> Sequence[S]: + ... + + def append(self, item: S, /) -> None: + MridCollection.append(self, item) + if self.sort_by is not None: + self._get_collection().sort(key=self.sort_by) # type: ignore[attr-defined] + + @overload + def __getitem__(self, index: int) -> S: + ... + + @overload + def __getitem__(self, index: slice) -> Sequence[S]: + ... + + def __getitem__(self, index: int | slice) -> S | Sequence[S]: + return self._get_collection()[index] diff --git a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py index 51a0f8937..128bd9944 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py @@ -59,14 +59,6 @@ def insert(self, index: int, item: T) -> None: else: existing.insert(index, item) - def append(self, item: T) -> None: - """ - Append an item to the collection. - Run optional validation. - Sort the collection if key lambda is provided. - """ - self.insert(len(self), item) - def pop(self, index: int = -1) -> T: """ Remove and return the item at ``index``. @@ -80,9 +72,7 @@ def pop(self, index: int = -1) -> T: raise IndexError("pop from empty list") item = existing.pop(index) - - if not existing: - self.clear() + self._post_remove(item) return item diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index c9cf9cec2..9221f5b69 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -53,15 +53,7 @@ def _get(self) -> list[T] | None: def _get_collection(self) -> list[T]: return getattr(self._instance, self._backing_name) or [] - def append(self, item: T) -> None: - """ - Append an item to the collection. - Run optional validation. - Sort the collection if key lambda is provided. - """ - if self.validate is not None: - self.validate(self._instance, item) - + def _append_raw(self, item: T) -> None: existing = getattr(self._instance, self._backing_name) if existing is None: existing = [item] @@ -69,16 +61,11 @@ def append(self, item: T) -> None: else: existing.append(item) - if self.sort_by is not None: - existing.sort(key=self.sort_by) - - def remove(self, item: T) -> None: - existing = self._get_collection() - existing.remove(item) - if not existing: - self.clear() + def _post_remove(self, item: T) -> None: + if not self._get_collection(): + self._clear_raw(self._get_collection()) - def clear(self) -> None: + def _clear_raw(self, collection) -> None: setattr(self._instance, self._backing_name, None) def __repr__(self) -> str: diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py index 33c9f41b0..a6f3dc916 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py @@ -4,11 +4,12 @@ # file, You can obtain one at https://mozilla.org/MPL/2.0/. from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.lazy_list import LazyList -from zepben.ewb.boilerplate.collections.mrid_collection import S, MridCollection +from zepben.ewb.boilerplate.collections.abstract_mrid_list import AbstractMridList +from zepben.ewb.boilerplate.collections.mrid_collection import S +from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class LazyMridList(LazyList[S], MridCollection[S]): +class LazyMridList(_IterableWrapper[S], AbstractMridList[S]): """ Nullable list implementation of :class:`MridCollection`. @@ -23,9 +24,17 @@ def __init__( validate=None, sort_by=None ) -> None: - super().__init__(private_field, validate, sort_by) + super().__init__(private_field) self.element_description = element_description self.backfill = backfill + self.validate = validate + self.sort_by = sort_by + + def _get(self) -> list[S] | None: + return getattr(self._instance, self._backing_name) + + def _get_collection(self) -> list[S]: + return self._get() or [] def _safe_get_by_mrid(self, mrid: str) -> S | None: existing = self._get() @@ -34,18 +43,26 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: found = next((element for element in existing if element.mrid == mrid), None) return found - def append(self, item: S) -> None: - """ - Append an item to the collection. - Check for mRID collisions with existing items. - Optionally fill the backref field on the added item. - Run optional validation. - Sort the collection if key lambda is provided. - """ - if not self._can_add_by_mrid(item): - return - - if self.backfill is not None: - self.backfill.apply(item, self._instance) - - super().append(item) + def _append_raw(self, item: S) -> None: + existing = self._get() + if existing is None: + setattr(self._instance, self._backing_name, [item]) + else: + existing.append(item) + + def _post_remove(self, item: S) -> None: + super()._post_remove(item) + if not self._get_collection(): + self._clear_raw(self._get_collection()) + + def _clear_raw(self, collection) -> None: + setattr(self._instance, self._backing_name, None) + + def _clear_and_copy(self, collection): + self._clear_raw(collection) + return collection + + def __repr__(self) -> str: + if self._instance is None: + return object.__repr__(self) + return repr(self._get_collection()) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py index a5e5b36eb..10ca9aa96 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py @@ -57,22 +57,7 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: def get_by_mrid(self, mrid: str) -> S: return self._get_or_empty()[mrid] - def append(self, item: S) -> None: - """ - Add an item to the collection. - Check for mRID collisions with existing items. - Optionally fill the backref field on the added item. - Run optional validation. - """ - if not self._can_add_by_mrid(item): - return - - if self.backfill is not None: - self.backfill.apply(item, self._instance) - - if self.validate is not None: - self.validate(self._instance, item) - + def _append_raw(self, item: S) -> None: existing = getattr(self._instance, self._backing_name) if existing is None: existing = {item.mrid: item} @@ -84,18 +69,28 @@ def __len__(self) -> int: return len(self._get_or_empty()) def __contains__(self, item: object) -> bool: - return self._get_or_empty().get(getattr(item, "mrid", None)) == item + return self._get_or_empty().get(getattr(item, "mrid", None)) is item - def remove(self, item) -> None: + def remove(self, item: S) -> None: existing = self._get_or_empty() - + if existing.get(item.mrid) is not item: + raise ValueError(f"{item!r} not in collection") del existing[item.mrid] - if not existing: - self.clear() + self._post_remove(item) - def clear(self) -> None: + def _post_remove(self, item: S) -> None: + if not self._get_or_empty(): + self._clear_raw(self._get_collection()) + super()._post_remove(item) + + def _clear_raw(self, collection) -> None: setattr(self._instance, self._backing_name, None) + def _clear_and_copy(self, collection): + former_items = self._get_collection() + self._clear_raw(collection) + return former_items + def __repr__(self) -> str: if self._instance is None: return object.__repr__(self) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py index eb41a0e79..d8f0d75ae 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_collection.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -21,6 +21,7 @@ class MridCollection(AbstractBackedCollection[S], ABC): _instance: Any element_description: str + backfill: Any = None @abstractmethod def _safe_get_by_mrid(self, mrid: str) -> S | None: ... @@ -49,3 +50,26 @@ def _can_add_by_mrid(self, element: S) -> bool: ) return False + + def append(self, item: S, /) -> None: + if not self._can_add_by_mrid(item): + return + + if self.backfill is not None: + self.backfill.apply(item, self._instance) + + super().append(item) + + def _post_remove(self, item: S, /) -> None: + if self.backfill is not None: + self.backfill.clear(item) + + def clear(self) -> None: + collection = self._get_collection() + if self.backfill is None: + self._clear_raw(collection) + return + + former_items = self._clear_and_copy(collection) + for item in former_items: + self.backfill.clear(item) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index 971ad9226..f56c3c2f7 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -7,12 +7,12 @@ from typing_extensions import Self from zepben.ewb.boilerplate.backfill import Backfill -from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList -from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection, S +from zepben.ewb.boilerplate.collections.abstract_mrid_list import AbstractMridList +from zepben.ewb.boilerplate.collections.mrid_collection import S from zepben.ewb.boilerplate.collections.wrapper import _IterableWrapper -class MridList(_IterableWrapper[S], AbstractBackedList[S], MridCollection[S]): +class MridList(_IterableWrapper[S], AbstractMridList[S]): """ mRID collection backed by a non-nullable list. @@ -55,39 +55,9 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: found = next((element for element in self._backing_list if element.mrid == mrid), None) return found - def append(self, item: S) -> None: - """ - Append an item to the collection. - Check for mRID collisions with existing items. - Optionally fill the backref field on the added item. - Run optional validation. - Sort the collection if key lambda is provided. - """ - if not self._can_add_by_mrid(item): - return - - if self.backfill is not None: - self.backfill.apply(item, self._instance) - - if self.validate is not None: - self.validate(self._instance, item) - + def _append_raw(self, item: S) -> None: self._backing_list.append(item) - if self.sort_by is not None: - self._backing_list.sort(key=self.sort_by) - - def remove(self, item: S) -> None: - self._backing_list.remove(item) - if self.backfill is not None: - self.backfill.clear(item) - - def clear(self) -> None: - if self.backfill is not None: - for item in self._backing_list: - self.backfill.clear(item) - self._backing_list.clear() - def __repr__(self) -> str: if self._instance is None: return object.__repr__(self) diff --git a/test/boilerplate/test_lazy_mrid_list.py b/test/boilerplate/test_lazy_mrid_list.py index 3874eb15b..664f0dc96 100644 --- a/test/boilerplate/test_lazy_mrid_list.py +++ b/test/boilerplate/test_lazy_mrid_list.py @@ -10,11 +10,11 @@ import pytest from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable -@dataclass(eq=False) -class Item: - mrid: str +class Item(Identifiable): + pass class RecordingBackfill: diff --git a/test/boilerplate/test_lazy_mrid_map.py b/test/boilerplate/test_lazy_mrid_map.py index f6e073cfc..e02718f1a 100644 --- a/test/boilerplate/test_lazy_mrid_map.py +++ b/test/boilerplate/test_lazy_mrid_map.py @@ -10,11 +10,11 @@ import pytest from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap +from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable -@dataclass(eq=False) -class Item: - mrid: str +class Item(Identifiable): + pass class RecordingBackfill: @@ -156,6 +156,16 @@ def test_remove_last_item_nulls_backing_map(): assert owner.backing_items is None +def test_remove_rejects_different_instance_with_same_mrid(): + item = Item("item") + owner = Owner({"item": item}) + + with pytest.raises(ValueError, match="not in collection"): + owner.items.remove(Item("item")) + + assert owner.backing_items == {"item": item} + + def test_clear_nulls_backing_map(): owner = Owner({"item": Item("item")}) diff --git a/test/boilerplate/test_mrid_collection.py b/test/boilerplate/test_mrid_collection.py index 2ddd5d333..c5aba5352 100644 --- a/test/boilerplate/test_mrid_collection.py +++ b/test/boilerplate/test_mrid_collection.py @@ -3,16 +3,14 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -from dataclasses import dataclass - import pytest from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection +from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable -@dataclass -class Item: - mrid: str +class Item(Identifiable): + pass class MridCollectionImpl(MridCollection[Item]): diff --git a/test/boilerplate/test_mrid_collection_lifecycle.py b/test/boilerplate/test_mrid_collection_lifecycle.py new file mode 100644 index 000000000..60d926ebe --- /dev/null +++ b/test/boilerplate/test_mrid_collection_lifecycle.py @@ -0,0 +1,80 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from dataclasses import dataclass, field + +import pytest + +from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList +from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap +from zepben.ewb.boilerplate.collections.mrid_list import MridList +from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable + + +class Item(Identifiable): + def __init__(self, mrid: str): + super().__init__(mrid) + self.owner = None + + +class TrackingBackfill: + def apply(self, item: Item, owner: object) -> None: + if item.owner is None: + item.owner = owner + if item.owner is not owner: + raise ValueError("item belongs to another owner") + + def clear(self, item: Item) -> None: + item.owner = None + + +@dataclass +class Owner: + list_backing: list[Item] = field(default_factory=list) + lazy_list_backing: list[Item] | None = field(default=None) + lazy_map_backing: dict[str, Item] | None = field(default=None) + + list_items = MridList(list_backing, "Item", backfill=TrackingBackfill()) + lazy_list_items = LazyMridList( + lazy_list_backing, + "Item", + backfill=TrackingBackfill(), + ) + lazy_map_items = LazyMridMap( + lazy_map_backing, + "Item", + backfill=TrackingBackfill(), + ) + + +@pytest.mark.parametrize( + "collection_name", + ["list_items", "lazy_list_items", "lazy_map_items"], +) +def test_remove_clears_backfill(collection_name: str): + owner = Owner() + item = Item("item") + collection = getattr(owner, collection_name) + collection.append(item) + + collection.remove(item) + + assert item.owner is None + assert list(collection) == [] + + +@pytest.mark.parametrize( + "collection_name", + ["list_items", "lazy_list_items", "lazy_map_items"], +) +def test_clear_clears_every_backfill(collection_name: str): + owner = Owner() + items = [Item("first"), Item("second")] + collection = getattr(owner, collection_name) + collection.extend(items) + + collection.clear() + + assert all(item.owner is None for item in items) + assert list(collection) == [] diff --git a/test/boilerplate/test_mrid_collections.py b/test/boilerplate/test_mrid_collections.py index 7dee097af..e08207e9a 100644 --- a/test/boilerplate/test_mrid_collections.py +++ b/test/boilerplate/test_mrid_collections.py @@ -3,19 +3,22 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. +from collections.abc import Sequence from dataclasses import dataclass, field import pytest +from zepben.ewb.boilerplate.collections.abstract_backed_list import AbstractBackedList +from zepben.ewb.boilerplate.collections.abstract_mrid_list import AbstractMridList from zepben.ewb.boilerplate.collections.lazy_mrid_list import LazyMridList from zepben.ewb.boilerplate.collections.lazy_mrid_map import LazyMridMap from zepben.ewb.boilerplate.collections.mrid_collection import MridCollection from zepben.ewb.boilerplate.collections.mrid_list import MridList +from zepben.ewb.model.cim.iec61970.base.core.identifiable import Identifiable -@dataclass(eq=False) -class Item: - mrid: str +class Item(Identifiable): + pass @dataclass @@ -29,6 +32,12 @@ class Container: lazy_map_items = LazyMridMap(lazy_map_backing, "Item") +def test_abstract_mrid_list_implements_sequence_directly(): + assert issubclass(AbstractMridList, MridCollection) + assert issubclass(AbstractMridList, Sequence) + assert not issubclass(AbstractMridList, AbstractBackedList) + + def assert_public_state( collections: tuple[MridCollection[Item], ...], expected: list[Item], diff --git a/test/boilerplate/test_mrid_list.py b/test/boilerplate/test_mrid_list.py index 674bbe8b9..465777013 100644 --- a/test/boilerplate/test_mrid_list.py +++ b/test/boilerplate/test_mrid_list.py @@ -160,6 +160,19 @@ def test_append_sorts_owner_backing_list(): assert owner.backing_feeders == [first, second] +def test_readding_same_item_still_sorts_owner_backing_list(): + owner = Owner() + first = Feeder("first") + second = Feeder("second") + owner.sorted_feeders.extend([first, second]) + second.mrid = "before-first" + + owner.sorted_feeders.append(second) + + assert owner.backing_feeders == [second, first] + assert len(owner.backing_feeders) == 2 + + def test_remove_removes_item_from_owner_backing_list(): owner = Owner() retained = Feeder("retained") From 45966a8b22cf7828bc2af72d8bb7602b9172c935 Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Thu, 6 Aug 2026 14:17:05 +1000 Subject: [PATCH 50/54] Exhaustive testing Signed-off-by: Boris Filin --- test/boilerplate/test_backed_descriptor.py | 186 ++++++++++ .../test_dataclass_base_exhaustive.py | 336 ++++++++++++++++++ 2 files changed, 522 insertions(+) create mode 100644 test/boilerplate/test_backed_descriptor.py create mode 100644 test/boilerplate/test_dataclass_base_exhaustive.py diff --git a/test/boilerplate/test_backed_descriptor.py b/test/boilerplate/test_backed_descriptor.py new file mode 100644 index 000000000..98a87d64f --- /dev/null +++ b/test/boilerplate/test_backed_descriptor.py @@ -0,0 +1,186 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from dataclasses import dataclass, field, fields + +import pytest + +from zepben.ewb.boilerplate.backed_descriptor import Alias, BackedDescriptor +from zepben.ewb.boilerplate.dataclass_base import DataclassBase, zb_dataclass + + +@zb_dataclass +class Owner(DataclassBase): + _value: int = field(default=1) + + value: int = BackedDescriptor(_value) + alias: int = Alias(value) + + +@pytest.mark.parametrize("private_field", [None, "_value", 1, object()]) +def test_constructor_rejects_non_field_and_non_descriptor(private_field): + with pytest.raises(TypeError, match="has to be an instance of dataclass Field"): + BackedDescriptor(private_field) + + +def test_constructor_accepts_dataclass_field(): + backing_field = field(default=1) + + descriptor = BackedDescriptor(backing_field) + + assert descriptor.private_field is backing_field + assert descriptor.name is None + assert descriptor._backing_name is None + + +def test_constructor_accepts_another_backed_descriptor(): + inner = BackedDescriptor(field(default=1)) + + outer = BackedDescriptor(inner) + + assert outer.private_field is inner + + +def test_alias_is_an_exact_class_alias(): + assert Alias is BackedDescriptor + + +def test_set_name_records_public_and_backing_names(): + assert Owner.value.name == "value" + assert Owner.value.__name__ == "value" + assert Owner.value._backing_name == "_value" + + +def test_class_access_returns_shared_descriptor(): + assert isinstance(Owner.value, BackedDescriptor) + assert Owner.value is Owner.__dict__["value"] + + +def test_instance_access_reads_backing_field(): + owner = Owner() + owner._value = 7 + + assert owner.value == 7 + + +def test_instance_assignment_writes_backing_field(): + owner = Owner() + + owner.value = 7 + + assert owner._value == 7 + assert owner.value == 7 + + +def test_constructor_assignment_uses_descriptor(): + owner = Owner(value=7) + + assert owner._value == 7 + assert owner.value == 7 + + +def test_descriptor_values_are_isolated_between_instances(): + first = Owner(value=1) + second = Owner(value=2) + + first.value = 3 + + assert first.value == 3 + assert second.value == 2 + + +def test_descriptor_can_alias_another_descriptor(): + owner = Owner() + + owner.alias = 8 + + assert owner.alias == 8 + assert owner.value == 8 + assert owner._value == 8 + + +def test_descriptor_chain_uses_public_name_of_inner_descriptor(): + assert Owner.alias._backing_name == "value" + assert Owner.alias.private_field is Owner.value + + +def test_annotated_descriptors_are_not_dataclass_fields_or_slots(): + assert [item.name for item in fields(Owner)] == ["_value"] + assert Owner.__slots__ == ("_value",) + assert "value" not in Owner.__annotations__ + assert "alias" not in Owner.__annotations__ + + +def test_get_with_null_instance_returns_descriptor_even_when_unbound(): + descriptor = BackedDescriptor(field(default=1)) + + assert descriptor.__get__(None, object) is descriptor + + +def test_set_before_backing_name_is_known_has_clear_error(): + descriptor = BackedDescriptor(field(default=1)) + + with pytest.raises(ValueError, match="not yet aware of the supporting field"): + descriptor.__set__(object(), 2) + + +def test_set_name_with_none_is_a_no_op(): + descriptor = BackedDescriptor(field(default=1)) + + descriptor.__set_name__(object, None) + + assert descriptor.name is None + assert descriptor._backing_name is None + assert not hasattr(descriptor, "__name__") + + +def test_existing_backing_name_is_not_overwritten_by_set_name(): + backing_field = field(default=1) + backing_field.name = "_original" + descriptor = BackedDescriptor(backing_field) + descriptor._backing_name = "_explicit" + + descriptor.__set_name__(object, "value") + + assert descriptor.name == "value" + assert descriptor._backing_name == "_explicit" + + +def test_repeated_set_name_updates_public_name_but_keeps_backing_name(): + backing_field = field(default=1) + backing_field.name = "_value" + descriptor = BackedDescriptor(backing_field) + + descriptor.__set_name__(object, "first") + descriptor.__set_name__(object, "second") + + assert descriptor.name == "second" + assert descriptor.__name__ == "second" + assert descriptor._backing_name == "_value" + + +def test_get_before_backing_name_is_known_currently_raises_type_error(): + descriptor = BackedDescriptor(field(default=1)) + + with pytest.raises(TypeError): + descriptor.__get__(object(), object) + + +@pytest.mark.xfail( + strict=True, + reason="Plain dataclass decoration does not rebind the Field name to the descriptor", +) +def test_descriptor_works_with_plain_non_slotted_dataclass(): + backing_field = field(default=1) + + @dataclass + class PlainOwner: + _value: int = backing_field + + value = BackedDescriptor(backing_field) + + owner = PlainOwner() + owner.value = 2 + + assert owner._value == 2 diff --git a/test/boilerplate/test_dataclass_base_exhaustive.py b/test/boilerplate/test_dataclass_base_exhaustive.py new file mode 100644 index 000000000..9545a22ef --- /dev/null +++ b/test/boilerplate/test_dataclass_base_exhaustive.py @@ -0,0 +1,336 @@ +# Copyright 2026 Zeppelin Bend Pty Ltd +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. +from dataclasses import Field, field, fields, is_dataclass + +import pytest + +from zepben.ewb.boilerplate.backed_descriptor import BackedDescriptor +from zepben.ewb.boilerplate.dataclass_base import ( + DataclassBase, + _is_set, + remove_descriptor_annotations, + resolve_default, + zb_dataclass, +) + + +# Exhaustive behavioural coverage for the custom dataclass lifecycle. + + +@zb_dataclass +class Example(DataclassBase): + required: int + scalar_default: str = "default" + factory_default: list[int] = field(default_factory=list) + init_false_default: int = field(default=42, init=False) + + +@zb_dataclass +class Parent(DataclassBase): + parent_required: int + overridden: int = 1 + + def __init__(self, parent_required: int, **kwargs): + self.parent_required = parent_required + super(Parent, self).__init__(**kwargs) + + +@zb_dataclass +class Child(Parent): + overridden: float = 2.5 + child_default: str = "child" + + +@zb_dataclass +class DescriptorExample(DataclassBase): + _value: int = field(default=0) + writes: list[int] = field(default_factory=list) + + value: int = BackedDescriptor(_value) + + @property + def recorded_value(self) -> int: + return self._value + + @recorded_value.setter + def recorded_value(self, value: int) -> None: + self.writes.append(value) + self._value = value + + +def test_zb_dataclass_creates_a_slotted_dataclass_without_generated_init(): + assert is_dataclass(Example) + assert Example.__init__ is DataclassBase.__init__ + assert set(Example.__slots__) == { + "required", + "scalar_default", + "factory_default", + "init_false_default", + } + assert not hasattr(Example(required=1), "__dict__") + + +def test_zb_dataclass_preserves_identity_equality_and_object_repr(): + first = Example(required=1) + second = Example(required=1) + + assert first == first + assert first != second + assert repr(first).startswith("<") + assert "Example object at" in repr(first) + + +def test_constructor_populates_scalar_and_factory_defaults(): + instance = Example(required=1) + + assert instance.scalar_default == "default" + assert instance.factory_default == [] + assert instance.init_false_default == 42 + + +def test_default_factory_is_evaluated_once_per_instance(): + first = Example(required=1) + second = Example(required=2) + + first.factory_default.append(1) + + assert first.factory_default == [1] + assert second.factory_default == [] + assert first.factory_default is not second.factory_default + + +def test_constructor_keywords_override_defaults(): + supplied = [1, 2] + + instance = Example( + required=3, + scalar_default="supplied", + factory_default=supplied, + ) + + assert instance.required == 3 + assert instance.scalar_default == "supplied" + assert instance.factory_default is supplied + + +def test_missing_required_field_reports_field_and_class(): + with pytest.raises( + TypeError, + match=r"Missing required field 'required' for Example", + ): + Example() + + +def test_init_false_field_uses_default_but_rejects_constructor_value(): + assert Example(required=1).init_false_default == 42 + + with pytest.raises( + TypeError, + match=r"Example.__init__\(\) got an unexpected keyword argument 'init_false_default'", + ): + Example(required=1, init_false_default=2) + + +def test_slots_reject_unknown_constructor_keywords(): + with pytest.raises(AttributeError, match="unknown"): + Example(required=1, unknown="value") + + +def test_preassigned_required_field_is_not_resolved_again(): + instance = Child(7) + + assert instance.parent_required == 7 + assert instance.overridden == 2.5 + assert instance.child_default == "child" + + +def test_inherited_field_can_be_overridden_by_keyword(): + instance = Child(7, overridden=9.5) + + assert instance.overridden == 9.5 + + +def test_subclass_field_override_uses_subclass_default_and_type(): + parent = Parent(1) + child = Child(1) + + assert parent.overridden == 1 + assert child.overridden == 2.5 + assert type(child.overridden) is float + + +def test_constructor_keyword_invokes_backed_descriptor(): + instance = DescriptorExample(value=12) + + assert instance.value == 12 + assert instance._value == 12 + + +def test_constructor_keyword_invokes_property_setter(): + instance = DescriptorExample(recorded_value=12) + + assert instance.recorded_value == 12 + assert instance.writes == [12] + + +def test_descriptor_annotations_are_excluded_from_fields_and_slots(): + assert [item.name for item in fields(DescriptorExample)] == [ + "_value", + "writes", + ] + assert set(DescriptorExample.__slots__) == {"_value", "writes"} + assert "value" not in DescriptorExample.__annotations__ + + +def test_post_init_is_explicitly_rejected_before_initialisation(): + @zb_dataclass + class WithPostInit(DataclassBase): + value: int = 1 + + def __post_init__(self): + raise AssertionError("must not be called") + + with pytest.raises(NotImplementedError, match="does not support __post_init__"): + WithPostInit() + + +def test_is_set_distinguishes_populated_and_unpopulated_slots(): + instance = Example.__new__(Example) + + assert not _is_set(instance, "required") + + instance.required = 1 + + assert _is_set(instance, "required") + + +def test_is_set_treats_attribute_error_from_descriptor_as_unset(): + class RaisingDescriptor: + def __get__(self, instance, owner=None): + raise AttributeError("missing") + + class Owner: + value = RaisingDescriptor() + + assert not _is_set(Owner(), "value") + + +def test_remove_descriptor_annotations_returns_same_class_and_new_mapping(): + class Descriptor: + def __get__(self, instance, owner=None): + return None + + class Undecorated: + plain: int + descriptor: int = Descriptor() + dc_field: int = field(default=1) + + original_annotations = Undecorated.__annotations__ + + result = remove_descriptor_annotations(Undecorated) + + assert result is Undecorated + assert Undecorated.__annotations__ == {"plain": int, "dc_field": int} + assert Undecorated.__annotations__ is not original_annotations + assert original_annotations == { + "plain": int, + "descriptor": int, + "dc_field": int, + } + + +@pytest.mark.parametrize("method_name", ["__get__", "__set__"]) +def test_remove_descriptor_annotations_recognises_either_descriptor_method( + method_name: str, +): + descriptor_type = type( + "PartialDescriptor", + (), + {method_name: lambda *args: None}, + ) + + class Undecorated: + value: int = descriptor_type() + + remove_descriptor_annotations(Undecorated) + + assert Undecorated.__annotations__ == {} + + +def test_remove_descriptor_annotations_preserves_dataclass_fields(): + class Undecorated: + value: int = field(default=1) + + remove_descriptor_annotations(Undecorated) + + assert Undecorated.__annotations__ == {"value": int} + assert isinstance(Undecorated.value, Field) + + +def test_remove_descriptor_annotations_handles_class_without_annotations(): + class Undecorated: + pass + + assert remove_descriptor_annotations(Undecorated) is Undecorated + assert Undecorated.__annotations__ == {} + + +def test_resolve_default_assigns_scalar_default(): + instance = Example.__new__(Example) + scalar_field = next(item for item in fields(Example) if item.name == "scalar_default") + + resolve_default(instance, scalar_field) + + assert instance.scalar_default == "default" + + +def test_resolve_default_calls_default_factory(): + instance = Example.__new__(Example) + factory_field = next(item for item in fields(Example) if item.name == "factory_default") + + resolve_default(instance, factory_field) + + assert instance.factory_default == [] + + +def test_resolve_default_rejects_field_without_default(): + instance = Example.__new__(Example) + required_field = next(item for item in fields(Example) if item.name == "required") + + with pytest.raises( + TypeError, + match=r"Missing required field 'required' for Example", + ): + resolve_default(instance, required_field) + + +@pytest.mark.xfail( + strict=True, + reason="Required backing fields are resolved before descriptor keywords are assigned", +) +def test_descriptor_keyword_can_supply_required_backing_field(): + @zb_dataclass + class RequiredBacking(DataclassBase): + _value: int = field() + value: int = BackedDescriptor(_value) + + instance = RequiredBacking(value=3) + + assert instance._value == 3 + + +@pytest.mark.xfail( + strict=True, + reason="slots=True returns a replacement class and breaks zero-argument super()", +) +def test_zb_dataclass_supports_zero_argument_super(): + @zb_dataclass + class ZeroArgSuper(DataclassBase): + value: int = 1 + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + assert ZeroArgSuper().value == 1 From 48d381c965144ed386c3d1e24a8ec5ae3adf27fb Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Thu, 6 Aug 2026 15:13:17 +1000 Subject: [PATCH 51/54] Docs Signed-off-by: Boris Filin --- .../ewb/boilerplate/backed_descriptor.py | 9 ++-- src/zepben/ewb/boilerplate/backfill.py | 4 ++ .../collections/abstract_backed_collection.py | 31 ++++++++++---- .../collections/abstract_backed_list.py | 13 ++++++ .../collections/abstract_mrid_list.py | 24 ++++++++--- .../collections/lazy_index_list.py | 20 +++++---- .../ewb/boilerplate/collections/lazy_list.py | 18 +++++--- .../boilerplate/collections/lazy_mrid_list.py | 16 +++++++- .../boilerplate/collections/lazy_mrid_map.py | 29 +++++++++++-- .../collections/mrid_collection.py | 27 +++++++++--- .../ewb/boilerplate/collections/mrid_list.py | 13 +++--- .../ewb/boilerplate/collections/wrapper.py | 20 ++++----- src/zepben/ewb/boilerplate/dataclass_base.py | 35 +++++++++------- .../relations/ac_line_segment_phase_list.py | 9 ++-- .../relations/battery_control_list.py | 10 ++--- .../boilerplate/relations/curve_data_list.py | 17 ++++---- .../relations/phase_impedance_data_list.py | 16 ++++---- .../relations/power_transformer_end_list.py | 17 ++++---- .../boilerplate/relations/terminal_list.py | 5 +++ .../relations/transformer_end_rated_s_list.py | 3 ++ test/boilerplate/test_mrid_collection.py | 24 +++++++++++ test/boilerplate/test_mrid_collections.py | 41 +++++++++++++++++++ 22 files changed, 295 insertions(+), 106 deletions(-) diff --git a/src/zepben/ewb/boilerplate/backed_descriptor.py b/src/zepben/ewb/boilerplate/backed_descriptor.py index 9a52eb6f3..2ed1a8bf7 100644 --- a/src/zepben/ewb/boilerplate/backed_descriptor.py +++ b/src/zepben/ewb/boilerplate/backed_descriptor.py @@ -9,10 +9,11 @@ class BackedDescriptor: - """ - A simple descriptor that references a dataclass field as a backing value. - This allows it to have internal side effects and stateful implementation. - Used for name shadowing and lazy fields (fields that can be None on instance level) + """Expose a dataclass field or another descriptor under a public name. + + Reads and writes are delegated to the backing attribute. Descriptor + chaining supports aliases, while field-backed descriptors support public + views over private or nullable dataclass storage. """ def __init__(self, private_field): diff --git a/src/zepben/ewb/boilerplate/backfill.py b/src/zepben/ewb/boilerplate/backfill.py index c0cca6157..4b57370a9 100644 --- a/src/zepben/ewb/boilerplate/backfill.py +++ b/src/zepben/ewb/boilerplate/backfill.py @@ -15,6 +15,8 @@ class Backfill: + """Maintains an element's reference to its owning object.""" + def __init__(self, backfill_prop: property) -> None: if backfill_prop.fget is None: raise TypeError(f"Cannot backfill a property without a getter: {backfill_prop!r}") @@ -37,6 +39,7 @@ def _get_backing_name(self) -> str: def apply(self, element: S, owner: Any) -> None: + """Set or verify ``element``'s reference to ``owner``.""" backing_name = self._get_backing_name() if getattr(element, backing_name) is None: setattr(element, backing_name, owner) @@ -46,6 +49,7 @@ def apply(self, element: S, owner: Any) -> None: raise ValueError(f"{element} `{self.backfill_prop.fget.__name__}` property references {ref}, expected {owner}.") def clear(self, element: S) -> None: + """Clear the owner reference on ``element``.""" backing_name = self._get_backing_name() setattr(element, backing_name, None) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py index ac4b84d85..141021d7f 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -10,33 +10,48 @@ class AbstractBackedCollection(Collection[T], Generic[T], ABC): + """A mutable collection whose contents are stored elsewhere. + + Implementations provide the current contents through + :meth:`_get_collection`. Element validation and the append lifecycle are + centralised here, with hooks for raw storage and removal cleanup. + + :meth:`_post_remove` is invoked after individual removal. :meth:`clear` + clears the backing storage directly, avoiding repeated removal bookkeeping + when bulk cleanup is unnecessary. + """ _instance: Any validate: Callable[[Any, T], object] | None = None @abstractmethod def _get_collection(self) -> Collection[T]: + """Return the current backing collection.""" ... def _append_raw(self, item: T, /) -> None: - """Append without running collection lifecycle hooks.""" + """Append ``item`` to storage without validation or other hooks.""" self._get_collection().append(item) # type: ignore[attr-defined] def _post_remove(self, item: T, /) -> None: - """Run cleanup after one item has been removed.""" + """Perform cleanup after ``item`` is removed.""" def _clear_raw(self, collection: Collection[T], /) -> None: - """Clear the backing storage without per-item cleanup.""" + """Clear ``collection`` without per-item cleanup.""" collection.clear() # type: ignore[attr-defined] def _clear_and_copy(self, collection: Collection[T], /) -> Collection[T]: - """Clear the backing storage and return its former contents.""" + """Clear ``collection`` and return its former elements.""" former_items = list(collection) self._clear_raw(collection) return former_items def append(self, item: T, /) -> None: - """Validate and append an item to the collection.""" + """Append ``item`` after validation. + + This performs validation and storage, but no mRID check, backfill, or + sorting. + """ if self.validate is not None: self.validate(self._instance, item) self._append_raw(item) @@ -47,21 +62,23 @@ def extend(self, items: Iterable[T] | None, /) -> None: self.append(element) def remove(self, item: T, /) -> None: - """Remove an item from the collection.""" + """Remove ``item`` and perform per-item cleanup.""" self._get_collection().remove(item) # type: ignore[attr-defined] self._post_remove(item) def clear(self) -> None: - """Remove all items from the collection.""" + """Clear the backing collection.""" self._clear_raw(self._get_collection()) def __len__(self) -> int: return len(self._get_collection()) def __iter__(self) -> Iterator[T]: + """Return an iterator over the backing collection.""" return iter(self._get_collection()) def __contains__(self, item: object) -> bool: + """Return whether the backing collection contains ``item``.""" return item in self._get_collection() def for_each_indexed(self, action: Callable[[int, T], object]) -> None: diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py index d2d5d686a..35d41990b 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_list.py @@ -14,14 +14,26 @@ class AbstractBackedList( Generic[T], ABC, ): + """An :class:`AbstractBackedCollection` with sequence-style access. + + Integer indexes return individual items and slices return sequences. When + a sort selector is supplied, additions reorder the backing list. Indexed + mutation is only exposed by specialisations such as :class:`LazyIndexList`. + """ sort_by: Callable[[T], Any] | None = None @abstractmethod def _get_collection(self) -> Sequence[T]: + """Return the current backing list.""" ... def append(self, item: T, /) -> None: + """Append ``item`` with validation and optional sorting. + + This performs validation, storage, and optional sorting, but no mRID + check or backfill. + """ super().append(item) if self.sort_by is not None: self._get_collection().sort(key=self.sort_by) # type: ignore[attr-defined] @@ -35,6 +47,7 @@ def __getitem__(self, index: slice) -> Sequence[T]: ... def __getitem__(self, index: int | slice) -> T | Sequence[T]: + """Return the item or slice at ``index``.""" collection = self._get_collection() if isinstance(index, slice): return collection[index] diff --git a/src/zepben/ewb/boilerplate/collections/abstract_mrid_list.py b/src/zepben/ewb/boilerplate/collections/abstract_mrid_list.py index b46655824..2de95d9ab 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_mrid_list.py @@ -10,24 +10,35 @@ class AbstractMridList(MridCollection[S], Sequence[S], ABC): """ - Common list behaviour for mRID-addressable collections. + List-shaped specialisation of :class:`MridCollection`. - NOTE: This can be simplified by inheriting from AbstractBackedList. - But that cannot be done on the Kotlin side, so for better parity, - this class duplicates some of the functionality from that class + This class represents the deliberate intersection of the mRID collection + and backed-list branches so downstream mRID list implementations do not + duplicate sequence delegation or sorting behaviour. It intentionally does + not inherit :class:`AbstractBackedList` to preserve SDK hierarchy parity. """ sort_by: Callable[[S], Any] | None = None @abstractmethod def _get_collection(self) -> Sequence[S]: + """Return the current backing list.""" ... def append(self, item: S, /) -> None: + """Append ``item`` when its mRID and validation permit it. + + This performs the mRID check, backfill, validation, storage, and + optional sorting. + """ MridCollection.append(self, item) if self.sort_by is not None: self._get_collection().sort(key=self.sort_by) # type: ignore[attr-defined] + @overload + def __getitem__(self, index: str) -> S: + ... + @overload def __getitem__(self, index: int) -> S: ... @@ -36,5 +47,8 @@ def __getitem__(self, index: int) -> S: def __getitem__(self, index: slice) -> Sequence[S]: ... - def __getitem__(self, index: int | slice) -> S | Sequence[S]: + def __getitem__(self, index: str | int | slice) -> S | Sequence[S]: + """Return an element by mRID or an item or slice by list index.""" + if isinstance(index, str): + return self.get_by_mrid(index) return self._get_collection()[index] diff --git a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py index 128bd9944..be13642b9 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py @@ -8,7 +8,7 @@ class LazyIndexList(LazyList[T]): """ - Lazy collection with list-style index-based insertion and deletion. + A :class:`LazyList` with index-based insertion and deletion. It retains the nullable backing-list behaviour of ``LazyList``, creating the backing list when an item is inserted and resetting it to @@ -16,6 +16,12 @@ class LazyIndexList(LazyList[T]): For example:: + @zb_dataclass + class Container(DataclassBase): + _items: list[str] | None = field(default=None) + items: LazyIndexList[str] = LazyIndexList(_items, "item") + + container = Container() container.items.insert(0, "value") assert container._items == ["value"] @@ -32,9 +38,10 @@ def __init__( self.element_description = element_description def insert(self, index: int, item: T) -> None: - """ - Insert an item into the collection at a given index. - Run optional validation. + """Insert ``item`` at ``index`` when the index is valid. + + The backing list is created if needed. This operation validates the + item but performs no mRID check, backfill, or sorting. """ size = len(self) @@ -60,8 +67,7 @@ def insert(self, index: int, item: T) -> None: existing.insert(index, item) def pop(self, index: int = -1) -> T: - """ - Remove and return the item at ``index``. + """Remove and return the item at ``index``. Uses normal Python list semantics, including support for negative indexes and raising ``IndexError`` when the index is invalid. @@ -77,5 +83,5 @@ def pop(self, index: int = -1) -> T: return item def __delitem__(self, index: int, /) -> None: - """Remove the item at the given index.""" + """Remove the item at ``index``.""" self.remove(self[index]) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_list.py b/src/zepben/ewb/boilerplate/collections/lazy_list.py index 9221f5b69..a7ad35b14 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_list.py @@ -13,8 +13,7 @@ class LazyList(_IterableWrapper[T], AbstractBackedList[T]): """ - Concrete collection wrapper that treats its backing field as a nullable - list. + A list-like wrapper backed by a nullable list. A backing value of ``None`` is exposed as an empty collection. The backing list is created when the first item is appended and reset to ``None`` when @@ -22,9 +21,10 @@ class LazyList(_IterableWrapper[T], AbstractBackedList[T]): For example:: - class Container: - _items = field(default=None) - items = LazyList(_items) + @zb_dataclass + class Container(DataclassBase): + _items: list[str] | None = field(default=None) + items: LazyList[str] = LazyList(_items) container = Container() @@ -51,9 +51,15 @@ def _get(self) -> list[T] | None: return getattr(self._instance, self._backing_name) def _get_collection(self) -> list[T]: + """Return the backing list, or an unbound empty list when absent.""" return getattr(self._instance, self._backing_name) or [] def _append_raw(self, item: T) -> None: + """Append ``item`` to the backing list, creating it if needed. + + This hook performs storage; validation and optional sorting are + inherited. + """ existing = getattr(self._instance, self._backing_name) if existing is None: existing = [item] @@ -62,10 +68,12 @@ def _append_raw(self, item: T) -> None: existing.append(item) def _post_remove(self, item: T) -> None: + """Reset the backing list after its last item is removed.""" if not self._get_collection(): self._clear_raw(self._get_collection()) def _clear_raw(self, collection) -> None: + """Reset the backing list to ``None``.""" setattr(self._instance, self._backing_name, None) def __repr__(self) -> str: diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py index a6f3dc916..430b4a575 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_list.py @@ -11,10 +11,12 @@ class LazyMridList(_IterableWrapper[S], AbstractMridList[S]): """ - Nullable list implementation of :class:`MridCollection`. + A nullable-list implementation of :class:`MridCollection`. Inherits mRID lookup and uniqueness semantics from ``MridCollection`` and - lazy backing-list behavior from ``LazyList``. + exposes mRID, integer-index, and slice reads through + :class:`AbstractMridList`. An absent backing list is exposed as empty; the + list is created by the first append and reset to ``None`` when emptied. """ def __init__( self, @@ -34,9 +36,11 @@ def _get(self) -> list[S] | None: return getattr(self._instance, self._backing_name) def _get_collection(self) -> list[S]: + """Return the backing list, or an unbound empty list when absent.""" return self._get() or [] def _safe_get_by_mrid(self, mrid: str) -> S | None: + """Return the element with ``mrid``, or ``None``.""" existing = self._get() if existing is None: return None @@ -44,6 +48,11 @@ def _safe_get_by_mrid(self, mrid: str) -> S | None: return found def _append_raw(self, item: S) -> None: + """Append ``item`` to the backing list, creating it if needed. + + This hook performs storage; mRID checking, backfill, validation, and + optional sorting are inherited. + """ existing = self._get() if existing is None: setattr(self._instance, self._backing_name, [item]) @@ -51,14 +60,17 @@ def _append_raw(self, item: S) -> None: existing.append(item) def _post_remove(self, item: S) -> None: + """Clear backfill and reset an empty backing list.""" super()._post_remove(item) if not self._get_collection(): self._clear_raw(self._get_collection()) def _clear_raw(self, collection) -> None: + """Reset the backing list to ``None``.""" setattr(self._instance, self._backing_name, None) def _clear_and_copy(self, collection): + """Reset the backing list and return its former elements.""" self._clear_raw(collection) return collection diff --git a/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py index 10ca9aa96..8fd01e4da 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_mrid_map.py @@ -13,7 +13,7 @@ class LazyMridMap(_IterableWrapper[S], MridCollection[S]): """ - Lazy mRID collection backed by a nullable dictionary. + An mRID collection backed by a nullable dictionary. Items are stored by their ``mrid`` while iteration exposes the dictionary values. A backing value of ``None`` is treated as an empty collection. The @@ -22,10 +22,18 @@ class LazyMridMap(_IterableWrapper[S], MridCollection[S]): For example:: + @zb_dataclass + class Container(DataclassBase): + _items: dict[str, Identifiable] | None = field(default=None) + items: MridCollection[Identifiable] = LazyMridMap(_items, "item") + + container = Container() + item = Identifiable("item") container.items.append(item) assert container._items == {item.mrid: item} assert container.items.get_by_mrid(item.mrid) is item + assert container.items[item.mrid] is item container.items.remove(item) assert container._items is None @@ -49,15 +57,26 @@ def _get_or_empty(self) -> dict[str, S]: return getattr(self._instance, self._backing_name) or {} def _get_collection(self) -> ValuesView[S]: + """Return the map values, or an unbound empty view when absent.""" return self._get_or_empty().values() def _safe_get_by_mrid(self, mrid: str) -> S | None: + """Return the element with ``mrid``, or ``None``.""" return self._get_or_empty().get(mrid, None) def get_by_mrid(self, mrid: str) -> S: + """Return the element with ``mrid``. + + :raises KeyError: If no element has the requested mRID. + """ return self._get_or_empty()[mrid] def _append_raw(self, item: S) -> None: + """Store ``item`` by mRID, creating the backing map if needed. + + This hook performs storage; mRID checking, backfill, and validation are + inherited. + """ existing = getattr(self._instance, self._backing_name) if existing is None: existing = {item.mrid: item} @@ -69,9 +88,11 @@ def __len__(self) -> int: return len(self._get_or_empty()) def __contains__(self, item: object) -> bool: + """Return whether ``item`` is stored under its mRID by identity.""" return self._get_or_empty().get(getattr(item, "mrid", None)) is item def remove(self, item: S) -> None: + """Remove ``item`` only when the stored instance is identical.""" existing = self._get_or_empty() if existing.get(item.mrid) is not item: raise ValueError(f"{item!r} not in collection") @@ -79,14 +100,17 @@ def remove(self, item: S) -> None: self._post_remove(item) def _post_remove(self, item: S) -> None: + """Reset an empty map and clear backfill for ``item``.""" if not self._get_or_empty(): self._clear_raw(self._get_collection()) super()._post_remove(item) def _clear_raw(self, collection) -> None: + """Reset the backing map to ``None``.""" setattr(self._instance, self._backing_name, None) def _clear_and_copy(self, collection): + """Reset the backing map and return its former values.""" former_items = self._get_collection() self._clear_raw(collection) return former_items @@ -95,6 +119,3 @@ def __repr__(self) -> str: if self._instance is None: return object.__repr__(self) return repr(self._get_or_empty()) - - def __getitem__(self, item) -> S: - return (self._get_or_empty())[item] diff --git a/src/zepben/ewb/boilerplate/collections/mrid_collection.py b/src/zepben/ewb/boilerplate/collections/mrid_collection.py index d8f0d75ae..403da4342 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_collection.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_collection.py @@ -13,10 +13,11 @@ class MridCollection(AbstractBackedCollection[S], ABC): - """ - Collection of objects identified by a unique ``mrid``. + """Base collection for objects identified by a unique ``mrid``. - Provides lookup by mRID and rejects distinct objects with duplicate mRIDs. + Provides lookup through :meth:`get_by_mrid` and read-only string indexing. + Additions enforce mRID uniqueness and apply optional backfill before base + collection validation. """ _instance: Any @@ -27,17 +28,24 @@ class MridCollection(AbstractBackedCollection[S], ABC): def _safe_get_by_mrid(self, mrid: str) -> S | None: ... def get_by_mrid(self, mrid: str) -> S: - """ - Get an element matching given ``mrid`` + """Return the element with ``mrid``. - raises KeyError if one is not present + :raises KeyError: If no element has the requested mRID. """ res = self._safe_get_by_mrid(mrid) if res is None: raise KeyError(mrid) return res + def __getitem__(self, mrid: str, /) -> S: + """Return the element with ``mrid``. + + :raises KeyError: If no element has the requested mRID. + """ + return self.get_by_mrid(mrid) + def _can_add_by_mrid(self, element: S) -> bool: + """Accept a new mRID, ignore the same instance, and reject collisions.""" existing = self._safe_get_by_mrid(element.mrid) if existing is None: @@ -52,6 +60,11 @@ def _can_add_by_mrid(self, element: S) -> bool: return False def append(self, item: S, /) -> None: + """Append ``item`` when its mRID and validation permit it. + + This performs the mRID check, backfill, validation, and storage, but no + sorting. + """ if not self._can_add_by_mrid(item): return @@ -61,10 +74,12 @@ def append(self, item: S, /) -> None: super().append(item) def _post_remove(self, item: S, /) -> None: + """Clear ``item``'s backfill after removal.""" if self.backfill is not None: self.backfill.clear(item) def clear(self) -> None: + """Clear the collection and all backfilled references.""" collection = self._get_collection() if self.backfill is None: self._clear_raw(collection) diff --git a/src/zepben/ewb/boilerplate/collections/mrid_list.py b/src/zepben/ewb/boilerplate/collections/mrid_list.py index f56c3c2f7..0bb6e78a2 100644 --- a/src/zepben/ewb/boilerplate/collections/mrid_list.py +++ b/src/zepben/ewb/boilerplate/collections/mrid_list.py @@ -13,12 +13,13 @@ class MridList(_IterableWrapper[S], AbstractMridList[S]): - """ - mRID collection backed by a non-nullable list. + """An mRID collection backed by a non-nullable list. - Items are kept in insertion order and retrieved by mRID using a linear search. - Appending enforces mRID uniqueness and can optionally apply backfill, - validation, and sorting. + Items retain list order and are retrieved by mRID using a linear search. + Without a sort selector this is insertion order; when sorting is configured, + the list is reordered after successful additions and disregarded re-adds of + the same instance. Appending enforces mRID uniqueness and can optionally + apply backfill and validation. Unlike ``LazyMridList``, clearing the collection leaves an empty backing list rather than resetting the backing field to ``None``. @@ -49,9 +50,11 @@ def __post_init__(self) -> None: self._backing_list = getattr(self._instance, self._backing_name) def _get_collection(self) -> Sequence[S]: + """Return the backing list.""" return self._backing_list def _safe_get_by_mrid(self, mrid: str) -> S | None: + """Return the element with ``mrid``, or ``None``.""" found = next((element for element in self._backing_list if element.mrid == mrid), None) return found diff --git a/src/zepben/ewb/boilerplate/collections/wrapper.py b/src/zepben/ewb/boilerplate/collections/wrapper.py index a388be93b..400d108e3 100644 --- a/src/zepben/ewb/boilerplate/collections/wrapper.py +++ b/src/zepben/ewb/boilerplate/collections/wrapper.py @@ -17,14 +17,15 @@ class _Wrapper(BackedDescriptor): """ - Descriptor base for collection views backed by another dataclass field. + Descriptor base for collection views backed by a dataclass field or alias. Access through an instance creates a short-lived wrapper bound to that instance and its backing field. For example:: - class Container: - _items = field(default=None) - items = LazyList(_items) + @zb_dataclass + class Container(DataclassBase): + _items: list[str] | None = field(default=None) + items: LazyList[str] = LazyList(_items) container = Container() @@ -59,9 +60,10 @@ def __init__(self, *args, **kwargs) -> None: self._backing_name = None def __get__(self, instance, _=None) -> Self: - """ - This creates a copy of self, pointing it to the specific instance that the wrapper is attached to. - From there on, the wrapper functions as a lazy list. + """Return a collection wrapper bound to ``instance``. + + A fresh wrapper is created on each instance access and points at the + descriptor's backing field. Class access returns the shared descriptor. """ if instance is None: return self @@ -94,9 +96,7 @@ def fget(self, instance): ... T = TypeVar("T") class _IterableWrapper(_WrapperFgetFix, AbstractBackedCollection[T], ABC): - """ - This class allows us to assign lists at init time to avoid special-case handling. - """ + """Allow an iterable to populate a backed collection during assignment.""" def __set__(self, instance, value) -> None: if instance is None: return diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index b09f4e064..0cf3a06f4 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -29,14 +29,17 @@ def remove_descriptor_annotations(cls: type[T]) -> T: descriptor-backed attributes before ``@dataclass`` runs, those attributes are left alone and can continue to behave as descriptors. - Use this decorator below ``@dataclass`` so that it is called first. Python - applies decorators from the bottom up:: - - @dataclass - class MyClass: + Use this decorator below ``@dataclass`` so that it is called first. A + slotted dataclass is required for field-backed descriptors to be rebound + after dataclass field names are assigned. Python applies decorators from + the bottom up:: + + @dataclass(slots=True) + @remove_descriptor_annotations + class MyClass: _x: int = field(default=0) - x: int = MyDescriptor("_x") + x: int = BackedDescriptor(_x) In the example above, ``x`` is annotated, but its class value is a descriptor. Without ``remove_descriptor_annotations``, ``@dataclass`` would treat ``x`` as @@ -44,8 +47,8 @@ class MyClass: repr, etc. With this decorator, the annotation for ``x`` is removed before dataclass processing, while normal fields such as ``_x`` are left intact. - Dataclass ``Field`` instances are themselves descriptors and are thus skipped. - In the example above, the slot for ``_x`` is still created. + Dataclass ``Field`` instances are explicitly preserved. In the example + above, the slot for ``_x`` is still created. Before decoration:: @@ -124,14 +127,18 @@ def resolve_default(instance, field: Field): @zb_dataclass class DataclassBase: - """ - Instantiate the default fields and interpret the kwargs like ``@dataclass`` does + """Base class providing keyword-driven initialisation for CIM dataclasses. + + It populates unset dataclass fields from their defaults or factories, then + assigns keyword arguments through :func:`setattr` so properties and custom + descriptors participate in construction. Subclass constructors may set + fields first and delegate the remaining keywords here. - This class serves as a base class for mostly init-less dataclasses used in CIM, - allowing custom inits to not break the entire inheritance tree. - It fills fields with default values, and treats kwargs the same way @dataclass does. + This is intentionally not a complete replacement for a generated dataclass + initializer: positional arguments belong to subclass constructors and + ``__post_init__`` is unsupported. - For more motivation, refer to `MANIFESTO.md` + See ``MANIFESTO.md`` for the design constraints behind this approach. """ def __init__(self, **kwargs) -> None: # Currently post init implementation requires a lot of extra logic, diff --git a/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py index 0fa921cd1..55b9a7b22 100644 --- a/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py +++ b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py @@ -10,10 +10,13 @@ class AcLineSegmentPhaseList(LazyMridList): + """A list of ``AcLineSegmentPhase`` objects for an ``AcLineSegment``.""" + def get_by_phase(self, phase: SinglePhaseKind): - """ - The individual phase models for an AcLineSegment. - `phase` the phase of the required [AcLineSegmentPhase] + """Return the individual phase model for an ``AcLineSegment``. + + :param phase: The phase of the required ``AcLineSegmentPhase``. + :raises KeyError: If no model has the requested phase. """ res = next((it for it in self if it.phase == phase), None) if res is None: diff --git a/src/zepben/ewb/boilerplate/relations/battery_control_list.py b/src/zepben/ewb/boilerplate/relations/battery_control_list.py index 233a59867..70fc5a021 100644 --- a/src/zepben/ewb/boilerplate/relations/battery_control_list.py +++ b/src/zepben/ewb/boilerplate/relations/battery_control_list.py @@ -10,13 +10,13 @@ class BatteryControlList(LazyMridList): + """A list of ``BatteryControl`` objects for a ``BatteryUnit``.""" + def get_by_mode(self, control_mode: BatteryControlMode): - """ - Get the `BatteryControl` identified by its `control_mode` + """Return a ``BatteryControl`` by its control mode. - `control_mode` the `BatteryControlMode` of the desired `BatteryControl` - Returns The `BatteryControl` with the specified `control_mode` if it exists - Raises `KeyError` if a `BatteryControl` with `control_mode` wasn't present. + :param control_mode: The mode of the required ``BatteryControl``. + :raises IndexError: If no control has the requested mode. """ for control in self: if control.control_mode == control_mode: diff --git a/src/zepben/ewb/boilerplate/relations/curve_data_list.py b/src/zepben/ewb/boilerplate/relations/curve_data_list.py index 85954a1d2..61cca8a2b 100644 --- a/src/zepben/ewb/boilerplate/relations/curve_data_list.py +++ b/src/zepben/ewb/boilerplate/relations/curve_data_list.py @@ -11,14 +11,13 @@ class CurveDataList(LazyList): + """A list of ``CurveData`` objects for a ``Curve``.""" def get(self, x: float) -> 'CurveData': - """ - Get the :class:`CurveData` identified by its `x_value`. + """Return point data by its x-value. - :param x: The X value of the required :class:`CurveData`. - :returns: The :class:`CurveData` with the specified `x` if it exists. - :raises KeyError: When no `CurveData` was found with `x`. + :param x: The x-value of the requested data. + :raises KeyError: If no data has the requested x-value. """ curve_data = next((it for it in self if it.x_value == x), None) if curve_data: @@ -26,12 +25,10 @@ def get(self, x: float) -> 'CurveData': raise KeyError(x) def remove_data_at(self, x: float) -> 'CurveData': - """ - Disassociate a :class:`CurveData` from this collection based on its `x_value`. + """Remove and return the data point with x-value ``x``. - :param x: The :class:`CurveData` to disassociate from this :class:`Curve`. - :returns: A reference to the removed :class:`CurveData`. - :raises IndexError: If no :class:`CurveData` with a value of `x` was not associated with this :class:`Curve`. + :param x: The x-value of the data point to remove. + :raises KeyError: If no data has the requested x-value. """ data = self.get(x) self.remove(data) diff --git a/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py index f1f2951e4..89dc167ae 100644 --- a/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py +++ b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py @@ -13,14 +13,14 @@ class PhaseImpedanceDataList(LazyList): + """A list of phase impedance data for a per-length phase impedance.""" + def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseImpedanceData: - """ - Get the matrix entry for the corresponding to and from phases. + """Return the matrix entry for the corresponding phases. - :param from_phase: The from_phase to lookup. - :param to_phase: The to_phase to lookup. - :returns: The :class:`PhaseImpedanceData` with the specified `from_phase` and `to_phase` if it exists. - :raises KeyError: When no `PhaseImpedanceData` was found with a matching `from_phase` and `to_phase`. + :param from_phase: The "from" phase to look up. + :param to_phase: The "to" phase to look up. + :raises KeyError: If no matching phase impedance data exists. """ phase_impedance_data = next((it for it in self if it.from_phase == from_phase and it.to_phase == to_phase), None) if phase_impedance_data: @@ -30,7 +30,5 @@ def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseIm @property def diagonal(self): - """ - Get only the diagonal elements of the matrix, i.e toPhase == fromPhase. - """ + """Return the diagonal entries where ``to_phase == from_phase``.""" return (pid for pid in self if pid.from_phase == pid.to_phase) diff --git a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py index ede88484f..f6a078bb7 100644 --- a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py +++ b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py @@ -13,14 +13,13 @@ class PowerTransformerEndList(LazyMridList['PowerTransformerEnd']): + """A list of ``PowerTransformerEnd`` objects for a transformer.""" def get_by_num(self, end_number: int) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `end_number`. + """Return a transformer end by its end number. - `end_number` The `end_number` of the `PowerTransformerEnd` in relation to this `PowerTransformer`s VectorGroup. - Returns The `PowerTransformerEnd` referred to by `end_number` - Raises IndexError if no `PowerTransformerEnd` was found with end_number `end_number`. + :param end_number: The number of the required transformer end. + :raises IndexError: If no transformer end has the requested number. """ end = next((it for it in self if it.end_number == end_number), None) if end: @@ -28,12 +27,10 @@ def get_by_num(self, end_number: int) -> PowerTransformerEnd: raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self._instance)}") def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: - """ - Get the `PowerTransformerEnd` on this `PowerTransformer` by its `terminal`. + """Return a transformer end by its terminal. - `terminal` The `terminal` to find a `PowerTransformerEnd` for. - Returns The `PowerTransformerEnd` connected to the specified `terminal` - Raises IndexError if no `PowerTransformerEnd` connected to `terminal` was found on this `PowerTransformer`. + :param terminal: The terminal of the required transformer end. + :raises IndexError: If no transformer end uses ``terminal``. """ end = next((it for it in self if it.terminal == terminal), None) diff --git a/src/zepben/ewb/boilerplate/relations/terminal_list.py b/src/zepben/ewb/boilerplate/relations/terminal_list.py index 3242be310..d4d533501 100644 --- a/src/zepben/ewb/boilerplate/relations/terminal_list.py +++ b/src/zepben/ewb/boilerplate/relations/terminal_list.py @@ -10,8 +10,13 @@ class TerminalsList(LazyMridList[Terminal]): + """A list of terminals for a conducting equipment instance.""" def get_by_sequence_number(self, sequence_number: int) -> Terminal: + """Return the terminal with ``sequence_number``. + + :raises IndexError: If no terminal has the requested sequence number. + """ term = next((it for it in self if it.sequence_number == sequence_number), None) if term is None: raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self._instance)}") diff --git a/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py index f62647491..f59e77ff0 100644 --- a/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py +++ b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py @@ -10,17 +10,20 @@ class TransformerEndRatedSList(LazyList[TransformerEndRatedS]): + """A list of rated-power entries for a ``PowerTransformerEnd``.""" def get_by_cooling_type( self, cooling_type: TransformerCoolingType, ) -> TransformerEndRatedS | None: + """Return the entry for ``cooling_type``, or ``None``.""" return next((rating for rating in self if rating.cooling_type == cooling_type), None) def remove_by_cooling_type( self, cooling_type: TransformerCoolingType, ) -> TransformerEndRatedS | None: + """Remove and return the entry for ``cooling_type``, if present.""" rating = self.get_by_cooling_type(cooling_type) if rating is not None: self.remove(rating) diff --git a/test/boilerplate/test_mrid_collection.py b/test/boilerplate/test_mrid_collection.py index c5aba5352..8b60744fc 100644 --- a/test/boilerplate/test_mrid_collection.py +++ b/test/boilerplate/test_mrid_collection.py @@ -53,6 +53,30 @@ def test_get_by_mrid_raises_for_missing_item(): collection.get_by_mrid("missing") +def test_getitem_returns_item_by_mrid(): + item = Item("item") + collection = MridCollectionImpl([item]) + + assert collection["item"] is item + + +def test_getitem_raises_for_missing_mrid(): + collection = MridCollectionImpl() + + with pytest.raises(KeyError, match="missing"): + collection["missing"] + + +def test_getitem_does_not_enable_item_assignment(): + item = Item("item") + collection = MridCollectionImpl([item]) + + with pytest.raises(TypeError, match="does not support item assignment"): + collection["item"] = Item("replacement") # type: ignore[index] + + assert collection["item"] is item + + def test_append_accepts_new_mrid(): item = Item("item") collection = MridCollectionImpl() diff --git a/test/boilerplate/test_mrid_collections.py b/test/boilerplate/test_mrid_collections.py index e08207e9a..978adcc6d 100644 --- a/test/boilerplate/test_mrid_collections.py +++ b/test/boilerplate/test_mrid_collections.py @@ -49,6 +49,47 @@ def assert_public_state( for item in expected: assert item in collection assert collection.get_by_mrid(item.mrid) is item + assert collection[item.mrid] is item + + +@pytest.mark.parametrize("collection_name", ["list_items", "lazy_list_items"]) +def test_mrid_lists_preserve_integer_and_slice_addressing(collection_name: str): + first = Item("first") + second = Item("second") + container = Container() + collection = getattr(container, collection_name) + collection.extend([first, second]) + + assert collection[0] is first + assert collection[-1] is second + assert collection[1:] == [second] + assert collection["first"] is first + + +@pytest.mark.parametrize( + ("collection_name", "index"), + [ + ("list_items", "item"), + ("list_items", 0), + ("lazy_list_items", "item"), + ("lazy_list_items", 0), + ("lazy_map_items", "item"), + ], +) +def test_mrid_collection_indexing_is_read_only( + collection_name: str, + index: str | int, +): + item = Item("item") + replacement = Item("replacement") + container = Container() + collection = getattr(container, collection_name) + collection.append(item) + + with pytest.raises(TypeError, match="does not support item assignment"): + collection[index] = replacement + + assert list(collection) == [item] def test_mrid_collections_have_the_same_public_effects(): From 7cdcd08ebd0e8f9efb0f173763cb7e3999bb19fe Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Thu, 6 Aug 2026 15:43:42 +1000 Subject: [PATCH 52/54] find_by Signed-off-by: Boris Filin --- .../boilerplate/collections/abstract_backed_collection.py | 4 ++++ .../ewb/boilerplate/relations/ac_line_segment_phase_list.py | 2 +- .../ewb/boilerplate/relations/battery_control_list.py | 6 +++--- src/zepben/ewb/boilerplate/relations/curve_data_list.py | 3 ++- .../ewb/boilerplate/relations/phase_impedance_data_list.py | 3 ++- .../ewb/boilerplate/relations/power_transformer_end_list.py | 4 ++-- src/zepben/ewb/boilerplate/relations/terminal_list.py | 2 +- .../boilerplate/relations/transformer_end_rated_s_list.py | 2 +- 8 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py index 141021d7f..2af012e55 100644 --- a/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py +++ b/src/zepben/ewb/boilerplate/collections/abstract_backed_collection.py @@ -81,6 +81,10 @@ def __contains__(self, item: object) -> bool: """Return whether the backing collection contains ``item``.""" return item in self._get_collection() + def find_by(self, predicate: Callable[[T], bool], /) -> T: + """Return first item that satisfies ``predicate`` or ``None`` if no items do""" + return next((it for it in self if predicate(it)), None) + def for_each_indexed(self, action: Callable[[int, T], object]) -> None: """Call the `action` on each item in the list.""" for index, item in enumerate(self._get_collection()): diff --git a/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py index 55b9a7b22..cc50276a8 100644 --- a/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py +++ b/src/zepben/ewb/boilerplate/relations/ac_line_segment_phase_list.py @@ -18,7 +18,7 @@ def get_by_phase(self, phase: SinglePhaseKind): :param phase: The phase of the required ``AcLineSegmentPhase``. :raises KeyError: If no model has the requested phase. """ - res = next((it for it in self if it.phase == phase), None) + res = self.find_by(lambda it: it.phase == phase) if res is None: raise KeyError(phase) return res diff --git a/src/zepben/ewb/boilerplate/relations/battery_control_list.py b/src/zepben/ewb/boilerplate/relations/battery_control_list.py index 70fc5a021..88f4de263 100644 --- a/src/zepben/ewb/boilerplate/relations/battery_control_list.py +++ b/src/zepben/ewb/boilerplate/relations/battery_control_list.py @@ -18,7 +18,7 @@ def get_by_mode(self, control_mode: BatteryControlMode): :param control_mode: The mode of the required ``BatteryControl``. :raises IndexError: If no control has the requested mode. """ - for control in self: - if control.control_mode == control_mode: - return control + res = self.find_by(lambda it: it.control_mode == control_mode) + if res is not None: + return res raise IndexError(f"No BatteryControl with a control_mode of {control_mode} was found in BatteryUnit {str(self)}") diff --git a/src/zepben/ewb/boilerplate/relations/curve_data_list.py b/src/zepben/ewb/boilerplate/relations/curve_data_list.py index 61cca8a2b..4dbc8bda5 100644 --- a/src/zepben/ewb/boilerplate/relations/curve_data_list.py +++ b/src/zepben/ewb/boilerplate/relations/curve_data_list.py @@ -19,7 +19,8 @@ def get(self, x: float) -> 'CurveData': :param x: The x-value of the requested data. :raises KeyError: If no data has the requested x-value. """ - curve_data = next((it for it in self if it.x_value == x), None) + + curve_data = self.find_by(lambda it: it.x_value == x) if curve_data: return curve_data raise KeyError(x) diff --git a/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py index 89dc167ae..37cd52063 100644 --- a/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py +++ b/src/zepben/ewb/boilerplate/relations/phase_impedance_data_list.py @@ -22,7 +22,8 @@ def get(self, from_phase: SinglePhaseKind, to_phase: SinglePhaseKind) -> PhaseIm :param to_phase: The "to" phase to look up. :raises KeyError: If no matching phase impedance data exists. """ - phase_impedance_data = next((it for it in self if it.from_phase == from_phase and it.to_phase == to_phase), None) + + phase_impedance_data = self.find_by(lambda it: it.to_phase == to_phase) if phase_impedance_data: return phase_impedance_data diff --git a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py index f6a078bb7..cb81761e8 100644 --- a/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py +++ b/src/zepben/ewb/boilerplate/relations/power_transformer_end_list.py @@ -21,7 +21,7 @@ def get_by_num(self, end_number: int) -> PowerTransformerEnd: :param end_number: The number of the required transformer end. :raises IndexError: If no transformer end has the requested number. """ - end = next((it for it in self if it.end_number == end_number), None) + end = self.find_by(lambda it: it.end_number == end_number) if end: return end raise IndexError(f"No TransformerEnd with end_number {end_number} was found in PowerTransformer {str(self._instance)}") @@ -33,7 +33,7 @@ def get_by_terminal(self, terminal: Terminal) -> PowerTransformerEnd: :raises IndexError: If no transformer end uses ``terminal``. """ - end = next((it for it in self if it.terminal == terminal), None) + end = self.find_by(lambda it: it.terminal == terminal) if end: return end raise IndexError(f"No TransformerEnd with terminal {terminal} was found in PowerTransformer {str(self._instance)}") diff --git a/src/zepben/ewb/boilerplate/relations/terminal_list.py b/src/zepben/ewb/boilerplate/relations/terminal_list.py index d4d533501..011332495 100644 --- a/src/zepben/ewb/boilerplate/relations/terminal_list.py +++ b/src/zepben/ewb/boilerplate/relations/terminal_list.py @@ -17,7 +17,7 @@ def get_by_sequence_number(self, sequence_number: int) -> Terminal: :raises IndexError: If no terminal has the requested sequence number. """ - term = next((it for it in self if it.sequence_number == sequence_number), None) + term = self.find_by(lambda it: it.sequence_number == sequence_number) if term is None: raise IndexError(f"No Terminal with sequence_number {sequence_number} was found in ConductingEquipment {str(self._instance)}") return term diff --git a/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py index f59e77ff0..424f199d7 100644 --- a/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py +++ b/src/zepben/ewb/boilerplate/relations/transformer_end_rated_s_list.py @@ -17,7 +17,7 @@ def get_by_cooling_type( cooling_type: TransformerCoolingType, ) -> TransformerEndRatedS | None: """Return the entry for ``cooling_type``, or ``None``.""" - return next((rating for rating in self if rating.cooling_type == cooling_type), None) + return self.find_by(lambda it: it.cooling_type == cooling_type) def remove_by_cooling_type( self, From 170ef5e1ee09b6f77579e6cbde791073597038ee Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Thu, 6 Aug 2026 16:14:02 +1000 Subject: [PATCH 53/54] Fix indexing error Signed-off-by: Boris Filin --- src/zepben/ewb/boilerplate/collections/lazy_index_list.py | 3 +-- .../model/cim/extensions/iec61968/assetinfo/relay_info.py | 2 +- .../ewb/model/cim/extensions/iec61970/base/feeder/loop.py | 6 +++--- .../iec61970/base/protection/protection_relay_function.py | 4 ++-- src/zepben/ewb/model/cim/iec61968/assets/asset.py | 2 +- src/zepben/ewb/model/cim/iec61968/common/location.py | 2 +- .../model/cim/iec61970/base/diagramlayout/diagram_object.py | 2 +- .../ewb/model/cim/iec61970/infiec61970/feeder/circuit.py | 4 ++-- test/cim/private_collection_validator.py | 2 +- 9 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py index be13642b9..93671705b 100644 --- a/src/zepben/ewb/boilerplate/collections/lazy_index_list.py +++ b/src/zepben/ewb/boilerplate/collections/lazy_index_list.py @@ -47,8 +47,7 @@ def insert(self, index: int, item: T) -> None: if not 0 <= index <= size: raise IndexError( - f"Unable to add {self.element_description} to " - f"{self._instance}. " + f"{self.element_description} could not be added to {self._instance}. " f"Sequence number {index} is invalid. " f"Expected a value between 0 and {size}. " "Make sure you are adding the items in order and there are " diff --git a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py index cf500fa23..74e7b49d8 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py +++ b/src/zepben/ewb/model/cim/extensions/iec61968/assetinfo/relay_info.py @@ -33,7 +33,7 @@ class RelayInfo(AssetInfo): reclose_delays: LazyIndexList[float] = LazyIndexList( _reclose_delays, - "float" + "A float" ) def set_delays(self, delays: List[float]) -> RelayInfo: diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py index 828b1c269..f2580e6e7 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/feeder/loop.py @@ -38,17 +38,17 @@ class Loop(IdentifiedObject): circuits: MridCollection[Circuit] = LazyMridList( _circuits, - "An Circuit", + "A Circuit", ) substations: MridCollection[Substation] = LazyMridList( _substations, - "An Substation", + "A Substation", ) energizing_substations: MridCollection[Substation] = LazyMridList( _energizing_substations, - "An Substation", + "A Substation", ) diff --git a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py index b92f7fe12..b63011a16 100644 --- a/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py +++ b/src/zepben/ewb/model/cim/extensions/iec61970/base/protection/protection_relay_function.py @@ -86,12 +86,12 @@ def relay_info(self, relay_info: Optional[RelayInfo]): thresholds: LazyIndexList[RelaySetting] = LazyIndexList( _thresholds, - "RelaySetting" + "A RelaySetting" ) time_limits: LazyIndexList[float] = LazyIndexList( _time_limits, - "float" + "A float" ) sensors: MridCollection[Sensor] = LazyMridList( diff --git a/src/zepben/ewb/model/cim/iec61968/assets/asset.py b/src/zepben/ewb/model/cim/iec61968/assets/asset.py index ea8989474..b0807aa2b 100644 --- a/src/zepben/ewb/model/cim/iec61968/assets/asset.py +++ b/src/zepben/ewb/model/cim/iec61968/assets/asset.py @@ -46,7 +46,7 @@ class Asset(IdentifiedObject, metaclass=ABCMeta): power_system_resources: MridCollection[PowerSystemResource] = LazyMridList( _power_system_resources, - "An PowerSystemResource", + "A PowerSystemResource", ) diff --git a/src/zepben/ewb/model/cim/iec61968/common/location.py b/src/zepben/ewb/model/cim/iec61968/common/location.py index c20302742..d1c6e5d6b 100644 --- a/src/zepben/ewb/model/cim/iec61968/common/location.py +++ b/src/zepben/ewb/model/cim/iec61968/common/location.py @@ -36,7 +36,7 @@ def __init__(self, *args, position_points=None, **kwargs): points: LazyIndexList[PositionPoint] = LazyIndexList( _position_points, - "PositionPoint", + "A PositionPoint", ) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py index a3cb235cf..ff8c19496 100644 --- a/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py +++ b/src/zepben/ewb/model/cim/iec61970/base/diagramlayout/diagram_object.py @@ -62,7 +62,7 @@ def diagram(self, diag): points: LazyIndexList[DiagramObjectPoint] = LazyIndexList( _diagram_object_points, - "DiagramObjectPoint", + "A DiagramObjectPoint", ) # region deprecated list boilerplate diff --git a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py index 5fda60a1b..e2ac6bb3d 100644 --- a/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py +++ b/src/zepben/ewb/model/cim/iec61970/infiec61970/feeder/circuit.py @@ -32,12 +32,12 @@ class Circuit(Line): end_terminals: MridCollection[Terminal] = LazyMridList( _end_terminals, - "An Terminal", + "A Terminal", ) end_substations: MridCollection[Substation] = LazyMridList( _end_substations, - "An Substation", + "A Substation", ) diff --git a/test/cim/private_collection_validator.py b/test/cim/private_collection_validator.py index e780da8b1..802ab38bb 100644 --- a/test/cim/private_collection_validator.py +++ b/test/cim/private_collection_validator.py @@ -376,7 +376,7 @@ def validate_before_removal(): # Adding to an invalid index is not valid. expected_message = ( - rf"Unable to add {other4.__class__.__name__} to {it}. \w* number 5 is invalid. Expected a value between 0 and {num(it)}. " + rf"An? {other4.__class__.__name__} could not be added to {it}. \w* number 5 is invalid. Expected a value between 0 and {num(it)}. " "Make sure you are adding the items in order and there are no gaps in the numbering." ) with pytest.raises(IndexError, match=expected_message): From 08b75d676d272587bfc6c50132c0e1b02439fb8a Mon Sep 17 00:00:00 2001 From: Boris Filin Date: Thu, 6 Aug 2026 16:50:17 +1000 Subject: [PATCH 54/54] remove version dependent tests Signed-off-by: Boris Filin --- .../ewb/boilerplate/backed_descriptor.py | 2 +- src/zepben/ewb/boilerplate/dataclass_base.py | 6 +++- .../test_dataclass_base_exhaustive.py | 30 ------------------- 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/src/zepben/ewb/boilerplate/backed_descriptor.py b/src/zepben/ewb/boilerplate/backed_descriptor.py index 2ed1a8bf7..2b9b030a9 100644 --- a/src/zepben/ewb/boilerplate/backed_descriptor.py +++ b/src/zepben/ewb/boilerplate/backed_descriptor.py @@ -19,7 +19,7 @@ class BackedDescriptor: def __init__(self, private_field): if not isinstance(private_field, Field) and not isinstance(private_field, BackedDescriptor): raise TypeError(f"private_field parameter of the Descriptor constructor has to be an instance of dataclass Field, instead is {private_field}") - self.private_field: Field | 'BackedDescriptor' = private_field + self.private_field: 'Field | BackedDescriptor' = private_field self._backing_name: str | None = None self.name = None diff --git a/src/zepben/ewb/boilerplate/dataclass_base.py b/src/zepben/ewb/boilerplate/dataclass_base.py index 0cf3a06f4..287c00cf6 100644 --- a/src/zepben/ewb/boilerplate/dataclass_base.py +++ b/src/zepben/ewb/boilerplate/dataclass_base.py @@ -108,7 +108,7 @@ def zb_dataclass(cls: type[T]) -> type[T]: )(cls), ) -def resolve_default(instance, field: Field): +def resolve_default(instance, field: Field, unsafe: bool=True): # Set field defaults if field.default is not MISSING: setattr(instance, field.name, field.default) @@ -147,12 +147,16 @@ def __init__(self, **kwargs) -> None: raise NotImplementedError("Current dataclass base does not support __post_init__ calls for redundancy reasons.") field_dict = {f.name: f for f in fields(type(self))} + # Manually assign defaults in dataclass fields for f in field_dict.values(): # We cannot just check kwargs because fields could be set in subclass __init__'s if _is_set(self, f.name) or f.name in kwargs: continue + # NOTE: This step does not allow for descriptors to set no-default fields. + # You can solve it by checking for missing values after the next section (kwarg application) + # However, it is slightly inefficient and not used for CIM, so we don't use it. resolve_default(self, f) # Assign all of the kwargs manually. diff --git a/test/boilerplate/test_dataclass_base_exhaustive.py b/test/boilerplate/test_dataclass_base_exhaustive.py index 9545a22ef..70612618e 100644 --- a/test/boilerplate/test_dataclass_base_exhaustive.py +++ b/test/boilerplate/test_dataclass_base_exhaustive.py @@ -304,33 +304,3 @@ def test_resolve_default_rejects_field_without_default(): match=r"Missing required field 'required' for Example", ): resolve_default(instance, required_field) - - -@pytest.mark.xfail( - strict=True, - reason="Required backing fields are resolved before descriptor keywords are assigned", -) -def test_descriptor_keyword_can_supply_required_backing_field(): - @zb_dataclass - class RequiredBacking(DataclassBase): - _value: int = field() - value: int = BackedDescriptor(_value) - - instance = RequiredBacking(value=3) - - assert instance._value == 3 - - -@pytest.mark.xfail( - strict=True, - reason="slots=True returns a replacement class and breaks zero-argument super()", -) -def test_zb_dataclass_supports_zero_argument_super(): - @zb_dataclass - class ZeroArgSuper(DataclassBase): - value: int = 1 - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - assert ZeroArgSuper().value == 1