From d0842764dc64b2922de0d0077794c560f43ae791 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 28 Mar 2026 19:24:43 -0300 Subject: [PATCH 1/9] ENH: Update README and requirements, refactor OpenRocket integration in cli.py --- README.md | 11 +- requirements.in | 2 +- rocketserializer/cli.py | 61 +++---- rocketserializer/openrocket_runtime.py | 237 +++++++++++++++++++++++++ test_ork2json-debugger.py | 11 +- tests/conftest.py | 53 +++--- 6 files changed, 303 insertions(+), 72 deletions(-) create mode 100644 rocketserializer/openrocket_runtime.py diff --git a/README.md b/README.md index c6114e4..667f698 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ pip install rocketserializer ### Java You need Java to be installed on your system to use `rocketserializer`. -We recommend downloading Java 17, which is required to run OpenRocket-23.09. +We recommend downloading Java 17, which is required to run recent OpenRocket +JARs (for example OpenRocket-24.12). https://www.oracle.com/java/technologies/downloads/ @@ -47,7 +48,7 @@ https://www.oracle.com/java/technologies/downloads/ You also need to download the OpenRocket JAR file. You can download it from the following link: -https://openrocket.info/downloads.html?vers=23.09#content-JAR +https://openrocket.info/downloads.html Each version of OpenRocket has its own jar file, and it is important to use the correct java version to run the jar file. @@ -61,7 +62,7 @@ will be automatically installed: - click>=8.0.0 - lxml - numpy -- orhelper==0.1.3 +- jpype1<1.5 - pyyaml - rocketpy>=1.1.0 - nbformat>=5.2.0 @@ -89,7 +90,7 @@ The options are the following: - `--filepath`: The .ork file to be serialized. - `--output` : Path to the output folder. If not set, the output will be saved in the same folder as the `filepath`. -- `--ork_jar` : Specify the path to the OpenRocket jar file. If not set, the library will try to find the jar file in the current directory. +- `--ork_jar` : Specify the path to the OpenRocket jar file. If not set, the library will use the newest `OpenRocket*.jar` found in the current directory. - `--encoding` : The encoding of the .ork file. By default, it is set to `utf-8`. - `--verbose` : If you want to see the progress of the serialization, set this option to True. By default, it is set to False. @@ -139,4 +140,4 @@ The 3 main ways of contributing to this project are: - If you allow us to use and share your .ork file, we can add it to the test suite. 3. **Developing new features and fixing bugs thorough pull requests on GitHub.** - If you want to develop new features, you are more than welcome to do so. - - Please reach out to the maintainers to discuss the new feature before starting the development. + - Please reach out to the maintainers to discuss the new feature before starting the development. diff --git a/requirements.in b/requirements.in index 46105a7..ceff935 100644 --- a/requirements.in +++ b/requirements.in @@ -2,7 +2,7 @@ bs4 click>=8.0.0 lxml numpy -orhelper==0.1.3 +jpype1<1.5 pyyaml rocketpy>=1.1.0 nbformat>=5.2.0 \ No newline at end of file diff --git a/rocketserializer/cli.py b/rocketserializer/cli.py index 05d2710..20ec89d 100644 --- a/rocketserializer/cli.py +++ b/rocketserializer/cli.py @@ -4,10 +4,10 @@ from pathlib import Path import click -import orhelper from ._helpers import extract_ork_from_zip, parse_ork_file from .nb_builder import NotebookBuilder +from .openrocket_runtime import OpenRocketSession, select_latest_openrocket_jar from .ork_extractor import ork_extractor logging.basicConfig( @@ -138,18 +138,15 @@ def ork2json(filepath, output=None, ork_jar=None, encoding="utf-8", verbose=Fals raise ValueError(message) if not ork_jar: - # get any .jar file in the current directory that starts with "OpenRocket" - ork_jar = [ - f for f in os.listdir() if f.startswith("OpenRocket") and f.endswith(".jar") - ] - if len(ork_jar) == 0: - raise ValueError( - "[ork2json] It was not possible to find the OpenRocket .jar file in " - "the current directory. Please specify the path to the .jar file." - ) - ork_jar = ork_jar[0] - logger.info( - "[ork2json] Found OpenRocket .jar file: '%s'", Path(ork_jar).as_posix() + ork_jar = select_latest_openrocket_jar(Path.cwd()) + logger.info("[ork2json] Found OpenRocket .jar file: '%s'", ork_jar.as_posix()) + else: + ork_jar = Path(ork_jar) + + if not ork_jar.exists(): + raise FileNotFoundError( + "[ork2json] The specified OpenRocket .jar file does not exist: " + f"'{ork_jar.as_posix()}'" ) if not output: @@ -160,17 +157,12 @@ def ork2json(filepath, output=None, ork_jar=None, encoding="utf-8", verbose=Fals Path(output).as_posix(), ) - # orhelper options are: OFF, ERROR, WARN, INFO, DEBUG, TRACE and ALL - # log_level = "OFF" if verbose else "OFF" - # TODO: even if the log level is set to OFF, the orhelper still prints msgs - - with orhelper.OpenRocketInstance(ork_jar, log_level="OFF") as instance: + with OpenRocketSession(ork_jar, log_level="OFF") as instance: # create the output folder if it does not exist if os.path.exists(output) is False: os.mkdir(output) - orh = orhelper.Helper(instance) - ork = orh.load_doc(str(filepath)) + ork = instance.load_doc(str(filepath)) settings = ork_extractor( bs=bs, @@ -217,21 +209,20 @@ def ork2notebook(filepath, output, ork_jar=None, encoding="utf-8", verbose=False "[ork2notebook] Output folder not specified. Using '%s' instead.", Path(output).as_posix(), ) - ork2json( - [ - "--filepath", - filepath, - "--output", - output, - "--ork_jar", - ork_jar, - "--encoding", - encoding, - "--verbose", - verbose, - ], - standalone_mode=False, - ) + args = [ + "--filepath", + str(filepath), + "--output", + str(output), + "--encoding", + str(encoding), + "--verbose", + str(verbose), + ] + if ork_jar: + args.extend(["--ork_jar", str(ork_jar)]) + + ork2json(args, standalone_mode=False) instance = NotebookBuilder(parameters_json=os.path.join(output, "parameters.json")) instance.build(destination=output) diff --git a/rocketserializer/openrocket_runtime.py b/rocketserializer/openrocket_runtime.py new file mode 100644 index 0000000..2ee25e6 --- /dev/null +++ b/rocketserializer/openrocket_runtime.py @@ -0,0 +1,237 @@ +import logging +import os +import re +from pathlib import Path + +import jpype +import jpype.imports + +logger = logging.getLogger(__name__) + + +def _jar_version_tuple(jar_path: Path): + match = re.search(r"OpenRocket[-_]?(\d+(?:\.\d+)*)", jar_path.name, re.IGNORECASE) + if not match: + return (0,) + + version = [] + for token in match.group(1).split("."): + if token.isdigit(): + version.append(int(token)) + return tuple(version) if version else (0,) + + +def select_latest_openrocket_jar(search_dir: Path): + jars = [ + path + for path in search_dir.iterdir() + if path.is_file() + and path.suffix.lower() == ".jar" + and path.name.lower().startswith("openrocket") + ] + + if not jars: + raise FileNotFoundError( + "It was not possible to find an OpenRocket .jar file in the current " + "directory. Please specify one explicitly with --ork_jar." + ) + + jars.sort( + key=lambda path: (_jar_version_tuple(path), path.name.lower()), reverse=True + ) + return jars[0] + + +def _extract_java_major(value: str): + if not value: + return None + + normalized = value.replace("\\", "/").lower() + + legacy = re.search(r"(?:jdk|jre)[-_]?1\.(\d+)", normalized) + if legacy: + return int(legacy.group(1)) + + modern = re.search(r"(?:jdk|jre|java)[-_]?(\d{2})", normalized) + if modern: + return int(modern.group(1)) + + return None + + +def _minimum_java_required(jar_path: Path): + version = _jar_version_tuple(jar_path) + if version and version[0] >= 23: + return 17 + return 8 + + +def _find_windows_jdk(minimum_major: int): + search_roots = [ + Path("C:/Program Files/Java"), + Path("C:/Program Files/Eclipse Adoptium"), + Path("C:/Program Files/AdoptOpenJDK"), + ] + + candidates = [] + for root in search_roots: + if not root.exists(): + continue + + for child in root.iterdir(): + if not child.is_dir(): + continue + major = _extract_java_major(child.name) + if major is None: + continue + if major >= minimum_major and (child / "bin" / "java.exe").exists(): + candidates.append((major, child)) + + if not candidates: + return None + + candidates.sort(key=lambda item: item[0], reverse=True) + return candidates[0][1] + + +def ensure_java_compatibility(jar_path: Path): + required_major = _minimum_java_required(jar_path) + if required_major <= 8: + return + + java_home_major = _extract_java_major(os.environ.get("JAVA_HOME", "")) + if java_home_major and java_home_major >= required_major: + return + + default_jvm_major = None + try: + default_jvm_major = _extract_java_major(jpype.getDefaultJVMPath()) + except Exception: + default_jvm_major = None + + if default_jvm_major and default_jvm_major >= required_major: + return + + if os.name != "nt": + logger.warning( + "OpenRocket %s requires Java %d+, but no compatible JVM was detected.", + jar_path.name, + required_major, + ) + return + + selected_jdk = _find_windows_jdk(required_major) + if not selected_jdk: + logger.warning( + "OpenRocket %s requires Java %d+, but no compatible JDK was found in " + "standard Windows locations.", + jar_path.name, + required_major, + ) + return + + os.environ["JAVA_HOME"] = str(selected_jdk) + os.environ["PATH"] = ( + str(selected_jdk / "bin") + os.pathsep + os.environ.get("PATH", "") + ) + logger.info( + "Using Java from '%s' for OpenRocket compatibility.", selected_jdk.as_posix() + ) + + +class OpenRocketSession: + def __init__(self, jar_path, log_level="OFF"): + self.jar_path = Path(jar_path) + if not self.jar_path.exists(): + raise FileNotFoundError( + f"Jar file '{self.jar_path.as_posix()}' does not exist" + ) + + self.log_level = log_level + self.openrocket = None + self.started = False + + def _resolve_packages(self): + try: + legacy = jpype.JPackage("net").sf.openrocket + _ = legacy.startup.Application + return legacy, legacy + except Exception: + modern = jpype.JPackage("info").openrocket + return modern.core, modern.swing + + @staticmethod + def _block_loader(gui_module, field_name): + try: + field = gui_module.getClass().getDeclaredField(field_name) + field.setAccessible(True) + loader = field.get(gui_module) + field.setAccessible(False) + loader.blockUntilLoaded() + except Exception: + # New OpenRocket versions can change internals; loading still works + # without explicitly waiting in most cases. + return + + def __enter__(self): + ensure_java_compatibility(self.jar_path) + + jvm_path = jpype.getDefaultJVMPath() + logger.info( + "Starting JVM from '%s' with OpenRocket '%s'", + jvm_path, + self.jar_path.as_posix(), + ) + + jpype.startJVM( + jvm_path, + "-ea", + f"-Djava.class.path={self.jar_path.as_posix()}", + ) + + self.openrocket, swing = self._resolve_packages() + + guice = jpype.JPackage("com").google.inject.Guice + logger_factory = jpype.JPackage("org").slf4j.LoggerFactory + logger_class = jpype.JPackage("ch").qos.logback.classic.Logger + logger_level = jpype.JPackage("ch").qos.logback.classic.Level + + gui_module = swing.startup.GuiModule() + plugin_module = self.openrocket.plugin.PluginModule() + + injector = guice.createInjector(gui_module, plugin_module) + + app = self.openrocket.startup.Application + app.setInjector(injector) + + gui_module.startLoader() + self._block_loader(gui_module, "presetLoader") + self._block_loader(gui_module, "motorLoader") + + root_logger = logger_factory.getLogger(logger_class.ROOT_LOGGER_NAME) + root_logger.setLevel( + getattr(logger_level, str(self.log_level), logger_level.ERROR) + ) + + self.started = True + return self + + def __exit__(self, ex_type, ex, tb): + try: + if jpype.isJVMStarted(): + try: + for window in jpype.java.awt.Window.getWindows(): + window.dispose() + except Exception: + pass + jpype.shutdownJVM() + finally: + self.started = False + + def load_doc(self, ork_filename: str): + if not self.started: + raise RuntimeError("OpenRocketSession has not been started") + + java_file = jpype.java.io.File(ork_filename) + loader = self.openrocket.file.GeneralRocketLoader(java_file) + return loader.load() diff --git a/test_ork2json-debugger.py b/test_ork2json-debugger.py index e0ed95d..37184f6 100644 --- a/test_ork2json-debugger.py +++ b/test_ork2json-debugger.py @@ -4,6 +4,11 @@ # NOTE: use this to run the python debugger # NOTE: restart the jupyter kernel if needed, so JVEM can restart -ork2json( - "examples/databank/Team24/rocket.ork", -) +if __name__ == "__main__": + ork2json( + [ + "--filepath", + "examples/databank/Team24/rocket.ork", + ], + standalone_mode=False, + ) diff --git a/tests/conftest.py b/tests/conftest.py index 71ec82c..e9f5623 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,64 +1,61 @@ -import json -import os +from pathlib import Path -import orhelper import pytest -from bs4 import BeautifulSoup from rocketserializer import ork_extractor +from rocketserializer._helpers import extract_ork_from_zip, parse_ork_file +from rocketserializer.openrocket_runtime import ( + OpenRocketSession, + select_latest_openrocket_jar, +) + +ROOT_DIR = Path(__file__).resolve().parents[1] def get_settings(filepath, output, ork): - bs = BeautifulSoup(open(filepath, encoding="utf-8").read(), features="xml") + bs, _ = parse_ork_file(filepath) - if not os.path.exists(output): - # create the output folder if it doesn't exist - os.makedirs(output) + output.mkdir(parents=True, exist_ok=True) settings = ork_extractor( bs=bs, - filepath=filepath, - output_folder=output, + filepath=str(filepath), + output_folder=str(output), ork=ork, - eng=None, ) return settings -with orhelper.OpenRocketInstance("tests/OpenRocket-15.03.jar", "OFF") as instance: - orh = orhelper.Helper(instance) +with OpenRocketSession(select_latest_openrocket_jar(ROOT_DIR), "OFF") as session: # Valetudo 2019 - filepath = "examples/ProjetoJupiter--Valetudo--2019/rocket.ork" - output = "tests/acceptance/ProjetoJupiter--Valetudo--2019/" - ork = orh.load_doc(filepath) + filepath = ROOT_DIR / "examples" / "ProjetoJupiter--Valetudo--2019" / "rocket.ork" + filepath = extract_ork_from_zip(filepath, filepath.parent) + output = ROOT_DIR / "tests" / "acceptance" / "ProjetoJupiter--Valetudo--2019" + ork = session.load_doc(str(filepath)) settings1 = get_settings(filepath, output, ork) - with open(output + "parameters.json", "w", encoding="utf-8") as f: - json.dump(settings1, f, indent=4, ensure_ascii=False) @pytest.fixture() def valetudo_settings(): return settings1 # NDRT 2020 - filepath = "examples/NDRT--Rocket--2020/rocket.ork" - output = "tests/acceptance/NDRT--Rocket--2020/" - ork = orh.load_doc(filepath) + filepath = ROOT_DIR / "examples" / "NDRT--Rocket--2020" / "rocket.ork" + filepath = extract_ork_from_zip(filepath, filepath.parent) + output = ROOT_DIR / "tests" / "acceptance" / "NDRT--Rocket--2020" + ork = session.load_doc(str(filepath)) settings2 = get_settings(filepath, output, ork) - with open(output + "parameters.json", "w", encoding="utf-8") as f: - json.dump(settings2, f, indent=4, ensure_ascii=False) @pytest.fixture() def ndrt_settings(): return settings2 # Bella Lui 2020 - filepath = "examples/EPFL--BellaLui--2020/rocket.ork" - output = "tests/acceptance/EPFL--BellaLui--2020/" - ork = orh.load_doc(filepath) + filepath = ROOT_DIR / "examples" / "EPFL--BellaLui--2020" / "rocket.ork" + filepath = extract_ork_from_zip(filepath, filepath.parent) + output = ROOT_DIR / "tests" / "acceptance" / "EPFL--BellaLui--2020" + ork = session.load_doc(str(filepath)) settings3 = get_settings(filepath, output, ork) - with open(output + "parameters.json", "w", encoding="utf-8") as f: - json.dump(settings3, f, indent=4, ensure_ascii=False) @pytest.fixture() def epfl_settings(): From 08233b679d7b5d18f6c17593cfdc403e86ebf679 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 28 Mar 2026 20:33:07 -0300 Subject: [PATCH 2/9] update linters to start using ruff --- Makefile | 23 +++++++++++++++++------ requirements-dev.txt | 6 ++---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 4a6e122..7abb542 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,20 @@ -lint: isort black +format: + @ruff check --select I --fix + @ruff format . + @echo Ruff formatting completed. -isort: - isort . -black: - black . +ruff-lint: + @echo Running ruff check... + @ruff check + @echo Ruff linter check completed. pylint: - pylint rocketserializer/ --output="pylint_report.txt" + @echo Running pylint check... + @pylint . + @echo Pylint check completed. + +lint: ruff-lint pylint tests: pytest @@ -18,3 +25,7 @@ tests: # tests-integration: +install: + pip install -r requirements.in + pip install -r requirements-dev.txt + pip install -e . diff --git a/requirements-dev.txt b/requirements-dev.txt index 68e1823..aee157a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,5 @@ -black[jupyter] pylint -isort -flake8 mypy pytest==7.4.0 -pytest-coverage \ No newline at end of file +pytest-coverage +ruff From 6cbee296175ba8cc7e88bd64f5c4ea500eaa4e16 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 28 Mar 2026 20:42:06 -0300 Subject: [PATCH 3/9] lint and format --- Makefile | 2 +- destroy-the-bank.py | 2 +- .../EPFL--BellaLui--2020/simulation.ipynb | 20 ++++---- examples/NDRT--Rocket--2020/simulation.ipynb | 21 ++++---- .../simulation.ipynb | 16 +++--- rocketserializer/__init__.py | 2 + rocketserializer/nb_builder.py | 4 -- rocketserializer/openrocket_runtime.py | 14 ++++-- tests/acceptance/test_ork_extractor.py | 2 +- tests/conftest.py | 49 ++++++++++--------- 10 files changed, 71 insertions(+), 61 deletions(-) diff --git a/Makefile b/Makefile index 7abb542..00db9c8 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ ruff-lint: pylint: @echo Running pylint check... - @pylint . + @pylint examples/ rocketserializer/ tests/ @echo Pylint check completed. lint: ruff-lint pylint diff --git a/destroy-the-bank.py b/destroy-the-bank.py index ab20670..ea1df97 100644 --- a/destroy-the-bank.py +++ b/destroy-the-bank.py @@ -47,6 +47,6 @@ def destroy_the_bank(file): for file in ork_files: try: destroy_the_bank(file) - except Exception as e: + except Exception: # Log any unexpected exceptions logging.exception(f"An unexpected error occurred in file: {file}") diff --git a/examples/EPFL--BellaLui--2020/simulation.ipynb b/examples/EPFL--BellaLui--2020/simulation.ipynb index c1f19e3..0ce4fd9 100644 --- a/examples/EPFL--BellaLui--2020/simulation.ipynb +++ b/examples/EPFL--BellaLui--2020/simulation.ipynb @@ -27,19 +27,17 @@ "metadata": {}, "outputs": [], "source": [ + "import datetime\n", + "\n", "from rocketpy import (\n", " Environment,\n", - " SolidMotor,\n", - " Rocket,\n", " Flight,\n", - " TrapezoidalFins,\n", - " EllipticalFins,\n", - " RailButtons,\n", " NoseCone,\n", + " Rocket,\n", + " SolidMotor,\n", " Tail,\n", - " Parachute,\n", - ")\n", - "import datetime" + " TrapezoidalFins,\n", + ")" ] }, { @@ -510,7 +508,11 @@ ] } ], - "metadata": {}, + "metadata": { + "language_info": { + "name": "python" + } + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/examples/NDRT--Rocket--2020/simulation.ipynb b/examples/NDRT--Rocket--2020/simulation.ipynb index cab35f8..d62624e 100644 --- a/examples/NDRT--Rocket--2020/simulation.ipynb +++ b/examples/NDRT--Rocket--2020/simulation.ipynb @@ -27,19 +27,18 @@ "metadata": {}, "outputs": [], "source": [ + "import datetime\n", + "\n", "from rocketpy import (\n", " Environment,\n", - " SolidMotor,\n", - " Rocket,\n", " Flight,\n", - " TrapezoidalFins,\n", - " EllipticalFins,\n", - " RailButtons,\n", " NoseCone,\n", - " Tail,\n", " Parachute,\n", - ")\n", - "import datetime" + " Rocket,\n", + " SolidMotor,\n", + " Tail,\n", + " TrapezoidalFins,\n", + ")" ] }, { @@ -541,7 +540,11 @@ ] } ], - "metadata": {}, + "metadata": { + "language_info": { + "name": "python" + } + }, "nbformat": 4, "nbformat_minor": 5 } diff --git a/examples/ProjetoJupiter--Valetudo--2019/simulation.ipynb b/examples/ProjetoJupiter--Valetudo--2019/simulation.ipynb index ec0a2a0..c5aaac0 100644 --- a/examples/ProjetoJupiter--Valetudo--2019/simulation.ipynb +++ b/examples/ProjetoJupiter--Valetudo--2019/simulation.ipynb @@ -31,24 +31,22 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "id": "20ba7738", "metadata": {}, "outputs": [], "source": [ + "import datetime\n", + "\n", "from rocketpy import (\n", " Environment,\n", - " SolidMotor,\n", - " Rocket,\n", " Flight,\n", - " TrapezoidalFins,\n", - " EllipticalFins,\n", - " RailButtons,\n", " NoseCone,\n", - " Tail,\n", " Parachute,\n", - ")\n", - "import datetime" + " Rocket,\n", + " SolidMotor,\n", + " TrapezoidalFins,\n", + ")" ] }, { diff --git a/rocketserializer/__init__.py b/rocketserializer/__init__.py index 08fa291..cc3abea 100644 --- a/rocketserializer/__init__.py +++ b/rocketserializer/__init__.py @@ -1 +1,3 @@ from .ork_extractor import ork_extractor + +__all__ = ["ork_extractor"] diff --git a/rocketserializer/nb_builder.py b/rocketserializer/nb_builder.py index 204760f..8f562eb 100644 --- a/rocketserializer/nb_builder.py +++ b/rocketserializer/nb_builder.py @@ -329,7 +329,6 @@ def build_fins(self, nb: nbf.v4.new_notebook) -> nbf.v4.new_notebook: text = "trapezoidal_fins = {}\n" nb["cells"].append(nbf.v4.new_code_cell(text)) for i in range(len(self.parameters["trapezoidal_fins"])): - trapezoidal_fins_i = self.parameters["trapezoidal_fins"][str(i)] number = trapezoidal_fins_i["number"] @@ -365,7 +364,6 @@ def build_fins(self, nb: nbf.v4.new_notebook) -> nbf.v4.new_notebook: text = "elliptical_fins = {}\n" nb["cells"].append(nbf.v4.new_code_cell(text)) for i in range(len(self.parameters["elliptical_fins"])): - elliptical_fins_i = self.parameters["elliptical_fins"][str(i)] number = elliptical_fins_i["number"] @@ -410,7 +408,6 @@ def build_tails(self, nb: nbf.v4.new_notebook) -> nbf.v4.new_notebook: text = "tails = {}\n" nb["cells"].append(nbf.v4.new_code_cell(text)) for i in range(len(self.parameters["tails"])): - tail_i = self.parameters["tails"][str(i)] top_radius = tail_i["top_radius"] @@ -467,7 +464,6 @@ def build_parachute(self, nb: nbf.v4.new_notebook) -> nbf.v4.new_notebook: text = "parachutes = {}\n" nb["cells"].append(nbf.v4.new_code_cell(text)) for i in range(len(self.parameters["parachutes"])): - parachute_i = self.parameters["parachutes"][str(i)] cd_s = parachute_i["cd"] * parachute_i["area"] deploy_event = parachute_i["deploy_event"] diff --git a/rocketserializer/openrocket_runtime.py b/rocketserializer/openrocket_runtime.py index 2ee25e6..9d8dd51 100644 --- a/rocketserializer/openrocket_runtime.py +++ b/rocketserializer/openrocket_runtime.py @@ -106,7 +106,11 @@ def ensure_java_compatibility(jar_path: Path): default_jvm_major = None try: default_jvm_major = _extract_java_major(jpype.getDefaultJVMPath()) - except Exception: + except ( + jpype.JVMNotFoundException, + jpype.JVMNotSupportedException, + OSError, + ): default_jvm_major = None if default_jvm_major and default_jvm_major >= required_major: @@ -156,7 +160,7 @@ def _resolve_packages(self): legacy = jpype.JPackage("net").sf.openrocket _ = legacy.startup.Application return legacy, legacy - except Exception: + except (AttributeError, TypeError, RuntimeError): modern = jpype.JPackage("info").openrocket return modern.core, modern.swing @@ -168,10 +172,10 @@ def _block_loader(gui_module, field_name): loader = field.get(gui_module) field.setAccessible(False) loader.blockUntilLoaded() - except Exception: + except (AttributeError, TypeError, RuntimeError, jpype.JException): # New OpenRocket versions can change internals; loading still works # without explicitly waiting in most cases. - return + pass def __enter__(self): ensure_java_compatibility(self.jar_path) @@ -222,7 +226,7 @@ def __exit__(self, ex_type, ex, tb): try: for window in jpype.java.awt.Window.getWindows(): window.dispose() - except Exception: + except (AttributeError, TypeError, RuntimeError, jpype.JException): pass jpype.shutdownJVM() finally: diff --git a/tests/acceptance/test_ork_extractor.py b/tests/acceptance/test_ork_extractor.py index 9bffebf..0dc86f4 100644 --- a/tests/acceptance/test_ork_extractor.py +++ b/tests/acceptance/test_ork_extractor.py @@ -16,7 +16,7 @@ ) def test_ork_extractor(expected_results_file, fixture, request): # load the expected results - with open(expected_results_file, "r") as f: + with open(expected_results_file, "r", encoding="utf-8") as f: expected_results = json.load(f) # get the settings from the fixture diff --git a/tests/conftest.py b/tests/conftest.py index e9f5623..dec3e76 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,50 +12,55 @@ ROOT_DIR = Path(__file__).resolve().parents[1] -def get_settings(filepath, output, ork): - bs, _ = parse_ork_file(filepath) +def get_settings(ork_filepath, output_dir, ork_document): + bs, _ = parse_ork_file(ork_filepath) - output.mkdir(parents=True, exist_ok=True) + output_dir.mkdir(parents=True, exist_ok=True) settings = ork_extractor( bs=bs, - filepath=str(filepath), - output_folder=str(output), - ork=ork, + filepath=str(ork_filepath), + output_folder=str(output_dir), + ork=ork_document, ) return settings with OpenRocketSession(select_latest_openrocket_jar(ROOT_DIR), "OFF") as session: - # Valetudo 2019 - filepath = ROOT_DIR / "examples" / "ProjetoJupiter--Valetudo--2019" / "rocket.ork" - filepath = extract_ork_from_zip(filepath, filepath.parent) - output = ROOT_DIR / "tests" / "acceptance" / "ProjetoJupiter--Valetudo--2019" - ork = session.load_doc(str(filepath)) - settings1 = get_settings(filepath, output, ork) + valetudo_filepath = ( + ROOT_DIR / "examples" / "ProjetoJupiter--Valetudo--2019" / "rocket.ork" + ) + valetudo_filepath = extract_ork_from_zip( + valetudo_filepath, valetudo_filepath.parent + ) + valetudo_output_dir = ( + ROOT_DIR / "tests" / "acceptance" / "ProjetoJupiter--Valetudo--2019" + ) + valetudo_doc = session.load_doc(str(valetudo_filepath)) + settings1 = get_settings(valetudo_filepath, valetudo_output_dir, valetudo_doc) @pytest.fixture() def valetudo_settings(): return settings1 # NDRT 2020 - filepath = ROOT_DIR / "examples" / "NDRT--Rocket--2020" / "rocket.ork" - filepath = extract_ork_from_zip(filepath, filepath.parent) - output = ROOT_DIR / "tests" / "acceptance" / "NDRT--Rocket--2020" - ork = session.load_doc(str(filepath)) - settings2 = get_settings(filepath, output, ork) + ndrt_filepath = ROOT_DIR / "examples" / "NDRT--Rocket--2020" / "rocket.ork" + ndrt_filepath = extract_ork_from_zip(ndrt_filepath, ndrt_filepath.parent) + ndrt_output_dir = ROOT_DIR / "tests" / "acceptance" / "NDRT--Rocket--2020" + ndrt_doc = session.load_doc(str(ndrt_filepath)) + settings2 = get_settings(ndrt_filepath, ndrt_output_dir, ndrt_doc) @pytest.fixture() def ndrt_settings(): return settings2 # Bella Lui 2020 - filepath = ROOT_DIR / "examples" / "EPFL--BellaLui--2020" / "rocket.ork" - filepath = extract_ork_from_zip(filepath, filepath.parent) - output = ROOT_DIR / "tests" / "acceptance" / "EPFL--BellaLui--2020" - ork = session.load_doc(str(filepath)) - settings3 = get_settings(filepath, output, ork) + epfl_filepath = ROOT_DIR / "examples" / "EPFL--BellaLui--2020" / "rocket.ork" + epfl_filepath = extract_ork_from_zip(epfl_filepath, epfl_filepath.parent) + epfl_output_dir = ROOT_DIR / "tests" / "acceptance" / "EPFL--BellaLui--2020" + epfl_doc = session.load_doc(str(epfl_filepath)) + settings3 = get_settings(epfl_filepath, epfl_output_dir, epfl_doc) @pytest.fixture() def epfl_settings(): From 5dbd76e7cfd56f596a89cec3b8be083d3b251392 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 28 Mar 2026 20:55:32 -0300 Subject: [PATCH 4/9] solve comments --- rocketserializer/cli.py | 5 ++--- rocketserializer/openrocket_runtime.py | 23 +++++++++++++++++------ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/rocketserializer/cli.py b/rocketserializer/cli.py index 20ec89d..c18f749 100644 --- a/rocketserializer/cli.py +++ b/rocketserializer/cli.py @@ -158,9 +158,8 @@ def ork2json(filepath, output=None, ork_jar=None, encoding="utf-8", verbose=Fals ) with OpenRocketSession(ork_jar, log_level="OFF") as instance: - # create the output folder if it does not exist - if os.path.exists(output) is False: - os.mkdir(output) + # create the output folder (including parents) if it does not exist + Path(output).mkdir(parents=True, exist_ok=True) ork = instance.load_doc(str(filepath)) diff --git a/rocketserializer/openrocket_runtime.py b/rocketserializer/openrocket_runtime.py index 9d8dd51..8b7034c 100644 --- a/rocketserializer/openrocket_runtime.py +++ b/rocketserializer/openrocket_runtime.py @@ -187,11 +187,18 @@ def __enter__(self): self.jar_path.as_posix(), ) - jpype.startJVM( - jvm_path, - "-ea", - f"-Djava.class.path={self.jar_path.as_posix()}", - ) + if jpype.isJVMStarted(): + logger.warning( + "JVM is already running; skipping startJVM. " + "Ensure the active JVM has '%s' on its classpath.", + self.jar_path.as_posix(), + ) + else: + jpype.startJVM( + jvm_path, + "-ea", + f"-Djava.class.path={self.jar_path.as_posix()}", + ) self.openrocket, swing = self._resolve_packages() @@ -228,7 +235,11 @@ def __exit__(self, ex_type, ex, tb): window.dispose() except (AttributeError, TypeError, RuntimeError, jpype.JException): pass - jpype.shutdownJVM() + # Do not call shutdownJVM() here: JPype <1.5 cannot restart the + # JVM in the same process, so shutting it down automatically would + # break any subsequent OpenRocketSession (e.g. in notebooks or + # programmatic use). The JVM is cleaned up by JPype's atexit hook + # when the process exits. finally: self.started = False From 2ccd2c94f722d6b10a63aacb35e6b84812982a05 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 28 Mar 2026 20:59:51 -0300 Subject: [PATCH 5/9] update github actions instructions --- .github/workflows/linter.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index b309921..5f83656 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -17,12 +17,14 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint + pip install ruff pylint pip install -r requirements.in - - uses: psf/black@stable - with: - options: "--check ." - jupyter: true + - name: Run ruff format check + run: | + ruff format --check . + - name: Run ruff lint + run: | + ruff check . - name: Run pylint run: | - pylint rocketserializer/ + pylint examples/ rocketserializer/ tests/ From e24d99b6e0798f4b6f51db71ee313fc47d736bdc Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 28 Mar 2026 21:05:27 -0300 Subject: [PATCH 6/9] update CI --- .github/workflows/linter.yml | 2 +- .github/workflows/test-pytest.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 5f83656..677ab11 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -17,7 +17,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install ruff pylint + pip install ruff pylint pytest pip install -r requirements.in - name: Run ruff format check run: | diff --git a/.github/workflows/test-pytest.yaml b/.github/workflows/test-pytest.yaml index c73dd21..d84e3f3 100644 --- a/.github/workflows/test-pytest.yaml +++ b/.github/workflows/test-pytest.yaml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - python-version: [3.9, 3.12] + python-version: [3.10, 3.14] env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python-version }} From 52e18972e91e037b19ec5096242c4af29912d5aa Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 28 Mar 2026 21:07:48 -0300 Subject: [PATCH 7/9] update Python version in CI matrix to include 3.9 --- .github/workflows/test-pytest.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-pytest.yaml b/.github/workflows/test-pytest.yaml index d84e3f3..e625778 100644 --- a/.github/workflows/test-pytest.yaml +++ b/.github/workflows/test-pytest.yaml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - python-version: [3.10, 3.14] + python-version: [3.9, 3.14] env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python-version }} From b294928222abaf65479b18e7f3fa09fa976e47c0 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 28 Mar 2026 21:08:21 -0300 Subject: [PATCH 8/9] fix warnings --- rocketserializer/_helpers.py | 2 +- rocketserializer/components/fins.py | 4 ++-- rocketserializer/components/nose_cone.py | 2 +- rocketserializer/components/parachute.py | 2 +- rocketserializer/components/rail_buttons.py | 2 +- rocketserializer/components/rocket.py | 4 ++-- rocketserializer/components/transition.py | 2 +- rocketserializer/ork_extractor.py | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/rocketserializer/_helpers.py b/rocketserializer/_helpers.py index 8f4a29f..23be671 100644 --- a/rocketserializer/_helpers.py +++ b/rocketserializer/_helpers.py @@ -76,7 +76,7 @@ def parse_ork_file(ork_path: Path): try: with open(ork_path, encoding="utf-8") as file: bs = BeautifulSoup(file, features="xml") - datapoints = bs.findAll("datapoint") + datapoints = bs.find_all("datapoint") logger.info( "Successfully parsed .ork file at '%s' with %d datapoints", ork_path.as_posix(), diff --git a/rocketserializer/components/fins.py b/rocketserializer/components/fins.py index cc16edc..505f463 100644 --- a/rocketserializer/components/fins.py +++ b/rocketserializer/components/fins.py @@ -26,7 +26,7 @@ def search_trapezoidal_fins(bs, elements): "sweep_length", "sweep_angle", "cant_angle", "section". """ settings = {} - fins = bs.findAll("trapezoidfinset") + fins = bs.find_all("trapezoidfinset") logger.info("A total of %d trapezoidal fin sets were detected", len(fins)) if len(fins) == 0: @@ -131,7 +131,7 @@ def search_elliptical_fins(bs, elements): "section". """ settings = {} - fins = bs.findAll("ellipticalfinset") + fins = bs.find_all("ellipticalfinset") logger.info("A total of %d elliptical fin sets were detected", len(fins)) if len(fins) == 0: diff --git a/rocketserializer/components/nose_cone.py b/rocketserializer/components/nose_cone.py index 3a3223f..48c33c4 100644 --- a/rocketserializer/components/nose_cone.py +++ b/rocketserializer/components/nose_cone.py @@ -31,7 +31,7 @@ def search_nosecone(bs, elements=None, rocket_radius=None, just_radius=False): if not nosecone: nosecones = list( filter( - lambda x: x.find("name").text == "Nosecone", bs.findAll("transition") + lambda x: x.find("name").text == "Nosecone", bs.find_all("transition") ) ) if len(nosecones) == 0: diff --git a/rocketserializer/components/parachute.py b/rocketserializer/components/parachute.py index 5214d81..6aa4ca6 100644 --- a/rocketserializer/components/parachute.py +++ b/rocketserializer/components/parachute.py @@ -25,7 +25,7 @@ def search_parachutes(bs): """ settings = {} - chutes = bs.findAll("parachute") + chutes = bs.find_all("parachute") logger.info("A total of %d parachutes were detected", len(chutes)) for idx, chute in enumerate(chutes): diff --git a/rocketserializer/components/rail_buttons.py b/rocketserializer/components/rail_buttons.py index 63cdea5..6e7da2d 100644 --- a/rocketserializer/components/rail_buttons.py +++ b/rocketserializer/components/rail_buttons.py @@ -23,7 +23,7 @@ def search_rail_buttons(bs, elements: dict) -> dict: name = str(lugs_elements[0]["name"]) angular_position = 0.0 - lugs = bs.findAll("launchlug") + lugs = bs.find_all("launchlug") for lug in lugs: if lug.find("name").text == name: angular_position = float(lug.find("radialdirection").text) diff --git a/rocketserializer/components/rocket.py b/rocketserializer/components/rocket.py index 38e6ece..6b7c850 100644 --- a/rocketserializer/components/rocket.py +++ b/rocketserializer/components/rocket.py @@ -51,8 +51,8 @@ def search_rocket(bs, datapoints, data_labels, burnout_position): def get_rocket_radius(bs): # We want to take the maximum radius of the rocket - tubes = bs.findAll("bodytube") - noses = bs.findAll("nosecone") + tubes = bs.find_all("bodytube") + noses = bs.find_all("nosecone") tubes_radius = [i.find("radius").text for i in tubes] noses_radius = [i.find("aftradius").text for i in noses] diff --git a/rocketserializer/components/transition.py b/rocketserializer/components/transition.py index 88d02ae..6cd2a2b 100644 --- a/rocketserializer/components/transition.py +++ b/rocketserializer/components/transition.py @@ -26,7 +26,7 @@ def search_transitions(bs, elements, ork): "bottom_radius", "length", "position". """ settings = {} - transitions = bs.findAll("transition") + transitions = bs.find_all("transition") logger.info("A total of %d transitions were found", len(transitions)) transitions_ork = [ diff --git a/rocketserializer/ork_extractor.py b/rocketserializer/ork_extractor.py index d50887a..8c96283 100644 --- a/rocketserializer/ork_extractor.py +++ b/rocketserializer/ork_extractor.py @@ -144,7 +144,7 @@ def __init_vectors(bs): time_vector : list The time vector. """ - datapoints = bs.findAll("datapoint") + datapoints = bs.find_all("datapoint") data_labels = bs.find("databranch").attrs["types"].split(",") time_vector = [float(datapoint.text.split(",")[0]) for datapoint in datapoints] From ba2b3d83fc41a0ebf23420cd1bc77547f7a10cf5 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Sat, 28 Mar 2026 21:15:57 -0300 Subject: [PATCH 9/9] fix python version in CI matrix to use 3.11 instead of 3.14 --- .github/workflows/test-pytest.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test-pytest.yaml b/.github/workflows/test-pytest.yaml index e625778..4c5794e 100644 --- a/.github/workflows/test-pytest.yaml +++ b/.github/workflows/test-pytest.yaml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - python-version: [3.9, 3.14] + python-version: [3.9, 3.11] env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python-version }}