From bd585ae8244230a59099f3ace5a74fa35fc3a4fd Mon Sep 17 00:00:00 2001 From: David Heejong Park Date: Fri, 11 Oct 2024 23:24:12 +0200 Subject: [PATCH 01/12] Added Cython option --- pythonfmu/builder.py | 25 ++++- .../src/pythonfmu/PySlaveInstance.cpp | 104 ++++++++++++------ 2 files changed, 91 insertions(+), 38 deletions(-) diff --git a/pythonfmu/builder.py b/pythonfmu/builder.py index da9275d..e0f7ed0 100644 --- a/pythonfmu/builder.py +++ b/pythonfmu/builder.py @@ -8,12 +8,15 @@ import tempfile import zipfile import inspect +import glob from pathlib import Path from typing import Iterable, Optional, Tuple, Union from xml.dom.minidom import parseString from xml.etree.ElementTree import Element, SubElement, tostring from .osutil import get_lib_extension, get_platform from .fmi2slave import FMI2_MODEL_OPTIONS, Fmi2Slave +from setuptools import setup +from Cython.Build import cythonize FilePath = Union[str, Path] HERE = Path(__file__).parent @@ -78,6 +81,7 @@ def build_FMU( documentation_folder: Optional[FilePath] = None, **options, ) -> Path: + has_cythonize: bool = options["cythonize"] script_file = Path(script_file) if not script_file.exists(): raise ValueError(f"No such file {script_file!s}") @@ -100,7 +104,15 @@ def build_FMU( with tempfile.TemporaryDirectory(prefix="pythonfmu_") as tempd: temp_dir = Path(tempd) - shutil.copy2(script_file, temp_dir) + if has_cythonize: + setup( + script_args=["build_ext", "--inplace"], + ext_modules=cythonize(str(script_file)), + ) + for bin_file in glob.glob(f"{script_file.stem}*.{'pyd' if sys.platform == 'win32' else 'so'}"): + shutil.copy2(bin_file, temp_dir) + else: + shutil.copy2(script_file, temp_dir) # Embed pythonfmu in the FMU so it does not need to be included dep_folder = temp_dir / "pythonfmu" @@ -129,7 +141,7 @@ def build_FMU( shutil.copy2(file_, temp_dir) model_identifier, xml = get_model_description( - temp_dir.absolute() / script_file.name, module_name + script_file if cythonize else temp_dir.absolute() / script_file.name, module_name ) dest_file = dest / f"{model_identifier}.fmu" @@ -153,6 +165,11 @@ def build_FMU( # Add information for the Python loader zip_fmu.writestr(str(resource.joinpath("slavemodule.txt")), module_name) + if cythonize: + zip_fmu.writestr(str(resource.joinpath("filetype.txt")), "bin") + else: + zip_fmu.writestr(str(resource.joinpath("filetype.txt")), "script") + # Add FMI API wrapping Python class source source_node = SubElement(type_node, "SourceFiles") sources = Path("sources") @@ -245,3 +262,7 @@ def create_command_parser(parser: argparse.ArgumentParser): ) parser.set_defaults(execute=FmuBuilder.build_FMU) + + parser.add_argument( + "-c", "--cythonize", dest="cythonize", action="store_true", help="Cythonize the script." + ) diff --git a/pythonfmu/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/pythonfmu/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp index c0bd190..fce9eea 100644 --- a/pythonfmu/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ b/pythonfmu/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp @@ -25,11 +25,69 @@ inline std::string getline(const std::string& fileName) return line; } + +std::string searchLeafClassName(PyObject* pLocals) +{ + std::string deepestFile = ""; + Py_ssize_t deepestChain = 0; + PyObject* key, * value; + Py_ssize_t pos = 0; + + while (PyDict_Next(pLocals, &pos, &key, &value)) { + // Check if element in namespace is a class + if (!PyType_Check(value)) { + continue; + } + + PyObject* pMroAttribute = PyObject_GetAttrString(value, "__mro__"); + + if (pMroAttribute != NULL && PySequence_Check(pMroAttribute)) { + std::regex pattern(" deepestChain && match[1] == "Fmi2Slave") { + deepestFile = PyBytes_AsString(PyUnicode_AsUTF8String(key)); + deepestChain = i; + } + } + } + Py_DECREF(pMroAttribute); + } + return deepestFile; +} + +PyObject* findClassFromBinary(const std::string& resources, const std::string& moduleName) { + + PyObject* pyModule = PyImport_ImportModule(moduleName.c_str()); + if (pyModule == nullptr) { + return nullptr; + } + + PyObject* pGlobals = PyModule_GetDict(pyModule); + auto deepestFile = searchLeafClassName(pGlobals); + + PyObject* pyClassName = Py_BuildValue("s", deepestFile.c_str()); + PyObject* pyClass = PyObject_GetAttr(pyModule, pyClassName); + + // Clean up Python objects + Py_DECREF(pyModule); + Py_DECREF(pyClassName); + return pyClass; + +} + PyObject* findClass(const std::string& resources, const std::string& moduleName) { // Initialize the Python interpreter std::string filename = resources + "/" + moduleName + ".py"; - std::string deepestFile = ""; - int deepestChain = 0; // Read and execute the Python file std::ifstream file; @@ -71,38 +129,7 @@ PyObject* findClass(const std::string& resources, const std::string& moduleName) } fileContents.clear(); - PyObject* key, * value; - Py_ssize_t pos = 0; - - while (PyDict_Next(pLocals, &pos, &key, &value)) { - // Check if element in namespace is a class - if (!PyType_Check(value)) { - continue; - } - - PyObject* pMroAttribute = PyObject_GetAttrString(value, "__mro__"); - - if (pMroAttribute != NULL && PySequence_Check(pMroAttribute)) { - std::regex pattern (" deepestChain && match[1] == "Fmi2Slave") { - deepestFile = PyBytes_AsString(PyUnicode_AsUTF8String(key)); - deepestChain = i; - } - } - } - Py_DECREF(pMroAttribute); - } + auto deepestFile = searchLeafClassName(pLocals); PyObject* pyClassName = Py_BuildValue("s", deepestFile.c_str()); PyObject* pyClass = PyObject_GetAttr(pyModule, pyClassName); @@ -150,8 +177,13 @@ PySlaveInstance::PySlaveInstance(std::string instanceName, std::string resources } std::string moduleName = getline(resources_ + "/slavemodule.txt"); - - pClass_ = findClass(resources_, moduleName); + std::string fileType = getline(resources_ + "/filetype.txt"); + + if (fileType == "bin") { + pClass_ = findClassFromBinary(resources_, moduleName); + } else { + pClass_ = findClass(resources_, moduleName); + } if (pClass_ == nullptr) { handle_py_exception("[ctor] findClass", gilState); } From 92815a5b368128b05922e0a050540b9637e8e1ab Mon Sep 17 00:00:00 2001 From: David Heejong Park Date: Fri, 11 Oct 2024 23:55:31 +0200 Subject: [PATCH 02/12] Dynamic loading of Cython --- pythonfmu/builder.py | 7 ++++++- requirements.txt | 5 +++-- setup.cfg | 5 +++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/pythonfmu/builder.py b/pythonfmu/builder.py index e0f7ed0..6f62649 100644 --- a/pythonfmu/builder.py +++ b/pythonfmu/builder.py @@ -16,7 +16,6 @@ from .osutil import get_lib_extension, get_platform from .fmi2slave import FMI2_MODEL_OPTIONS, Fmi2Slave from setuptools import setup -from Cython.Build import cythonize FilePath = Union[str, Path] HERE = Path(__file__).parent @@ -82,6 +81,12 @@ def build_FMU( **options, ) -> Path: has_cythonize: bool = options["cythonize"] + if has_cythonize: + cython_build_module = importlib.util.find_spec("Cython.Build") + cython = importlib.util.module_from_spec(cython_build_module) + cython_build_module.loader.exec_module(cython) + cythonize = cython.cythonize + script_file = Path(script_file) if not script_file.exists(): raise ValueError(f"No such file {script_file!s}") diff --git a/requirements.txt b/requirements.txt index c6cea94..d605ef5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -fmpy -pytest \ No newline at end of file +pytest~=8.3.3 +fmpy~=0.3.21 +cython~=3.0.11 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 3094b1f..20aa6a9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,8 +23,9 @@ classifiers = Programming Language :: Python :: 3.8 Topic :: Scientific/Engineering tests_require = - pytest - fmpy + pytest~=8.3.3 + fmpy~=0.3.21 + cython~=3.0.11 [options] include_package_data = True From 0a8bab1b6fe6219c7dbc1313aac4847530b527b8 Mon Sep 17 00:00:00 2001 From: David Heejong Park Date: Sat, 12 Oct 2024 00:26:12 +0200 Subject: [PATCH 03/12] Compiler directive --- pythonfmu/builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonfmu/builder.py b/pythonfmu/builder.py index 6f62649..8c5dcc3 100644 --- a/pythonfmu/builder.py +++ b/pythonfmu/builder.py @@ -112,7 +112,7 @@ def build_FMU( if has_cythonize: setup( script_args=["build_ext", "--inplace"], - ext_modules=cythonize(str(script_file)), + ext_modules=cythonize(str(script_file), compiler_directives={"language_level": "3"}), ) for bin_file in glob.glob(f"{script_file.stem}*.{'pyd' if sys.platform == 'win32' else 'so'}"): shutil.copy2(bin_file, temp_dir) From e27e1c45fee6c94af85fd92e4137bf0e4b2dca95 Mon Sep 17 00:00:00 2001 From: David Heejong Park Date: Wed, 16 Oct 2024 07:35:27 +0200 Subject: [PATCH 04/12] Updated help and README --- README.md | 1 + pythonfmu/builder.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f5821ca..0a5aeef 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ optional arguments: -d DEST, --dest DEST Where to save the FMU. --doc DOCUMENTATION_FOLDER Documentation folder to include in the FMU. + -c, --cythonize Compile the Python script into a binary module using Cython. --no-external-tool If given, needsExecutionTool=false --no-variable-step If given, canHandleVariableCommunicationStepSize=false --interpolate-inputs If given, canInterpolateInputs=true diff --git a/pythonfmu/builder.py b/pythonfmu/builder.py index 8c5dcc3..c26c0ed 100644 --- a/pythonfmu/builder.py +++ b/pythonfmu/builder.py @@ -146,7 +146,7 @@ def build_FMU( shutil.copy2(file_, temp_dir) model_identifier, xml = get_model_description( - script_file if cythonize else temp_dir.absolute() / script_file.name, module_name + script_file if has_cythonize else temp_dir.absolute() / script_file.name, module_name ) dest_file = dest / f"{model_identifier}.fmu" @@ -170,7 +170,7 @@ def build_FMU( # Add information for the Python loader zip_fmu.writestr(str(resource.joinpath("slavemodule.txt")), module_name) - if cythonize: + if has_cythonize: zip_fmu.writestr(str(resource.joinpath("filetype.txt")), "bin") else: zip_fmu.writestr(str(resource.joinpath("filetype.txt")), "script") From e5686d0320d3c4fbc708bc1f7cad448ad1c1e8cd Mon Sep 17 00:00:00 2001 From: Hee Jong Park Date: Wed, 25 Mar 2026 14:31:13 +0100 Subject: [PATCH 05/12] Update workflow to trigger on all branches --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ea9e4a5..cd9ec5e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,8 @@ name: CI on: push: branches: - - master + - * +# - master pull_request: branches: - '*' From 6bb5dc777eca326cbe65c68999943a5330fe5e3a Mon Sep 17 00:00:00 2001 From: Hee Jong Park Date: Wed, 25 Mar 2026 14:31:59 +0100 Subject: [PATCH 06/12] Update workflow to trigger on all branches --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cd9ec5e..68eb0d1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,7 +3,7 @@ name: CI on: push: branches: - - * + - '**' # - master pull_request: branches: From be45733f4f3fa541328248ce20b6de0028fd58fc Mon Sep 17 00:00:00 2001 From: Hee Jong Park Date: Wed, 25 Mar 2026 16:40:29 +0100 Subject: [PATCH 07/12] Refactor Cython integration and improve class search functionality --- pythonfmu/__main__.py | 2 +- pythonfmu/builder.py | 38 +- .../src/pythonfmu/PySlaveInstance.cpp | 718 ------------------ setup.cfg | 6 +- src/pythonfmu/PySlaveInstance.cpp | 107 ++- 5 files changed, 97 insertions(+), 774 deletions(-) delete mode 100644 pythonfmu/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp diff --git a/pythonfmu/__main__.py b/pythonfmu/__main__.py index da264d4..a235822 100644 --- a/pythonfmu/__main__.py +++ b/pythonfmu/__main__.py @@ -1,7 +1,7 @@ import argparse from pythonfmu import builder, csvbuilder, deploy -from ._version import __version__ +from pythonfmu._version import __version__ def cli_main(): diff --git a/pythonfmu/builder.py b/pythonfmu/builder.py index 073aaec..3dbaed7 100644 --- a/pythonfmu/builder.py +++ b/pythonfmu/builder.py @@ -215,13 +215,6 @@ def build_FMU( documentation_folder (FilePath): Optional additional documentation (beyond modelDescription) newargs (dict): Optional dict of replacements of model class __init__() arguments. """ - has_cythonize: bool = options["cythonize"] - if has_cythonize: - cython_build_module = importlib.util.find_spec("Cython.Build") - cython = importlib.util.module_from_spec(cython_build_module) - cython_build_module.loader.exec_module(cython) - cythonize = cython.cythonize - script_file = Path(script_file) if not script_file.exists(): raise ValueError(f"No such file {script_file!s}") @@ -265,15 +258,30 @@ def build_FMU( else: shutil.copy2(script_file, temp_dir) + has_cythonize: bool = options["cythonize"] + if has_cythonize: - setup( - script_args=["build_ext", "--inplace"], - ext_modules=cythonize(str(temp_dir / script_file.name), - compiler_directives={"language_level": "3"}), - ) - for bin_file in glob.glob( - f"{temp_dir / script_file.stem}*.{'pyd' if sys.platform == 'win32' else 'so'}"): - shutil.copy2(bin_file, temp_dir) + cython_build_module = importlib.util.find_spec("Cython.Build") + cython = importlib.util.module_from_spec(cython_build_module) + cython_build_module.loader.exec_module(cython) + cythonize = cython.cythonize + + with tempfile.TemporaryDirectory(prefix="pythonfmu_build_") as _build_dir: + build_dir = Path(_build_dir) + setup( + script_args=["build_ext"], + ext_modules=cythonize( + str(temp_dir.absolute() / script_file.name), + language_level="3", + build_dir=str(build_dir.absolute()), + ), + options={ + "build": {"build_lib": str(build_dir.absolute())} + } + ) + for bin_file in glob.glob( + f"{build_dir / script_file.stem}*.{'pyd' if sys.platform == 'win32' else 'so'}"): + shutil.copy2(bin_file, temp_dir) # Embed pythonfmu in the FMU so it does not need to be included dep_folder = temp_dir / "pythonfmu" diff --git a/pythonfmu/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp b/pythonfmu/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp deleted file mode 100644 index 53bdf79..0000000 --- a/pythonfmu/pythonfmu-export/src/pythonfmu/PySlaveInstance.cpp +++ /dev/null @@ -1,718 +0,0 @@ -#include "pythonfmu/IPyState.hpp" -#include "pythonfmu/PySlaveInstance.hpp" - -#include "pythonfmu/PyState.hpp" - -#include "cppfmu/cppfmu_cs.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace pythonfmu -{ - -inline std::string getline(const std::string& fileName) -{ - std::string line; - std::ifstream infile(fileName); - std::getline(infile, line); - return line; -} - -std::string searchLeafClassName(PyObject* pLocals) -{ - std::string deepestFile = ""; - Py_ssize_t deepestChain = 0; - PyObject* key, * value; - Py_ssize_t pos = 0; - - while (PyDict_Next(pLocals, &pos, &key, &value)) { - // Check if element in namespace is a class - if (!PyType_Check(value)) { - continue; - } - - PyObject* pMroAttribute = PyObject_GetAttrString(value, "__mro__"); - - if (pMroAttribute != NULL && PySequence_Check(pMroAttribute)) { - std::regex pattern(" deepestChain && match[1] == "Fmi2Slave") { - deepestFile = PyBytes_AsString(PyUnicode_AsUTF8String(key)); - deepestChain = i; - } - } - } - Py_DECREF(pMroAttribute); - } - return deepestFile; -} - -PyObject* findClassFromBinary(const std::string& resources, const std::string& moduleName) { - - PyObject* pyModule = PyImport_ImportModule(moduleName.c_str()); - if (pyModule == nullptr) { - return nullptr; - } - - PyObject* pGlobals = PyModule_GetDict(pyModule); - auto deepestFile = searchLeafClassName(pGlobals); - - PyObject* pyClassName = Py_BuildValue("s", deepestFile.c_str()); - PyObject* pyClass = PyObject_GetAttr(pyModule, pyClassName); - - // Clean up Python objects - Py_DECREF(pyModule); - Py_DECREF(pyClassName); - return pyClass; - -} - -PyObject* findClass(const std::string& resources, const std::string& moduleName) { - // Initialize the Python interpreter - std::string filename = resources + "/" + moduleName + ".py"; - - // Read and execute the Python file - std::ifstream file; - file.open(filename); - - if (!file.is_open()) { - return nullptr; - } - - std::stringstream fileContents; - std::string line; - - while (std::getline(file, line)) { - fileContents << line << "\n"; - } - - // Compile python code so classes are added to the namespace - PyObject* pyModule = PyImport_ImportModule(moduleName.c_str()); - - if (pyModule == nullptr) { - return nullptr; - } - PyObject* pGlobals = PyModule_GetDict(pyModule); - PyObject* pLocals = PyDict_New(); - PyObject* pCode = Py_CompileString(fileContents.str().c_str(), moduleName.c_str(), Py_file_input); - - if (pCode != NULL) { - PyObject* pResult = PyEval_EvalCode(pCode, pGlobals, pLocals); - Py_XDECREF(pResult); - } else { - PyErr_Print(); // Handle compilation error - Py_Finalize(); - Py_DECREF(pGlobals); - Py_DECREF(pyModule); - Py_DECREF(pLocals); - Py_DECREF(pCode); - file.close(); - return nullptr; - } - - fileContents.clear(); - auto deepestFile = searchLeafClassName(pLocals); - - PyObject* pyClassName = Py_BuildValue("s", deepestFile.c_str()); - PyObject* pyClass = PyObject_GetAttr(pyModule, pyClassName); - - // Clean up Python objects - Py_DECREF(pCode); - Py_DECREF(pLocals); - Py_DECREF(pyModule); - Py_DECREF(pGlobals); - file.close(); - Py_DECREF(pyClassName); - return pyClass; -} - -inline void py_safe_run(const std::function& f) -{ - PyGILState_STATE gil_state = PyGILState_Ensure(); - f(gil_state); - PyGILState_Release(gil_state); -} - -PySlaveInstance::PySlaveInstance(std::string instanceName, std::string resources, const cppfmu::Logger& logger, const bool visible, std::shared_ptr pyState) - : pyState_{ std::move(pyState) } - , instanceName_(std::move(instanceName)) - , resources_(std::move(resources)) - , logger_(logger) - , visible_(visible) -{ - py_safe_run([this](PyGILState_STATE gilState) { - // Append resources path to python sys path - PyObject* sys_module = PyImport_ImportModule("sys"); - if (sys_module == nullptr) { - handle_py_exception("[ctor] PyImport_ImportModule", gilState); - } - PyObject* sys_path = PyObject_GetAttrString(sys_module, "path"); - Py_DECREF(sys_module); - if (sys_path == nullptr) { - handle_py_exception("[ctor] PyObject_GetAttrString", gilState); - } - int success = PyList_Insert(sys_path, 0, PyUnicode_FromString(resources_.c_str())); - - Py_DECREF(sys_path); - if (success != 0) { - handle_py_exception("[ctor] PyList_Insert", gilState); - } - - std::string moduleName = getline(resources_ + "/slavemodule.txt"); - std::string fileType = getline(resources_ + "/filetype.txt"); - - if (fileType == "bin") { - pClass_ = findClassFromBinary(resources_, moduleName); - } else { - pClass_ = findClass(resources_, moduleName); - } - if (pClass_ == nullptr) { - handle_py_exception("[ctor] findClass", gilState); - } - - - initialize(gilState); - }); -} - -void PySlaveInstance::clearLogBuffer() const -{ - clearLogStrBuffer(); - - PyObject* debugField = Py_BuildValue("s", "debug"); - PyObject* msgField = Py_BuildValue("s", "msg"); - PyObject* categoryField = Py_BuildValue("s", "category"); - PyObject* statusField = Py_BuildValue("s", "status"); - - if ( pMessages_ != NULL && PyList_Check(pMessages_) ) { - auto size = PyList_Size(pMessages_); - for (auto i = 0; i < size; i++) { - PyObject* msg = PyList_GetItem(pMessages_, i); - - auto debugAttr = PyObject_GetAttr(msg, debugField); - auto msgAttr = PyObject_GetAttr(msg, msgField); - auto categoryAttr = PyObject_GetAttr(msg, categoryField); - auto statusAttr = PyObject_GetAttr(msg, statusField); - - auto statusValue = static_cast(PyLong_AsLong(statusAttr)); - - PyObject* msgValue = PyUnicode_AsEncodedString(msgAttr, "utf-8", nullptr); - char* msgStr = PyBytes_AsString(msgValue); - logStrBuffer.emplace_back(msgValue); - - const char* categoryStr = ""; - if (categoryAttr != Py_None) { - PyObject* categoryValue = PyUnicode_AsEncodedString(categoryAttr, "utf-8", nullptr); - categoryStr = PyBytes_AsString(categoryValue); - logStrBuffer.emplace_back(categoryValue); - } - - if (PyObject_IsTrue(debugAttr)) { - const_cast(logger_).DebugLog(statusValue, categoryStr, msgStr); - } else { - const_cast(logger_).Log(statusValue, categoryStr, msgStr); - } - } - PyList_SetSlice(pMessages_, 0, size, nullptr); - } - Py_DECREF(debugField); - Py_DECREF(msgField); - Py_DECREF(categoryField); - Py_DECREF(statusField); -} - -void PySlaveInstance::initialize(PyGILState_STATE gilState) -{ - Py_XDECREF(pInstance_); - Py_XDECREF(pMessages_); - - PyObject* args = PyTuple_New(0); - PyObject* kwargs = Py_BuildValue("{ss,ss,sn,si}", - "instance_name", instanceName_.c_str(), - "resources", resources_.c_str(), - "logger", &logger_, - "visible", visible_); - pInstance_ = PyObject_Call(pClass_, args, kwargs); - Py_DECREF(args); - Py_DECREF(kwargs); - if (pInstance_ == nullptr) { - handle_py_exception("[initialize] PyObject_Call", gilState); - } - pMessages_ = PyObject_CallMethod(pInstance_, "_get_log_queue", nullptr); -} - -void PySlaveInstance::SetupExperiment(cppfmu::FMIBoolean, cppfmu::FMIReal, cppfmu::FMIReal startTime, cppfmu::FMIBoolean, cppfmu::FMIReal) -{ - py_safe_run([this, startTime](PyGILState_STATE gilState) { - auto f = PyObject_CallMethod(pInstance_, "setup_experiment", "(d)", startTime); - if (f == nullptr) { - handle_py_exception("[setupExperiment] PyObject_CallMethod", gilState); - } - Py_DECREF(f); - clearLogBuffer(); - }); -} - -void PySlaveInstance::EnterInitializationMode() -{ - py_safe_run([this](PyGILState_STATE gilState) { - auto f = PyObject_CallMethod(pInstance_, "enter_initialization_mode", nullptr); - if (f == nullptr) { - handle_py_exception("[enterInitializationMode] PyObject_CallMethod", gilState); - } - Py_DECREF(f); - clearLogBuffer(); - }); -} - -void PySlaveInstance::ExitInitializationMode() -{ - py_safe_run([this](PyGILState_STATE gilState) { - auto f = PyObject_CallMethod(pInstance_, "exit_initialization_mode", nullptr); - if (f == nullptr) { - handle_py_exception("[exitInitializationMode] PyObject_CallMethod", gilState); - } - Py_DECREF(f); - clearLogBuffer(); - }); -} - -bool PySlaveInstance::DoStep(cppfmu::FMIReal currentTime, cppfmu::FMIReal stepSize, cppfmu::FMIBoolean, cppfmu::FMIReal& endOfStep) -{ - bool status; - py_safe_run([this, &status, currentTime, stepSize](PyGILState_STATE gilState) { - auto f = PyObject_CallMethod(pInstance_, "do_step", "(dd)", currentTime, stepSize); - if (f == nullptr) { - handle_py_exception("[doStep] PyObject_CallMethod", gilState); - } - status = static_cast(PyObject_IsTrue(f)); - Py_DECREF(f); - clearLogBuffer(); - }); - - return status; -} - -void PySlaveInstance::Reset() -{ - py_safe_run([this](PyGILState_STATE gilState) { - initialize(gilState); - }); -} - -void PySlaveInstance::Terminate() -{ - py_safe_run([this](PyGILState_STATE gilState) { - auto f = PyObject_CallMethod(pInstance_, "terminate", nullptr); - if (f == nullptr) { - handle_py_exception("[terminate] PyObject_CallMethod", gilState); - } - Py_DECREF(f); - clearLogBuffer(); - }); -} - -void PySlaveInstance::SetReal(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIReal* values) -{ - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { - PyObject* vrs = PyList_New(nvr); - PyObject* refs = PyList_New(nvr); - for (int i = 0; i < nvr; i++) { - PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); - PyList_SetItem(refs, i, Py_BuildValue("d", values[i])); - } - - auto f = PyObject_CallMethod(pInstance_, "set_real", "(OO)", vrs, refs); - Py_DECREF(vrs); - Py_DECREF(refs); - if (f == nullptr) { - handle_py_exception("[setReal] PyObject_CallMethod", gilState); - } - Py_DECREF(f); - clearLogBuffer(); - }); -} - -void PySlaveInstance::SetInteger(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIInteger* values) -{ - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { - PyObject* vrs = PyList_New(nvr); - PyObject* refs = PyList_New(nvr); - for (int i = 0; i < nvr; i++) { - PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); - PyList_SetItem(refs, i, Py_BuildValue("i", values[i])); - } - - auto f = PyObject_CallMethod(pInstance_, "set_integer", "(OO)", vrs, refs); - Py_DECREF(vrs); - Py_DECREF(refs); - if (f == nullptr) { - handle_py_exception("[setInteger] PyObject_CallMethod", gilState); - } - Py_DECREF(f); - clearLogBuffer(); - }); -} - -void PySlaveInstance::SetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, const cppfmu::FMIBoolean* values) -{ - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { - PyObject* vrs = PyList_New(nvr); - PyObject* refs = PyList_New(nvr); - for (int i = 0; i < nvr; i++) { - PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); - PyList_SetItem(refs, i, PyBool_FromLong(values[i])); - } - - auto f = PyObject_CallMethod(pInstance_, "set_boolean", "(OO)", vrs, refs); - Py_DECREF(vrs); - Py_DECREF(refs); - if (f == nullptr) { - handle_py_exception("[setBoolean] PyObject_CallMethod", gilState); - } - Py_DECREF(f); - clearLogBuffer(); - }); -} - -void PySlaveInstance::SetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString const* values) -{ - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { - PyObject* vrs = PyList_New(nvr); - PyObject* refs = PyList_New(nvr); - for (int i = 0; i < nvr; i++) { - PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); - PyList_SetItem(refs, i, Py_BuildValue("s", values[i])); - } - - auto f = PyObject_CallMethod(pInstance_, "set_string", "(OO)", vrs, refs); - Py_DECREF(vrs); - Py_DECREF(refs); - if (f == nullptr) { - handle_py_exception("[setString] PyObject_CallMethod", gilState); - } - Py_DECREF(f); - clearLogBuffer(); - }); -} - -void PySlaveInstance::GetReal(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIReal* values) const -{ - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { - PyObject* vrs = PyList_New(nvr); - for (int i = 0; i < nvr; i++) { - PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); - } - - auto refs = PyObject_CallMethod(pInstance_, "get_real", "O", vrs); - Py_DECREF(vrs); - if (refs == nullptr) { - handle_py_exception("[getReal] PyObject_CallMethod", gilState); - } - - for (int i = 0; i < nvr; i++) { - PyObject* value = PyList_GetItem(refs, i); - values[i] = PyFloat_AsDouble(value); - } - Py_DECREF(refs); - clearLogBuffer(); - }); -} - -void PySlaveInstance::GetInteger(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIInteger* values) const -{ - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { - PyObject* vrs = PyList_New(nvr); - for (int i = 0; i < nvr; i++) { - PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); - } - auto refs = PyObject_CallMethod(pInstance_, "get_integer", "O", vrs); - Py_DECREF(vrs); - if (refs == nullptr) { - handle_py_exception("[getInteger] PyObject_CallMethod", gilState); - } - - for (int i = 0; i < nvr; i++) { - PyObject* value = PyList_GetItem(refs, i); - values[i] = static_cast(PyLong_AsLong(value)); - } - Py_DECREF(refs); - clearLogBuffer(); - }); -} - -void PySlaveInstance::GetBoolean(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIBoolean* values) const -{ - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { - PyObject* vrs = PyList_New(nvr); - for (int i = 0; i < nvr; i++) { - PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); - } - auto refs = PyObject_CallMethod(pInstance_, "get_boolean", "O", vrs); - Py_DECREF(vrs); - if (refs == nullptr) { - handle_py_exception("[getBoolean] PyObject_CallMethod", gilState); - } - - for (int i = 0; i < nvr; i++) { - PyObject* value = PyList_GetItem(refs, i); - values[i] = PyObject_IsTrue(value); - } - Py_DECREF(refs); - clearLogBuffer(); - }); -} - -void PySlaveInstance::GetString(const cppfmu::FMIValueReference* vr, std::size_t nvr, cppfmu::FMIString* values) const -{ - py_safe_run([this, &vr, nvr, &values](PyGILState_STATE gilState) { - clearStrBuffer(); - PyObject* vrs = PyList_New(nvr); - for (int i = 0; i < nvr; i++) { - PyList_SetItem(vrs, i, Py_BuildValue("i", vr[i])); - } - auto refs = PyObject_CallMethod(pInstance_, "get_string", "O", vrs); - Py_DECREF(vrs); - if (refs == nullptr) { - handle_py_exception("[getString] PyObject_CallMethod", gilState); - } - - for (int i = 0; i < nvr; i++) { - PyObject* value = PyUnicode_AsEncodedString(PyList_GetItem(refs, i), "utf-8", nullptr); - values[i] = PyBytes_AsString(value); - strBuffer.emplace_back(value); - } - Py_DECREF(refs); - clearLogBuffer(); - }); -} - -void PySlaveInstance::GetFMUstate(fmi2FMUstate& state) -{ - py_safe_run([this, &state](PyGILState_STATE gilState) { - auto f = PyObject_CallMethod(pInstance_, "_get_fmu_state", nullptr); - if (f == nullptr) { - handle_py_exception("[_get_fmu_state] PyObject_CallMethod", gilState); - } - state = reinterpret_cast(f); - clearLogBuffer(); - }); -} - -void PySlaveInstance::SetFMUstate(const fmi2FMUstate& state) -{ - py_safe_run([this, &state](PyGILState_STATE gilState) { - auto pyState = reinterpret_cast(state); - auto f = PyObject_CallMethod(pInstance_, "_set_fmu_state", "(O)", pyState); - if (f == nullptr) { - handle_py_exception("[_set_fmu_state] PyObject_CallMethod", gilState); - } - clearLogBuffer(); - }); -} - -void PySlaveInstance::FreeFMUstate(fmi2FMUstate& state) -{ - py_safe_run([this, &state](PyGILState_STATE gilState) { - auto f = reinterpret_cast(state); - Py_XDECREF(f); - }); -} - -size_t PySlaveInstance::SerializedFMUstateSize(const fmi2FMUstate& state) -{ - size_t size; - py_safe_run([this, &state, &size](PyGILState_STATE gilState) { - auto pyState = reinterpret_cast(state); - PyObject* pyStateBytes = PyObject_CallMethod(pClass_, "_fmu_state_to_bytes", "(O)", pyState); - if (pyStateBytes == nullptr) { - handle_py_exception("[SerializedFMUstateSize] PyObject_CallMethod", gilState); - } - size = PyBytes_Size(pyStateBytes); - Py_DECREF(pyStateBytes); - clearLogBuffer(); - }); - return size; -} - -void PySlaveInstance::SerializeFMUstate(const fmi2FMUstate& state, fmi2Byte* bytes, size_t size) -{ - py_safe_run([this, &state, &bytes, size](PyGILState_STATE gilState) { - auto pyState = reinterpret_cast(state); - PyObject* pyStateBytes = PyObject_CallMethod(pClass_, "_fmu_state_to_bytes", "(O)", pyState); - if (pyStateBytes == nullptr) { - handle_py_exception("[SerializeFMUstate] PyObject_CallMethod", gilState); - } - char* c = PyBytes_AsString(pyStateBytes); - if (c == nullptr) { - handle_py_exception("[SerializeFMUstate] PyBytes_AsString", gilState); - } - for (int i = 0; i < size; i++) { - bytes[i] = c[i]; - } - Py_DECREF(pyStateBytes); - clearLogBuffer(); - }); -} - -void PySlaveInstance::DeSerializeFMUstate(const fmi2Byte bytes[], size_t size, fmi2FMUstate& state) -{ - py_safe_run([this, &bytes, size, &state](PyGILState_STATE gilState) { - PyObject* pyStateBytes = PyBytes_FromStringAndSize(bytes, size); - if (pyStateBytes == nullptr) { - handle_py_exception("[DeSerializeFMUstate] PyBytes_FromStringAndSize", gilState); - } - PyObject* pyState = PyObject_CallMethod(pClass_, "_fmu_state_from_bytes", "(O)", pyStateBytes); - if (pyState == nullptr) { - handle_py_exception("[DeSerializeFMUstate] PyObject_CallMethod", gilState); - } - state = reinterpret_cast(pyState); - Py_DECREF(pyStateBytes); - clearLogBuffer(); - }); -} - -void PySlaveInstance::handle_py_exception(const std::string& what, PyGILState_STATE gilState) const -{ - auto err = PyErr_Occurred(); - if (err != nullptr) { - cleanPyObject(); - - PyObject *pExcType, *pExcValue, *pExcTraceback; - PyErr_Fetch(&pExcType, &pExcValue, &pExcTraceback); - - std::ostringstream oss; - oss << "Fatal py exception encountered: "; - oss << what << "\n"; - if (pExcValue != nullptr) { - PyObject* pRepr = PyObject_Repr(pExcValue); - PyObject* pyStr = PyUnicode_AsEncodedString(pRepr, "utf-8", nullptr); - oss << PyBytes_AsString(pyStr); - Py_DECREF(pyStr); - Py_DECREF(pRepr); - } else { - oss << "unknown error"; - } - - PyErr_Clear(); - - Py_XDECREF(pExcType); - Py_XDECREF(pExcValue); - Py_XDECREF(pExcTraceback); - - PyGILState_Release(gilState); - - auto msg = oss.str(); - throw cppfmu::FatalError(msg.c_str()); - } -} - -PySlaveInstance::~PySlaveInstance() -{ - py_safe_run([this](PyGILState_STATE gilState) { - cleanPyObject(); - }); -} - -} // namespace pythonfmu - -namespace { - std::mutex pyStateMutex{}; - std::shared_ptr pyState{}; -} - -cppfmu::UniquePtr CppfmuInstantiateSlave( - cppfmu::FMIString instanceName, - cppfmu::FMIString, - cppfmu::FMIString fmuResourceLocation, - cppfmu::FMIString, - cppfmu::FMIReal, - cppfmu::FMIBoolean visible, - cppfmu::FMIBoolean, - cppfmu::Memory memory, - const cppfmu::Logger& logger) -{ - - // Convert URI %20 to space - auto resources = std::regex_replace(std::string(fmuResourceLocation), std::regex("%20"), " "); - auto find = resources.find("file://"); - - if (find != std::string::npos) { -#ifdef _MSC_VER - resources.replace(find, 8, ""); -#else - resources.replace(find, 7, ""); -#endif - } - - { - auto const ensurePyStateAlive = [&]() { - auto const lock = std::lock_guard{pyStateMutex}; - if (nullptr == pyState) pyState = std::make_shared(); - }; - - ensurePyStateAlive(); - return cppfmu::AllocateUnique( - memory, instanceName, resources, logger, visible, pyState); - } -} - -extern "C" { - - // The PyState instance owns it's own thread for constructing and destroying the Py* from the same thread. - // Creation of an std::thread increments ref counter of a shared library. So, when the client unloads the library - // the library won't be freed, as std::thread is alive, and the std::thread itself waits for de-initialization request. - // Thus, use DllMain on Windows and __attribute__((destructor)) on Linux for signaling to the PyState about de-initialization. - void finalizePythonInterpreter() - { - pyState = nullptr; - } -} - -namespace -{ -#ifdef _WIN32 -#include - - BOOL APIENTRY DllMain(HMODULE hModule, - DWORD ul_reason_for_call, - LPVOID lpReserved) - { - switch (ul_reason_for_call) { - case DLL_PROCESS_ATTACH: - break; - case DLL_THREAD_ATTACH: - case DLL_THREAD_DETACH: - break; - case DLL_PROCESS_DETACH: - finalizePythonInterpreter(); - break; - } - return TRUE; -} -#elif defined(__linux__) - __attribute__((destructor)) void onLibraryUnload() - { - finalizePythonInterpreter(); -} -#else -#error port the code -#endif -} diff --git a/setup.cfg b/setup.cfg index c7b39b3..a27c346 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,9 +23,9 @@ classifiers = Programming Language :: Python :: 3.8 Topic :: Scientific/Engineering tests_require = - pytest~=8.3.3 - fmpy~=0.3.21 - cython~=3.0.11 + pytest + fmpy + cython [options] include_package_data = True diff --git a/src/pythonfmu/PySlaveInstance.cpp b/src/pythonfmu/PySlaveInstance.cpp index f05c866..69bbf29 100644 --- a/src/pythonfmu/PySlaveInstance.cpp +++ b/src/pythonfmu/PySlaveInstance.cpp @@ -26,12 +26,70 @@ std::string getline(const std::filesystem::path& fileName) return line; } +std::string searchLeafClassName(PyObject* pLocals) +{ + std::string deepestFile = ""; + Py_ssize_t deepestChain = 0; + PyObject* key, * value; + Py_ssize_t pos = 0; + + while (PyDict_Next(pLocals, &pos, &key, &value)) { + // Check if element in namespace is a class + if (!PyType_Check(value)) { + continue; + } + + PyObject* pMroAttribute = PyObject_GetAttrString(value, "__mro__"); + + if (pMroAttribute != NULL && PySequence_Check(pMroAttribute)) { + std::regex pattern(" deepestChain && match[1] == "Fmi2Slave") { + deepestFile = PyBytes_AsString(PyUnicode_AsUTF8String(key)); + deepestChain = i; + } + } + } + Py_DECREF(pMroAttribute); + } + return deepestFile; +} + +PyObject* findClassFromBinary(const std::string& resources, const std::string& moduleName) +{ + + PyObject* pyModule = PyImport_ImportModule(moduleName.c_str()); + if (pyModule == nullptr) { + return nullptr; + } + + PyObject* pGlobals = PyModule_GetDict(pyModule); + auto deepestFile = searchLeafClassName(pGlobals); + + PyObject* pyClassName = Py_BuildValue("s", deepestFile.c_str()); + PyObject* pyClass = PyObject_GetAttr(pyModule, pyClassName); + + // Clean up Python objects + Py_DECREF(pyModule); + Py_DECREF(pyClassName); + return pyClass; + +} + PyObject* findClass(const std::string& resources, const std::string& moduleName) { // Initialize the Python interpreter std::string filename = resources + "/" + moduleName + ".py"; - std::string deepestFile; - int deepestChain = 0; // Read and execute the Python file std::ifstream file; @@ -73,38 +131,7 @@ PyObject* findClass(const std::string& resources, const std::string& moduleName) } fileContents.clear(); - PyObject *key, *value; - Py_ssize_t pos = 0; - - while (PyDict_Next(pLocals, &pos, &key, &value)) { - // Check if element in namespace is a class - if (!PyType_Check(value)) { - continue; - } - - PyObject* pMroAttribute = PyObject_GetAttrString(value, "__mro__"); - - if (pMroAttribute != nullptr && PySequence_Check(pMroAttribute)) { - std::regex pattern(" deepestChain && match[1] == "Fmi2Slave") { - deepestFile = PyBytes_AsString(PyUnicode_AsUTF8String(key)); - deepestChain = i; - } - } - } - Py_DECREF(pMroAttribute); - } + auto deepestFile = searchLeafClassName(pLocals); PyObject* pyClassName = Py_BuildValue("s", deepestFile.c_str()); PyObject* pyClass = PyObject_GetAttr(pyModule, pyClassName); @@ -153,11 +180,17 @@ class PySlaveInstance : public SlaveInstance } std::string moduleName = getline(resourceLocation() + "/slavemodule.txt"); + std::string fileType = getline(resourceLocation() + "/filetype.txt"); + if (fileType == "bin") { + pClass_ = findClassFromBinary(resourceLocation(), moduleName); + } else { pClass_ = findClass(resourceLocation(), moduleName); - if (pClass_ == nullptr) { - handle_py_exception("[ctor] findClass", gilState); - } + } + if (pClass_ == nullptr) { + handle_py_exception("[ctor] findClass", gilState); + } + initialize(gilState); }); From ad0e5d3980f99a3a92f2a7586e0ea1b1a98b02de Mon Sep 17 00:00:00 2001 From: Hee Jong Park Date: Wed, 25 Mar 2026 18:08:47 +0100 Subject: [PATCH 08/12] Add 'info' command to display FMU details and create info.py module --- pythonfmu/__main__.py | 9 ++- pythonfmu/info.py | 165 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 pythonfmu/info.py diff --git a/pythonfmu/__main__.py b/pythonfmu/__main__.py index a235822..553f2c2 100644 --- a/pythonfmu/__main__.py +++ b/pythonfmu/__main__.py @@ -1,6 +1,6 @@ import argparse -from pythonfmu import builder, csvbuilder, deploy +from pythonfmu import builder, csvbuilder, deploy, info from pythonfmu._version import __version__ @@ -50,6 +50,13 @@ def default_execution(**kwargs): ) deploy.create_command_parser(deploy_parser) + info_parser = subparsers.add_parser( + "info", + description="Display information about a generated FMU: supported platforms, compiled models (.pyd/.so) with Python implementation and version, and package dependencies.", + help="Display information about a generated FMU." + ) + info.create_command_parser(info_parser) + options = vars(parser.parse_args()) execute = options.pop("execute") execute(**options) diff --git a/pythonfmu/info.py b/pythonfmu/info.py new file mode 100644 index 0000000..c6787ac --- /dev/null +++ b/pythonfmu/info.py @@ -0,0 +1,165 @@ +import argparse +import re +import zipfile +from pathlib import Path, PurePosixPath +from typing import Union + + +def info(fmu: Union[str, Path]) -> None: + """Display information about a generated FMU. + + Inspects the FMU archive and prints: + - Supported platforms derived from FMI binaries in the binaries/ directory + (e.g. Windows (x64), Linux (x64)) + - Compiled model(s) (.pyd / .so) found in the resources/ directory, + including Python implementation (CPython, PyPy, GraalPy), + Python version (e.g. cp314), and platform architecture + - Python package dependencies from resources/requirements.txt + + Args: + fmu (str or pathlib.Path): Path to the FMU file. + """ + fmu = Path(fmu) + if not fmu.exists(): + raise FileNotFoundError(f"FMU file not found: {fmu}") + + with zipfile.ZipFile(fmu) as zf: + names = zf.namelist() + + # --- Supported platforms (FMI binaries in binaries/) --- + platform_folders = { + PurePosixPath(n).parent.name + for n in names + if n.startswith("binaries/") + and not n.endswith("/") + } + + if platform_folders: + print("Supported platforms:") + for folder in sorted(platform_folders): + print(f" {_friendly_platform(folder)}") + else: + print("No supported platforms found (no FMI binaries).") + + # --- Locate .pyd / .so compiled models in resources/ (first level only) --- + bin_files = [ + n for n in names + if n.startswith("resources/") + and "/" not in n[len("resources/"):] + and (n.endswith(".pyd") or n.endswith(".so")) + ] + + if bin_files: + print("\nCompiled model(s):") + for bf in bin_files: + fname = PurePosixPath(bf).name + print(f" {fname}") + _print_extension_info(fname) + else: + print("\nNo compiled model (.pyd / .so) found.") + + # --- Requirements --- + req_path = "resources/requirements.txt" + if req_path in names: + print("\nPython package dependencies (requirements.txt):") + with zf.open(req_path) as rf: + content = rf.read().decode("utf-8") + for line in content.splitlines(): + stripped = line.strip() + if stripped and not stripped.startswith("#"): + print(f" {stripped}") + else: + print("\nNo requirements.txt found in resources/.") + + +def _friendly_platform(folder: str) -> str: + """Map an FMI binary folder name to platform name. + + Examples: + win64 -> Windows (x64) + linux64 -> Linux (x64) + darwin64 -> macOS (x64) + """ + _PLATFORM_LABELS = { + "win64": "Windows (x64)", + "win32": "Windows (x86)", + "linux64": "Linux (x64)", + "linux32": "Linux (x86)", + "darwin64": "macOS (x64)", + "darwin32": "macOS (x86)", + } + return _PLATFORM_LABELS.get(folder, folder) + + +def _print_extension_info(filename: str) -> None: + """Parse a compiled Python extension filename and print platform and Python version. + + Recognised patterns (CPython, PyPy, GraalPy): + modulename.cpython-314-x86_64-linux-gnu.so + modulename.cp314-win_amd64.pyd + modulename.pypy310-pp73-x86_64-linux-gnu.so + modulename.pypy39-pp73-win_amd64.pyd + modulename.graalpy-24_1-native-x86_64-linux-gnu.so + """ + stem = filename.rsplit(".", 1)[0] # drop final extension + + impl_name: str | None = None + version_tag: str | None = None + version_display: str | None = None + + cp_match = re.search(r"[._](cpython-|cp)(\d+)", stem) + pypy_match = re.search(r"[._](pypy)(\d+)", stem) + graalpy_match = re.search(r"[._](graalpy)-([0-9_]+)", stem) + + if cp_match: + raw_ver = cp_match.group(2) + impl_name = "CPython" + version_tag = f"cp{raw_ver}" + version_display = f"{raw_ver[0]}.{raw_ver[1:]}" + elif pypy_match: + raw_ver = pypy_match.group(2) + impl_name = "PyPy" + version_tag = f"pp{raw_ver}" + version_display = f"{raw_ver[0]}.{raw_ver[1:]}" + elif graalpy_match: + raw_ver = graalpy_match.group(2) # e.g. "24_1" + impl_name = "GraalPy" + version_tag = f"graalpy-{raw_ver}" + version_display = raw_ver.replace("_", ".") + + if impl_name is not None: + print(f" Python impl : {impl_name}") + print(f" Python version : {version_tag} ({impl_name} {version_display})") + else: + print(" Python impl : unknown") + print(" Python version : unknown") + + # Platform / architecture – everything after the implementation+version tag. + # Build a combined pattern that matches any of the three known prefixes. + plat_match = re.search( + r"[._](?:cpython-|cp|pypy|graalpy-)[\d_]+(?:-pp\d+)?[.-](.+)", stem + ) + if plat_match: + platform_tag = plat_match.group(1) + print(f" Platform arch : {platform_tag}") + else: + # Fallback: derive from extension + if filename.endswith(".pyd"): + print(" Platform arch : win (exact arch unknown)") + elif filename.endswith(".so"): + print(" Platform arch : linux/macOS (exact arch unknown)") + else: + print(" Platform arch : unknown") + + +def create_command_parser(parser: argparse.ArgumentParser): + parser.add_argument( + "-f", + "--file", + dest="fmu", + help="Path to the FMU file to inspect.", + required=True, + ) + + parser.set_defaults(execute=info) + From e4b7a8b501686452b9b3870f509ea748ab4dc3ed Mon Sep 17 00:00:00 2001 From: Hee Jong Park Date: Wed, 25 Mar 2026 18:09:28 +0100 Subject: [PATCH 09/12] Update workflow to trigger only on the master branch --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 68eb0d1..ea9e4a5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -3,8 +3,7 @@ name: CI on: push: branches: - - '**' -# - master + - master pull_request: branches: - '*' From 94409faac229d0c6b6d71bf941edc37280a43eac Mon Sep 17 00:00:00 2001 From: Hee Jong Park Date: Wed, 25 Mar 2026 18:21:22 +0100 Subject: [PATCH 10/12] Updated README.me --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/README.md b/README.md index eddff53..36930f0 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,52 @@ optional arguments: Requirements or environment file. ``` +### How do I inspect a generated FMU? + +Use `pythonfmu info` to display information about a generated FMU. It reports the supported platforms, +compiled models (`.pyd` / `.so`) with their Python implementation (e.g. CPython) and version, +and Python package dependencies. + +> **Note:** Compiled models are only present when the FMU was built with the `--cythonize` (`-c`) flag, +> which compiles the Python script into a native binary module using Cython. +> Currently, only the main Python script is compiled — additional Python dependencies +> included as project files are **not** compiled. + +``` +pythonfmu info -f my.fmu +``` + +``` +usage: pythonfmu info [-h] -f FMU + +Display information about a generated FMU: supported platforms, compiled +models (.pyd/.so) with Python implementation and version, and package +dependencies. + +optional arguments: + -h, --help show this help message and exit + -f, --file FMU Path to the FMU file to inspect. +``` + +Example output: + +``` +Supported platforms: + macOS (x64) + Linux (x64) + Windows (x64) + +Compiled model(s): + demoslave.cp314-win_amd64.pyd + Python impl : CPython + Python version : cp314 (CPython 3.14) + Platform arch : win_amd64 + +Python package dependencies (requirements.txt): + numpy + scipy +``` + ### Example: #### Write the script From f383174056ff1dc37bed76ec2d59b8ef1adf9617 Mon Sep 17 00:00:00 2001 From: Hee Jong Park Date: Mon, 30 Mar 2026 12:03:18 +0200 Subject: [PATCH 11/12] Updated README.me --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 36930f0..add4c7d 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,12 @@ and Python package dependencies. > Currently, only the main Python script is compiled — additional Python dependencies > included as project files are **not** compiled. +> **Warning:** `--cythonize` does **not guarantee** source code protection and should not be relied upon +> for that purpose. Furthermore, without proper use of Cython-specific constructs (e.g. `cdef`, `cfunc`, `cdef class`), +> the compiled binary still exposes all public interfaces. +> Refer to the [Cython Extension Types](https://cython.readthedocs.io/en/latest/src/userguide/extension_types.html) +> for constructs that can help restrict visibility. + ``` pythonfmu info -f my.fmu ``` From 115e648cb4b402531f93a0f0f978bb952bdae67b Mon Sep 17 00:00:00 2001 From: Hee Jong Park Date: Mon, 30 Mar 2026 12:58:27 +0200 Subject: [PATCH 12/12] Updated README.me --- README.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index add4c7d..cd7aa0d 100644 --- a/README.md +++ b/README.md @@ -93,11 +93,14 @@ and Python package dependencies. > Currently, only the main Python script is compiled — additional Python dependencies > included as project files are **not** compiled. -> **Warning:** `--cythonize` does **not guarantee** source code protection and should not be relied upon -> for that purpose. Furthermore, without proper use of Cython-specific constructs (e.g. `cdef`, `cfunc`, `cdef class`), -> the compiled binary still exposes all public interfaces. -> Refer to the [Cython Extension Types](https://cython.readthedocs.io/en/latest/src/userguide/extension_types.html) -> for constructs that can help restrict visibility. +> **Warning:** `--cythonize` **does not guarantee** protection against reverse +> engineering and **should not be** used for that purpose. Furthermore, without proper use of Cython-specific +> constructs (e.g., `cdef`, `cfunc`, `cdef class`), the +> compiled binary +> still exposes all public interfaces. Refer to +> the [Cython Extension Types](https://cython.readthedocs.io/en/latest/src/userguide/extension_types.html) for +> constructs +> that can help restrict visibility of functions and class attributes when importing binary modules. ``` pythonfmu info -f my.fmu