Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/moldflow/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}",
Expand Down
39 changes: 36 additions & 3 deletions src/moldflow/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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(
Expand All @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions src/moldflow/locale/de-DE/LC_MESSAGES/locale.de-DE.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 3 additions & 0 deletions src/moldflow/locale/en-US/LC_MESSAGES/locale.en-US.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 3 additions & 0 deletions src/moldflow/locale/es-ES/LC_MESSAGES/locale.es-ES.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 3 additions & 0 deletions src/moldflow/locale/fr-FR/LC_MESSAGES/locale.fr-FR.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 3 additions & 0 deletions src/moldflow/locale/it-IT/LC_MESSAGES/locale.it-IT.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 3 additions & 0 deletions src/moldflow/locale/ja-JP/LC_MESSAGES/locale.ja-JP.po
Original file line number Diff line number Diff line change
Expand Up @@ -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} が範囲内であることを確認しています"

Expand Down
3 changes: 3 additions & 0 deletions src/moldflow/locale/ko-KR/LC_MESSAGES/locale.ko-KR.po
Original file line number Diff line number Diff line change
Expand Up @@ -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}가 범위 내에 있는지 확인 중"

Expand Down
3 changes: 3 additions & 0 deletions src/moldflow/locale/pt-PT/LC_MESSAGES/locale.pt-PT.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
3 changes: 3 additions & 0 deletions src/moldflow/locale/zh-CN/LC_MESSAGES/locale.zh-CN.po
Original file line number Diff line number Diff line change
Expand Up @@ -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}是否在范围内"

Expand Down
3 changes: 3 additions & 0 deletions src/moldflow/locale/zh-TW/LC_MESSAGES/locale.zh-TW.po
Original file line number Diff line number Diff line change
Expand Up @@ -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} 是否在範圍內"

Expand Down
16 changes: 11 additions & 5 deletions src/moldflow/plot_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions tests/api/unit_tests/test_unit_plot_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down
75 changes: 75 additions & 0 deletions tests/core/test_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -31,6 +33,7 @@
VALID_BOOL,
VALID_INT,
VALID_FLOAT,
INVALID_STR,
list_intersection,
)

Expand Down Expand Up @@ -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):
"""
Expand Down
2 changes: 1 addition & 1 deletion version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"major": "27",
"minor": "0",
"patch": "0"
"patch": "1"
}
Loading