diff --git a/flow360/component/simulation/framework/entity_base.py b/flow360/component/simulation/framework/entity_base.py index 2b794d0a0..a3dd6c989 100644 --- a/flow360/component/simulation/framework/entity_base.py +++ b/flow360/component/simulation/framework/entity_base.py @@ -3,7 +3,6 @@ from __future__ import annotations import hashlib -import uuid from abc import ABCMeta from collections import defaultdict from typing import Annotated, ClassVar, List, Optional, Union, get_args, get_origin @@ -15,11 +14,6 @@ from flow360.component.simulation.utils import is_exact_instance -def generate_uuid(): - """generate a unique identifier for non-persistent entities. Required by front end.""" - return str(uuid.uuid4()) - - class EntityBase(Flow360BaseModel, metaclass=ABCMeta): """ Base class for dynamic entity types. diff --git a/flow360/component/simulation/framework/entity_selector.py b/flow360/component/simulation/framework/entity_selector.py index 36a353d94..fc2efba73 100644 --- a/flow360/component/simulation/framework/entity_selector.py +++ b/flow360/component/simulation/framework/entity_selector.py @@ -14,6 +14,7 @@ from typing_extensions import Self from flow360.component.simulation.framework.base_model import Flow360BaseModel +from flow360.component.simulation.framework.entity_utils import generate_uuid # These corresponds to the private_attribute_entity_type_name of supported entity types. TargetClass = Literal["Surface", "Edge", "GenericVolume", "GeometryBodyGroup"] @@ -49,6 +50,14 @@ class EntitySelector(Flow360BaseModel): """ target_class: TargetClass = pd.Field() + description: Optional[str] = pd.Field( + None, description="Customizable description of the selector." + ) + selector_id: str = pd.Field( + default_factory=generate_uuid, + description="[Internal] Unique identifier for the selector.", + frozen=True, + ) # Unique name for global reuse name: str = pd.Field(description="Unique name for this selector.") logic: Literal["AND", "OR"] = pd.Field("AND") @@ -204,6 +213,7 @@ def match( attribute: Literal["name"] = "name", syntax: Literal["glob", "regex"] = "glob", logic: Literal["AND", "OR"] = "AND", + description: Optional[str] = None, ) -> EntitySelector: """ Create an EntitySelector for this class and seed it with one matches predicate. @@ -227,7 +237,10 @@ def match( _validate_selector_pattern("match", pattern) selector = generate_entity_selector_from_class( - selector_name=name, entity_class=cls, logic=logic + selector_name=name, + entity_class=cls, + logic=logic, + selector_description=description, ) selector.match(pattern, attribute=attribute, syntax=syntax) return selector @@ -243,6 +256,7 @@ def not_match( attribute: Literal["name"] = "name", syntax: Literal["glob", "regex"] = "glob", logic: Literal["AND", "OR"] = "AND", + description: Optional[str] = None, ) -> EntitySelector: """Create an EntitySelector and seed a notMatches predicate. @@ -261,11 +275,15 @@ def not_match( _validate_selector_pattern("not_match", pattern) selector = generate_entity_selector_from_class( - selector_name=name, entity_class=cls, logic=logic + selector_name=name, + entity_class=cls, + logic=logic, + selector_description=description, ) selector.not_match(pattern, attribute=attribute, syntax=syntax) return selector + # pylint: disable=too-many-arguments @classmethod def any_of( cls, @@ -275,6 +293,7 @@ def any_of( name: str, attribute: Literal["name"] = "name", logic: Literal["AND", "OR"] = "AND", + description: Optional[str] = None, ) -> EntitySelector: """Create an EntitySelector and seed an in predicate. @@ -292,11 +311,15 @@ def any_of( _validate_selector_values("any_of", values) selector = generate_entity_selector_from_class( - selector_name=name, entity_class=cls, logic=logic + selector_name=name, + entity_class=cls, + logic=logic, + selector_description=description, ) selector.any_of(values, attribute=attribute) return selector + # pylint: disable=too-many-arguments @classmethod def not_any_of( cls, @@ -306,6 +329,7 @@ def not_any_of( name: str, attribute: Literal["name"] = "name", logic: Literal["AND", "OR"] = "AND", + description: Optional[str] = None, ) -> EntitySelector: """Create an EntitySelector and seed a notIn predicate. @@ -320,14 +344,20 @@ def not_any_of( _validate_selector_values("not_any_of", values) # type: ignore[arg-type] selector = generate_entity_selector_from_class( - selector_name=name, entity_class=cls, logic=logic + selector_name=name, + entity_class=cls, + logic=logic, + selector_description=description, ) selector.not_any_of(values, attribute=attribute) return selector def generate_entity_selector_from_class( - selector_name: str, entity_class: type, logic: Literal["AND", "OR"] = "AND" + selector_name: str, + entity_class: type, + logic: Literal["AND", "OR"] = "AND", + selector_description: Optional[str] = None, ) -> EntitySelector: """ Create a new selector for the given entity class. @@ -340,7 +370,13 @@ def generate_entity_selector_from_class( class_name in allowed_classes ), f"Unknown entity class: {entity_class} for generating entity selector." - return EntitySelector(name=selector_name, target_class=class_name, logic=logic, children=[]) + return EntitySelector( + name=selector_name, + description=selector_description, + target_class=class_name, + logic=logic, + children=[], + ) ########## EXPANSION IMPLEMENTATION ########## diff --git a/flow360/component/simulation/framework/entity_utils.py b/flow360/component/simulation/framework/entity_utils.py new file mode 100644 index 000000000..73e1f556f --- /dev/null +++ b/flow360/component/simulation/framework/entity_utils.py @@ -0,0 +1,8 @@ +"""Shared utilities for entity operations.""" + +import uuid + + +def generate_uuid(): + """generate a unique identifier for non-persistent entities. Required by front end.""" + return str(uuid.uuid4()) diff --git a/flow360/component/simulation/framework/updater.py b/flow360/component/simulation/framework/updater.py index 54edd5c48..71cb1a66d 100644 --- a/flow360/component/simulation/framework/updater.py +++ b/flow360/component/simulation/framework/updater.py @@ -11,7 +11,7 @@ import re from typing import Any -from flow360.component.simulation.framework.entity_base import generate_uuid +from flow360.component.simulation.framework.entity_utils import generate_uuid from flow360.component.simulation.framework.updater_functions import ( fix_ghost_sphere_schema, populate_entity_id_with_name, diff --git a/flow360/component/simulation/models/surface_models.py b/flow360/component/simulation/models/surface_models.py index c50a36f2c..cc53c0cbb 100644 --- a/flow360/component/simulation/models/surface_models.py +++ b/flow360/component/simulation/models/surface_models.py @@ -9,7 +9,8 @@ import flow360.component.simulation.units as u from flow360.component.simulation.framework.base_model import Flow360BaseModel -from flow360.component.simulation.framework.entity_base import EntityList, generate_uuid +from flow360.component.simulation.framework.entity_base import EntityList +from flow360.component.simulation.framework.entity_utils import generate_uuid from flow360.component.simulation.framework.expressions import StringExpression from flow360.component.simulation.framework.single_attribute_base import ( SingleAttributeModel, diff --git a/flow360/component/simulation/models/volume_models.py b/flow360/component/simulation/models/volume_models.py index 084c632cb..a231dc1a3 100644 --- a/flow360/component/simulation/models/volume_models.py +++ b/flow360/component/simulation/models/volume_models.py @@ -10,7 +10,8 @@ import flow360.component.simulation.units as u from flow360.component.simulation.framework.base_model import Flow360BaseModel -from flow360.component.simulation.framework.entity_base import EntityList, generate_uuid +from flow360.component.simulation.framework.entity_base import EntityList +from flow360.component.simulation.framework.entity_utils import generate_uuid from flow360.component.simulation.framework.expressions import ( StringExpression, validate_angle_expression_of_t_seconds, diff --git a/flow360/component/simulation/outputs/output_entities.py b/flow360/component/simulation/outputs/output_entities.py index 1206ace24..d20fc97eb 100644 --- a/flow360/component/simulation/outputs/output_entities.py +++ b/flow360/component/simulation/outputs/output_entities.py @@ -6,7 +6,8 @@ import pydantic as pd from flow360.component.simulation.framework.base_model import Flow360BaseModel -from flow360.component.simulation.framework.entity_base import EntityBase, generate_uuid +from flow360.component.simulation.framework.entity_base import EntityBase +from flow360.component.simulation.framework.entity_utils import generate_uuid from flow360.component.simulation.outputs.output_fields import IsoSurfaceFieldNames from flow360.component.simulation.unit_system import LengthType from flow360.component.simulation.user_code.core.types import ( diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index d5938dfaa..0d017e9e1 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -12,7 +12,8 @@ import flow360.component.simulation.units as u from flow360.component.simulation.framework.base_model import Flow360BaseModel -from flow360.component.simulation.framework.entity_base import EntityList, generate_uuid +from flow360.component.simulation.framework.entity_base import EntityList +from flow360.component.simulation.framework.entity_utils import generate_uuid from flow360.component.simulation.framework.expressions import StringExpression from flow360.component.simulation.framework.unique_list import UniqueItemList from flow360.component.simulation.models.surface_models import EntityListAllowingGhost diff --git a/flow360/component/simulation/primitives.py b/flow360/component/simulation/primitives.py index 940507dcf..a38f6ad04 100644 --- a/flow360/component/simulation/primitives.py +++ b/flow360/component/simulation/primitives.py @@ -17,12 +17,9 @@ rotation_matrix_from_axis_and_angle, ) from flow360.component.simulation.framework.base_model import Flow360BaseModel -from flow360.component.simulation.framework.entity_base import ( - EntityBase, - EntityList, - generate_uuid, -) +from flow360.component.simulation.framework.entity_base import EntityBase, EntityList from flow360.component.simulation.framework.entity_selector import SelectorFactory +from flow360.component.simulation.framework.entity_utils import generate_uuid from flow360.component.simulation.framework.multi_constructor_model_base import ( MultiConstructorBaseModel, ) diff --git a/tests/simulation/framework/test_entity_selector_fluent_api.py b/tests/simulation/framework/test_entity_selector_fluent_api.py index 838e85a04..6aef198b7 100644 --- a/tests/simulation/framework/test_entity_selector_fluent_api.py +++ b/tests/simulation/framework/test_entity_selector_fluent_api.py @@ -146,3 +146,16 @@ def test_edge_class_basic_match(): assert [e["name"] for e in stored if e["private_attribute_entity_type_name"] == "Edge"] == [ "edgeA" ] + + +def test_selector_factory_propagates_description(): + """ + Test: SelectorFactory methods propagate description into EntitySelector instances. + + Expected behavior: + - Passing description to Surface.match() stores it on the resulting selector. + - model_dump() contains the provided description for serialization/round-trip. + """ + selector = Surface.match("*", name="desc_selector", description="Select all surfaces") + assert selector.description == "Select all surfaces" + assert selector.model_dump()["description"] == "Select all surfaces" diff --git a/tests/simulation/services/test_entity_processing_service.py b/tests/simulation/services/test_entity_processing_service.py index 94644d272..3007f97b9 100644 --- a/tests/simulation/services/test_entity_processing_service.py +++ b/tests/simulation/services/test_entity_processing_service.py @@ -122,6 +122,7 @@ def test_validate_model_materializes_dict_and_preserves_selectors(): "target_class": "Surface", "name": "some_selector_name", "logic": "AND", + "selector_id": "some_selector_id", "children": [{"attribute": "name", "operator": "matches", "value": "*"}], } outputs = params.get("outputs") or []