From a4193c500c976aabfcfe2c26f3dfd94a51507ec1 Mon Sep 17 00:00:00 2001 From: Randy Frank <89219420+randallfrank@users.noreply.github.com> Date: Thu, 9 Nov 2023 17:27:55 -0500 Subject: [PATCH 1/7] Update for EnSight 242 In 2024 R2 there is an extended __repr__ for performance. --- src/ansys/pyensight/core/session.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/ansys/pyensight/core/session.py b/src/ansys/pyensight/core/session.py index 86de95673b2..cb28d1401c9 100644 --- a/src/ansys/pyensight/core/session.py +++ b/src/ansys/pyensight/core/session.py @@ -1510,6 +1510,13 @@ def _convert_ctor(self, s: str) -> str: tail = s.find(", cached:yes") if tail == -1: break + # Subtype (PartType:, AnnotType:, ToolType:) + subtype = None + for name in ("PartType:", "AnnotType:", "ToolType:"): + location = s.find(name) + if (location != -1) and (location > id): + subtype = int(s[location + len(name) :].split(",")[0]) + break # isolate the block to replace prefix = s[:start] suffix = s[tail + tail_len :] @@ -1526,8 +1533,11 @@ def _convert_ctor(self, s: str) -> str: else: subclass_info = "" if attr_id is not None: - # if a "subclass" case and no subclass attrid value, ask for it... - if classname_lookup is not None: + if subtype is not None: + # the 2024 R2 interface includes the subtype + subclass_info = f",attr_id={attr_id}, attr_value={subtype}" + elif classname_lookup is not None: + # if a "subclass" case and no subclass attrid value, ask for it... remote_name = self.remote_obj(objid) cmd = f"{remote_name}.getattr({attr_id})" attr_value = self.cmd(cmd) From d201790b37153d69139b5cbece35eb37704c9bd4 Mon Sep 17 00:00:00 2001 From: Randy Frank <89219420+randallfrank@users.noreply.github.com> Date: Thu, 30 Nov 2023 16:25:50 -0500 Subject: [PATCH 2/7] Include initial "Owned" support Make it possible to use remotely cached, owned objects. --- src/ansys/pyensight/core/ensight_grpc.py | 21 +++++++++++- src/ansys/pyensight/core/ensobj.py | 20 ++++++++--- src/ansys/pyensight/core/session.py | 42 ++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/ansys/pyensight/core/ensight_grpc.py b/src/ansys/pyensight/core/ensight_grpc.py index b2681696009..e24269546c1 100644 --- a/src/ansys/pyensight/core/ensight_grpc.py +++ b/src/ansys/pyensight/core/ensight_grpc.py @@ -38,6 +38,7 @@ def __init__(self, host: str = "127.0.0.1", port: int = 12345, secret_key: str = self._stub = None self._dsg_stub = None self._security_token = secret_key + self._session_name: str = "" # Streaming APIs # Event (strings) self._event_stream = None @@ -71,6 +72,19 @@ def security_token(self) -> str: def security_token(self, name: str) -> None: self._security_token = name + @property + def session_name(self) -> str: + """The gRPC server session name + + EnSight gRPC calls can include the session name via 'session_name' metadata. + A client session may provide a session name via this property. + """ + return self._session_name + + @session_name.setter + def session_name(self, name: str) -> None: + self._session_name = name + def shutdown(self, stop_ensight: bool = False, force: bool = False) -> None: """Close down the gRPC connection @@ -148,7 +162,9 @@ def connect(self, timeout: float = 15.0) -> None: def _metadata(self) -> List[Tuple[bytes, Union[str, bytes]]]: """Compute the gRPC stream metadata - Compute the list to be passed to the gRPC calls for things like security. + Compute the list to be passed to the gRPC calls for things like security + and the session name. + """ ret: List[Tuple[bytes, Union[str, bytes]]] = list() s: Union[str, bytes] @@ -157,6 +173,9 @@ def _metadata(self) -> List[Tuple[bytes, Union[str, bytes]]]: if type(s) == str: s = s.encode("utf-8") ret.append((b"shared_secret", s)) + if self._session_name: + s = self._session_name.encode("utf-8") + ret.append((b"session_name", s)) return ret def render( diff --git a/src/ansys/pyensight/core/ensobj.py b/src/ansys/pyensight/core/ensobj.py index da2efb66640..8b9f801bcab 100644 --- a/src/ansys/pyensight/core/ensobj.py +++ b/src/ansys/pyensight/core/ensobj.py @@ -29,9 +29,10 @@ class ENSOBJ(object): classes are ENS_TOOL, ENS_PART and ENS_ANNOT. attr_value : The attribute value associated with any specified attr_id. - - Returns - ------- + owned : bool + If True, the object is assumed to be "owned" by this interpreter. + This means that the lifecycle of the ENSOBJ instance in EnSight is + dictated by the lifecycle of this proxy object. """ @@ -41,6 +42,7 @@ def __init__( objid: int, attr_id: Optional[int] = None, attr_value: Optional[int] = None, + owned: Optional[bool] = None, ) -> None: self._session = session self._objid = objid @@ -48,6 +50,10 @@ def __init__( self._attr_value = attr_value self._session.add_ensobj_instance(self) self.attr_list = self.populate_attr_list() + # True if this Python instance "owns" the ENSOBJ instance (via EnSight proxy cache) + self._is_owned = False + if owned: + self._is_owned = True def __eq__(self, obj): return self._objid == obj._objid @@ -63,6 +69,9 @@ def __OBJID__(self) -> int: # noqa: N802 return self._objid def _remote_obj(self) -> str: + """Convert the object into a string appropriate for use in the + remote EnSight session. Usually, this is some form of + ensight.objs.wrap_id().""" return self._session.remote_obj(self._objid) def getattr(self, attrid: Any) -> Any: @@ -447,7 +456,10 @@ def __str__(self) -> str: # self.DESCRIPTION is a gRPC call that can fail for default objects desc_text = "" desc = f", desc: '{desc_text}'" - return f"Class: {self.__class__.__name__}{desc}, CvfObjID: {self._objid}, cached:no" + owned = "" + if self._is_owned: + owned = ", Owned" + return f"Class: {self.__class__.__name__}{desc}{owned}, CvfObjID: {self._objid}, cached:no" def __repr__(self) -> str: """Custom __repr__ method used by the stub API. diff --git a/src/ansys/pyensight/core/session.py b/src/ansys/pyensight/core/session.py index cb28d1401c9..79dcf929fb5 100644 --- a/src/ansys/pyensight/core/session.py +++ b/src/ansys/pyensight/core/session.py @@ -12,6 +12,7 @@ """ import atexit import importlib.util +import uuid from os import listdir import os.path import platform @@ -124,6 +125,8 @@ def __init__( rest_api: bool = False, sos: bool = False, ) -> None: + # every session instance needs a unique name that can be used as a cache key + self._session_name = str(uuid.uuid1()) # when objects come into play, we can reuse them, so hash ID to instance here self._ensobj_hash: Dict[int, "ENSOBJ"] = {} self._language = "en" @@ -170,6 +173,7 @@ def __init__( self._grpc = ensight_grpc.EnSightGRPC( host=self._hostname, port=self._grpc_port, secret_key=self._secret_key ) + self._grpc.session_name = self._session_name # establish the connection with retry self._establish_connection(validate=True) @@ -259,6 +263,13 @@ def _check_rest_connection(self) -> None: time.sleep(0.5) raise RuntimeError("Unable to establish a REST connection to EnSight.") + @property + def name(self) -> str: + """The session name is a unique identifier for this Session instance. It + is used by EnSight to maintain session specific data values within the + EnSight instance.""" + return self._session_name + @property def language(self) -> str: """Current language specification for the EnSight session. Various @@ -937,11 +948,36 @@ def render(self, width: int, height: int, aa: int = 1) -> bytes: self._establish_connection() return self._grpc.render(width=width, height=height, aa=aa) + def _release_remote_objects(self, object_id: Optional[int] = None): + """ + Send a command to the remote EnSight session to drop a specific object + or all objects from the remote object cache. + + Parameters + ---------- + object_id: int, optional + The specific object to drop from the cache. If no objects are specified, + then all remote objects associated with this session will be dropped. + + """ + obj_str = "" + if object_id: + obj_str = f", id={object_id}" + cmd = f"ensight.objs.release_id('{self.name}'{obj_str})" + _ = self.cmd(cmd, do_eval=False) + def close(self) -> None: """Close the session. Close the current session and its gRPC connection. """ + # if version 242 or higher, free any objects we have cached there + if self.cei_suffix >= '242': + try: + self._release_remote_objects() + except RuntimeError: + # handle some intermediate EnSight builds. + pass if self._launcher and self._halt_ensight_on_close: self._launcher.close(self) else: @@ -1473,6 +1509,8 @@ def _convert_ctor(self, s: str) -> str: Class: ENS_GLOBALS, CvfObjID: 221, cached:yes Class: ENS_PART, desc: 'Sphere', CvfObjID: 1078, cached:no + Class: ENS_PART, desc: 'engine', PartType: 0, CvfObjID: 1097, cached:no + Class: ENS_GROUP, desc: '', Owned, CvfObjID: 1043, cached:no This method detects strings like those and converts them into strings like these:: @@ -1517,6 +1555,8 @@ def _convert_ctor(self, s: str) -> str: if (location != -1) and (location > id): subtype = int(s[location + len(name) :].split(",")[0]) break + # Owned flag + owned_flag = "Owned," in s[start + 7 : tail] # isolate the block to replace prefix = s[:start] suffix = s[tail + tail_len :] @@ -1544,6 +1584,8 @@ def _convert_ctor(self, s: str) -> str: if attr_value in classname_lookup: classname = classname_lookup[attr_value] subclass_info = f",attr_id={attr_id}, attr_value={attr_value}" + if owned_flag: + subclass_info += ",owned=True" replace_text = f"session.ensight.objs.{classname}(session, {objid}{subclass_info})" if replace_text is None: break From e1a7eaf093dd25d9d2018f45f8173c30ee21033d Mon Sep 17 00:00:00 2001 From: Randy Frank <89219420+randallfrank@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:15:33 -0500 Subject: [PATCH 3/7] Improve performance Speed up object transmission. Support remote object references. Clean up docs. --- .github/workflows/ci_cd.yml | 4 +- .github/workflows/nightly-docs.yml | 2 +- .github/workflows/nightly.yml | 2 +- pyproject.toml | 2 +- src/ansys/pyensight/core/ensobj.py | 53 +++++----- src/ansys/pyensight/core/listobj.py | 31 +++++- src/ansys/pyensight/core/session.py | 129 +++++++++++++----------- src/ansys/pyensight/core/utils/parts.py | 36 +++---- 8 files changed, 150 insertions(+), 109 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index bdab24affb2..db7a574e2ee 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -99,7 +99,7 @@ jobs: runs-on: ubuntu-latest needs: doc-style steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Login in Github Container registry uses: docker/login-action@v2 @@ -185,7 +185,7 @@ jobs: needs: tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Microsoft Teams Notification uses: jdcargile/ms-teams-notification@v1.3 with: diff --git a/.github/workflows/nightly-docs.yml b/.github/workflows/nightly-docs.yml index 84cba328759..d82a13114ed 100644 --- a/.github/workflows/nightly-docs.yml +++ b/.github/workflows/nightly-docs.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Login in Github Container registry uses: docker/login-action@v2 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2ea99ab44a3..acf4ff35104 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -74,7 +74,7 @@ jobs: needs: [ nightly_test, nightly_build ] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Microsoft Teams Notification uses: jdcargile/ms-teams-notification@v1.3 with: diff --git a/pyproject.toml b/pyproject.toml index f28d74a9396..740514fd93d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ tests = [ "docker>=6.1.0", ] doc = [ - "Sphinx==7.0.1", + "Sphinx==7.2.6", "numpydoc==1.5.0", "ansys-sphinx-theme==0.9.9", "sphinx-copybutton==0.5.2", diff --git a/src/ansys/pyensight/core/ensobj.py b/src/ansys/pyensight/core/ensobj.py index 8b9f801bcab..876ed3b4d74 100644 --- a/src/ansys/pyensight/core/ensobj.py +++ b/src/ansys/pyensight/core/ensobj.py @@ -3,7 +3,7 @@ The ensobj module provides the base class to all EnSight proxy objects """ -from typing import TYPE_CHECKING, Any, List, Optional, no_type_check +from typing import TYPE_CHECKING, Any, Optional, no_type_check if TYPE_CHECKING: from ansys.pyensight.core import Session @@ -48,12 +48,14 @@ def __init__( self._objid = objid self._attr_id = attr_id self._attr_value = attr_value - self._session.add_ensobj_instance(self) - self.attr_list = self.populate_attr_list() + # True if this Python instance "owns" the ENSOBJ instance (via EnSight proxy cache) - self._is_owned = False if owned: self._is_owned = True + else: + self._is_owned = False + # do not put this object in the cache if it is owned, allow gc + self._session.add_ensobj_instance(self) def __eq__(self, obj): return self._objid == obj._objid @@ -64,6 +66,20 @@ def __lt__(self, obj): def __hash__(self): return self._objid + def __del__(self): + # release the session to allow for garbage collection + tmp_session = self._session + self._session = None + if self._is_owned: + try: + cmd = f"ensight.objs.release_id('{tmp_session.name}', {self.__OBJID__})" + tmp_session.cmd(cmd, do_eval=False) + except Exception: + # This could happen at any time, including outside + # the scope of the session, so we need to be + # ready for any error. + pass + @property def __OBJID__(self) -> int: # noqa: N802 return self._objid @@ -231,16 +247,6 @@ def attrinfo(self, attrid: Optional[Any] = None) -> dict: return self._session.cmd(f"{self._remote_obj()}.attrinfo()") return self._session.cmd(f"{self._remote_obj()}.attrinfo({attrid.__repr__()})") - def populate_attr_list(self) -> List[str]: - """Populates a list with attributes. - - Returns - ------- - List[str] - The list with the attributes. - """ - return [k for k, _ in self.attrinfo().items()] - def attrissensitive(self, attrid: Any) -> bool: """Check to see if a given attribute is 'sensitive' @@ -446,16 +452,17 @@ def destroy(self) -> None: def __str__(self) -> str: desc = "" - if self._session.ensight.objs.enums.DESCRIPTION in self.attr_list: - try: - if hasattr(self, "DESCRIPTION"): - desc_text = self.DESCRIPTION - else: + if hasattr(self.__class__, "attr_list"): + if self._session.ensight.objs.enums.DESCRIPTION in self.__class__.attr_list: + try: + if hasattr(self, "DESCRIPTION"): + desc_text = self.DESCRIPTION + else: + desc_text = "" + except RuntimeError: + # self.DESCRIPTION is a gRPC call that can fail for default objects desc_text = "" - except RuntimeError: - # self.DESCRIPTION is a gRPC call that can fail for default objects - desc_text = "" - desc = f", desc: '{desc_text}'" + desc = f", desc: '{desc_text}'" owned = "" if self._is_owned: owned = ", Owned" diff --git a/src/ansys/pyensight/core/listobj.py b/src/ansys/pyensight/core/listobj.py index 8ea1a9d2ae4..b5178b3c70d 100644 --- a/src/ansys/pyensight/core/listobj.py +++ b/src/ansys/pyensight/core/listobj.py @@ -5,7 +5,19 @@ """ from collections.abc import Iterable import fnmatch -from typing import Any, List, Optional, SupportsIndex, TypeVar, no_type_check, overload +from typing import ( + TYPE_CHECKING, + Any, + List, + Optional, + SupportsIndex, + TypeVar, + no_type_check, + overload, +) + +if TYPE_CHECKING: + from ansys.pyensight.core import Session from ansys.pyensight.core.ensobj import ENSOBJ @@ -44,8 +56,9 @@ class ensobjlist(List[T]): # noqa: N801 """ - def __init__(self, *args, **kwargs) -> None: + def __init__(self, *args, session: Optional["Session"] = None, **kwargs) -> None: super().__init__(*args, **kwargs) + self._session = session @staticmethod def _is_iterable(arg: Any) -> bool: @@ -92,7 +105,7 @@ def find( value_list = value if not self._is_iterable(value): value_list = [value] - out_list: ensobjlist[Any] = ensobjlist() + out_list: ensobjlist[Any] = ensobjlist(session=self._session) for item in self: if isinstance(item, ENSOBJ): try: @@ -112,7 +125,16 @@ def find( break except RuntimeError: pass - # TODO: handle group + if group: + # This is a bit of a hack, but the find() method generates a local list of + # proxy objects. We want to put that in a group. We do that by running + # a script in EnSight that creates an empty group and then adds those + # children to the group. The output becomes the remote referenced ENS_GROUP. + if self._session is not None: + ens_group_cmd = "ensight.objs.core.VPORTS.find('__unknown__', group=1)" + ens_group = self._session.cmd(ens_group_cmd) + ens_group.addchild(out_list) + out_list = ens_group return out_list def set_attr(self, attr: Any, value: Any) -> int: @@ -176,7 +198,6 @@ def get_attr(self, attr: Any, default: Optional[Any] = None): >>> state = session.ensight.core.PARTS.get_attr(session.ensight.objs.enums.VISIBLE) """ - objid_list = [] session = None objid_list = [x.__OBJID__ for x in self if isinstance(x, ENSOBJ)] for item in self: diff --git a/src/ansys/pyensight/core/session.py b/src/ansys/pyensight/core/session.py index 79dcf929fb5..df889beaf51 100644 --- a/src/ansys/pyensight/core/session.py +++ b/src/ansys/pyensight/core/session.py @@ -12,7 +12,6 @@ """ import atexit import importlib.util -import uuid from os import listdir import os.path import platform @@ -23,6 +22,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple from urllib.parse import urlparse from urllib.request import url2pathname +import uuid import webbrowser from ansys.pyensight.core.enscontext import EnsContext @@ -198,6 +198,54 @@ def __init__( # call close() twice on this class if needed. atexit.register(self.close) + # Speed up subtype lookups: + self._subtype_tables = {} + part_lookup_dict = dict() + part_lookup_dict[0] = "ENS_PART_MODEL" + part_lookup_dict[1] = "ENS_PART_CLIP" + part_lookup_dict[2] = "ENS_PART_CONTOUR" + part_lookup_dict[3] = "ENS_PART_DISCRETE_PARTICLE" + part_lookup_dict[4] = "ENS_PART_FRAME" + part_lookup_dict[5] = "ENS_PART_ISOSURFACE" + part_lookup_dict[6] = "ENS_PART_PARTICLE_TRACE" + part_lookup_dict[7] = "ENS_PART_PROFILE" + part_lookup_dict[8] = "ENS_PART_VECTOR_ARROW" + part_lookup_dict[9] = "ENS_PART_ELEVATED_SURFACE" + part_lookup_dict[10] = "ENS_PART_DEVELOPED_SURFACE" + part_lookup_dict[15] = "ENS_PART_BUILT_UP" + part_lookup_dict[16] = "ENS_PART_TENSOR_GLYPH" + part_lookup_dict[17] = "ENS_PART_FX_VORTEX_CORE" + part_lookup_dict[18] = "ENS_PART_FX_SHOCK" + part_lookup_dict[19] = "ENS_PART_FX_SEP_ATT" + part_lookup_dict[20] = "ENS_PART_MAT_INTERFACE" + part_lookup_dict[21] = "ENS_PART_POINT" + part_lookup_dict[22] = "ENS_PART_AXISYMMETRIC" + part_lookup_dict[24] = "ENS_PART_VOF" + part_lookup_dict[25] = "ENS_PART_AUX_GEOM" + part_lookup_dict[26] = "ENS_PART_FILTER" + self._subtype_tables["ENS_PART"] = part_lookup_dict + annot_lookup_dict = dict() + annot_lookup_dict[0] = "ENS_ANNOT_TEXT" + annot_lookup_dict[1] = "ENS_ANNOT_LINE" + annot_lookup_dict[2] = "ENS_ANNOT_LOGO" + annot_lookup_dict[3] = "ENS_ANNOT_LGND" + annot_lookup_dict[4] = "ENS_ANNOT_MARKER" + annot_lookup_dict[5] = "ENS_ANNOT_ARROW" + annot_lookup_dict[6] = "ENS_ANNOT_DIAL" + annot_lookup_dict[7] = "ENS_ANNOT_GAUGE" + annot_lookup_dict[8] = "ENS_ANNOT_SHAPE" + self._subtype_tables["ENS_ANNOT"] = annot_lookup_dict + tool_lookup_dict = dict() + tool_lookup_dict[0] = "ENS_TOOL_CURSOR" + tool_lookup_dict[1] = "ENS_TOOL_LINE" + tool_lookup_dict[2] = "ENS_TOOL_PLANE" + tool_lookup_dict[3] = "ENS_TOOL_BOX" + tool_lookup_dict[4] = "ENS_TOOL_CYLINDER" + tool_lookup_dict[5] = "ENS_TOOL_CONE" + tool_lookup_dict[6] = "ENS_TOOL_SPHERE" + tool_lookup_dict[7] = "ENS_TOOL_REVOLUTION" + self._subtype_tables["ENS_TOOL"] = annot_lookup_dict + def __repr__(self): # if this is called from in the ctor, self.launcher might be None. session_dir = "" @@ -895,7 +943,8 @@ def cmd(self, value: str, do_eval: bool = True) -> Any: ret = self._grpc.command(value, do_eval=do_eval) if do_eval: ret = self._convert_ctor(ret) - return eval(ret, dict(session=self, ensobjlist=ensobjlist)) + value = eval(ret, dict(session=self, ensobjlist=ensobjlist)) + return value return ret def geometry(self, what: str = "glb") -> bytes: @@ -972,7 +1021,7 @@ def close(self) -> None: Close the current session and its gRPC connection. """ # if version 242 or higher, free any objects we have cached there - if self.cei_suffix >= '242': + if self.cei_suffix >= "242": try: self._release_remote_objects() except RuntimeError: @@ -1450,55 +1499,13 @@ def _obj_attr_subtype(self, classname: str) -> Tuple[Optional[int], Optional[dic """ if classname == "ENS_PART": - part_lookup_dict = dict() - part_lookup_dict[0] = "ENS_PART_MODEL" - part_lookup_dict[1] = "ENS_PART_CLIP" - part_lookup_dict[2] = "ENS_PART_CONTOUR" - part_lookup_dict[3] = "ENS_PART_DISCRETE_PARTICLE" - part_lookup_dict[4] = "ENS_PART_FRAME" - part_lookup_dict[5] = "ENS_PART_ISOSURFACE" - part_lookup_dict[6] = "ENS_PART_PARTICLE_TRACE" - part_lookup_dict[7] = "ENS_PART_PROFILE" - part_lookup_dict[8] = "ENS_PART_VECTOR_ARROW" - part_lookup_dict[9] = "ENS_PART_ELEVATED_SURFACE" - part_lookup_dict[10] = "ENS_PART_DEVELOPED_SURFACE" - part_lookup_dict[15] = "ENS_PART_BUILT_UP" - part_lookup_dict[16] = "ENS_PART_TENSOR_GLYPH" - part_lookup_dict[17] = "ENS_PART_FX_VORTEX_CORE" - part_lookup_dict[18] = "ENS_PART_FX_SHOCK" - part_lookup_dict[19] = "ENS_PART_FX_SEP_ATT" - part_lookup_dict[20] = "ENS_PART_MAT_INTERFACE" - part_lookup_dict[21] = "ENS_PART_POINT" - part_lookup_dict[22] = "ENS_PART_AXISYMMETRIC" - part_lookup_dict[24] = "ENS_PART_VOF" - part_lookup_dict[25] = "ENS_PART_AUX_GEOM" - part_lookup_dict[26] = "ENS_PART_FILTER" - return self.ensight.objs.enums.PARTTYPE, part_lookup_dict + return self.ensight.objs.enums.PARTTYPE, self._subtype_tables[classname] elif classname == "ENS_ANNOT": - annot_lookup_dict = dict() - annot_lookup_dict[0] = "ENS_ANNOT_TEXT" - annot_lookup_dict[1] = "ENS_ANNOT_LINE" - annot_lookup_dict[2] = "ENS_ANNOT_LOGO" - annot_lookup_dict[3] = "ENS_ANNOT_LGND" - annot_lookup_dict[4] = "ENS_ANNOT_MARKER" - annot_lookup_dict[5] = "ENS_ANNOT_ARROW" - annot_lookup_dict[6] = "ENS_ANNOT_DIAL" - annot_lookup_dict[7] = "ENS_ANNOT_GAUGE" - annot_lookup_dict[8] = "ENS_ANNOT_SHAPE" - return self.ensight.objs.enums.ANNOTTYPE, annot_lookup_dict + return self.ensight.objs.enums.ANNOTTYPE, self._subtype_tables[classname] elif classname == "ENS_TOOL": - tool_lookup_dict = dict() - tool_lookup_dict[0] = "ENS_TOOL_CURSOR" - tool_lookup_dict[1] = "ENS_TOOL_LINE" - tool_lookup_dict[2] = "ENS_TOOL_PLANE" - tool_lookup_dict[3] = "ENS_TOOL_BOX" - tool_lookup_dict[4] = "ENS_TOOL_CYLINDER" - tool_lookup_dict[5] = "ENS_TOOL_CONE" - tool_lookup_dict[6] = "ENS_TOOL_SPHERE" - tool_lookup_dict[7] = "ENS_TOOL_REVOLUTION" - return self.ensight.objs.enums.TOOLTYPE, tool_lookup_dict + return self.ensight.objs.enums.TOOLTYPE, self._subtype_tables[classname] return None, None @@ -1533,30 +1540,33 @@ def _convert_ctor(self, s: str) -> str: """ self._prune_hash() + offset = 0 while True: # Find the object repl block to replace - id = s.find("CvfObjID:") + id = s.find("CvfObjID:", offset) if id == -1: break - start = s.find("Class: ") + start = s.find("Class: ", offset) if (start == -1) or (start > id): break tail_len = 11 - tail = s.find(", cached:no") + tail = s.find(", cached:no", offset) if tail == -1: tail_len = 12 - tail = s.find(", cached:yes") + tail = s.find(", cached:yes", offset) if tail == -1: break + # just this object substring + tmp = s[start + 7 : tail] # Subtype (PartType:, AnnotType:, ToolType:) subtype = None for name in ("PartType:", "AnnotType:", "ToolType:"): - location = s.find(name) - if (location != -1) and (location > id): - subtype = int(s[location + len(name) :].split(",")[0]) + location = tmp.find(name) + if location != -1: + subtype = int(tmp[location + len(name) :].split(",")[0]) break # Owned flag - owned_flag = "Owned," in s[start + 7 : tail] + owned_flag = "Owned," in tmp # isolate the block to replace prefix = s[:start] suffix = s[tail + tail_len :] @@ -1575,7 +1585,9 @@ def _convert_ctor(self, s: str) -> str: if attr_id is not None: if subtype is not None: # the 2024 R2 interface includes the subtype - subclass_info = f",attr_id={attr_id}, attr_value={subtype}" + if (classname_lookup is not None) and (subtype in classname_lookup): + classname = classname_lookup[subtype] + subclass_info = f",attr_id={attr_id}, attr_value={subtype}" elif classname_lookup is not None: # if a "subclass" case and no subclass attrid value, ask for it... remote_name = self.remote_obj(objid) @@ -1589,10 +1601,11 @@ def _convert_ctor(self, s: str) -> str: replace_text = f"session.ensight.objs.{classname}(session, {objid}{subclass_info})" if replace_text is None: break + offset = start + len(replace_text) s = prefix + replace_text + suffix s = s.strip() if s.startswith("[") and s.endswith("]"): - s = "ensobjlist(" + s + ")" + s = f"ensobjlist({s}, session=session)" return s def capture_context(self, full_context: bool = False) -> "enscontext.EnsContext": diff --git a/src/ansys/pyensight/core/utils/parts.py b/src/ansys/pyensight/core/utils/parts.py index 01403d8a8be..dfe2532dc80 100644 --- a/src/ansys/pyensight/core/utils/parts.py +++ b/src/ansys/pyensight/core/utils/parts.py @@ -460,8 +460,8 @@ def create_particle_trace_from_points( Create a particle trace part from a list o points. Returns the ``ENS_PART`` generated. - Parameters: - ----------- + Parameters + ---------- name: str The name of part to be generated @@ -549,8 +549,8 @@ def create_particle_trace_from_line( Create a particle trace part from a line. Returns the ``ENS_PART`` generated. - Parameters: - ----------- + Parameters + ---------- name: str The name of part to be generated @@ -647,8 +647,8 @@ def create_particle_trace_from_plane( Create a particle trace part from a plane. Returns the ``ENS_PART`` generated. - Parameters: - ----------- + Parameters + ---------- name: str The name of part to be generated @@ -755,8 +755,8 @@ def create_particle_trace_from_parts( Create a particle trace part from a list of seed parts. Returns the ``ENS_PART`` generated. - Parameters: - ----------- + Parameters + ---------- name: str The name of part to be generated @@ -794,7 +794,7 @@ def create_particle_trace_from_parts( ==================== ================================================= PART_EMIT_FROM_NODES Emit from the nodes of the part PART_EMIT_FROM_AREA Create an area of equidistant points for emission - ================== ================================================= + ==================== ================================================= If not provided, it will default to ``PART_EMIT_FROM_NODES`` num_points: int @@ -862,8 +862,8 @@ def add_emitter_points_to_particle_trace_part( Add point emitters to an existing particle trace. The function will return the updated ``ENS_PART`` object. - Parameters: - ----------- + Parameters + ---------- particle_trace_part: The particle trace part to be added emitters to. @@ -896,8 +896,8 @@ def add_emitter_line_to_particle_trace_part( Add a line emitter to an existing particle trace. The function will return the updated ``ENS_PART`` object. - Parameters: - ----------- + Parameters + ---------- particle_trace_part: The particle trace part to be added emitters to. @@ -938,8 +938,8 @@ def add_emitter_plane_to_particle_trace_part( Add a plane emitter to an existing particle trace. The function will return the updated ``ENS_PART`` object. - Parameters: - ----------- + Parameters + ---------- particle_trace_part: The particle trace part to be added emitters to. @@ -989,8 +989,8 @@ def add_emitter_parts_to_particle_trace_part( Add a list of part emitters to an existing particle trace. The function will return the updated ``ENS_PART`` object. - Parameters: - ----------- + Parameters + ---------- particle_trace_part: The particle trace part to be added emitters to. @@ -1007,7 +1007,7 @@ def add_emitter_parts_to_particle_trace_part( ==================== ================================================= PART_EMIT_FROM_NODES Emit from the nodes of the part PART_EMIT_FROM_AREA Create an area of equidistant points for emission - ================== ================================================= + ==================== ================================================= If not provided, it will default to ``PART_EMIT_FROM_NODES`` num_points: int From 5193b62d25a4469ed78bd1784b471e0ec9e03467 Mon Sep 17 00:00:00 2001 From: Mario Ostieri Date: Wed, 6 Dec 2023 09:06:24 +0000 Subject: [PATCH 4/7] fix unit tests --- src/ansys/pyensight/core/session.py | 2 +- tests/conftest.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ansys/pyensight/core/session.py b/src/ansys/pyensight/core/session.py index df889beaf51..eb4c23a306e 100644 --- a/src/ansys/pyensight/core/session.py +++ b/src/ansys/pyensight/core/session.py @@ -244,7 +244,7 @@ def __init__( tool_lookup_dict[5] = "ENS_TOOL_CONE" tool_lookup_dict[6] = "ENS_TOOL_SPHERE" tool_lookup_dict[7] = "ENS_TOOL_REVOLUTION" - self._subtype_tables["ENS_TOOL"] = annot_lookup_dict + self._subtype_tables["ENS_TOOL"] = tool_lookup_dict def __repr__(self): # if this is called from in the ctor, self.launcher might be None. diff --git a/tests/conftest.py b/tests/conftest.py index 390024a42a0..45db7502ad1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -138,4 +138,5 @@ def mocked_session(mocker, tmpdir, enshell_mock) -> "Session": timeout=120.0, ) session._build_utils_interface() + session._cei_suffix = "345" return session From 2a82489e92b950c3ddd2362afd4c7634710134d5 Mon Sep 17 00:00:00 2001 From: Randy Frank <89219420+randallfrank@users.noreply.github.com> Date: Wed, 6 Dec 2023 10:56:58 -0500 Subject: [PATCH 5/7] Add tests and improve coverage Include a test for the ENS_GROUP remote object. Re-organize the code a bit to improve coverage. --- src/ansys/pyensight/core/ensight_grpc.py | 4 +-- src/ansys/pyensight/core/ensobj.py | 2 +- src/ansys/pyensight/core/listobj.py | 1 - src/ansys/pyensight/core/session.py | 4 +-- tests/example_tests/test_remote_objects.py | 31 ++++++++++++++++++++++ 5 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 tests/example_tests/test_remote_objects.py diff --git a/src/ansys/pyensight/core/ensight_grpc.py b/src/ansys/pyensight/core/ensight_grpc.py index e24269546c1..4eeb9384408 100644 --- a/src/ansys/pyensight/core/ensight_grpc.py +++ b/src/ansys/pyensight/core/ensight_grpc.py @@ -173,8 +173,8 @@ def _metadata(self) -> List[Tuple[bytes, Union[str, bytes]]]: if type(s) == str: s = s.encode("utf-8") ret.append((b"shared_secret", s)) - if self._session_name: - s = self._session_name.encode("utf-8") + if self.session_name: + s = self.session_name.encode("utf-8") ret.append((b"session_name", s)) return ret diff --git a/src/ansys/pyensight/core/ensobj.py b/src/ansys/pyensight/core/ensobj.py index 876ed3b4d74..12725f6e5d2 100644 --- a/src/ansys/pyensight/core/ensobj.py +++ b/src/ansys/pyensight/core/ensobj.py @@ -74,7 +74,7 @@ def __del__(self): try: cmd = f"ensight.objs.release_id('{tmp_session.name}', {self.__OBJID__})" tmp_session.cmd(cmd, do_eval=False) - except Exception: + except Exception: # pragma: no cover # This could happen at any time, including outside # the scope of the session, so we need to be # ready for any error. diff --git a/src/ansys/pyensight/core/listobj.py b/src/ansys/pyensight/core/listobj.py index b5178b3c70d..39abc1f45ad 100644 --- a/src/ansys/pyensight/core/listobj.py +++ b/src/ansys/pyensight/core/listobj.py @@ -161,7 +161,6 @@ def set_attr(self, attr: Any, value: Any) -> int: >>> session.ensight.objs.core.PARTS.set_attr("VISIBLE", True) """ - objid_list = [] session = None objid_list = [x.__OBJID__ for x in self if isinstance(x, ENSOBJ)] for item in self: diff --git a/src/ansys/pyensight/core/session.py b/src/ansys/pyensight/core/session.py index eb4c23a306e..f77f6d4ee1b 100644 --- a/src/ansys/pyensight/core/session.py +++ b/src/ansys/pyensight/core/session.py @@ -309,7 +309,7 @@ def _check_rest_connection(self) -> None: except Exception: pass time.sleep(0.5) - raise RuntimeError("Unable to establish a REST connection to EnSight.") + raise RuntimeError("Unable to establish a REST connection to EnSight.") # pragma: no cover @property def name(self) -> str: @@ -1024,7 +1024,7 @@ def close(self) -> None: if self.cei_suffix >= "242": try: self._release_remote_objects() - except RuntimeError: + except RuntimeError: # pragma: no cover # handle some intermediate EnSight builds. pass if self._launcher and self._halt_ensight_on_close: diff --git a/tests/example_tests/test_remote_objects.py b/tests/example_tests/test_remote_objects.py new file mode 100644 index 00000000000..c7e0b7a9afa --- /dev/null +++ b/tests/example_tests/test_remote_objects.py @@ -0,0 +1,31 @@ +from ansys.pyensight.core import DockerLauncher, LocalLauncher + +import gc +import pytest + + +def test_remote_objects(tmpdir, pytestconfig: pytest.Config): + data_dir = tmpdir.mkdir("datadir") + use_local = pytestconfig.getoption("use_local_launcher") + if use_local: + launcher = LocalLauncher() + else: + launcher = DockerLauncher(data_directory=data_dir, use_dev=True) + session = launcher.start() + session.load_data(f"{session.cei_home}/ensight{session.cei_suffix}/data/guard_rail/crash.case") + + # call __str__ on an ENSOBJ object w/o DESCRIPTION attribute (for coverage) + print(session.ensight.objs.core) + + if session.cei_suffix >= "242": + # Create an ENS_GROUP object (a remote object) + g = session.ensight.objs.core.PARTS.find('*rail*', wildcard=1, group=1) + assert "ENS_GROUP" in g.__str__(), "ensobjlist.find() did not return an ENS_GROUP instance" + assert "Owned" in g.__str__(), "Remote ENS_GROUP is not 'Owned'" + assert "Owned" not in g.CHILDREN.__str__(), "Objects in ENS_GROUP are incorrectly 'Owned'" + + # Exercise the custom __del__() method + g = None + gc.collect() + + session.close() From 02b06682a79e68414c91d339eb908ddd14d18391 Mon Sep 17 00:00:00 2001 From: Randy Frank <89219420+randallfrank@users.noreply.github.com> Date: Wed, 6 Dec 2023 11:04:08 -0500 Subject: [PATCH 6/7] Reformat the code Apply formatting rules. --- tests/example_tests/test_remote_objects.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/example_tests/test_remote_objects.py b/tests/example_tests/test_remote_objects.py index c7e0b7a9afa..3fc51dc9411 100644 --- a/tests/example_tests/test_remote_objects.py +++ b/tests/example_tests/test_remote_objects.py @@ -1,6 +1,6 @@ -from ansys.pyensight.core import DockerLauncher, LocalLauncher - import gc + +from ansys.pyensight.core import DockerLauncher, LocalLauncher import pytest @@ -16,10 +16,10 @@ def test_remote_objects(tmpdir, pytestconfig: pytest.Config): # call __str__ on an ENSOBJ object w/o DESCRIPTION attribute (for coverage) print(session.ensight.objs.core) - + if session.cei_suffix >= "242": # Create an ENS_GROUP object (a remote object) - g = session.ensight.objs.core.PARTS.find('*rail*', wildcard=1, group=1) + g = session.ensight.objs.core.PARTS.find("*rail*", wildcard=1, group=1) assert "ENS_GROUP" in g.__str__(), "ensobjlist.find() did not return an ENS_GROUP instance" assert "Owned" in g.__str__(), "Remote ENS_GROUP is not 'Owned'" assert "Owned" not in g.CHILDREN.__str__(), "Objects in ENS_GROUP are incorrectly 'Owned'" From f846d69c78740fa79f53aee6a0a7fe4cd920759f Mon Sep 17 00:00:00 2001 From: Randy Frank <89219420+randallfrank@users.noreply.github.com> Date: Wed, 6 Dec 2023 12:14:25 -0500 Subject: [PATCH 7/7] Mark more code as not covered Additional "catchall" exceptions that should never be reached marked as not part of the coverage test. --- src/ansys/pyensight/core/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ansys/pyensight/core/session.py b/src/ansys/pyensight/core/session.py index f77f6d4ee1b..bdac8517000 100644 --- a/src/ansys/pyensight/core/session.py +++ b/src/ansys/pyensight/core/session.py @@ -1067,7 +1067,7 @@ def _build_utils_interface(self) -> None: # Create an instance, using ensight as the EnSight interface # and place it in this module. setattr(self._ensight.utils, _name, _the_class(self._ensight)) - except Exception as e: + except Exception as e: # pragma: no cover # Warn on import errors print(f"Error loading ensight.utils from: '{_filename}' : {e}")