diff --git a/CHANGELOG.md b/CHANGELOG.md index f0c0bd9..dae322a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security - N/A +## [27.0.1] - 2026-04-18 + +### Added +- N/A + +### Changed +- N/A + +### Deprecated +- N/A + +### Removed +- N/A + +### Fixed +- `PlotManager.export_to_vtk()` no longer appends a `.vtk` suffix to the export path; the path is treated as a VTK export **folder** root, matching Synergy UI behavior. Uses `prepare_folder_path()` in `helper.py` to validate the path and create parent directories only. + +### Security +- N/A + ## [27.0.0] - 2026-01-21 ### Added @@ -170,7 +190,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial version aligned with Moldflow Synergy 2026.0.1 - Python 3.10-3.13 compatibility -[Unreleased]: https://github.com/Autodesk/moldflow-api/compare/v26.0.5...HEAD +[Unreleased]: https://github.com/Autodesk/moldflow-api/compare/v27.0.1...HEAD +[27.0.1]: https://github.com/Autodesk/moldflow-api/releases/tag/v27.0.1 +[27.0.0]: https://github.com/Autodesk/moldflow-api/releases/tag/v27.0.0 [26.0.5]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.5 [26.0.4]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.4 [26.0.3]: https://github.com/Autodesk/moldflow-api/releases/tag/v26.0.3 diff --git a/src/moldflow/common.py b/src/moldflow/common.py index 49a10a6..416b2f9 100644 --- a/src/moldflow/common.py +++ b/src/moldflow/common.py @@ -175,6 +175,7 @@ class LogMessage(Enum): CHECK_NEGATIVE = ("Checking {value} is negative", logging.DEBUG) CHECK_INDEX_IN_RANGE = ("Checking index {index} is in range", logging.DEBUG) CHECK_FILE_EXTENSION = ("Checking file extension {file_name}", logging.DEBUG) + CHECK_FOLDER_PATH = ("Checking folder path {folder_path}", logging.DEBUG) CHECK_EXPECTED_VALUES = ("Checking {value} is in expected values", logging.DEBUG) FAIL_INIT_WITH_ENV = ( "Could not initialize with Instance ID: {value}", diff --git a/src/moldflow/helper.py b/src/moldflow/helper.py index d9c682e..f5fe595 100644 --- a/src/moldflow/helper.py +++ b/src/moldflow/helper.py @@ -237,6 +237,22 @@ def check_index(index: int, min_value: int, max_value: int): process_log(__name__, LogMessage.VALID_INPUT) +def _create_required_parent_directories(path: str): + """ + Create parent directories for a given path. + + Trailing separators are normalized first so ``dirname`` resolves to the + parent of the final path component (never the export root / file leaf). + + Args: + path (str): The path to create parent directories for. + """ + directory = os.path.dirname(os.path.normpath(path)) + if not directory: + return + os.makedirs(directory, exist_ok=True) + + def check_file_extension(file_name: str, extensions: tuple | str): """ Check if the file name has a valid extension. @@ -247,9 +263,7 @@ def check_file_extension(file_name: str, extensions: tuple | str): process_log(__name__, LogMessage.CHECK_FILE_EXTENSION, locals(), file_name=file_name) check_type(file_name, str) check_type(extensions, (str, tuple)) - directory = os.path.dirname(file_name) - if directory: - os.makedirs(directory, exist_ok=True) + _create_required_parent_directories(file_name) default = extensions if isinstance(extensions, str) else extensions[0] if not file_name.endswith(extensions): process_log( @@ -263,6 +277,25 @@ def check_file_extension(file_name: str, extensions: tuple | str): return file_name +def prepare_folder_path(folder_path: str) -> str: + """ + Validate and prepare a folder-style export path. + + Ensures parent directories exist. Does not modify the path or append a file extension. + + Args: + folder_path (str): Full path to the export root folder (bare name, relative path, + or absolute path). + + Returns: + str: The same path, unchanged. + """ + process_log(__name__, LogMessage.CHECK_FOLDER_PATH, locals(), folder_path=folder_path) + check_type(folder_path, str) + _create_required_parent_directories(folder_path) + return folder_path + + def check_expected_values(value, expected_values: tuple): """ Check if the value is in the expected values. diff --git a/src/moldflow/locale/de-DE/LC_MESSAGES/locale.de-DE.po b/src/moldflow/locale/de-DE/LC_MESSAGES/locale.de-DE.po index 3828e8a..9c468bc 100644 --- a/src/moldflow/locale/de-DE/LC_MESSAGES/locale.de-DE.po +++ b/src/moldflow/locale/de-DE/LC_MESSAGES/locale.de-DE.po @@ -9,6 +9,9 @@ msgstr "Abbrechen" msgid "Checking file extension {file_name}" msgstr "Überprüfen der Dateierweiterung {file_name}" +msgid "Checking folder path {folder_path}" +msgstr "Überprüfen des Ordnerpfads {folder_path}" + msgid "Checking index {index} is in range" msgstr "Überprüfen, ob der Index {index} im Bereich liegt" diff --git a/src/moldflow/locale/en-US/LC_MESSAGES/locale.en-US.po b/src/moldflow/locale/en-US/LC_MESSAGES/locale.en-US.po index d0d1571..9d967cb 100644 --- a/src/moldflow/locale/en-US/LC_MESSAGES/locale.en-US.po +++ b/src/moldflow/locale/en-US/LC_MESSAGES/locale.en-US.po @@ -9,6 +9,9 @@ msgstr "Cancel" msgid "Checking file extension {file_name}" msgstr "Checking file extension {file_name}" +msgid "Checking folder path {folder_path}" +msgstr "Checking folder path {folder_path}" + msgid "Checking index {index} is in range" msgstr "Checking index {index} is in range" diff --git a/src/moldflow/locale/es-ES/LC_MESSAGES/locale.es-ES.po b/src/moldflow/locale/es-ES/LC_MESSAGES/locale.es-ES.po index ce166eb..a55e0f6 100644 --- a/src/moldflow/locale/es-ES/LC_MESSAGES/locale.es-ES.po +++ b/src/moldflow/locale/es-ES/LC_MESSAGES/locale.es-ES.po @@ -9,6 +9,9 @@ msgstr "Cancelar" msgid "Checking file extension {file_name}" msgstr "Comprobando la extensión del archivo {file_name}" +msgid "Checking folder path {folder_path}" +msgstr "Comprobando la ruta de la carpeta {folder_path}" + msgid "Checking index {index} is in range" msgstr "Comprobando que el índice {index} está dentro del rango" diff --git a/src/moldflow/locale/fr-FR/LC_MESSAGES/locale.fr-FR.po b/src/moldflow/locale/fr-FR/LC_MESSAGES/locale.fr-FR.po index f45060e..f80be81 100644 --- a/src/moldflow/locale/fr-FR/LC_MESSAGES/locale.fr-FR.po +++ b/src/moldflow/locale/fr-FR/LC_MESSAGES/locale.fr-FR.po @@ -9,6 +9,9 @@ msgstr "Annuler" msgid "Checking file extension {file_name}" msgstr "Vérification de l'extension du fichier {file_name}" +msgid "Checking folder path {folder_path}" +msgstr "Vérification du chemin du dossier {folder_path}" + msgid "Checking index {index} is in range" msgstr "Vérification que l'indice {index} est dans l'intervalle" diff --git a/src/moldflow/locale/it-IT/LC_MESSAGES/locale.it-IT.po b/src/moldflow/locale/it-IT/LC_MESSAGES/locale.it-IT.po index 693f216..e8ad04e 100644 --- a/src/moldflow/locale/it-IT/LC_MESSAGES/locale.it-IT.po +++ b/src/moldflow/locale/it-IT/LC_MESSAGES/locale.it-IT.po @@ -9,6 +9,9 @@ msgstr "Annulla" msgid "Checking file extension {file_name}" msgstr "Verifica dell'estensione del file {file_name}" +msgid "Checking folder path {folder_path}" +msgstr "Verifica del percorso della cartella {folder_path}" + msgid "Checking index {index} is in range" msgstr "Verifica che l'indice {index} sia nell'intervallo" diff --git a/src/moldflow/locale/ja-JP/LC_MESSAGES/locale.ja-JP.po b/src/moldflow/locale/ja-JP/LC_MESSAGES/locale.ja-JP.po index 3a1dfc1..9680773 100644 --- a/src/moldflow/locale/ja-JP/LC_MESSAGES/locale.ja-JP.po +++ b/src/moldflow/locale/ja-JP/LC_MESSAGES/locale.ja-JP.po @@ -9,6 +9,9 @@ msgstr "キャンセル" msgid "Checking file extension {file_name}" msgstr "ファイル拡張子 {file_name} を確認しています" +msgid "Checking folder path {folder_path}" +msgstr "フォルダー パス {folder_path} を確認しています" + msgid "Checking index {index} is in range" msgstr "インデックス {index} が範囲内であることを確認しています" diff --git a/src/moldflow/locale/ko-KR/LC_MESSAGES/locale.ko-KR.po b/src/moldflow/locale/ko-KR/LC_MESSAGES/locale.ko-KR.po index 5f6976a..630b372 100644 --- a/src/moldflow/locale/ko-KR/LC_MESSAGES/locale.ko-KR.po +++ b/src/moldflow/locale/ko-KR/LC_MESSAGES/locale.ko-KR.po @@ -9,6 +9,9 @@ msgstr "취소" msgid "Checking file extension {file_name}" msgstr "파일 확장자 {file_name} 확인 중" +msgid "Checking folder path {folder_path}" +msgstr "폴더 경로 {folder_path} 확인 중" + msgid "Checking index {index} is in range" msgstr "인덱스 {index}가 범위 내에 있는지 확인 중" diff --git a/src/moldflow/locale/pt-PT/LC_MESSAGES/locale.pt-PT.po b/src/moldflow/locale/pt-PT/LC_MESSAGES/locale.pt-PT.po index 9b1a92a..4a9f8ee 100644 --- a/src/moldflow/locale/pt-PT/LC_MESSAGES/locale.pt-PT.po +++ b/src/moldflow/locale/pt-PT/LC_MESSAGES/locale.pt-PT.po @@ -9,6 +9,9 @@ msgstr "Cancelar" msgid "Checking file extension {file_name}" msgstr "A verificar a extensão do ficheiro {file_name}" +msgid "Checking folder path {folder_path}" +msgstr "A verificar o caminho da pasta {folder_path}" + msgid "Checking index {index} is in range" msgstr "A verificar se o índice {index} está no intervalo" diff --git a/src/moldflow/locale/zh-CN/LC_MESSAGES/locale.zh-CN.po b/src/moldflow/locale/zh-CN/LC_MESSAGES/locale.zh-CN.po index a73c1e2..1a68283 100644 --- a/src/moldflow/locale/zh-CN/LC_MESSAGES/locale.zh-CN.po +++ b/src/moldflow/locale/zh-CN/LC_MESSAGES/locale.zh-CN.po @@ -9,6 +9,9 @@ msgstr "取消" msgid "Checking file extension {file_name}" msgstr "正在检查文件扩展名{file_name}" +msgid "Checking folder path {folder_path}" +msgstr "正在检查文件夹路径{folder_path}" + msgid "Checking index {index} is in range" msgstr "正在检查索引{index}是否在范围内" diff --git a/src/moldflow/locale/zh-TW/LC_MESSAGES/locale.zh-TW.po b/src/moldflow/locale/zh-TW/LC_MESSAGES/locale.zh-TW.po index 4ebf02b..cc1e7d8 100644 --- a/src/moldflow/locale/zh-TW/LC_MESSAGES/locale.zh-TW.po +++ b/src/moldflow/locale/zh-TW/LC_MESSAGES/locale.zh-TW.po @@ -9,6 +9,9 @@ msgstr "取消" msgid "Checking file extension {file_name}" msgstr "正在檢查檔案副檔名 {file_name}" +msgid "Checking folder path {folder_path}" +msgstr "正在檢查資料夾路徑 {folder_path}" + msgid "Checking index {index} is in range" msgstr "正在檢查索引 {index} 是否在範圍內" diff --git a/src/moldflow/plot_manager.py b/src/moldflow/plot_manager.py index a454811..733734e 100644 --- a/src/moldflow/plot_manager.py +++ b/src/moldflow/plot_manager.py @@ -16,10 +16,16 @@ from .material_plot import MaterialPlot from .user_plot import UserPlot from .common import MaterialDatabase, MaterialIndex, PlotType, SystemUnits -from .helper import check_type, get_enum_value, check_file_extension, coerce_optional_dispatch +from .helper import ( + check_type, + get_enum_value, + check_file_extension, + prepare_folder_path, + coerce_optional_dispatch, +) from .com_proxy import safe_com from .errors import raise_save_error -from .constants import XML_FILE_EXT, SDZ_FILE_EXT, FBX_FILE_EXT, ELE_FILE_EXT, VTK_FILE_EXT +from .constants import XML_FILE_EXT, SDZ_FILE_EXT, FBX_FILE_EXT, ELE_FILE_EXT class PlotManager: @@ -1011,10 +1017,10 @@ def create_material_plot( def export_to_vtk(self, file_name: str, binary_format: bool = True) -> bool: """ - Export the results to a VTK file. + Export the results to a VTK output folder. Args: - file_name (str): The name of the VTK file. + file_name (str): The name of the VTK output folder. binary_format (bool): Use Binary (True) or ASCII (False). Default: True. Returns: @@ -1023,7 +1029,7 @@ def export_to_vtk(self, file_name: str, binary_format: bool = True) -> bool: process_log(__name__, LogMessage.FUNCTION_CALL, locals(), name="export_to_vtk") check_type(file_name, str) check_type(binary_format, bool) - file_name = check_file_extension(file_name, VTK_FILE_EXT) + file_name = prepare_folder_path(file_name) result = self.plot_manager.ExportToVTK(file_name, binary_format) if not result: raise_save_error(saving="Results", file_name=file_name) diff --git a/tests/api/unit_tests/test_unit_plot_manager.py b/tests/api/unit_tests/test_unit_plot_manager.py index 1a08e07..7b284e1 100644 --- a/tests/api/unit_tests/test_unit_plot_manager.py +++ b/tests/api/unit_tests/test_unit_plot_manager.py @@ -1273,7 +1273,8 @@ def test_save_functions2_save_error( + [ ("ExportToVTK", "export_to_vtk", ("sample.vtk", x), ("sample.vtk", x)) for x in VALID_BOOL - ], + ] + + [("ExportToVTK", "export_to_vtk", ("vtk_out", x), ("vtk_out", x)) for x in VALID_BOOL], ) # pylint: disable-next=R0913, R0917 def test_save_functions( @@ -1321,7 +1322,8 @@ def test_save_functions( SystemUnits, ) ] - + [("ExportToVTK", "export_to_vtk", ("sample.vtk", x)) for x in VALID_BOOL], + + [("ExportToVTK", "export_to_vtk", ("sample.vtk", x)) for x in VALID_BOOL] + + [("ExportToVTK", "export_to_vtk", ("SupportBeam-API-All", x)) for x in VALID_BOOL], ) # pylint: disable-next=R0913, R0917 def test_save_functions_save_error( diff --git a/tests/core/test_helper.py b/tests/core/test_helper.py index 6c7155e..76e3d7c 100644 --- a/tests/core/test_helper.py +++ b/tests/core/test_helper.py @@ -5,12 +5,14 @@ Test helper.py """ +import os from unittest.mock import Mock from enum import Enum import inspect import pytest from moldflow.helper import ( check_file_extension, + prepare_folder_path, check_index, check_is_non_negative, check_is_non_zero, @@ -31,6 +33,7 @@ VALID_BOOL, VALID_INT, VALID_FLOAT, + INVALID_STR, list_intersection, ) @@ -278,6 +281,78 @@ def test_check_file_extension_invalid(self, file_name, extensions, _, caplog): check_file_extension(file_name, extensions) assert _("default") in caplog.text + def test_check_file_extension_creates_parent_dir(self, _, caplog, tmp_path): + """ + When the path includes a parent, check_file_extension creates intermediate directories. + """ + file_path = os.path.join(str(tmp_path), "exports", "nested", "data.xml") + assert check_file_extension(file_path, ".xml") == file_path + assert (tmp_path / "exports" / "nested").is_dir() + assert _("Valid") in caplog.text + + def test_check_file_extension_without_parent_directory(self, _, caplog, tmp_path, monkeypatch): + """ + With no directory component (bare filename), no directories are created. + """ + monkeypatch.chdir(tmp_path) + assert check_file_extension("data.xml", ".xml") == "data.xml" + assert not list(tmp_path.iterdir()) + assert _("Valid") in caplog.text + + def test_prepare_folder_path_bare_name(self, _, caplog, tmp_path, monkeypatch): + """ + Test prepare_folder_path returns the path unchanged and does not append an extension. + """ + monkeypatch.chdir(tmp_path) + assert prepare_folder_path("SupportBeam-API-All") == "SupportBeam-API-All" + assert _("Valid") in caplog.text + + def test_prepare_folder_path_relative_with_parent(self, _, caplog, tmp_path, monkeypatch): + """ + Relative paths with a parent segment stay under cwd (isolated via tmp_path). + """ + monkeypatch.chdir(tmp_path) + rel = os.path.join("ExportFormat", "TestFolder") + assert prepare_folder_path(rel) == rel + assert (tmp_path / "ExportFormat").is_dir() + assert _("Valid") in caplog.text + + def test_prepare_folder_path_creates_parent_dir(self, _, caplog, tmp_path): + """ + When the path includes a parent, prepare_folder_path creates intermediate directories. + """ + folder_path = os.path.join(str(tmp_path), "vtk_out", "run1") + assert prepare_folder_path(folder_path) == folder_path + assert (tmp_path / "vtk_out").is_dir() + assert _("Valid") in caplog.text + + def test_prepare_folder_path_trailing_sep_creates_parents_only(self, _, caplog, tmp_path): + """ + Trailing path separators must not make makedirs target the export root itself. + """ + folder_path = os.path.join(str(tmp_path), "vtk", "run1") + os.sep + assert prepare_folder_path(folder_path) == folder_path + assert (tmp_path / "vtk").is_dir() + assert not (tmp_path / "vtk" / "run1").exists() + assert _("Valid") in caplog.text + + def test_prepare_folder_path_without_parent_directory(self, _, caplog, tmp_path, monkeypatch): + """ + With no directory component (bare folder name), no directories are created. + """ + monkeypatch.chdir(tmp_path) + assert prepare_folder_path("export_root") == "export_root" + assert not list(tmp_path.iterdir()) + assert _("Valid") in caplog.text + + @pytest.mark.parametrize("bad", INVALID_STR) + def test_prepare_folder_path_invalid_type(self, bad): + """ + Test prepare_folder_path rejects non-str paths. + """ + with pytest.raises(TypeError): + prepare_folder_path(bad) + @pytest.mark.parametrize("value, expected_values", [(x, (1, 2, 3)) for x in tuple(range(1, 3))]) def test_check_expected_values(self, value, expected_values, _, caplog): """ diff --git a/version.json b/version.json index 57c7a4d..31ed73a 100644 --- a/version.json +++ b/version.json @@ -1,5 +1,5 @@ { "major": "27", "minor": "0", - "patch": "0" + "patch": "1" }