diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index fb01cb47cc..d80b99d8c1 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -740,6 +740,7 @@ jobs: NUM_CORES: 1 ANSYS_WORKBENCH_LOGGING_FILTER_LEVEL: 0 BUILD_CHEATSHEET: true + PYMECHANICAL_GALLERY_PARALLEL: "4" CONTAINER_STABLE_EXIT: ${{ needs.container-stability-check.outputs.container_stable_exit }} run: | . /env/bin/activate diff --git a/doc/changelog.d/1572.test.md b/doc/changelog.d/1572.test.md new file mode 100644 index 0000000000..54b7e1d122 --- /dev/null +++ b/doc/changelog.d/1572.test.md @@ -0,0 +1 @@ +Parallel example test diff --git a/doc/source/conf.py b/doc/source/conf.py index 2665ac0d4b..7fe5635837 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -13,6 +13,15 @@ from pathlib import Path import warnings +# Sphinx-Gallery parallel workers are separate processes that never import this +# file. PyVista reads PYVISTA_* from the environment at package import time, so +# set these before importing pyvista; child processes inherit os.environ. +os.environ.setdefault("PYVISTA_BUILDING_GALLERY", "true") +os.environ.setdefault("PYVISTA_OFF_SCREEN", "true") +# Parallel gallery workers may reuse a process; embedded App() then needs +# BUILDING_GALLERY so a later example can attach via _share instead of erroring. +os.environ.setdefault("PYMECHANICAL_BUILDING_GALLERY", "true") + from ansys_sphinx_theme import ansys_favicon, get_version_match import pyvista from pyvista.plotting.utilities.sphinx_gallery import DynamicScraper @@ -22,9 +31,6 @@ import ansys.mechanical.core as pymechanical from ansys.mechanical.core.embedding.initializer import SUPPORTED_MECHANICAL_EMBEDDING_VERSIONS -# necessary when building the sphinx gallery -pymechanical.BUILDING_GALLERY = True - # Ensure that offscreen rendering is used for docs generation pyvista.OFF_SCREEN = True @@ -182,11 +188,21 @@ copybutton_prompt_is_regexp = True # -- Sphinx Gallery Options --------------------------------------------------- +# Parallel example execution (separate Python subprocess per running example). +# Each subprocess can host one embedded Mechanical App, so N workers means up +# to N concurrent Mechanical processes—only use N>1 when licenses and RAM allow. +# https://sphinx-gallery.github.io/stable/configuration.html#parallel-gallery-builds +try: + _gallery_parallel = max(1, int(os.environ.get("PYMECHANICAL_GALLERY_PARALLEL", "1"))) +except ValueError: + _gallery_parallel = 1 + sphinx_gallery_conf = { # convert rst to md for ipynb "pypandoc": True, # path to your examples scripts "examples_dirs": ["../../examples/"], + "abort_on_example_error": True, # path where to save gallery generated examples "gallery_dirs": ["examples/gallery_examples"], # Pattern to search for example files "filename_pattern": r"\.py", @@ -202,6 +218,7 @@ # Files to ignore "ignore_pattern": "flycheck*", # noqa: E501 "thumbnail_size": (350, 350), + "parallel": _gallery_parallel, } # -- Options for HTML output ------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 041b75cc66..65aadbf109 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ tests = [ doc = [ "sphinx>=8.2.3,<9", + "joblib>=1.4.0,<2", "ansys-sphinx-theme[autoapi,changelog]>=1.7.0,<2", "imageio-ffmpeg>=0.6.0,<1", "imageio>=2.37.2,<3", diff --git a/src/ansys/mechanical/core/__init__.py b/src/ansys/mechanical/core/__init__.py index a6da4c7426..d75c561d81 100644 --- a/src/ansys/mechanical/core/__init__.py +++ b/src/ansys/mechanical/core/__init__.py @@ -75,5 +75,10 @@ from ansys.mechanical.core.pool import LocalMechanicalPool -BUILDING_GALLERY = False -"""Whether or not to build gallery examples.""" +_gallery_env = os.environ.get("PYMECHANICAL_BUILDING_GALLERY", "").strip().lower() +BUILDING_GALLERY = _gallery_env in ("1", "true", "yes") +"""Gallery mode for embedded ``App`` (shared instance when a process runs multiple examples). + +Parallel Sphinx-Gallery workers inherit ``PYMECHANICAL_BUILDING_GALLERY=true`` from the +environment; they do not execute ``doc/source/conf.py``. +""" diff --git a/src/ansys/mechanical/core/embedding/resolver.py b/src/ansys/mechanical/core/embedding/resolver.py index bb46a5b103..1588a7a958 100644 --- a/src/ansys/mechanical/core/embedding/resolver.py +++ b/src/ansys/mechanical/core/embedding/resolver.py @@ -27,6 +27,28 @@ starting in version 23.1 and on Linux starting in version 23.2 """ +from pathlib import Path +import sys + + +def _sys_path_has_shadow_ansys_dir(path_entry: str) -> bool: + """Check whether *path_entry* contains a subdirectory named exactly ``Ansys``. + + Such a folder is imported as a namespace package and shadows the CLR + ``Ansys`` module from ``clr.AddReference``, causing + ``module 'Ansys' has no attribute 'Mechanical'``. + + Only the exact spelling ``Ansys`` is treated as a shadow so + ``site-packages/ansys`` is not removed from ``sys.path``. + """ + base = Path(path_entry) if path_entry else Path.cwd() + try: + if not base.is_dir(): + return False + return any(p.is_dir() and p.name == "Ansys" for p in base.iterdir()) + except OSError: + return False + def resolve(version): """Resolve function for all versions of Ansys Mechanical.""" @@ -34,15 +56,23 @@ def resolve(version): import System # isort: skip clr.AddReference("Ansys.Mechanical.Embedding") - import Ansys # isort: skip + + _original_sys_path = sys.path[:] + try: + sys.path[:] = [p for p in sys.path if not _sys_path_has_shadow_ansys_dir(p)] + import Ansys # isort: skip + finally: + sys.path[:] = _original_sys_path try: assembly_resolver = Ansys.Mechanical.Embedding.AssemblyResolver resolve_handler = assembly_resolver.MechanicalResolveEventHandler System.AppDomain.CurrentDomain.AssemblyResolve += resolve_handler - except AttributeError: - error_msg = """Unable to resolve Mechanical assemblies. Please ensure the following: - 1. Mechanical is installed. - 2. A folder with the name "Ansys" does not exist in the same directory as the script being run. - """ - raise AttributeError(error_msg) + except AttributeError as err: + error_msg = ( + "Unable to resolve Mechanical assemblies. Please ensure the following:\n" + " 1. Mechanical is installed.\n" + ' 2. A subdirectory named exactly "Ansys" does not exist on sys.path ' + "(e.g. next to an example script), which shadows the CLR Ansys module.\n" + ) + raise AttributeError(error_msg) from err