diff --git a/doc/changelog.d/2188.fixed.md b/doc/changelog.d/2188.fixed.md new file mode 100644 index 0000000000..86cc4c09f9 --- /dev/null +++ b/doc/changelog.d/2188.fixed.md @@ -0,0 +1 @@ +Prevent tool and command classes instantiation outside of modeler object diff --git a/src/ansys/geometry/core/designer/geometry_commands.py b/src/ansys/geometry/core/designer/geometry_commands.py index bc08ec36a9..2d54c1583e 100644 --- a/src/ansys/geometry/core/designer/geometry_commands.py +++ b/src/ansys/geometry/core/designer/geometry_commands.py @@ -69,7 +69,7 @@ TangentCondition, ) from ansys.geometry.core.designer.selection import NamedSelection -from ansys.geometry.core.errors import protect_grpc +from ansys.geometry.core.errors import GeometryRuntimeError, protect_grpc from ansys.geometry.core.math.plane import Plane from ansys.geometry.core.math.point import Point3D from ansys.geometry.core.math.vector import UnitVector3D @@ -135,11 +135,30 @@ class GeometryCommands: ---------- grpc_client : GrpcClient gRPC client to use for the geometry commands. + _internal_use : bool, optional + Internal flag to prevent direct instantiation by users. + This parameter is for internal use only. + + Raises + ------ + GeometryRuntimeError + If the class is instantiated directly by users instead + of through the modeler. + + Notes + ----- + This class should not be instantiated directly. Use + ``modeler.geometry_commands`` instead. """ @protect_grpc - def __init__(self, grpc_client: GrpcClient): + def __init__(self, grpc_client: GrpcClient, _internal_use: bool = False): """Initialize an instance of the ``GeometryCommands`` class.""" + if not _internal_use: + raise GeometryRuntimeError( + "GeometryCommands should not be instantiated directly. " + "Use 'modeler.geometry_commands' to access geometry commands." + ) self._grpc_client = grpc_client self._commands_stub = CommandsStub(self._grpc_client.channel) diff --git a/src/ansys/geometry/core/modeler.py b/src/ansys/geometry/core/modeler.py index 03ce400ded..711732aa46 100644 --- a/src/ansys/geometry/core/modeler.py +++ b/src/ansys/geometry/core/modeler.py @@ -117,11 +117,11 @@ def __init__( self._design: Optional["Design"] = None # Enabling tools/commands for all: repair and prepare tools, geometry commands - self._measurement_tools = MeasurementTools(self._grpc_client) - self._repair_tools = RepairTools(self._grpc_client, self) - self._prepare_tools = PrepareTools(self._grpc_client) - self._geometry_commands = GeometryCommands(self._grpc_client) - self._unsupported = UnsupportedCommands(self._grpc_client, self) + self._measurement_tools = MeasurementTools(self._grpc_client, _internal_use=True) + self._repair_tools = RepairTools(self._grpc_client, self, _internal_use=True) + self._prepare_tools = PrepareTools(self._grpc_client, _internal_use=True) + self._geometry_commands = GeometryCommands(self._grpc_client, _internal_use=True) + self._unsupported = UnsupportedCommands(self._grpc_client, self, _internal_use=True) @property def client(self) -> GrpcClient: diff --git a/src/ansys/geometry/core/tools/measurement_tools.py b/src/ansys/geometry/core/tools/measurement_tools.py index f7b4135135..db7a477d1c 100644 --- a/src/ansys/geometry/core/tools/measurement_tools.py +++ b/src/ansys/geometry/core/tools/measurement_tools.py @@ -60,10 +60,29 @@ class MeasurementTools: ---------- grpc_client : GrpcClient gRPC client to use for the measurement tools. + _internal_use : bool, optional + Internal flag to prevent direct instantiation by users. + This parameter is for internal use only. + + Raises + ------ + GeometryRuntimeError + If the class is instantiated directly by users instead + of through the modeler. + + Notes + ----- + This class should not be instantiated directly. Use + ``modeler.measurement_tools`` instead. """ - def __init__(self, grpc_client: GrpcClient): + def __init__(self, grpc_client: GrpcClient, _internal_use: bool = False): """Initialize measurement tools class.""" + if not _internal_use: + raise GeometryRuntimeError( + "MeasurementTools should not be instantiated directly. " + "Use 'modeler.measurement_tools' to access measurement tools." + ) self._grpc_client = grpc_client @min_backend_version(24, 2, 0) diff --git a/src/ansys/geometry/core/tools/prepare_tools.py b/src/ansys/geometry/core/tools/prepare_tools.py index 8526b41a62..880990da17 100644 --- a/src/ansys/geometry/core/tools/prepare_tools.py +++ b/src/ansys/geometry/core/tools/prepare_tools.py @@ -27,6 +27,7 @@ from ansys.geometry.core.connection import GrpcClient from ansys.geometry.core.connection.backend import BackendType +from ansys.geometry.core.errors import GeometryRuntimeError from ansys.geometry.core.logger import LOG from ansys.geometry.core.misc.auxiliary import ( get_bodies_from_ids, @@ -51,10 +52,28 @@ class PrepareTools: ---------- grpc_client : GrpcClient Active supporting geometry service instance for design modeling. + _internal_use : bool, optional + Internal flag to prevent direct instantiation by users. + This parameter is for internal use only. + + Raises + ------ + GeometryRuntimeError + If the class is instantiated directly by users instead of through the modeler. + + Notes + ----- + This class should not be instantiated directly. Use + ``modeler.prepare_tools`` instead. """ - def __init__(self, grpc_client: GrpcClient): + def __init__(self, grpc_client: GrpcClient, _internal_use: bool = False): """Initialize Prepare Tools class.""" + if not _internal_use: + raise GeometryRuntimeError( + "PrepareTools should not be instantiated directly. " + "Use 'modeler.prepare_tools' to access prepare tools." + ) self._grpc_client = grpc_client @min_backend_version(25, 1, 0) diff --git a/src/ansys/geometry/core/tools/repair_tools.py b/src/ansys/geometry/core/tools/repair_tools.py index 5742c97754..1a399f3f80 100644 --- a/src/ansys/geometry/core/tools/repair_tools.py +++ b/src/ansys/geometry/core/tools/repair_tools.py @@ -27,6 +27,7 @@ import ansys.geometry.core as pyansys_geometry from ansys.geometry.core.connection import GrpcClient +from ansys.geometry.core.errors import GeometryRuntimeError from ansys.geometry.core.misc.auxiliary import ( get_bodies_from_ids, get_design_from_body, @@ -61,10 +62,37 @@ class RepairTools: - """Repair tools for PyAnsys Geometry.""" - - def __init__(self, grpc_client: GrpcClient, modeler: "Modeler"): + """Repair tools for PyAnsys Geometry. + + Parameters + ---------- + grpc_client : GrpcClient + Active supporting geometry service instance for design modeling. + modeler : Modeler + The parent modeler instance. + _internal_use : bool, optional + Internal flag to prevent direct instantiation by users. + This parameter is for internal use only. + + Raises + ------ + GeometryRuntimeError + If the class is instantiated directly by users instead + of through the modeler. + + Notes + ----- + This class should not be instantiated directly. Use + ``modeler.repair_tools`` instead. + """ + + def __init__(self, grpc_client: GrpcClient, modeler: "Modeler", _internal_use: bool = False): """Initialize a new instance of the ``RepairTools`` class.""" + if not _internal_use: + raise GeometryRuntimeError( + "RepairTools should not be instantiated directly. " + "Use 'modeler.repair_tools' to access repair tools." + ) self._modeler = modeler self._grpc_client = grpc_client diff --git a/src/ansys/geometry/core/tools/unsupported.py b/src/ansys/geometry/core/tools/unsupported.py index 9252693627..fbc2fe4ab2 100644 --- a/src/ansys/geometry/core/tools/unsupported.py +++ b/src/ansys/geometry/core/tools/unsupported.py @@ -33,7 +33,7 @@ ) from ansys.api.geometry.v0.unsupported_pb2_grpc import UnsupportedStub from ansys.geometry.core.connection import GrpcClient -from ansys.geometry.core.errors import protect_grpc +from ansys.geometry.core.errors import GeometryRuntimeError, protect_grpc from ansys.geometry.core.misc.auxiliary import get_all_bodies_from_design from ansys.geometry.core.misc.checks import ( min_backend_version, @@ -72,10 +72,29 @@ class UnsupportedCommands: gRPC client to use for the geometry commands. modeler : Modeler Modeler instance to use for the geometry commands. + _internal_use : bool, optional + Internal flag to prevent direct instantiation by users. + This parameter is for internal use only. + + Raises + ------ + GeometryRuntimeError + If the class is instantiated directly by users instead of through the modeler. + + Notes + ----- + This class should not be instantiated directly. Use + ``modeler.unsupported`` instead. + """ - def __init__(self, grpc_client: GrpcClient, modeler: "Modeler"): + def __init__(self, grpc_client: GrpcClient, modeler: "Modeler", _internal_use: bool = False): """Initialize an instance of the ``UnsupportedCommands`` class.""" + if not _internal_use: + raise GeometryRuntimeError( + "UnsupportedCommands should not be instantiated directly. " + "Use 'modeler.unsupported' to access unsupported commands." + ) self._grpc_client = grpc_client self._unsupported_stub = UnsupportedStub(self._grpc_client.channel) self.__id_map = {} diff --git a/tests/integration/test_issues.py b/tests/integration/test_issues.py index db3037ac8c..9c4a0898fc 100644 --- a/tests/integration/test_issues.py +++ b/tests/integration/test_issues.py @@ -24,7 +24,10 @@ from pathlib import Path import numpy as np +import pytest +from ansys.geometry.core.designer.geometry_commands import GeometryCommands +from ansys.geometry.core.errors import GeometryRuntimeError from ansys.geometry.core.math import ( UNITVECTOR3D_X, UNITVECTOR3D_Y, @@ -37,6 +40,12 @@ from ansys.geometry.core.misc import DEFAULT_UNITS, UNITS, Angle, Distance from ansys.geometry.core.modeler import Modeler from ansys.geometry.core.sketch import Sketch +from ansys.geometry.core.tools import ( + MeasurementTools, + PrepareTools, + RepairTools, + UnsupportedCommands, +) from .conftest import FILES_DIR @@ -325,3 +334,40 @@ def test_issue_1813_edge_start_end_non_default_units(modeler: Modeler): finally: # Reset the default units to meters DEFAULT_UNITS.LENGTH = UNITS.meter + + +def test_issue_2184_prevent_raw_instantiation_of_tools_and_commands(): + """Test that raw instantiation of tools and commands is prevented. + + For more info see + https://github.com/ansys/pyansys-geometry/issues/2184 + """ + # Test UnsupportedCommands + with pytest.raises( + GeometryRuntimeError, match="UnsupportedCommands should not be instantiated directly" + ): + UnsupportedCommands(None, None) + + # Test RepairTools + with pytest.raises( + GeometryRuntimeError, match="RepairTools should not be instantiated directly" + ): + RepairTools(None, None) + + # Test PrepareTools + with pytest.raises( + GeometryRuntimeError, match="PrepareTools should not be instantiated directly" + ): + PrepareTools(None) + + # Test MeasurementTools + with pytest.raises( + GeometryRuntimeError, match="MeasurementTools should not be instantiated directly" + ): + MeasurementTools(None) + + # Test GeometryCommands + with pytest.raises( + GeometryRuntimeError, match="GeometryCommands should not be instantiated directly" + ): + GeometryCommands(None) diff --git a/tests/integration/test_repair_tools.py b/tests/integration/test_repair_tools.py index c02d5b54ba..1d66322824 100644 --- a/tests/integration/test_repair_tools.py +++ b/tests/integration/test_repair_tools.py @@ -38,7 +38,6 @@ UnsimplifiedFaceProblemAreas, ) from ansys.geometry.core.tools.repair_tool_message import RepairToolMessage -from ansys.geometry.core.tools.repair_tools import RepairTools from .conftest import FILES_DIR @@ -731,8 +730,7 @@ def test_problem_area_fix_no_data(modeler: Modeler, problem_area_class, kwargs): ) def test_repair_tools_no_bodies(modeler: Modeler, method_name): """Test RepairTools methods when bodies is empty or None.""" - grpc_client = modeler.client - repair_tools = RepairTools(grpc_client, modeler) + repair_tools = modeler.repair_tools method = getattr(repair_tools, method_name) # Test with an empty list of bodies @@ -755,8 +753,7 @@ def test_repair_tools_no_bodies(modeler: Modeler, method_name): ) def test_repair_tools_find_and_fix_no_bodies(modeler: Modeler, method_name): """Test RepairTools find_and_fix methods when bodies is empty or None.""" - grpc_client = modeler.client - repair_tools = RepairTools(grpc_client, modeler) + repair_tools = modeler.repair_tools method = getattr(repair_tools, method_name) # Test with an empty list of bodies