From 023b59b0595d55c013836c0b3b0bc8c016673e9f Mon Sep 17 00:00:00 2001 From: Dominik Gresch Date: Thu, 17 Apr 2025 14:43:58 +0200 Subject: [PATCH] Add a feature tree Add helper functions to either print a tree of ACP objects, or print it. In the documentation, show the tree structure, and explain how it affects the access and creation of objects. To implement the feature tree, record metadata when creating mapping properties (value type, and read-only status), and use a separate _exposed_grpc_property subclass to indicate it is a mapping. --- doc/source/api/example_helpers.rst | 11 -- doc/source/api/extras.rst | 26 +++ doc/source/api/index.rst | 2 +- .../user_guide/concepts/feature_tree.rst | 110 +++++++++++++ doc/source/user_guide/concepts/index.rst | 1 + doc/source/user_guide/concepts/store.rst | 2 +- doc/source/user_guide/howto/print_model.rst | 126 +++++++-------- src/ansys/acp/core/_model_printer.py | 53 +++++-- .../_tree_objects/_grpc_helpers/mapping.py | 16 +- .../_grpc_helpers/property_helper.py | 13 ++ src/ansys/acp/core/extras/__init__.py | 5 +- src/ansys/acp/core/extras/feature_tree.py | 65 ++++++++ tests/unittests/test_feature_tree.py | 89 +++++++++++ tests/unittests/test_tree_printer.py | 149 ++++++++++++++---- 14 files changed, 548 insertions(+), 120 deletions(-) delete mode 100644 doc/source/api/example_helpers.rst create mode 100644 doc/source/api/extras.rst create mode 100644 doc/source/user_guide/concepts/feature_tree.rst create mode 100644 src/ansys/acp/core/extras/feature_tree.py create mode 100644 tests/unittests/test_feature_tree.py diff --git a/doc/source/api/example_helpers.rst b/doc/source/api/example_helpers.rst deleted file mode 100644 index 2abb344303..0000000000 --- a/doc/source/api/example_helpers.rst +++ /dev/null @@ -1,11 +0,0 @@ -Example helpers ---------------- - -.. currentmodule:: ansys.acp.core.extras.example_helpers - -.. autosummary:: - :toctree: _autosummary - - ExampleKeys - get_example_file - set_plot_theme diff --git a/doc/source/api/extras.rst b/doc/source/api/extras.rst new file mode 100644 index 0000000000..35b98646ee --- /dev/null +++ b/doc/source/api/extras.rst @@ -0,0 +1,26 @@ +Extras +------ + +Example helpers +~~~~~~~~~~~~~~~ + +.. currentmodule:: ansys.acp.core.extras.example_helpers + +.. autosummary:: + :toctree: _autosummary + + ExampleKeys + get_example_file + set_plot_theme + + +Feature tree +~~~~~~~~~~~~ + +.. currentmodule:: ansys.acp.core.extras.feature_tree + +.. autosummary:: + :toctree: _autosummary + + get_feature_tree + print_feature_tree diff --git a/doc/source/api/index.rst b/doc/source/api/index.rst index bdfc694178..01be685ed2 100644 --- a/doc/source/api/index.rst +++ b/doc/source/api/index.rst @@ -25,7 +25,7 @@ API reference other_utils dpf_integration_helpers mechanical_integration_helpers - example_helpers + extras internal {% else %} The API reference is not available in this documentation build. diff --git a/doc/source/user_guide/concepts/feature_tree.rst b/doc/source/user_guide/concepts/feature_tree.rst new file mode 100644 index 0000000000..17b6c909c8 --- /dev/null +++ b/doc/source/user_guide/concepts/feature_tree.rst @@ -0,0 +1,110 @@ +.. _feature_tree: + +Feature tree +------------ + +.. testsetup:: + + import ansys.acp.core as pyacp + + acp = pyacp.launch_acp() + model = acp.import_model("../tests/data/minimal_complete_model_no_matml_link.acph5") + +The following tree shows the hierarchy of PyACP objects: + +.. doctest:: + + >>> pyacp.extras.feature_tree.print_feature_tree() + Model + ├── Material + ├── Fabric + ├── Stackup + ├── SubLaminate + ├── ElementSet + ├── EdgeSet + ├── CADGeometry + │ └── CADComponent (read-only) + ├── VirtualGeometry + ├── Rosette + ├── LookUpTable1D + │ └── LookUpTable1DColumn + ├── LookUpTable3D + │ └── LookUpTable3DColumn + ├── ParallelSelectionRule + ├── CylindricalSelectionRule + ├── SphericalSelectionRule + ├── TubeSelectionRule + ├── CutOffSelectionRule + ├── GeometricalSelectionRule + ├── VariableOffsetSelectionRule + ├── BooleanSelectionRule + ├── OrientedSelectionSet + ├── ModelingGroup + │ ├── ModelingPly + │ │ └── ProductionPly (read-only) + │ │ └── AnalysisPly (read-only) + │ ├── InterfaceLayer + │ └── ButtJointSequence + ├── ImportedModelingGroup + │ └── ImportedModelingPly + │ └── ImportedProductionPly (read-only) + │ └── ImportedAnalysisPly (read-only) + ├── SamplingPoint + ├── SectionCut + ├── SolidModel + │ ├── ExtrusionGuide + │ ├── SnapToGeometry + │ ├── SolidElementSet (read-only) + │ ├── CutOffGeometry + │ ├── AnalysisPly (read-only) + │ └── InterfaceLayer (read-only) + ├── ImportedSolidModel + │ ├── SolidElementSet (read-only) + │ ├── CutOffGeometry + │ ├── LayupMappingObject + │ │ ├── AnalysisPly (read-only) + │ │ └── ImportedAnalysisPly (read-only) + │ ├── AnalysisPly (read-only) + │ └── ImportedAnalysisPly (read-only) + ├── Sensor + └── FieldDefinition + + + +This structure determines how objects can be created, accessed, and stored in the model. + + +For example, :class:`.ModelingPly` is a child of :class:`.ModelingGroup`, which is a child of :class:`.Model`. To access a specific modeling ply, you can traverse this tree hierarchy: + +.. doctest:: + + >>> model + + >>> model.modeling_groups + + >>> modeling_group = model.modeling_groups["ModelingGroup.1"] + >>> modeling_group.modeling_plies + + >>> modeling_ply = modeling_group.modeling_plies["ModelingPly.1"] + >>> modeling_ply + + +To create a new modeling ply, you can use the :meth:`.ModelingGroup.create_modeling_ply` method: + +.. doctest:: + + >>> new_ply = modeling_group.create_modeling_ply(name="New Ply") + >>> new_ply + + +When cloning and storing a modeling ply, the ``parent`` argument must be a :class:`.ModelingGroup` object: + +.. doctest:: + + >>> other_modeling_group = model.create_modeling_group() + >>> cloned_ply = modeling_ply.clone() + >>> cloned_ply + + >>> cloned_ply.store(parent=other_modeling_group) + >>> cloned_ply + diff --git a/doc/source/user_guide/concepts/index.rst b/doc/source/user_guide/concepts/index.rst index 7f4f4d87e6..9df85138a8 100644 --- a/doc/source/user_guide/concepts/index.rst +++ b/doc/source/user_guide/concepts/index.rst @@ -8,5 +8,6 @@ These guides explain the PyACP concepts. .. toctree:: :maxdepth: 2 + feature_tree material_property_sets store diff --git a/doc/source/user_guide/concepts/store.rst b/doc/source/user_guide/concepts/store.rst index 878d276b10..14f6fd48c4 100644 --- a/doc/source/user_guide/concepts/store.rst +++ b/doc/source/user_guide/concepts/store.rst @@ -65,7 +65,7 @@ You can make changes to the unstored material, but they are lost when the Python >>> material.density.rho = 8000 -To store a material in an ACP model, call its :meth:`store <.Material.store>` method. The ``parent`` parameter determines where in the model the material is stored. In this case, the parent is the model itself. +To store a material in an ACP model, call its :meth:`store <.Material.store>` method. The ``parent`` parameter determines where in the model the material is stored. In this case, the parent is the model itself. See :ref:`feature_tree` for more information on how to determine the ``parent`` object. .. doctest:: diff --git a/doc/source/user_guide/howto/print_model.rst b/doc/source/user_guide/howto/print_model.rst index 3930f7f901..d5c229649b 100644 --- a/doc/source/user_guide/howto/print_model.rst +++ b/doc/source/user_guide/howto/print_model.rst @@ -22,26 +22,26 @@ You can print the tree structure using the :func:`.print_model` function: >>> pyacp.print_model(model) 'ACP Model' - Materials - 'Structural Steel' - Fabrics - 'Fabric.1' - Element Sets - 'All_Elements' - Edge Sets - 'ns_edge' - Rosettes - 'Global Coordinate System' - Oriented Selection Sets - 'OrientedSelectionSet.1' - Modeling Groups - 'ModelingGroup.1' - Modeling Plies - 'ModelingPly.1' - Production Plies - 'P1__ModelingPly.1' - Analysis Plies - 'P1L1__ModelingPly.1' + ├── Materials + │ └── 'Structural Steel' + ├── Fabrics + │ └── 'Fabric.1' + ├── Element Sets + │ └── 'All_Elements' + ├── Edge Sets + │ └── 'ns_edge' + ├── Rosettes + │ └── 'Global Coordinate System' + ├── Oriented Selection Sets + │ └── 'OrientedSelectionSet.1' + └── Modeling Groups + └── 'ModelingGroup.1' + └── Modeling Plies + └── 'ModelingPly.1' + └── Production Plies + └── 'P1__ModelingPly.1' + └── Analysis Plies + └── 'P1L1__ModelingPly.1' @@ -70,47 +70,47 @@ The ``hide_empty`` label can be set to ``False`` to also show empty groups: >>> pyacp.print_model(model, hide_empty=False) 'ACP Model' - Materials - 'Structural Steel' - Fabrics - 'Fabric.1' - Stackups - Sublaminates - Element Sets - 'All_Elements' - Edge Sets - 'ns_edge' - Cad Geometries - Virtual Geometries - Rosettes - 'Global Coordinate System' - Lookup Tables 1d - Lookup Tables 3d - Parallel Selection Rules - Cylindrical Selection Rules - Spherical Selection Rules - Tube Selection Rules - Cut Off Selection Rules - Geometrical Selection Rules - Variable Offset Selection Rules - Boolean Selection Rules - Oriented Selection Sets - 'OrientedSelectionSet.1' - Modeling Groups - 'ModelingGroup.1' - Modeling Plies - 'ModelingPly.1' - Production Plies - 'P1__ModelingPly.1' - Analysis Plies - 'P1L1__ModelingPly.1' - Interface Layers - Butt Joint Sequences - Imported Modeling Groups - Sampling Points - Section Cuts - Solid Models - Imported Solid Models - Sensors - Field Definitions + ├── Materials + │ └── 'Structural Steel' + ├── Fabrics + │ └── 'Fabric.1' + ├── Stackups + ├── Sublaminates + ├── Element Sets + │ └── 'All_Elements' + ├── Edge Sets + │ └── 'ns_edge' + ├── Cad Geometries + ├── Virtual Geometries + ├── Rosettes + │ └── 'Global Coordinate System' + ├── Lookup Tables 1d + ├── Lookup Tables 3d + ├── Parallel Selection Rules + ├── Cylindrical Selection Rules + ├── Spherical Selection Rules + ├── Tube Selection Rules + ├── Cut Off Selection Rules + ├── Geometrical Selection Rules + ├── Variable Offset Selection Rules + ├── Boolean Selection Rules + ├── Oriented Selection Sets + │ └── 'OrientedSelectionSet.1' + ├── Modeling Groups + │ └── 'ModelingGroup.1' + │ ├── Modeling Plies + │ │ └── 'ModelingPly.1' + │ │ └── Production Plies + │ │ └── 'P1__ModelingPly.1' + │ │ └── Analysis Plies + │ │ └── 'P1L1__ModelingPly.1' + │ ├── Interface Layers + │ └── Butt Joint Sequences + ├── Imported Modeling Groups + ├── Sampling Points + ├── Section Cuts + ├── Solid Models + ├── Imported Solid Models + ├── Sensors + └── Field Definitions diff --git a/src/ansys/acp/core/_model_printer.py b/src/ansys/acp/core/_model_printer.py index 48c763be8b..a8a8e5c056 100644 --- a/src/ansys/acp/core/_model_printer.py +++ b/src/ansys/acp/core/_model_printer.py @@ -45,16 +45,45 @@ def __init__(self, label: str, children: list["Node"] | None = None): self.label = label self.children: list["Node"] = children if children else [] - def __str__(self, level: int | None = 0) -> str: - assert level is not None - four_spaces = " " - ret = four_spaces * level + self.label + os.linesep - for child in self.children: - ret += child.__str__(level + 1) - return ret - - -def print_model(model: Model, *, hide_empty: bool = True) -> None: + def __str__(self) -> str: + return self._to_string(_prefix="", show_lines=True) + + def _to_string( + self, + *, + show_lines: bool, + _prefix: str = "", + _is_last_child: bool = True, + _is_root: bool = True, + ) -> str: + if show_lines: + elbow = "└── " + line = "│ " + tee = "├── " + empty = " " + else: + elbow = line = tee = empty = " " * 4 + if _is_root: + res = _prefix + self.label + os.linesep + else: + res = _prefix + (elbow if _is_last_child else tee) + self.label + os.linesep + for i, child in enumerate(self.children): + if _is_root: + new_prefix = _prefix + elif _is_last_child: + new_prefix = _prefix + empty + else: + new_prefix = _prefix + line + res += child._to_string( + _prefix=new_prefix, + show_lines=show_lines, + _is_last_child=(i == len(self.children) - 1), + _is_root=False, + ) + return res + + +def print_model(model: Model, *, hide_empty: bool = True, show_lines: bool = True) -> None: """Print a tree representation of the model. Parameters @@ -63,9 +92,11 @@ def print_model(model: Model, *, hide_empty: bool = True) -> None: pyACP model hide_empty : Whether to hide empty collections. + show_lines : + Whether to show lines connecting the nodes. """ - return print(get_model_tree(model, hide_empty=hide_empty)) + return print(get_model_tree(model, hide_empty=hide_empty)._to_string(show_lines=show_lines)) def get_model_tree(model: Model, *, hide_empty: bool = True) -> Node: diff --git a/src/ansys/acp/core/_tree_objects/_grpc_helpers/mapping.py b/src/ansys/acp/core/_tree_objects/_grpc_helpers/mapping.py index f46b3b747a..281f2363ed 100644 --- a/src/ansys/acp/core/_tree_objects/_grpc_helpers/mapping.py +++ b/src/ansys/acp/core/_tree_objects/_grpc_helpers/mapping.py @@ -39,7 +39,7 @@ from ..base import CreatableTreeObject, ServerWrapper, TreeObject, TreeObjectBase from ..enums import Status from .exceptions import wrap_grpc_errors -from .property_helper import _exposed_grpc_property, _wrap_doc +from .property_helper import _exposed_grpc_mapping_property, _wrap_doc from .protocols import EditableAndReadableResourceStub, ObjectInfo, ReadableResourceStub ValueT = TypeVar("ValueT", bound=TreeObjectBase) @@ -297,7 +297,12 @@ def collection_property(self: ParentT) -> Mapping[ValueT]: stub=stub_class(channel=self._server_wrapper.channel), ) - return _wrap_doc(_exposed_grpc_property(collection_property), doc=doc) + return _wrap_doc( + _exposed_grpc_mapping_property( # type: ignore + collection_property, value_type=object_class, read_only=True + ), + doc=doc, + ) P = ParamSpec("P") @@ -355,4 +360,9 @@ def collection_property(self: ParentT) -> MutableMapping[CreatableValueT]: stub=stub_class(channel=self._server_wrapper.channel), ) - return _wrap_doc(_exposed_grpc_property(collection_property), doc=doc) + return _wrap_doc( + _exposed_grpc_mapping_property( + collection_property, value_type=object_class, read_only=False # type: ignore + ), + doc=doc, + ) 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 c15b6b72ab..dcecfb95c6 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 @@ -58,6 +58,7 @@ # subclasses. # See https://github.com/python/mypy/issues/6158 _exposed_grpc_property = property + _exposed_grpc_mapping_property = property else: class _exposed_grpc_property(property): @@ -69,6 +70,18 @@ class _exposed_grpc_property(property): pass + class _exposed_grpc_mapping_property(_exposed_grpc_property): + """Mark a property as a mapping. + + Wrapper around 'property', used to signal that the object should + be collected into the '_GRPC_PROPERTIES' class attribute. + """ + + def __init__(self, *args, value_type, read_only: bool, **kwargs): + super().__init__(*args, **kwargs) + self._value_type = value_type + self._read_only = read_only + T = TypeVar("T", bound=type[GrpcObjectBase]) diff --git a/src/ansys/acp/core/extras/__init__.py b/src/ansys/acp/core/extras/__init__.py index 9bafef7981..489fce8b7f 100644 --- a/src/ansys/acp/core/extras/__init__.py +++ b/src/ansys/acp/core/extras/__init__.py @@ -21,7 +21,7 @@ # SOFTWARE. """Extras of the Ansys Composites PrepPost module.""" -from ansys.acp.core.extras.example_helpers import ( +from .example_helpers import ( FLAT_PLATE_SHELL_CAMERA, FLAT_PLATE_SOLID_CAMERA, RACE_CARE_NOSE_CAMERA_METER, @@ -29,6 +29,7 @@ get_example_file, set_plot_theme, ) +from .feature_tree import get_feature_tree, print_feature_tree __all__ = [ "ExampleKeys", @@ -37,4 +38,6 @@ "get_example_file", "RACE_CARE_NOSE_CAMERA_METER", "set_plot_theme", + "print_feature_tree", + "get_feature_tree", ] diff --git a/src/ansys/acp/core/extras/feature_tree.py b/src/ansys/acp/core/extras/feature_tree.py new file mode 100644 index 0000000000..fc5955e449 --- /dev/null +++ b/src/ansys/acp/core/extras/feature_tree.py @@ -0,0 +1,65 @@ +# Copyright (C) 2022 - 2025 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. + +"""Helper functions to show the structure of the ACP feature tree.""" + +from .._model_printer import Node +from .._tree_objects._grpc_helpers.property_helper import _exposed_grpc_mapping_property +from .._tree_objects.base import TreeObjectBase +from .._tree_objects.model import Model + +__all__ = [ + "print_feature_tree", + "get_feature_tree", +] + + +def print_feature_tree(show_lines: bool = True) -> None: + """Print a tree representation of the PyACP features. + + Parameters + ---------- + show_lines : + Whether to show lines connecting the nodes. + + """ + print(get_feature_tree()._to_string(show_lines=show_lines)) + + +def get_feature_tree() -> Node: + """Get a tree representation of the PyACP features.""" + return _get_feature_tree_impl(Model, False) + + +def _get_feature_tree_impl(root_cls: type[TreeObjectBase], read_only: bool) -> Node: + label = root_cls.__name__ + if read_only: + label += " (read-only)" + grpc_properties = [getattr(root_cls, attr_name) for attr_name in root_cls._GRPC_PROPERTIES] + return Node( + label=label, + children=[ + _get_feature_tree_impl(prop._value_type, read_only=prop._read_only) # type: ignore + for prop in grpc_properties + if isinstance(prop, _exposed_grpc_mapping_property) + ], + ) diff --git a/tests/unittests/test_feature_tree.py b/tests/unittests/test_feature_tree.py new file mode 100644 index 0000000000..d6ebf0fa03 --- /dev/null +++ b/tests/unittests/test_feature_tree.py @@ -0,0 +1,89 @@ +# Copyright (C) 2022 - 2025 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. + +import os +import textwrap + +from ansys.acp.core.extras.feature_tree import get_feature_tree + + +def test_feature_tree(): + """Test that the feature tree is correct.""" + tree = get_feature_tree() + expected = textwrap.dedent( + """\ + Model + ├── Material + ├── Fabric + ├── Stackup + ├── SubLaminate + ├── ElementSet + ├── EdgeSet + ├── CADGeometry + │ └── CADComponent (read-only) + ├── VirtualGeometry + ├── Rosette + ├── LookUpTable1D + │ └── LookUpTable1DColumn + ├── LookUpTable3D + │ └── LookUpTable3DColumn + ├── ParallelSelectionRule + ├── CylindricalSelectionRule + ├── SphericalSelectionRule + ├── TubeSelectionRule + ├── CutOffSelectionRule + ├── GeometricalSelectionRule + ├── VariableOffsetSelectionRule + ├── BooleanSelectionRule + ├── OrientedSelectionSet + ├── ModelingGroup + │ ├── ModelingPly + │ │ └── ProductionPly (read-only) + │ │ └── AnalysisPly (read-only) + │ ├── InterfaceLayer + │ └── ButtJointSequence + ├── ImportedModelingGroup + │ └── ImportedModelingPly + │ └── ImportedProductionPly (read-only) + │ └── ImportedAnalysisPly (read-only) + ├── SamplingPoint + ├── SectionCut + ├── SolidModel + │ ├── ExtrusionGuide + │ ├── SnapToGeometry + │ ├── SolidElementSet (read-only) + │ ├── CutOffGeometry + │ ├── AnalysisPly (read-only) + │ └── InterfaceLayer (read-only) + ├── ImportedSolidModel + │ ├── SolidElementSet (read-only) + │ ├── CutOffGeometry + │ ├── LayupMappingObject + │ │ ├── AnalysisPly (read-only) + │ │ └── ImportedAnalysisPly (read-only) + │ ├── AnalysisPly (read-only) + │ └── ImportedAnalysisPly (read-only) + ├── Sensor + └── FieldDefinition + """ + ) + assert str(tree) == expected.replace("\n", os.linesep) diff --git a/tests/unittests/test_tree_printer.py b/tests/unittests/test_tree_printer.py index bf5d7cb361..72699c14f2 100644 --- a/tests/unittests/test_tree_printer.py +++ b/tests/unittests/test_tree_printer.py @@ -32,30 +32,34 @@ def case_simple_model(acp_instance, model_data_dir): input_file_path = model_data_dir / "minimal_complete_model_no_matml_link.acph5" model = acp_instance.import_model(name="minimal_complete", path=input_file_path) model.update() - return model, textwrap.dedent( - """\ - 'minimal_complete' - Materials - 'Structural Steel' - Fabrics - 'Fabric.1' - Element Sets - 'All_Elements' - Edge Sets - 'ns_edge' - Rosettes - 'Global Coordinate System' - Oriented Selection Sets - 'OrientedSelectionSet.1' - Modeling Groups - 'ModelingGroup.1' - Modeling Plies - 'ModelingPly.1' - Production Plies - 'P1__ModelingPly.1' - Analysis Plies - 'P1L1__ModelingPly.1' - """ + return ( + model, + False, + textwrap.dedent( + """\ + 'minimal_complete' + Materials + 'Structural Steel' + Fabrics + 'Fabric.1' + Element Sets + 'All_Elements' + Edge Sets + 'ns_edge' + Rosettes + 'Global Coordinate System' + Oriented Selection Sets + 'OrientedSelectionSet.1' + Modeling Groups + 'ModelingGroup.1' + Modeling Plies + 'ModelingPly.1' + Production Plies + 'P1__ModelingPly.1' + Analysis Plies + 'P1L1__ModelingPly.1' + """ + ), ) @@ -80,8 +84,11 @@ def case_more_objects(acp_instance, model_data_dir): model.create_lookup_table_3d() model.create_sensor() - return model, textwrap.dedent( - """\ + return ( + model, + False, + textwrap.dedent( + """\ 'minimal_complete' Materials 'Structural Steel' @@ -135,15 +142,99 @@ def case_more_objects(acp_instance, model_data_dir): Sensors 'Sensor' """ + ), + ) + + +def case_more_objects_lines(acp_instance, model_data_dir): + input_file_path = model_data_dir / "minimal_complete_model_no_matml_link.acph5" + model = acp_instance.import_model(name="minimal_complete", path=input_file_path) + + model.update() + + model.create_edge_set() + model.create_stackup() + model.create_sublaminate() + model.create_virtual_geometry() + model.create_cad_geometry() + model.create_parallel_selection_rule() + model.create_cylindrical_selection_rule() + model.create_tube_selection_rule() + model.create_cut_off_selection_rule() + model.create_geometrical_selection_rule() + model.create_boolean_selection_rule() + model.create_lookup_table_1d() + model.create_lookup_table_3d() + model.create_sensor() + + return ( + model, + True, + textwrap.dedent( + """\ + 'minimal_complete' + ├── Materials + │ └── 'Structural Steel' + ├── Fabrics + │ └── 'Fabric.1' + ├── Stackups + │ └── 'Stackup' + ├── Sublaminates + │ └── 'SubLaminate' + ├── Element Sets + │ └── 'All_Elements' + ├── Edge Sets + │ ├── 'ns_edge' + │ └── 'EdgeSet' + ├── Cad Geometries + │ └── 'CADGeometry' + ├── Virtual Geometries + │ └── 'VirtualGeometry' + ├── Rosettes + │ └── 'Global Coordinate System' + ├── Lookup Tables 1d + │ └── 'LookUpTable1D' + │ └── Columns + │ └── 'Location' + ├── Lookup Tables 3d + │ └── 'LookUpTable3D' + │ └── Columns + │ └── 'Location' + ├── Parallel Selection Rules + │ └── 'ParallelSelectionrule' + ├── Cylindrical Selection Rules + │ └── 'CylindricalSelectionrule' + ├── Tube Selection Rules + │ └── 'TubeSelectionrule' + ├── Cut Off Selection Rules + │ └── 'CutOffSelectionrule' + ├── Geometrical Selection Rules + │ └── 'GeometricalSelectionrule' + ├── Boolean Selection Rules + │ └── 'BooleanSelectionrule' + ├── Oriented Selection Sets + │ └── 'OrientedSelectionSet.1' + ├── Modeling Groups + │ └── 'ModelingGroup.1' + │ └── Modeling Plies + │ └── 'ModelingPly.1' + │ └── Production Plies + │ └── 'P1__ModelingPly.1' + │ └── Analysis Plies + │ └── 'P1L1__ModelingPly.1' + └── Sensors + └── 'Sensor' + """ + ), ) -@parametrize_with_cases("model,expected", cases=".", glob="*") -def test_printed_model(model, expected): +@parametrize_with_cases("model,show_lines,expected", cases=".", glob="*") +def test_printed_model(model, show_lines, expected): """ Test that model tree looks correct. """ tree = get_model_tree(model) - assert str(tree) == expected.replace("\n", os.linesep) + assert tree._to_string(show_lines=show_lines) == expected.replace("\n", os.linesep)