From 1d08505d41c936c6ec129b6d5eb857533cfd5fec Mon Sep 17 00:00:00 2001 From: MINHYUK PARK Date: Tue, 15 Sep 2026 17:32:05 +0900 Subject: [PATCH 1/2] Read saved MAL language path as UTF-8 --- mal_gui/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mal_gui/app.py b/mal_gui/app.py index 358dd3b..f3ecd77 100644 --- a/mal_gui/app.py +++ b/mal_gui/app.py @@ -53,7 +53,7 @@ def __init__(self, parent=None): os.makedirs(os.path.dirname(self.config_file_path), exist_ok=True) self.config = configparser.ConfigParser() - self.config.read(self.config_file_path) + self.config.read(self.config_file_path, encoding="utf-8") self.selected_lang_file = self.config.get( "Settings", "langFilePath", fallback=None ) From 78dd4772cf31d94e627e14776f889001e6172f42 Mon Sep 17 00:00:00 2001 From: MINHYUK PARK Date: Tue, 15 Sep 2026 17:34:02 +0900 Subject: [PATCH 2/2] Fix association selection for inherited asset types Use the MAL Toolbox subtype check to include child assets in association choices. Prevent accepting an empty choice and cover inherited endpoints in both directions with pytest. --- mal_gui/connection_dialog.py | 19 ++++---- tests/test_connection_dialog.py | 86 +++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 tests/test_connection_dialog.py diff --git a/mal_gui/connection_dialog.py b/mal_gui/connection_dialog.py index 1efe2e4..1225f24 100644 --- a/mal_gui/connection_dialog.py +++ b/mal_gui/connection_dialog.py @@ -65,9 +65,8 @@ def __init__( for fieldname, association in possible_assocs.items(): field = association.get_field(fieldname) - # If assoc ends with end_assets type, give that assoc - # as option in list widget - if field.asset == self.end_asset.lg_asset: + # Association endpoints also accept subassets of their declared type. + if self.end_asset.lg_asset.is_subasset_of(field.asset): assoc_list_item = QListWidgetItem( self.start_asset.name + "." @@ -91,6 +90,7 @@ def __init__( self.ok_button = QPushButton("OK") self.ok_button.clicked.connect(self.ok_button_clicked) self.ok_button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) + self.ok_button.setEnabled(self.association_list_widget.count() > 0) button_layout.addWidget(self.ok_button) self.cancel_button = QPushButton("Cancel") @@ -112,15 +112,16 @@ def filter_items(self, text): def ok_button_clicked(self): selected_item = self.association_list_widget.currentItem() + if selected_item is None: + return - if selected_item: - data = selected_item.data(Qt.UserRole) + data = selected_item.data(Qt.UserRole) - from_asset: ModelAsset = data.get("from") - to_asset: ModelAsset = data.get("to") - self.field_name: str = data.get("fieldname") + from_asset: ModelAsset = data.get("from") + to_asset: ModelAsset = data.get("to") + self.field_name: str = data.get("fieldname") - print(f"{from_asset}.{self.field_name} = {to_asset} chosen") + print(f"{from_asset}.{self.field_name} = {to_asset} chosen") self.accept() diff --git a/tests/test_connection_dialog.py b/tests/test_connection_dialog.py new file mode 100644 index 0000000..b51a1e5 --- /dev/null +++ b/tests/test_connection_dialog.py @@ -0,0 +1,86 @@ +from types import SimpleNamespace + +import pytest +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QDialog + +from maltoolbox.language import LanguageGraphAsset, LanguageGraphAssociation +from maltoolbox.language.language_graph_assoc import ( + LanguageGraphAssociationField, + link_association_to_assets, +) +from maltoolbox.model import ModelAsset + +from mal_gui.connection_dialog import AssociationConnectionDialog + + +@pytest.fixture +def machine_execution_assets(app): + machine = LanguageGraphAsset("Machine") + infotainment = LanguageGraphAsset("InfotainmentSystem", own_super_asset=machine) + application = LanguageGraphAsset("VehicularApplication") + operating_system = LanguageGraphAsset( + "OperatingSystem", own_super_asset=application + ) + + association = LanguageGraphAssociation( + name="machineExecution", + left_field=LanguageGraphAssociationField(machine, "hostMachine", 0, 1), + right_field=LanguageGraphAssociationField( + application, "machineExecutedApps", 0, 0 + ), + ) + link_association_to_assets(association, machine, application) + + return { + asset.name: ModelAsset(asset.name, asset_id, asset) + for asset_id, asset in enumerate( + (machine, infotainment, application, operating_system) + ) + } + + +def make_dialog(start_asset, end_asset): + start_item = SimpleNamespace(asset=start_asset, asset_type=start_asset.type) + end_item = SimpleNamespace(asset=end_asset, asset_type=end_asset.type) + return AssociationConnectionDialog(start_item, end_item, None, None) + + +@pytest.mark.parametrize( + ("start_type", "end_type", "expected_fieldname"), + [ + ("InfotainmentSystem", "OperatingSystem", "machineExecutedApps"), + ("OperatingSystem", "InfotainmentSystem", "hostMachine"), + ("Machine", "VehicularApplication", "machineExecutedApps"), + ], +) +def test_inherited_endpoint_types_are_available( + machine_execution_assets, start_type, end_type, expected_fieldname +): + start_asset = machine_execution_assets[start_type] + end_asset = machine_execution_assets[end_type] + dialog = make_dialog(start_asset, end_asset) + + assert dialog.association_list_widget.count() == 1 + assert dialog.ok_button.isEnabled() + selected_item = dialog.association_list_widget.currentItem() + assert selected_item.data(Qt.UserRole)["fieldname"] == expected_fieldname + + dialog.ok_button_clicked() + assert dialog.result() == QDialog.Accepted + assert dialog.field_name == expected_fieldname + + start_asset.add_associated_assets(expected_fieldname, {end_asset}) + assert end_asset in start_asset.associated_assets[expected_fieldname] + + +def test_empty_selection_cannot_be_accepted(machine_execution_assets): + unrelated_asset = ModelAsset("Unrelated", 4, LanguageGraphAsset("Unrelated")) + dialog = make_dialog(machine_execution_assets["Machine"], unrelated_asset) + + assert dialog.association_list_widget.count() == 0 + assert not dialog.ok_button.isEnabled() + + dialog.ok_button_clicked() + assert dialog.result() != QDialog.Accepted + assert dialog.field_name is None