From c1f7dc3c8440ddf22f739135d390a905664b28c5 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Wed, 21 Aug 2024 11:12:06 +0200 Subject: [PATCH 01/13] Add regression test for loading models with non-empty EdgePropertyList --- tests/unittests/test_sublaminate.py | 45 ++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/tests/unittests/test_sublaminate.py b/tests/unittests/test_sublaminate.py index 67fc566c4d..3205f96cc2 100644 --- a/tests/unittests/test_sublaminate.py +++ b/tests/unittests/test_sublaminate.py @@ -20,9 +20,13 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +import pathlib +import tempfile + import pytest -from ansys.acp.core import FabricWithAngle, Lamina, SymmetryType +from ansys.acp.core import Fabric, FabricWithAngle, Lamina, Stackup, SymmetryType +from ansys.acp.core._typing_helper import PATH from .common.tree_object_tester import NoLockedMixin, ObjectPropertiesToTest, TreeObjectTester @@ -104,3 +108,42 @@ def test_add_lamina(parent_object): assert sublaminate.materials[1].angle == 0.0 assert sublaminate.materials[2].material == fabric1 assert sublaminate.materials[2].angle == -45.0 + + +def test_load_with_existing_sublamina(acp_instance, parent_object): + """Regression test for bug #561. + + Checks that sublaminates are correctly loaded from a saved model. + """ + model = parent_object + # GIVEN: a model with a sublaminate which has three materials + fabric1 = model.create_fabric() + fabric1.material = model.create_material() + stackup = model.create_stackup() + stackup.add_fabric(fabric1, angle=30.0) + stackup.add_fabric(fabric1, angle=-30.0) + + sublaminate = model.create_sublaminate(name="test_sublaminate") + sublaminate.add_material(fabric1, angle=45.0) + sublaminate.add_material(stackup, angle=0.0) + sublaminate.add_material(fabric1, angle=-45.0) + + # WHEN: the model is saved and loaded + with tempfile.TemporaryDirectory() as tmp_dir: + if acp_instance.is_remote: + file_path: PATH = pathlib.Path(tmp_dir) / "model.acph5" + else: + file_path = "model.acph5" + model.save(file_path) + acp_instance.clear() + model = acp_instance.import_model(path=file_path) + + # THEN: the sublaminate is still present and has the same materials + sublaminate = model.sublaminates["test_sublaminate"] + assert len(sublaminate.materials) == 3 + assert sublaminate.materials[0].angle == 45.0 + assert sublaminate.materials[1].angle == 0.0 + assert sublaminate.materials[2].angle == 45.0 + assert isinstance(sublaminate.materials[0].material, Fabric) + assert isinstance(sublaminate.materials[1].material, Stackup) + assert isinstance(sublaminate.materials[2].material, Fabric) From 4a13294abf4f62334c9cff7ef21ab0526cc85313 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Wed, 21 Aug 2024 15:46:56 +0200 Subject: [PATCH 02/13] Fix EdgePropertyList being empty when constructed via _from_object_info --- .../_grpc_helpers/edge_property_list.py | 100 ++++++++++++------ .../_grpc_helpers/polymorphic_from_pb.py | 17 +-- .../_grpc_helpers/property_helper.py | 21 +++- .../_tree_objects/_grpc_helpers/protocols.py | 6 +- .../_tree_objects/linked_selection_rule.py | 2 +- .../acp/core/_tree_objects/sublaminate.py | 2 +- tests/unittests/test_sublaminate.py | 11 +- 7 files changed, 113 insertions(+), 46 deletions(-) diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py index 0daf9bd68a..e7fbc737be 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py @@ -85,6 +85,16 @@ class EdgePropertyList(ObjectCacheMixin, MutableSequence[ValueT]): For instance, element sets of an oriented element set. """ + __slots__ = ( + "_object_list_store", + "_parent_object", + "_parent_was_stored", + "_object_type", + "_attribute_name", + "_name", + "_object_constructor", + ) + @classmethod @constructor_with_cache( # NOTE greschd Feb'23: @@ -137,49 +147,73 @@ def __init__( _attribute_name: str, _from_pb_constructor: Callable[[CreatableTreeObject, Message, Callable[[], None]], ValueT], ) -> None: - getter = grpc_data_getter(_attribute_name, from_protobuf=list) - setter = grpc_data_setter(_attribute_name, to_protobuf=lambda x: x) - self._parent_object = _parent_object + self._parent_was_stored = self._parent_object._is_stored self._object_type = _object_type + self._attribute_name = _attribute_name self._name = _attribute_name.split(".")[-1] + self._object_list_store = self._get_object_list_from_parent() + self._object_constructor: Callable[[Message], ValueT] = ( lambda pb_object: _from_pb_constructor( self._parent_object, pb_object, self._apply_changes ) ) - # get initial object list - def get_object_list_from_parent_object() -> list[ValueT]: - obj_list = [] - for item in getter(_parent_object): - obj_list.append(self._object_constructor(item)) - return obj_list - - self._object_list = get_object_list_from_parent_object() - - def set_object_list(items: list[ValueT]) -> None: - """Set the object list on the parent AND updates the internal object list.""" - pb_obj_list = [] - - for item in items: - if not isinstance(item, _object_type): - raise TypeError( - f"Expected items of type {_object_type}, got type {type(item)} instead. " - f"Item: {item}." - ) - if not item._check(): - raise RuntimeError("Cannot initialize incomplete object.") - pb_obj_list.append(item._to_pb_object()) - # update callback in case item was copied from another tree object - # or if it is a new object - item._set_callback_apply_changes(self._apply_changes) - setter(_parent_object, pb_obj_list) - # keep object list in sync with the backend. This is needed for the in-place editing - self._object_list = items - - self._set_object_list = set_object_list + @property + def _object_list(self) -> list[ValueT]: + if self._parent_object._is_stored and not self._parent_was_stored: + # There are two scenarios when the parent object becomes + # stored: + # - The _object_list already contained some values. In this case, we + # simply keep it, and make a (inexaustive) check that the size + # matches. + # - The parent object was default-constructed and then its _pb_object + # was then replaced (e.g. by a call to _from_object_info). In this + # case, the empty '_object_list' is no longer reflecting the actual + # state, and needs to be replaced. + # In general, we don't replace the _object_list to ensure any references + # to its elements are correctly updated (and retain the ability to + # update the object itself), but here it's not a concern since this + # only happens within the constructor. + if self._object_list_store: + assert len(self._object_list_store) == len(self._get_object_list_from_parent()) + else: + self._object_list_store = self._get_object_list_from_parent() + self._parent_was_stored = True + return self._object_list_store + + def _get_object_list_from_parent(self) -> list[ValueT]: + obj_list = [] + for item in grpc_data_getter(self._attribute_name, from_protobuf=list)(self._parent_object): + obj_list.append(self._object_constructor(item)) + return obj_list + + def _set_object_list(self, items: list[ValueT]) -> None: + """Set the object list on the parent AND updates the internal object list.""" + # if not self._parent_object._is_stored: + # if items: # accept empty list to allow for default construction + # raise RuntimeError("Cannot set object list before the parent object is stored.") + # else: + pb_obj_list = [] + for item in items: + if not isinstance(item, self._object_type): + raise TypeError( + f"Expected items of type {self._object_type}, got type {type(item)} instead. " + f"Item: {item}." + ) + if not item._check(): + raise RuntimeError("Cannot initialize incomplete object.") + pb_obj_list.append(item._to_pb_object()) + # update callback in case item was copied from another tree object + # or if it is a new object + item._set_callback_apply_changes(self._apply_changes) + grpc_data_setter(self._attribute_name, to_protobuf=lambda x: x)( + self._parent_object, pb_obj_list + ) + # keep object list in sync with the backend. This is needed for the in-place editing + self._object_list_store = items def __len__(self) -> int: return len(self._object_list) diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/polymorphic_from_pb.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/polymorphic_from_pb.py index cc9f85c5ee..e2fc6e7821 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/polymorphic_from_pb.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/polymorphic_from_pb.py @@ -22,13 +22,16 @@ from __future__ import annotations +import typing from typing import Protocol -import grpc from typing_extensions import Self from ansys.api.acp.v0.base_pb2 import ResourcePath +if typing.TYPE_CHECKING: + from ..base import ServerWrapper + __all__ = ["CreatableFromResourcePath", "tree_object_from_resource_path"] @@ -36,12 +39,14 @@ class CreatableFromResourcePath(Protocol): """Interface for objects that can be created from a resource path.""" @classmethod - def _from_resource_path(cls, resource_path: ResourcePath, channel: grpc.Channel) -> Self: ... + def _from_resource_path( + cls, resource_path: ResourcePath, server_wrapper: ServerWrapper + ) -> Self: ... def tree_object_from_resource_path( resource_path: ResourcePath, - channel: grpc.Channel, + server_wrapper: ServerWrapper, allowed_types: tuple[type[CreatableFromResourcePath], ...] | None = None, ) -> CreatableFromResourcePath | None: """Instantiate a tree object from its resource path. @@ -50,8 +55,8 @@ def tree_object_from_resource_path( ---------- resource_path : Resource path of the object. - channel : - gRPC channel to the server. + server_wrapper : + Representation of the ACP server. allowed_types : Allowed types of the object. If None, all registered types are allowed. """ @@ -72,4 +77,4 @@ def tree_object_from_resource_path( f"Resource path {resource_path.value} does not point to a valid " f"object type. Allowed types: {allowed_types}" ) - return resource_class._from_resource_path(resource_path, channel) + return resource_class._from_resource_path(resource_path, server_wrapper=server_wrapper) diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py index 61c55bf6ab..27d30f9f9f 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py @@ -36,6 +36,7 @@ from ansys.api.acp.v0.base_pb2 import ResourcePath from ..._utils.property_protocols import ReadOnlyProperty, ReadWriteProperty +from ..base import TreeObjectBase from .polymorphic_from_pb import CreatableFromResourcePath, tree_object_from_resource_path from .protocols import Editable, GrpcObjectBase, ObjectInfo, Readable @@ -97,7 +98,9 @@ def inner(self: Readable) -> CreatableFromResourcePath | None: self._get() object_resource_path = _get_data_attribute(self._pb_object, name) - return tree_object_from_resource_path(object_resource_path, self._channel) + return tree_object_from_resource_path( + object_resource_path, server_wrapper=self._server_wrapper + ) return inner @@ -128,6 +131,20 @@ def inner(self: Readable) -> Any: return inner +def grpc_linked_object_setter( + name: str, to_protobuf: _TO_PROTOBUF_T[TreeObjectBase | None] +) -> Callable[[Editable, TreeObjectBase | None], None]: + """Create a setter method which updates the linked object via the gRPC Put endpoint.""" + func = grpc_data_setter(name, to_protobuf) + + def inner(self: Editable, value: TreeObjectBase | None) -> None: + if value is not None and not value._is_stored: + raise Exception("Cannot link to an unstored object.") + func(self, value) + + return inner + + def grpc_data_setter( name: str, to_protobuf: _TO_PROTOBUF_T[_SET_T] ) -> Callable[[Editable, _SET_T], None]: @@ -301,7 +318,7 @@ def to_protobuf(obj: TreeObjectBase | None) -> ResourcePath: return _wrap_doc( _exposed_grpc_property(grpc_linked_object_getter(name)).setter( # Resource path represents an object that is not set as an empty string - grpc_data_setter(name=name, to_protobuf=to_protobuf) + grpc_linked_object_setter(name=name, to_protobuf=to_protobuf) ), doc=doc, ) diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py index c5c1a818f7..0c1c0747a7 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py @@ -24,6 +24,7 @@ from collections.abc import Iterable import textwrap +import typing from typing import Any, Protocol from google.protobuf.message import Message @@ -38,6 +39,9 @@ ListRequest, ) +if typing.TYPE_CHECKING: + from ..base import ServerWrapper + class CreateRequest(Protocol): """Interface definition for CreateRequest messages. @@ -176,7 +180,7 @@ def _get_if_stored(self) -> None: ... def _is_stored(self) -> bool: ... @property - def _channel(self) -> grpc.Channel: ... + def _server_wrapper(self) -> ServerWrapper: ... _pb_object: Any diff --git a/src/ansys/acp/core/_tree_objects/linked_selection_rule.py b/src/ansys/acp/core/_tree_objects/linked_selection_rule.py index b955365e60..9d159addbb 100644 --- a/src/ansys/acp/core/_tree_objects/linked_selection_rule.py +++ b/src/ansys/acp/core/_tree_objects/linked_selection_rule.py @@ -212,7 +212,7 @@ def _from_pb_object( allowed_types = tuple(allowed_types_list) selection_rule = tree_object_from_resource_path( - resource_path=message.resource_path, channel=parent_object._channel + resource_path=message.resource_path, server_wrapper=parent_object._server_wrapper ) if not isinstance(selection_rule, allowed_types): raise TypeError( diff --git a/src/ansys/acp/core/_tree_objects/sublaminate.py b/src/ansys/acp/core/_tree_objects/sublaminate.py index 2fb3dca08b..7fe1b01fe4 100644 --- a/src/ansys/acp/core/_tree_objects/sublaminate.py +++ b/src/ansys/acp/core/_tree_objects/sublaminate.py @@ -107,7 +107,7 @@ def _from_pb_object( apply_changes: Callable[[], None], ) -> Lamina: material = tree_object_from_resource_path( - resource_path=message.material, channel=parent_object._channel + resource_path=message.material, server_wrapper=parent_object._server_wrapper ) if not isinstance(material, get_args(_LINKABLE_MATERIAL_TYPES)): diff --git a/tests/unittests/test_sublaminate.py b/tests/unittests/test_sublaminate.py index 3205f96cc2..723d0845d3 100644 --- a/tests/unittests/test_sublaminate.py +++ b/tests/unittests/test_sublaminate.py @@ -110,7 +110,7 @@ def test_add_lamina(parent_object): assert sublaminate.materials[2].angle == -45.0 -def test_load_with_existing_sublamina(acp_instance, parent_object): +def test_load_with_existing_sublaminate(acp_instance, parent_object): """Regression test for bug #561. Checks that sublaminates are correctly loaded from a saved model. @@ -127,6 +127,13 @@ def test_load_with_existing_sublamina(acp_instance, parent_object): sublaminate.add_material(fabric1, angle=45.0) sublaminate.add_material(stackup, angle=0.0) sublaminate.add_material(fabric1, angle=-45.0) + assert len(sublaminate.materials) == 3 + assert sublaminate.materials[0].angle == 45.0 + assert sublaminate.materials[1].angle == 0.0 + assert sublaminate.materials[2].angle == -45.0 + assert isinstance(sublaminate.materials[0].material, Fabric) + assert isinstance(sublaminate.materials[1].material, Stackup) + assert isinstance(sublaminate.materials[2].material, Fabric) # WHEN: the model is saved and loaded with tempfile.TemporaryDirectory() as tmp_dir: @@ -143,7 +150,7 @@ def test_load_with_existing_sublamina(acp_instance, parent_object): assert len(sublaminate.materials) == 3 assert sublaminate.materials[0].angle == 45.0 assert sublaminate.materials[1].angle == 0.0 - assert sublaminate.materials[2].angle == 45.0 + assert sublaminate.materials[2].angle == -45.0 assert isinstance(sublaminate.materials[0].material, Fabric) assert isinstance(sublaminate.materials[1].material, Stackup) assert isinstance(sublaminate.materials[2].material, Fabric) From 5bb80044ee338eaa5d56f0a2956f1382d6004e08 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Wed, 21 Aug 2024 16:07:47 +0200 Subject: [PATCH 03/13] Enhance typing --- .../_tree_objects/_grpc_helpers/property_helper.py | 12 ++++-------- .../core/_tree_objects/_grpc_helpers/protocols.py | 4 ++++ src/ansys/acp/core/_tree_objects/base.py | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py index 27d30f9f9f..0c628d51a0 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/property_helper.py @@ -28,7 +28,6 @@ from __future__ import annotations from functools import reduce -import typing from typing import Any, Callable, TypeVar from google.protobuf.message import Message @@ -36,7 +35,6 @@ from ansys.api.acp.v0.base_pb2 import ResourcePath from ..._utils.property_protocols import ReadOnlyProperty, ReadWriteProperty -from ..base import TreeObjectBase from .polymorphic_from_pb import CreatableFromResourcePath, tree_object_from_resource_path from .protocols import Editable, GrpcObjectBase, ObjectInfo, Readable @@ -132,12 +130,12 @@ def inner(self: Readable) -> Any: def grpc_linked_object_setter( - name: str, to_protobuf: _TO_PROTOBUF_T[TreeObjectBase | None] -) -> Callable[[Editable, TreeObjectBase | None], None]: + name: str, to_protobuf: _TO_PROTOBUF_T[Readable | None] +) -> Callable[[Editable, Readable | None], None]: """Create a setter method which updates the linked object via the gRPC Put endpoint.""" func = grpc_data_setter(name, to_protobuf) - def inner(self: Editable, value: TreeObjectBase | None) -> None: + def inner(self: Editable, value: Readable | None) -> None: if value is not None and not value._is_stored: raise Exception("Cannot link to an unstored object.") func(self, value) @@ -301,10 +299,8 @@ def grpc_link_property( Types which are allowed to be set on the property. An error will be raised if an object of a different type is set. """ - if typing.TYPE_CHECKING: - from ..base import TreeObjectBase - def to_protobuf(obj: TreeObjectBase | None) -> ResourcePath: + def to_protobuf(obj: Readable | None) -> ResourcePath: if obj is None: return ResourcePath(value="") if not isinstance(obj, allowed_types): diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py index 0c1c0747a7..334b5e8bfa 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py @@ -37,6 +37,7 @@ Empty, GetRequest, ListRequest, + ResourcePath, ) if typing.TYPE_CHECKING: @@ -182,6 +183,9 @@ def _is_stored(self) -> bool: ... @property def _server_wrapper(self) -> ServerWrapper: ... + @property + def _resource_path(self) -> ResourcePath: ... + _pb_object: Any diff --git a/src/ansys/acp/core/_tree_objects/base.py b/src/ansys/acp/core/_tree_objects/base.py index 21e495c5a6..9bc0be135b 100644 --- a/src/ansys/acp/core/_tree_objects/base.py +++ b/src/ansys/acp/core/_tree_objects/base.py @@ -479,5 +479,6 @@ def inner(self: T, /, *args: P.args, **kwargs: P.kwargs) -> R: if typing.TYPE_CHECKING: # Ensure that the ReadOnlyTreeObject satisfies the Gettable interface _x: Readable = typing.cast(ReadOnlyTreeObject, None) - # Ensure that the TreeObject satisfies the Editable interface + # Ensure that the TreeObject satisfies the Editable and Readable interfaces _y: Editable = typing.cast(TreeObject, None) + _z: Readable = typing.cast(TreeObject, None) From 8ca5ced078a43d40980878a03f4777b53da57969 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Wed, 21 Aug 2024 16:13:21 +0200 Subject: [PATCH 04/13] Add type check for CreatableFromResourcePath --- src/ansys/acp/core/_tree_objects/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ansys/acp/core/_tree_objects/base.py b/src/ansys/acp/core/_tree_objects/base.py index 9bc0be135b..d8ba323d25 100644 --- a/src/ansys/acp/core/_tree_objects/base.py +++ b/src/ansys/acp/core/_tree_objects/base.py @@ -43,6 +43,7 @@ from .._utils.resource_paths import to_parts from ._grpc_helpers.exceptions import wrap_grpc_errors from ._grpc_helpers.linked_object_helpers import linked_path_fields, unlink_objects +from ._grpc_helpers.polymorphic_from_pb import CreatableFromResourcePath from ._grpc_helpers.property_helper import ( _get_data_attribute, grpc_data_property, @@ -482,3 +483,6 @@ def inner(self: T, /, *args: P.args, **kwargs: P.kwargs) -> R: # Ensure that the TreeObject satisfies the Editable and Readable interfaces _y: Editable = typing.cast(TreeObject, None) _z: Readable = typing.cast(TreeObject, None) + + # Ensure the TreeObjectBase satisfies the CreatableFromResourcePath interface + _a: CreatableFromResourcePath = typing.cast(TreeObjectBase, None) From baeb344cfa1b6e287e7341fff4de10d3ef896286 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Wed, 21 Aug 2024 17:04:18 +0200 Subject: [PATCH 05/13] Fix new test --- tests/unittests/test_sublaminate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unittests/test_sublaminate.py b/tests/unittests/test_sublaminate.py index 723d0845d3..d56c8d5f37 100644 --- a/tests/unittests/test_sublaminate.py +++ b/tests/unittests/test_sublaminate.py @@ -137,7 +137,7 @@ def test_load_with_existing_sublaminate(acp_instance, parent_object): # WHEN: the model is saved and loaded with tempfile.TemporaryDirectory() as tmp_dir: - if acp_instance.is_remote: + if not acp_instance.is_remote: file_path: PATH = pathlib.Path(tmp_dir) / "model.acph5" else: file_path = "model.acph5" From c9a0a482bb9495718669465577629468ce81bf3b Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Wed, 21 Aug 2024 17:09:37 +0200 Subject: [PATCH 06/13] Fix order of attribute creation in EdgePropertyList --- .../acp/core/_tree_objects/_grpc_helpers/edge_property_list.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py index e7fbc737be..855ee22f97 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py @@ -153,13 +153,12 @@ def __init__( self._attribute_name = _attribute_name self._name = _attribute_name.split(".")[-1] - self._object_list_store = self._get_object_list_from_parent() - self._object_constructor: Callable[[Message], ValueT] = ( lambda pb_object: _from_pb_constructor( self._parent_object, pb_object, self._apply_changes ) ) + self._object_list_store = self._get_object_list_from_parent() @property def _object_list(self) -> list[ValueT]: From b6bd031126a7b18f2ab03fc0ef94f97741778548 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Wed, 21 Aug 2024 20:41:46 +0200 Subject: [PATCH 07/13] Add more test cases for the edge property list --- tests/unittests/test_sublaminate.py | 88 ++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 28 deletions(-) diff --git a/tests/unittests/test_sublaminate.py b/tests/unittests/test_sublaminate.py index d56c8d5f37..dca98c57e9 100644 --- a/tests/unittests/test_sublaminate.py +++ b/tests/unittests/test_sublaminate.py @@ -25,7 +25,7 @@ import pytest -from ansys.acp.core import Fabric, FabricWithAngle, Lamina, Stackup, SymmetryType +from ansys.acp.core import FabricWithAngle, Lamina, SymmetryType from ansys.acp.core._typing_helper import PATH from .common.tree_object_tester import NoLockedMixin, ObjectPropertiesToTest, TreeObjectTester @@ -110,30 +110,31 @@ def test_add_lamina(parent_object): assert sublaminate.materials[2].angle == -45.0 -def test_load_with_existing_sublaminate(acp_instance, parent_object): +@pytest.fixture +def simple_sublaminate(parent_object): + sublaminate = parent_object.create_sublaminate(name="simple_sublaminate") + sublaminate.add_material( + parent_object.create_fabric(name="fabric1", material=parent_object.create_material()), + angle=0.0, + ) + sublaminate.add_material( + parent_object.create_fabric(name="fabric2", material=parent_object.create_material()), + angle=10.0, + ) + return sublaminate + + +def test_load_with_existing_sublaminate(acp_instance, parent_object, simple_sublaminate): """Regression test for bug #561. Checks that sublaminates are correctly loaded from a saved model. """ model = parent_object - # GIVEN: a model with a sublaminate which has three materials - fabric1 = model.create_fabric() - fabric1.material = model.create_material() - stackup = model.create_stackup() - stackup.add_fabric(fabric1, angle=30.0) - stackup.add_fabric(fabric1, angle=-30.0) - - sublaminate = model.create_sublaminate(name="test_sublaminate") - sublaminate.add_material(fabric1, angle=45.0) - sublaminate.add_material(stackup, angle=0.0) - sublaminate.add_material(fabric1, angle=-45.0) - assert len(sublaminate.materials) == 3 - assert sublaminate.materials[0].angle == 45.0 - assert sublaminate.materials[1].angle == 0.0 - assert sublaminate.materials[2].angle == -45.0 - assert isinstance(sublaminate.materials[0].material, Fabric) - assert isinstance(sublaminate.materials[1].material, Stackup) - assert isinstance(sublaminate.materials[2].material, Fabric) + # GIVEN: a model with a sublaminate which has two materials + sublaminate = simple_sublaminate + assert len(sublaminate.materials) == 2 + assert [m.angle for m in sublaminate.materials] == [0.0, 10.0] + assert [m.material.name for m in sublaminate.materials] == ["fabric1", "fabric2"] # WHEN: the model is saved and loaded with tempfile.TemporaryDirectory() as tmp_dir: @@ -146,11 +147,42 @@ def test_load_with_existing_sublaminate(acp_instance, parent_object): model = acp_instance.import_model(path=file_path) # THEN: the sublaminate is still present and has the same materials - sublaminate = model.sublaminates["test_sublaminate"] - assert len(sublaminate.materials) == 3 - assert sublaminate.materials[0].angle == 45.0 - assert sublaminate.materials[1].angle == 0.0 - assert sublaminate.materials[2].angle == -45.0 - assert isinstance(sublaminate.materials[0].material, Fabric) - assert isinstance(sublaminate.materials[1].material, Stackup) - assert isinstance(sublaminate.materials[2].material, Fabric) + sublaminate = model.sublaminates["simple_sublaminate"] + assert len(sublaminate.materials) == 2 + assert [m.angle for m in sublaminate.materials] == [0.0, 10.0] + assert [m.material.name for m in sublaminate.materials] == ["fabric1", "fabric2"] + + +def test_clone_edge_property_list(parent_object, simple_sublaminate): + model = parent_object + # GIVEN: a model with a sublaminate which has two materials + sublaminate = simple_sublaminate + assert len(sublaminate.materials) == 2 + assert [m.angle for m in sublaminate.materials] == [0.0, 10.0] + assert [m.material.name for m in sublaminate.materials] == ["fabric1", "fabric2"] + + # WHEN: cloning the sublaminate, then storing the clone + sublaminate_clone = sublaminate.clone() + sublaminate_clone.store(parent=model) + + # THEN: the clone is stored and has the same materials + assert len(sublaminate.materials) == 2 + assert [m.angle for m in sublaminate.materials] == [0.0, 10.0] + assert [m.material.name for m in sublaminate.materials] == ["fabric1", "fabric2"] + + +def test_clone_edge_property_list_cleared(parent_object, simple_sublaminate): + model = parent_object + # GIVEN: a model with a sublaminate which has two materials + sublaminate = simple_sublaminate + assert len(sublaminate.materials) == 2 + assert [m.angle for m in sublaminate.materials] == [0.0, 10.0] + assert [m.material.name for m in sublaminate.materials] == ["fabric1", "fabric2"] + + # WHEN: cloning the sublaminate, removing the materials, then storing the clone + sublaminate_clone = sublaminate.clone() + sublaminate_clone.materials = [] + sublaminate_clone.store(parent=model) + + # THEN: the clone is stored and has no materials + assert len(sublaminate_clone.materials) == 0 From a55460c16c67c2cc32fe24e5008c2713c1ce03fe Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Thu, 22 Aug 2024 08:25:01 +0200 Subject: [PATCH 08/13] Clean up diff --- .../core/_tree_objects/_grpc_helpers/edge_property_list.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py index 855ee22f97..f12c292aeb 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py @@ -191,10 +191,6 @@ def _get_object_list_from_parent(self) -> list[ValueT]: def _set_object_list(self, items: list[ValueT]) -> None: """Set the object list on the parent AND updates the internal object list.""" - # if not self._parent_object._is_stored: - # if items: # accept empty list to allow for default construction - # raise RuntimeError("Cannot set object list before the parent object is stored.") - # else: pb_obj_list = [] for item in items: if not isinstance(item, self._object_type): From 07d70d7b67b4c3438e27d1d281fb5ec5933f5414 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Thu, 22 Aug 2024 09:56:46 +0200 Subject: [PATCH 09/13] Disallow accessing EdgePropertyList on unstored objects --- .../_grpc_helpers/edge_property_list.py | 11 +- tests/unittests/test_edge_property_list.py | 155 ++++++++++++++++++ tests/unittests/test_sublaminate.py | 82 --------- 3 files changed, 164 insertions(+), 84 deletions(-) create mode 100644 tests/unittests/test_edge_property_list.py diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py index f12c292aeb..b1be83167a 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py @@ -393,7 +393,13 @@ def define_edge_property_list( ) -> Any: """Define a list of linked tree objects with link properties.""" - def getter(self: CreatableTreeObject) -> EdgePropertyList[GenericEdgePropertyType]: + def getter( + self: CreatableTreeObject, *, check_stored: bool = True + ) -> EdgePropertyList[GenericEdgePropertyType]: + if check_stored and not self._is_stored: + raise RuntimeError( + f"Cannot access property {attribute_name.split('.')[-1]} on an object that is not stored." + ) return EdgePropertyList._initialize_with_cache( parent_object=self, object_type=value_type, @@ -402,7 +408,8 @@ def getter(self: CreatableTreeObject) -> EdgePropertyList[GenericEdgePropertyTyp ) def setter(self: CreatableTreeObject, value: list[GenericEdgePropertyType]) -> None: - getter(self)[:] = value + # allow wholesale replacement on unstored objects + getter(self, check_stored=False)[:] = value return _wrap_doc(_exposed_grpc_property(getter).setter(setter), doc=doc) diff --git a/tests/unittests/test_edge_property_list.py b/tests/unittests/test_edge_property_list.py new file mode 100644 index 0000000000..5c4c5a6c4e --- /dev/null +++ b/tests/unittests/test_edge_property_list.py @@ -0,0 +1,155 @@ +# Copyright (C) 2022 - 2024 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""Tests for the EdgePropertyList container.""" + +import pathlib +import tempfile + +import pytest + +from ansys.acp.core import Lamina, SubLaminate +from ansys.acp.core._typing_helper import PATH + + +@pytest.fixture +def simple_model(load_model_from_tempfile): + with load_model_from_tempfile() as model: + yield model + + +@pytest.fixture +def simple_sublaminate(simple_model, check_simple_sublaminate): + """Simple sublaminate whose materials are stored in an edge property list.""" + sublaminate = simple_model.create_sublaminate(name="simple_sublaminate") + sublaminate.add_material( + simple_model.create_fabric(name="fabric1", material=simple_model.create_material()), + angle=0.0, + ) + sublaminate.add_material( + simple_model.create_fabric(name="fabric2", material=simple_model.create_material()), + angle=10.0, + ) + check_simple_sublaminate(sublaminate) + return sublaminate + + +@pytest.fixture +def check_simple_sublaminate(): + """Provides a function to check the simple sublaminate.""" + + def check(sublaminate): + assert sublaminate.name == "simple_sublaminate" + assert len(sublaminate.materials) == 2 + assert [m.angle for m in sublaminate.materials] == [0.0, 10.0] + assert [m.material.name for m in sublaminate.materials] == ["fabric1", "fabric2"] + + return check + + +def test_save_load_with_existing_entries( + acp_instance, simple_model, simple_sublaminate, check_simple_sublaminate +): + """Regression test for bug #561. + + Checks that sublaminates are correctly loaded from a saved model. + """ + model = simple_model + # GIVEN: a model with a sublaminate which has two materials + sublaminate = simple_sublaminate + + # WHEN: the model is saved and loaded + with tempfile.TemporaryDirectory() as tmp_dir: + if not acp_instance.is_remote: + file_path: PATH = pathlib.Path(tmp_dir) / "model.acph5" + else: + file_path = "model.acph5" + model.save(file_path) + acp_instance.clear() + model = acp_instance.import_model(path=file_path) + + # THEN: the sublaminate is still present and has the same materials + sublaminate = model.sublaminates["simple_sublaminate"] + check_simple_sublaminate(sublaminate) + + +def test_clone_store(simple_model, simple_sublaminate, check_simple_sublaminate): + """Check that the edge property list is preserved when cloning and storing an object.""" + model = simple_model + # GIVEN: a model with a sublaminate which has two materials + sublaminate = simple_sublaminate + + # WHEN: cloning the sublaminate, then storing the clone + sublaminate_clone = sublaminate.clone() + sublaminate_clone.store(parent=model) + + # THEN: the clone is stored and has the same materials + check_simple_sublaminate(sublaminate_clone) + + +def test_clone_access_raises(simple_model, simple_sublaminate, check_simple_sublaminate): + """Check that the EdgePropertyList cannot be accessed on an unstored object.""" + model = simple_model + # GIVEN: a model with a sublaminate which has two materials + sublaminate = simple_sublaminate + + # WHEN: cloning the sublaminate + sublaminate_clone = sublaminate.clone() + + # THEN: accessing the materials raises an error + with pytest.raises(RuntimeError): + sublaminate_clone.materials + + +def test_clone_clear_store(simple_model, simple_sublaminate): + """Check that the edge property list can be cleared on a cloned object.""" + model = simple_model + # GIVEN: a model with a sublaminate which has two materials + sublaminate = simple_sublaminate + + # WHEN: cloning the sublaminate, removing the materials, then storing the clone + sublaminate_clone = sublaminate.clone() + sublaminate_clone.materials = [] + sublaminate_clone.store(parent=model) + + # THEN: the clone is stored and has no materials + assert len(sublaminate_clone.materials) == 0 + + +def test_store_with_entries(simple_model, check_simple_sublaminate): + """Check that a sublaminate can be created with materials, and then stored.""" + fabric1 = simple_model.create_fabric(name="fabric1", material=simple_model.create_material()) + fabric2 = simple_model.create_fabric(name="fabric2", material=simple_model.create_material()) + + sublaminate = SubLaminate( + name="simple_sublaminate", + materials=[Lamina(material=fabric1, angle=0.0), Lamina(material=fabric2, angle=10.0)], + ) + sublaminate.store(parent=simple_model) + check_simple_sublaminate(sublaminate) + + +def test_wrong_type_raises(simple_model): + """Check that assigning a wrong type to the materials raises an error.""" + sublaminate = simple_model.create_sublaminate(name="simple_sublaminate") + with pytest.raises(TypeError): + sublaminate.materials = [1] diff --git a/tests/unittests/test_sublaminate.py b/tests/unittests/test_sublaminate.py index dca98c57e9..67fc566c4d 100644 --- a/tests/unittests/test_sublaminate.py +++ b/tests/unittests/test_sublaminate.py @@ -20,13 +20,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import pathlib -import tempfile - import pytest from ansys.acp.core import FabricWithAngle, Lamina, SymmetryType -from ansys.acp.core._typing_helper import PATH from .common.tree_object_tester import NoLockedMixin, ObjectPropertiesToTest, TreeObjectTester @@ -108,81 +104,3 @@ def test_add_lamina(parent_object): assert sublaminate.materials[1].angle == 0.0 assert sublaminate.materials[2].material == fabric1 assert sublaminate.materials[2].angle == -45.0 - - -@pytest.fixture -def simple_sublaminate(parent_object): - sublaminate = parent_object.create_sublaminate(name="simple_sublaminate") - sublaminate.add_material( - parent_object.create_fabric(name="fabric1", material=parent_object.create_material()), - angle=0.0, - ) - sublaminate.add_material( - parent_object.create_fabric(name="fabric2", material=parent_object.create_material()), - angle=10.0, - ) - return sublaminate - - -def test_load_with_existing_sublaminate(acp_instance, parent_object, simple_sublaminate): - """Regression test for bug #561. - - Checks that sublaminates are correctly loaded from a saved model. - """ - model = parent_object - # GIVEN: a model with a sublaminate which has two materials - sublaminate = simple_sublaminate - assert len(sublaminate.materials) == 2 - assert [m.angle for m in sublaminate.materials] == [0.0, 10.0] - assert [m.material.name for m in sublaminate.materials] == ["fabric1", "fabric2"] - - # WHEN: the model is saved and loaded - with tempfile.TemporaryDirectory() as tmp_dir: - if not acp_instance.is_remote: - file_path: PATH = pathlib.Path(tmp_dir) / "model.acph5" - else: - file_path = "model.acph5" - model.save(file_path) - acp_instance.clear() - model = acp_instance.import_model(path=file_path) - - # THEN: the sublaminate is still present and has the same materials - sublaminate = model.sublaminates["simple_sublaminate"] - assert len(sublaminate.materials) == 2 - assert [m.angle for m in sublaminate.materials] == [0.0, 10.0] - assert [m.material.name for m in sublaminate.materials] == ["fabric1", "fabric2"] - - -def test_clone_edge_property_list(parent_object, simple_sublaminate): - model = parent_object - # GIVEN: a model with a sublaminate which has two materials - sublaminate = simple_sublaminate - assert len(sublaminate.materials) == 2 - assert [m.angle for m in sublaminate.materials] == [0.0, 10.0] - assert [m.material.name for m in sublaminate.materials] == ["fabric1", "fabric2"] - - # WHEN: cloning the sublaminate, then storing the clone - sublaminate_clone = sublaminate.clone() - sublaminate_clone.store(parent=model) - - # THEN: the clone is stored and has the same materials - assert len(sublaminate.materials) == 2 - assert [m.angle for m in sublaminate.materials] == [0.0, 10.0] - assert [m.material.name for m in sublaminate.materials] == ["fabric1", "fabric2"] - - -def test_clone_edge_property_list_cleared(parent_object, simple_sublaminate): - model = parent_object - # GIVEN: a model with a sublaminate which has two materials - sublaminate = simple_sublaminate - assert len(sublaminate.materials) == 2 - assert [m.angle for m in sublaminate.materials] == [0.0, 10.0] - assert [m.material.name for m in sublaminate.materials] == ["fabric1", "fabric2"] - - # WHEN: cloning the sublaminate, removing the materials, then storing the clone - sublaminate_clone = sublaminate.clone() - sublaminate_clone.materials = [] - sublaminate_clone.store(parent=model) - - # THEN: the clone is stored and has no materials - assert len(sublaminate_clone.materials) == 0 From 4e6b329c612f8619ee2abe1661e779e878785fab Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Thu, 22 Aug 2024 10:09:00 +0200 Subject: [PATCH 10/13] Mark test_edge_property_list_parent_store as expected failure --- tests/unittests/test_object_permanence.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unittests/test_object_permanence.py b/tests/unittests/test_object_permanence.py index b6f850358d..c09f6b1e02 100644 --- a/tests/unittests/test_object_permanence.py +++ b/tests/unittests/test_object_permanence.py @@ -145,6 +145,9 @@ def test_edge_property_list_parent_deleted(model): def test_edge_property_list_parent_store(model): """Check that the edge property list identity is unique even after its parent is stored.""" + pytest.xfail( + "We no longer allow accessing the edge property list while the parent is unstored." + ) stackup = pyacp.Stackup() fabrics = stackup.fabrics stackup.store(parent=model) From c5c89631fd21210ba4a0f056dc29a7a4d5bb81ab Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Thu, 22 Aug 2024 15:55:33 +0200 Subject: [PATCH 11/13] Add another EdgePropertyList test --- tests/unittests/test_edge_property_list.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/unittests/test_edge_property_list.py b/tests/unittests/test_edge_property_list.py index 5c4c5a6c4e..da924b8a4e 100644 --- a/tests/unittests/test_edge_property_list.py +++ b/tests/unittests/test_edge_property_list.py @@ -27,7 +27,7 @@ import pytest -from ansys.acp.core import Lamina, SubLaminate +from ansys.acp.core import Fabric, Lamina, SubLaminate from ansys.acp.core._typing_helper import PATH @@ -153,3 +153,13 @@ def test_wrong_type_raises(simple_model): sublaminate = simple_model.create_sublaminate(name="simple_sublaminate") with pytest.raises(TypeError): sublaminate.materials = [1] + + +def test_incomplete_object_check(simple_model): + """Check that unstored objects cannot be added to the edge property list.""" + sublaminate = simple_model.create_sublaminate(name="simple_sublaminate") + with pytest.raises(RuntimeError) as e: + sublaminate.materials.append( + Lamina(material=Fabric(material=simple_model.create_material()), angle=0.0) + ) + assert "incomplete object" in str(e.value) From 5dd861e023bb5f8073b1d43ac17d361012ed39be Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Thu, 22 Aug 2024 16:15:33 +0200 Subject: [PATCH 12/13] Add no-cover pragmas to ignore type checking code also on codecov --- .../core/_tree_objects/_grpc_helpers/polymorphic_from_pb.py | 2 +- src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py | 2 +- src/ansys/acp/core/_tree_objects/_mesh_data.py | 2 +- src/ansys/acp/core/_tree_objects/base.py | 4 ++-- src/ansys/acp/core/_tree_objects/linked_selection_rule.py | 2 +- src/ansys/acp/core/_tree_objects/oriented_selection_set.py | 2 +- src/ansys/acp/core/_tree_objects/virtual_geometry.py | 2 +- src/ansys/acp/core/_workflow.py | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/polymorphic_from_pb.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/polymorphic_from_pb.py index e2fc6e7821..25ba3f996c 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/polymorphic_from_pb.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/polymorphic_from_pb.py @@ -29,7 +29,7 @@ from ansys.api.acp.v0.base_pb2 import ResourcePath -if typing.TYPE_CHECKING: +if typing.TYPE_CHECKING: # pragma: no cover from ..base import ServerWrapper __all__ = ["CreatableFromResourcePath", "tree_object_from_resource_path"] diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py index 334b5e8bfa..fdc2410dda 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/protocols.py @@ -40,7 +40,7 @@ ResourcePath, ) -if typing.TYPE_CHECKING: +if typing.TYPE_CHECKING: # pragma: no cover from ..base import ServerWrapper diff --git a/src/ansys/acp/core/_tree_objects/_mesh_data.py b/src/ansys/acp/core/_tree_objects/_mesh_data.py index 6e0bfb54a9..415382bf13 100644 --- a/src/ansys/acp/core/_tree_objects/_mesh_data.py +++ b/src/ansys/acp/core/_tree_objects/_mesh_data.py @@ -44,7 +44,7 @@ nodal_data_type_to_pb, ) -if typing.TYPE_CHECKING: +if typing.TYPE_CHECKING: # pragma: no cover from .model import MeshData # avoid circular import __all__ = [ diff --git a/src/ansys/acp/core/_tree_objects/base.py b/src/ansys/acp/core/_tree_objects/base.py index d8ba323d25..b006695556 100644 --- a/src/ansys/acp/core/_tree_objects/base.py +++ b/src/ansys/acp/core/_tree_objects/base.py @@ -62,7 +62,7 @@ ) from ._object_cache import ObjectCacheMixin, constructor_with_cache -if typing.TYPE_CHECKING: +if typing.TYPE_CHECKING: # pragma: no cover from .._server import ACP @@ -477,7 +477,7 @@ def inner(self: T, /, *args: P.args, **kwargs: P.kwargs) -> R: return decorator -if typing.TYPE_CHECKING: +if typing.TYPE_CHECKING: # pragma: no cover # Ensure that the ReadOnlyTreeObject satisfies the Gettable interface _x: Readable = typing.cast(ReadOnlyTreeObject, None) # Ensure that the TreeObject satisfies the Editable and Readable interfaces diff --git a/src/ansys/acp/core/_tree_objects/linked_selection_rule.py b/src/ansys/acp/core/_tree_objects/linked_selection_rule.py index 9d159addbb..f4df67d4fc 100644 --- a/src/ansys/acp/core/_tree_objects/linked_selection_rule.py +++ b/src/ansys/acp/core/_tree_objects/linked_selection_rule.py @@ -45,7 +45,7 @@ from .tube_selection_rule import TubeSelectionRule from .variable_offset_selection_rule import VariableOffsetSelectionRule -if typing.TYPE_CHECKING: +if typing.TYPE_CHECKING: # pragma: no cover # Since the 'LinkedSelectionRule' class is used by the boolean selection rule, # this would cause a circular import at run-time. from .boolean_selection_rule import BooleanSelectionRule diff --git a/src/ansys/acp/core/_tree_objects/oriented_selection_set.py b/src/ansys/acp/core/_tree_objects/oriented_selection_set.py index bc426134b3..f3b52fbcf6 100644 --- a/src/ansys/acp/core/_tree_objects/oriented_selection_set.py +++ b/src/ansys/acp/core/_tree_objects/oriented_selection_set.py @@ -79,7 +79,7 @@ "OrientedSelectionSetNodalData", ] -if typing.TYPE_CHECKING: +if typing.TYPE_CHECKING: # pragma: no cover # Since the 'LinkedSelectionRule' class is used by the boolean selection rule, # this would cause a circular import at run-time. from .. import BooleanSelectionRule, GeometricalSelectionRule diff --git a/src/ansys/acp/core/_tree_objects/virtual_geometry.py b/src/ansys/acp/core/_tree_objects/virtual_geometry.py index 9ad4a02ff4..2a25ace61f 100644 --- a/src/ansys/acp/core/_tree_objects/virtual_geometry.py +++ b/src/ansys/acp/core/_tree_objects/virtual_geometry.py @@ -39,7 +39,7 @@ from .enums import status_type_from_pb, virtual_geometry_dimension_from_pb from .object_registry import register -if typing.TYPE_CHECKING: +if typing.TYPE_CHECKING: # pragma: no cover from .cad_component import CADComponent diff --git a/src/ansys/acp/core/_workflow.py b/src/ansys/acp/core/_workflow.py index 23dd579b3e..fcbdf6adbc 100644 --- a/src/ansys/acp/core/_workflow.py +++ b/src/ansys/acp/core/_workflow.py @@ -33,7 +33,7 @@ from ._typing_helper import PATH # Avoid dependencies on pydpf-composites and dpf-core if it is not used -if typing.TYPE_CHECKING: +if typing.TYPE_CHECKING: # pragma: no cover from ansys.dpf.composites.data_sources import ContinuousFiberCompositesFiles from ansys.dpf.core import UnitSystem From 661edaf3e1f23a01ad605f40b7d164dcc1a6f5c9 Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Fri, 23 Aug 2024 14:41:38 +0200 Subject: [PATCH 13/13] Fix instantiation of EdgePropertyList with existing objects in the Protobuf msg --- .../_grpc_helpers/edge_property_list.py | 6 ++++- tests/unittests/test_edge_property_list.py | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py index b1be83167a..032abbb79f 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/edge_property_list.py @@ -158,7 +158,11 @@ def __init__( self._parent_object, pb_object, self._apply_changes ) ) - self._object_list_store = self._get_object_list_from_parent() + if self._parent_object._is_stored: + self._object_list_store = self._get_object_list_from_parent() + else: + # Cannot instantiate the objects if the server is not available + self._object_list_store = [] @property def _object_list(self) -> list[ValueT]: diff --git a/tests/unittests/test_edge_property_list.py b/tests/unittests/test_edge_property_list.py index da924b8a4e..40cca5a226 100644 --- a/tests/unittests/test_edge_property_list.py +++ b/tests/unittests/test_edge_property_list.py @@ -135,6 +135,28 @@ def test_clone_clear_store(simple_model, simple_sublaminate): assert len(sublaminate_clone.materials) == 0 +def test_clone_assign_store(simple_model, simple_sublaminate): + """Check that the edge property list can be changed on a cloned object.""" + model = simple_model + # GIVEN: a model with a sublaminate which has two materials + sublaminate = simple_sublaminate + + # WHEN: cloning the sublaminate, setting new materials, then storing the clone + sublaminate_clone = sublaminate.clone() + import gc + + gc.collect() + fabric = simple_model.create_fabric(name="new_fabric", material=simple_model.create_material()) + new_materials = [Lamina(material=fabric, angle=3.0)] + sublaminate_clone.materials = new_materials + sublaminate_clone.store(parent=model) + + # THEN: the clone is stored and has no materials + assert len(sublaminate_clone.materials) == 1 + assert sublaminate_clone.materials[0].material.name == "new_fabric" + assert sublaminate_clone.materials[0].angle == 3.0 + + def test_store_with_entries(simple_model, check_simple_sublaminate): """Check that a sublaminate can be created with materials, and then stored.""" fabric1 = simple_model.create_fabric(name="fabric1", material=simple_model.create_material())