Source code for pySimBlocks.tools.blocks_registry

# ******************************************************************************
#                                  pySimBlocks
#                     Copyright (c) 2026 Université de Lille & INRIA
# ******************************************************************************
#  This program is free software: you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or (at your
#  option) any later version.
#
#  This program is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
#  for more details.
#
#  You should have received a copy of the GNU Lesser General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
# ******************************************************************************
#  Authors: see Authors.txt
# ******************************************************************************

from __future__ import annotations

import importlib
import inspect
from pathlib import Path
from typing import Dict, Optional

from pySimBlocks.gui.blocks.block_meta import BlockMeta

# Mapping: category -> block_type -> BlockMeta instance.
BlockRegistry = Dict[str, Dict[str, BlockMeta]]


[docs] def load_block_registry( metadata_root: Path | str | None = None, ) -> BlockRegistry: """Load all BlockMeta subclasses from the GUI blocks directory. Recursively scans metadata_root for Python files and registers every BlockMeta subclass found. Defaults to ``pySimBlocks/gui/blocks/``. Args: metadata_root: Root directory to scan. Defaults to the package's ``gui/blocks/`` directory. Returns: Registry mapping category names to dicts of block_type -> BlockMeta. Raises: FileNotFoundError: If metadata_root does not exist. ValueError: If two files define a BlockMeta with the same type within the same category. """ if metadata_root is None: metadata_root = Path(__file__).parents[1] / "gui" / "blocks" else: metadata_root = Path(metadata_root).resolve() if not metadata_root.exists(): raise FileNotFoundError( f"blocks_metadata directory not found: {metadata_root}" ) registry: BlockRegistry = {} for py_path in metadata_root.rglob("*.py"): _register_block_from_py(py_path, registry) return registry
# -------------------------------------------------------------------------- # Private helpers # -------------------------------------------------------------------------- def _register_block_from_py( py_path: Path, registry: BlockRegistry, ) -> None: """Import a .py file and register all BlockMeta subclasses it contains.""" module_name = _path_to_module(py_path) module = importlib.import_module(module_name) doc_path = _resolve_doc_path(py_path) for _, obj in inspect.getmembers(module, inspect.isclass): if not issubclass(obj, BlockMeta) or obj is BlockMeta: continue meta: BlockMeta = obj() meta.doc_path = doc_path category = meta.category block_type = meta.type registry.setdefault(category, {}) if block_type in registry[category]: raise ValueError( f"Duplicate block type '{block_type}' in category '{category}'.\n" f"Conflict in module: {module_name}" ) registry[category][block_type] = meta def _path_to_module(py_path: Path) -> str: """Convert a file path to a dotted Python module name. Example: ``pySimBlocks/gui/blocks/operators/sum.py`` -> ``pySimBlocks.gui.blocks.operators.sum`` Args: py_path: Absolute path to a Python source file. Returns: Dotted module name relative to the package root. Raises: RuntimeError: If py_path is not inside the package root. """ py_path = py_path.with_suffix("") package_root = Path(__file__).parents[1] # pySimBlocks/ try: rel_path = py_path.relative_to(package_root) except ValueError: raise RuntimeError( f"File {py_path} is not inside package root {package_root}" ) return package_root.name + "." + rel_path.as_posix().replace("/", ".") def _resolve_doc_path(py_path: Path) -> Optional[Path]: """Resolve the Markdown documentation file for a GUI block module. Example: ``gui/blocks/systems/sofa/sofa_plant.py`` -> ``docs/blocks/systems/sofa/sofa_plant.md`` Args: py_path: Path to the GUI block Python file. Returns: Path to the corresponding .md file if it exists, else None. """ try: parts = list(py_path.parts) idx = parts.index("gui") except ValueError: return None doc_root = Path(*parts[:idx]) / "docs" rel = Path(*parts[idx + 1 :]).with_suffix(".md") doc_path = doc_root / rel return doc_path if doc_path.exists() else None if __name__ == "__main__": registry = load_block_registry()