From ceacee63bb778cd5fa5281fd6a1f17754dc124d0 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Mon, 26 Oct 2026 16:42:23 +0000 Subject: [PATCH 1/2] fix(files): show each multi-tool filament type once in file metadata --- BlocksScreen/helper_methods.py | 19 +++++++++++++++++ .../lib/panels/widgets/PrintTab/filesPage.py | 21 +------------------ .../lib/panels/widgets/metadataPage.py | 2 ++ tests/widgets/test_metadata_page_unit.py | 11 ++++++++++ 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/BlocksScreen/helper_methods.py b/BlocksScreen/helper_methods.py index 85f829fb..15819e00 100644 --- a/BlocksScreen/helper_methods.py +++ b/BlocksScreen/helper_methods.py @@ -8,6 +8,7 @@ import ctypes import enum +import json import logging import math import os @@ -318,6 +319,24 @@ def format_weight(grams: float) -> str: return f"{grams / 1000:.2f}kg" if grams > 499 else f"{grams:.2f}g" +def parse_materials(filament_type: object) -> list[str]: + """filament_type (str, list, JSON or CSV) as unique material names, in order.""" + if isinstance(filament_type, str): + text = filament_type.strip() + if text.startswith("[") and text.endswith("]"): + try: + types = json.loads(text) + except json.JSONDecodeError: + types = [text] + else: + types = text.split(",") + elif isinstance(filament_type, list): + types = filament_type + else: + types = [filament_type] if filament_type else [] + return list(dict.fromkeys(name for t in types if (name := str(t).strip()))) + + def normalize( value: float, r_min: float = 0.0, diff --git a/BlocksScreen/lib/panels/widgets/PrintTab/filesPage.py b/BlocksScreen/lib/panels/widgets/PrintTab/filesPage.py index db83b60d..4e5cb687 100644 --- a/BlocksScreen/lib/panels/widgets/PrintTab/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/PrintTab/filesPage.py @@ -1,4 +1,3 @@ -import json import logging import helper_methods @@ -457,28 +456,10 @@ def _format_file_meta(self, filedata: dict) -> str: filament = self._filament_label(filedata.get("filament_type")) return f"{filament} - {self._format_print_time(seconds)}" - @staticmethod - def _parse_materials(filament_type) -> list[str]: - """filament_type (str, list, JSON or CSV) as material names.""" - if isinstance(filament_type, str): - text = filament_type.strip() - if text.startswith("[") and text.endswith("]"): - try: - types = json.loads(text) - except json.JSONDecodeError: - types = [text] - else: - types = text.split(",") - elif isinstance(filament_type, list): - types = filament_type - else: - types = [filament_type] if filament_type else [] - return [name for t in types if (name := str(t).strip())] - @classmethod def _filament_label(cls, filament_type) -> str: """filament_type as a comma-joined label.""" - label = ",".join(dict.fromkeys(cls._parse_materials(filament_type))) + label = ",".join(helper_methods.parse_materials(filament_type)) if not label or label == "Unknown": return "Unknown Filament" return label diff --git a/BlocksScreen/lib/panels/widgets/metadataPage.py b/BlocksScreen/lib/panels/widgets/metadataPage.py index 0b906191..443c0404 100644 --- a/BlocksScreen/lib/panels/widgets/metadataPage.py +++ b/BlocksScreen/lib/panels/widgets/metadataPage.py @@ -188,6 +188,8 @@ def _raw_value(self, key: str, value: object) -> str | None: """Value text, None to skip.""" if value is None or value in ("", [], {}, "Unknown"): return None + if key == "filament_type": + return ", ".join(helper_methods.parse_materials(value)) or None if isinstance(value, bool): return "Yes" if value else "No" if isinstance(value, (int, float)): diff --git a/tests/widgets/test_metadata_page_unit.py b/tests/widgets/test_metadata_page_unit.py index c184076d..103e5e91 100644 --- a/tests/widgets/test_metadata_page_unit.py +++ b/tests/widgets/test_metadata_page_unit.py @@ -111,3 +111,14 @@ def test_slicer_nozzle_temp_beats_fallback(render, direction): def test_unknown_material_has_no_nozzle_fallback(render, direction): joined = " ".join(render({"filament_type": "Unobtainium"}, direction)) assert "Nozzle Temperature" not in joined + + +@pytest.mark.parametrize( + ("raw", "shown"), + [('["PLA", "PLA"]', "PLA"), ('["PLA", "PETG", "PLA"]', "PLA, PETG")], +) +def test_multi_tool_filament_type_is_deduped(render, raw, shown): + """Moonraker sends a JSON list per tool; repeats collapse to one name each.""" + texts = render({"filament_type": raw}, QtCore.Qt.LayoutDirection.LeftToRight) + assert shown in texts + assert not any("[" in t for t in texts) From 0240d772ea46e7339b2faf733039ea1cd0adf21a Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 27 Oct 2026 10:59:42 +0000 Subject: [PATCH 2/2] fix(files): split semicolon filament lists and skip the nozzle fallback for mixed materials --- BlocksScreen/helper_methods.py | 11 +++++-- .../lib/panels/widgets/PrintTab/filesPage.py | 7 ++-- .../lib/panels/widgets/metadataPage.py | 7 ++-- tests/test_helper_methods_unit.py | 32 +++++++++++++++++++ tests/widgets/test_metadata_page_unit.py | 11 +++++++ 5 files changed, 58 insertions(+), 10 deletions(-) create mode 100644 tests/test_helper_methods_unit.py diff --git a/BlocksScreen/helper_methods.py b/BlocksScreen/helper_methods.py index 15819e00..cd0cedab 100644 --- a/BlocksScreen/helper_methods.py +++ b/BlocksScreen/helper_methods.py @@ -13,6 +13,7 @@ import math import os import pathlib +import re import shutil import struct import tempfile @@ -320,7 +321,7 @@ def format_weight(grams: float) -> str: def parse_materials(filament_type: object) -> list[str]: - """filament_type (str, list, JSON or CSV) as unique material names, in order.""" + """filament_type (str, list, JSON, CSV or ;-list) as unique names, [] if unknown.""" if isinstance(filament_type, str): text = filament_type.strip() if text.startswith("[") and text.endswith("]"): @@ -329,12 +330,16 @@ def parse_materials(filament_type: object) -> list[str]: except json.JSONDecodeError: types = [text] else: - types = text.split(",") + # PrusaSlicer, SuperSlicer and OrcaSlicer separate tools with ";". + types = re.split(r"[,;]", text) elif isinstance(filament_type, list): types = filament_type else: types = [filament_type] if filament_type else [] - return list(dict.fromkeys(name for t in types if (name := str(t).strip()))) + names = list( + dict.fromkeys(name for t in types if t is not None and (name := str(t).strip())) + ) + return [] if names == ["Unknown"] else names def normalize( diff --git a/BlocksScreen/lib/panels/widgets/PrintTab/filesPage.py b/BlocksScreen/lib/panels/widgets/PrintTab/filesPage.py index 4e5cb687..6b82efaa 100644 --- a/BlocksScreen/lib/panels/widgets/PrintTab/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/PrintTab/filesPage.py @@ -459,10 +459,9 @@ def _format_file_meta(self, filedata: dict) -> str: @classmethod def _filament_label(cls, filament_type) -> str: """filament_type as a comma-joined label.""" - label = ",".join(helper_methods.parse_materials(filament_type)) - if not label or label == "Unknown": - return "Unknown Filament" - return label + return ", ".join(helper_methods.parse_materials(filament_type)) or ( + "Unknown Filament" + ) def _delayed_scrollbar_update(self) -> None: """Update scrollbar after model changes.""" diff --git a/BlocksScreen/lib/panels/widgets/metadataPage.py b/BlocksScreen/lib/panels/widgets/metadataPage.py index 443c0404..ce255c6b 100644 --- a/BlocksScreen/lib/panels/widgets/metadataPage.py +++ b/BlocksScreen/lib/panels/widgets/metadataPage.py @@ -164,10 +164,11 @@ def on_show_widget(self, text: str, filedata: dict | None = None) -> None: self._rows_layout.setRowStretch(row, 1 if row < used else 0) def _filament_nozzle_temp(self, filament_type: object) -> int | None: - """Typical nozzle temp for the material, or None.""" - if not isinstance(filament_type, str): + """Typical nozzle temp for a single-material print, or None.""" + materials = helper_methods.parse_materials(filament_type) + if len(materials) != 1: return None - name = filament_type.upper() + name = materials[0].upper() for material, temp in _FILAMENT_NOZZLE_TEMP.items(): if material in name: return temp diff --git a/tests/test_helper_methods_unit.py b/tests/test_helper_methods_unit.py new file mode 100644 index 00000000..caa49f8c --- /dev/null +++ b/tests/test_helper_methods_unit.py @@ -0,0 +1,32 @@ +"""helper_methods.parse_materials edge cases.""" + +import importlib.util +from pathlib import Path + +import pytest + +# Some conftests stub helper_methods, so load the real file under a private name. +_hm_path = Path(__file__).resolve().parents[1] / "BlocksScreen/helper_methods.py" +_spec = importlib.util.spec_from_file_location("_real_helper_methods", _hm_path) +_hm = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(_hm) + + +@pytest.mark.parametrize( + ("raw", "expected"), + [ + ("PLA", ["PLA"]), + ("PLA, PETG,PLA", ["PLA", "PETG"]), + ("PLA;PETG;PLA", ["PLA", "PETG"]), + ('["PLA", "PETG", "PLA"]', ["PLA", "PETG"]), + (["PLA", None, "PLA"], ["PLA"]), + ("[1, null]", ["1"]), + ("[bad", ["[bad"]), + ('["Unknown", "Unknown"]', []), + ("Unknown", []), + ("", []), + (None, []), + ], +) +def test_parse_materials(raw, expected): + assert _hm.parse_materials(raw) == expected diff --git a/tests/widgets/test_metadata_page_unit.py b/tests/widgets/test_metadata_page_unit.py index 103e5e91..f1215116 100644 --- a/tests/widgets/test_metadata_page_unit.py +++ b/tests/widgets/test_metadata_page_unit.py @@ -107,6 +107,17 @@ def test_slicer_nozzle_temp_beats_fallback(render, direction): assert "235 °C" not in joined +@pytest.mark.parametrize( + ("raw", "temp"), [('["PETG", "PETG"]', "235 °C"), ('["PETG", "PLA"]', None)] +) +def test_nozzle_fallback_needs_a_single_material(render, raw, temp): + """Mixed tools have no one typical nozzle temp.""" + joined = " ".join(render({"filament_type": raw}, _DIRECTIONS[0])) + assert ("Nozzle Temperature" in joined) is (temp is not None) + if temp: + assert temp in joined + + @pytest.mark.parametrize("direction", _DIRECTIONS) def test_unknown_material_has_no_nozzle_fallback(render, direction): joined = " ".join(render({"filament_type": "Unobtainium"}, direction))