diff --git a/cura/API/Interface/__init__.py b/cura/API/Interface/__init__.py index 742254a1a4..cec174bf0a 100644 --- a/cura/API/Interface/__init__.py +++ b/cura/API/Interface/__init__.py @@ -3,7 +3,6 @@ from typing import TYPE_CHECKING -from UM.PluginRegistry import PluginRegistry from cura.API.Interface.Settings import Settings if TYPE_CHECKING: @@ -23,9 +22,6 @@ if TYPE_CHECKING: class Interface: - # For now we use the same API version to be consistent. - VERSION = PluginRegistry.APIVersion - def __init__(self, application: "CuraApplication") -> None: # API methods specific to the settings portion of the UI self.settings = Settings(application) diff --git a/cura/API/__init__.py b/cura/API/__init__.py index ad07452c1a..b3e702263a 100644 --- a/cura/API/__init__.py +++ b/cura/API/__init__.py @@ -4,7 +4,6 @@ from typing import Optional, TYPE_CHECKING from PyQt5.QtCore import QObject, pyqtProperty -from UM.PluginRegistry import PluginRegistry from cura.API.Backups import Backups from cura.API.Interface import Interface from cura.API.Account import Account @@ -22,7 +21,6 @@ if TYPE_CHECKING: class CuraAPI(QObject): # For now we use the same API version to be consistent. - VERSION = PluginRegistry.APIVersion __instance = None # type: "CuraAPI" _application = None # type: CuraApplication @@ -62,4 +60,4 @@ class CuraAPI(QObject): @property def interface(self) -> "Interface": - return self._interface \ No newline at end of file + return self._interface diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 547c3dae71..1589f16afc 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -489,7 +489,9 @@ class BuildVolume(SceneNode): def _updateRaftThickness(self): old_raft_thickness = self._raft_thickness - self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value") + if self._global_container_stack.extruders: + # This might be called before the extruder stacks have initialised, in which case getting the adhesion_type fails + self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value") self._raft_thickness = 0.0 if self._adhesion_type == "raft": self._raft_thickness = ( diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index a44c3a50e5..47cc94f972 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -134,7 +134,7 @@ except ImportError: CuraVersion = "master" # [CodeStyle: Reflecting imported value] CuraBuildType = "" CuraDebugMode = False - CuraSDKVersion = "" + CuraSDKVersion = "5.0.0" class CuraApplication(QtApplication): @@ -164,6 +164,7 @@ class CuraApplication(QtApplication): super().__init__(name = "cura", app_display_name = CuraAppDisplayName, version = CuraVersion, + api_version = CuraSDKVersion, buildtype = CuraBuildType, is_debug_mode = CuraDebugMode, tray_icon_name = "cura-icon-32.png", diff --git a/cura/Machines/QualityManager.py b/cura/Machines/QualityManager.py index fc8262de52..a784d17f0b 100644 --- a/cura/Machines/QualityManager.py +++ b/cura/Machines/QualityManager.py @@ -16,7 +16,7 @@ from .QualityGroup import QualityGroup from .QualityNode import QualityNode if TYPE_CHECKING: - from UM.Settings.DefinitionContainer import DefinitionContainer + from UM.Settings.Interfaces import DefinitionContainerInterface from cura.Settings.GlobalStack import GlobalStack from .QualityChangesGroup import QualityChangesGroup from cura.CuraApplication import CuraApplication @@ -538,7 +538,7 @@ class QualityManager(QObject): # Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended # shares the same set of qualities profiles as Ultimaker 3. # -def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainer", +def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainerInterface", default_definition_id: str = "fdmprinter") -> str: machine_definition_id = default_definition_id if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)): diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index 39124c5ba3..0c03ae615b 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -272,7 +272,7 @@ class ConvexHullDecorator(SceneNodeDecorator): head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored) # Min head hull is used for the push free - convex_hull = self._compute2DConvexHeadFull() + convex_hull = self._compute2DConvexHull() if convex_hull: return convex_hull.getMinkowskiHull(head_and_fans) return None diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 3cfca1a944..133e04e8fc 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -419,13 +419,13 @@ class ContainerManager(QObject): self._container_name_filters[name_filter] = entry ## Import single profile, file_url does not have to end with curaprofile - @pyqtSlot(QUrl, result="QVariantMap") - def importProfile(self, file_url: QUrl): + @pyqtSlot(QUrl, result = "QVariantMap") + def importProfile(self, file_url: QUrl) -> Dict[str, str]: if not file_url.isValid(): - return + return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)} path = file_url.toLocalFile() if not path: - return + return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)} return self._container_registry.importProfile(path) @pyqtSlot(QObject, QUrl, str) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 11640adc0f..9f44d075e0 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -5,12 +5,12 @@ import os import re import configparser -from typing import cast, Optional - +from typing import cast, Dict, Optional from PyQt5.QtWidgets import QMessageBox from UM.Decorators import override from UM.Settings.ContainerFormatError import ContainerFormatError +from UM.Settings.Interfaces import ContainerInterface from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerStack import ContainerStack from UM.Settings.InstanceContainer import InstanceContainer @@ -28,7 +28,7 @@ from . import GlobalStack import cura.CuraApplication from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch -from cura.ReaderWriters.ProfileReader import NoProfileException +from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") @@ -161,20 +161,20 @@ class CuraContainerRegistry(ContainerRegistry): ## Imports a profile from a file # - # \param file_name \type{str} the full path and filename of the profile to import - # \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key - # containing a message for the user - def importProfile(self, file_name): + # \param file_name The full path and filename of the profile to import. + # \return Dict with a 'status' key containing the string 'ok' or 'error', + # and a 'message' key containing a message for the user. + def importProfile(self, file_name: str) -> Dict[str, str]: Logger.log("d", "Attempting to import profile %s", file_name) if not file_name: - return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags or !", "Failed to import profile from {0}: {1}", file_name, "Invalid path")} + return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}: {1}", file_name, "Invalid path")} plugin_registry = PluginRegistry.getInstance() extension = file_name.split(".")[-1] global_stack = Application.getInstance().getGlobalContainerStack() if not global_stack: - return + return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Can't import profile from {0} before a printer is added.", file_name)} machine_extruders = [] for position in sorted(global_stack.extruders): @@ -183,7 +183,7 @@ class CuraContainerRegistry(ContainerRegistry): for plugin_id, meta_data in self._getIOPlugins("profile_reader"): if meta_data["profile_reader"][0]["extension"] != extension: continue - profile_reader = plugin_registry.getPluginObject(plugin_id) + profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id)) try: profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. except NoProfileException: @@ -221,13 +221,13 @@ class CuraContainerRegistry(ContainerRegistry): # Make sure we have a profile_definition in the file: if profile_definition is None: break - machine_definition = self.findDefinitionContainers(id = profile_definition) - if not machine_definition: + machine_definitions = self.findDefinitionContainers(id = profile_definition) + if not machine_definitions: Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition) return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "This profile {0} contains incorrect data, could not import it.", file_name) } - machine_definition = machine_definition[0] + machine_definition = machine_definitions[0] # Get the expected machine definition. # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... @@ -274,11 +274,12 @@ class CuraContainerRegistry(ContainerRegistry): setting_value = global_profile.getProperty(qc_setting_key, "value") setting_definition = global_stack.getSettingDefinition(qc_setting_key) - new_instance = SettingInstance(setting_definition, profile) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - profile.addInstance(new_instance) - profile.setDirty(True) + if setting_definition is not None: + new_instance = SettingInstance(setting_definition, profile) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + profile.addInstance(new_instance) + profile.setDirty(True) global_profile.removeInstance(qc_setting_key, postpone_emit=True) extruder_profiles.append(profile) @@ -290,7 +291,7 @@ class CuraContainerRegistry(ContainerRegistry): for profile_index, profile in enumerate(profile_or_list): if profile_index == 0: # This is assumed to be the global profile - profile_id = (global_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_") + profile_id = (cast(ContainerInterface, global_stack.getBottom()).getId() + "_" + name_seed).lower().replace(" ", "_") elif profile_index < len(machine_extruders) + 1: # This is assumed to be an extruder profile diff --git a/cura/Settings/CuraFormulaFunctions.py b/cura/Settings/CuraFormulaFunctions.py index 1db01857f8..9ef80bd3d4 100644 --- a/cura/Settings/CuraFormulaFunctions.py +++ b/cura/Settings/CuraFormulaFunctions.py @@ -5,6 +5,7 @@ from typing import Any, List, Optional, TYPE_CHECKING from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext from UM.Settings.SettingFunction import SettingFunction +from UM.Logger import Logger if TYPE_CHECKING: from cura.CuraApplication import CuraApplication @@ -38,7 +39,11 @@ class CuraFormulaFunctions: extruder_position = int(machine_manager.defaultExtruderPosition) global_stack = machine_manager.activeMachine - extruder_stack = global_stack.extruders[str(extruder_position)] + try: + extruder_stack = global_stack.extruders[str(extruder_position)] + except KeyError: + Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available" % (property_key, extruder_position)) + return None value = extruder_stack.getRawProperty(property_key, "value", context = context) if isinstance(value, SettingFunction): diff --git a/plugins/ChangeLogPlugin/ChangeLog.py b/plugins/ChangeLogPlugin/ChangeLog.py index 723c83a021..eeec5edf9b 100644 --- a/plugins/ChangeLogPlugin/ChangeLog.py +++ b/plugins/ChangeLogPlugin/ChangeLog.py @@ -1,4 +1,4 @@ -# Copyright (c) 2015 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM.i18n import i18nCatalog @@ -29,6 +29,7 @@ class ChangeLog(Extension, QObject,): self._change_logs = None Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) Application.getInstance().getPreferences().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium + self.setMenuName(catalog.i18nc("@item:inmenu", "Changelog")) self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog) def getChangeLogs(self): diff --git a/plugins/LegacyProfileReader/DictionaryOfDoom.json b/plugins/LegacyProfileReader/DictionaryOfDoom.json index 0be413dd2c..f65cc271d1 100644 --- a/plugins/LegacyProfileReader/DictionaryOfDoom.json +++ b/plugins/LegacyProfileReader/DictionaryOfDoom.json @@ -1,6 +1,6 @@ { "source_version": "15.04", - "target_version": 3, + "target_version": "4.5", "translation": { "machine_nozzle_size": "nozzle_size", diff --git a/plugins/LegacyProfileReader/LegacyProfileReader.py b/plugins/LegacyProfileReader/LegacyProfileReader.py index cd577218d5..013bab6f11 100644 --- a/plugins/LegacyProfileReader/LegacyProfileReader.py +++ b/plugins/LegacyProfileReader/LegacyProfileReader.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import configparser # For reading the legacy profile INI files. @@ -6,6 +6,7 @@ import io import json # For reading the Dictionary of Doom. import math # For mathematical operations included in the Dictionary of Doom. import os.path # For concatenating the path to the plugin and the relative path to the Dictionary of Doom. +from typing import Dict from UM.Application import Application # To get the machine manager to create the new profile in. from UM.Logger import Logger # Logging errors. @@ -33,10 +34,11 @@ class LegacyProfileReader(ProfileReader): # \param json The JSON file to load the default setting values from. This # should not be a URL but a pre-loaded JSON handle. # \return A dictionary of the default values of the legacy Cura version. - def prepareDefaults(self, json): + def prepareDefaults(self, json: Dict[str, Dict[str, str]]) -> Dict[str, str]: defaults = {} - for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict. - defaults[key] = json["defaults"][key] + if "defaults" in json: + for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict. + defaults[key] = json["defaults"][key] return defaults ## Prepares the local variables that can be used in evaluation of computing @@ -80,11 +82,10 @@ class LegacyProfileReader(ProfileReader): Logger.log("i", "Importing legacy profile from file " + file_name + ".") container_registry = ContainerRegistry.getInstance() profile_id = container_registry.uniqueName("Imported Legacy Profile") - profile = InstanceContainer(profile_id) # Create an empty profile. - parser = configparser.ConfigParser(interpolation = None) + input_parser = configparser.ConfigParser(interpolation = None) try: - parser.read([file_name]) # Parse the INI file. + input_parser.read([file_name]) # Parse the INI file. except Exception as e: Logger.log("e", "Unable to open legacy profile %s: %s", file_name, str(e)) return None @@ -92,7 +93,7 @@ class LegacyProfileReader(ProfileReader): # Legacy Cura saved the profile under the section "profile_N" where N is the ID of a machine, except when you export in which case it saves it in the section "profile". # Since importing multiple machine profiles is out of scope, just import the first section we find. section = "" - for found_section in parser.sections(): + for found_section in input_parser.sections(): if found_section.startswith("profile"): section = found_section break @@ -110,15 +111,13 @@ class LegacyProfileReader(ProfileReader): return None defaults = self.prepareDefaults(dict_of_doom) - legacy_settings = self.prepareLocals(parser, section, defaults) #Gets the settings from the legacy profile. + legacy_settings = self.prepareLocals(input_parser, section, defaults) #Gets the settings from the legacy profile. - #Check the target version in the Dictionary of Doom with this application version. - if "target_version" not in dict_of_doom: - Logger.log("e", "Dictionary of Doom has no target version. Is it the correct JSON file?") - return None - if InstanceContainer.Version != dict_of_doom["target_version"]: - Logger.log("e", "Dictionary of Doom of legacy profile reader (version %s) is not in sync with the current instance container version (version %s)!", dict_of_doom["target_version"], str(InstanceContainer.Version)) - return None + # Serialised format into version 4.5. Do NOT upgrade this, let the version upgrader handle it. + output_parser = configparser.ConfigParser(interpolation = None) + output_parser.add_section("general") + output_parser.add_section("metadata") + output_parser.add_section("values") if "translation" not in dict_of_doom: Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?") @@ -127,7 +126,7 @@ class LegacyProfileReader(ProfileReader): quality_definition = current_printer_definition.getMetaDataEntry("quality_definition") if not quality_definition: quality_definition = current_printer_definition.getId() - profile.setDefinition(quality_definition) + output_parser["general"]["definition"] = quality_definition for new_setting in dict_of_doom["translation"]: # Evaluate all new settings that would get a value from the translations. old_setting_expression = dict_of_doom["translation"][new_setting] compiled = compile(old_setting_expression, new_setting, "eval") @@ -140,37 +139,34 @@ class LegacyProfileReader(ProfileReader): definitions = current_printer_definition.findDefinitions(key = new_setting) if definitions: if new_value != value_using_defaults and definitions[0].default_value != new_value: # Not equal to the default in the new Cura OR the default in the legacy Cura. - profile.setProperty(new_setting, "value", new_value) # Store the setting in the profile! + output_parser["values"][new_setting] = str(new_value) # Store the setting in the profile! - if len(profile.getAllKeys()) == 0: + if len(output_parser["values"]) == 0: Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.") - profile.setMetaDataEntry("type", "profile") - # don't know what quality_type it is based on, so use "normal" by default - profile.setMetaDataEntry("quality_type", "normal") - profile.setName(profile_id) - profile.setDirty(True) + output_parser["general"]["version"] = "4" + output_parser["general"]["name"] = profile_id + output_parser["metadata"]["type"] = "quality_changes" + output_parser["metadata"]["quality_type"] = "normal" # Don't know what quality_type it is based on, so use "normal" by default. + output_parser["metadata"]["position"] = "0" # We only support single extrusion. + output_parser["metadata"]["setting_version"] = "5" # What the dictionary of doom is made for. - #Serialise and deserialise in order to perform the version upgrade. - parser = configparser.ConfigParser(interpolation = None) - data = profile.serialize() - parser.read_string(data) - parser["general"]["version"] = "1" - if parser.has_section("values"): - parser["settings"] = parser["values"] - del parser["values"] + # Serialise in order to perform the version upgrade. stream = io.StringIO() - parser.write(stream) + output_parser.write(stream) data = stream.getvalue() - profile.deserialize(data) - # The definition can get reset to fdmprinter during the deserialization's upgrade. Here we set the definition - # again. - profile.setDefinition(quality_definition) + profile = InstanceContainer(profile_id) + profile.deserialize(data) # Also performs the version upgrade. + profile.setDirty(True) #We need to return one extruder stack and one global stack. global_container_id = container_registry.uniqueName("Global Imported Legacy Profile") + # We duplicate the extruder profile into the global stack. + # This may introduce some settings that are global in the extruder stack and some settings that are per-extruder in the global stack. + # We don't care about that. The engine will ignore them anyway. global_profile = profile.duplicate(new_id = global_container_id, new_name = profile_id) #Needs to have the same name as the extruder profile. + del global_profile.getMetaData()["position"] # Has no position because it's global. global_profile.setDirty(True) profile_definition = "fdmprinter" diff --git a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py new file mode 100644 index 0000000000..480a61f301 --- /dev/null +++ b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py @@ -0,0 +1,190 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import configparser # An input for some functions we're testing. +import os.path # To find the integration test .ini files. +import pytest # To register tests with. +import unittest.mock # To mock the application, plug-in and container registry out. + +import UM.Application # To mock the application out. +import UM.PluginRegistry # To mock the plug-in registry out. +import UM.Settings.ContainerRegistry # To mock the container registry out. +import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function. + +import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module. +from LegacyProfileReader import LegacyProfileReader # The module we're testing. + +@pytest.fixture +def legacy_profile_reader(): + return LegacyProfileReader() + +test_prepareDefaultsData = [ + { + "defaults": + { + "foo": "bar" + }, + "cheese": "delicious" + }, + { + "cat": "fluffy", + "dog": "floofy" + } +] + +@pytest.mark.parametrize("input", test_prepareDefaultsData) +def test_prepareDefaults(legacy_profile_reader, input): + output = legacy_profile_reader.prepareDefaults(input) + if "defaults" in input: + assert input["defaults"] == output + else: + assert output == {} + +test_prepareLocalsData = [ + ( # Ordinary case. + { # Parser data. + "profile": + { + "layer_height": "0.2", + "infill_density": "30" + } + }, + { # Defaults. + "layer_height": "0.1", + "infill_density": "20", + "line_width": "0.4" + } + ), + ( # Empty data. + { # Parser data. + "profile": + { + } + }, + { # Defaults. + } + ), + ( # All defaults. + { # Parser data. + "profile": + { + } + }, + { # Defaults. + "foo": "bar", + "boo": "far" + } + ), + ( # Multiple config sections. + { # Parser data. + "some_other_name": + { + "foo": "bar" + }, + "profile": + { + "foo": "baz" #Not the same as in some_other_name + } + }, + { # Defaults. + "foo": "bla" + } + ) +] + +@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsData) +def test_prepareLocals(legacy_profile_reader, parser_data, defaults): + parser = configparser.ConfigParser() + parser.read_dict(parser_data) + + output = legacy_profile_reader.prepareLocals(parser, "profile", defaults) + + assert set(defaults.keys()) <= set(output.keys()) # All defaults must be in there. + assert set(parser_data["profile"]) <= set(output.keys()) # All overwritten values must be in there. + for key in output: + if key in parser_data["profile"]: + assert output[key] == parser_data["profile"][key] # If overwritten, must be the overwritten value. + else: + assert output[key] == defaults[key] # Otherwise must be equal to the default. + +test_prepareLocalsNoSectionErrorData = [ + ( # Section does not exist. + { # Parser data. + "some_other_name": + { + "foo": "bar" + }, + }, + { # Defaults. + "foo": "baz" + } + ) +] + +## Test cases where a key error is expected. +@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsNoSectionErrorData) +def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, defaults): + parser = configparser.ConfigParser() + parser.read_dict(parser_data) + + with pytest.raises(configparser.NoSectionError): + legacy_profile_reader.prepareLocals(parser, "profile", defaults) + +intercepted_data = "" + +@pytest.mark.parametrize("file_name", ["normal_case.ini"]) +def test_read(legacy_profile_reader, file_name): + # Mock out all dependencies. Quite a lot! + global_stack = unittest.mock.MagicMock() + global_stack.getProperty = unittest.mock.MagicMock(return_value = 1) # For machine_extruder_count setting. + def getMetaDataEntry(key, default_value = ""): + if key == "quality_definition": + return "mocked_quality_definition" + if key == "has_machine_quality": + return "True" + global_stack.definition.getMetaDataEntry = getMetaDataEntry + global_stack.definition.getId = unittest.mock.MagicMock(return_value = "mocked_global_definition") + application = unittest.mock.MagicMock() + application.getGlobalContainerStack = unittest.mock.MagicMock(return_value = global_stack) + application_getInstance = unittest.mock.MagicMock(return_value = application) + container_registry = unittest.mock.MagicMock() + container_registry_getInstance = unittest.mock.MagicMock(return_value = container_registry) + container_registry.uniqueName = unittest.mock.MagicMock(return_value = "Imported Legacy Profile") + container_registry.findDefinitionContainers = unittest.mock.MagicMock(return_value = [global_stack.definition]) + UM.Settings.InstanceContainer.setContainerRegistry(container_registry) + plugin_registry = unittest.mock.MagicMock() + plugin_registry_getInstance = unittest.mock.MagicMock(return_value = plugin_registry) + plugin_registry.getPluginPath = unittest.mock.MagicMock(return_value = os.path.dirname(LegacyProfileReaderModule.__file__)) + + # Mock out the resulting InstanceContainer so that we can intercept the data before it's passed through the version upgrader. + def deserialize(self, data): # Intercepts the serialised data that we'd perform the version upgrade from when deserializing. + global intercepted_data + intercepted_data = data + + parser = configparser.ConfigParser() + parser.read_string(data) + self._metadata["position"] = parser["metadata"]["position"] + def duplicate(self, new_id, new_name): + self._metadata["id"] = new_id + self._metadata["name"] = new_name + return self + + with unittest.mock.patch.object(UM.Application.Application, "getInstance", application_getInstance): + with unittest.mock.patch.object(UM.Settings.ContainerRegistry.ContainerRegistry, "getInstance", container_registry_getInstance): + with unittest.mock.patch.object(UM.PluginRegistry.PluginRegistry, "getInstance", plugin_registry_getInstance): + with unittest.mock.patch.object(UM.Settings.InstanceContainer.InstanceContainer, "deserialize", deserialize): + with unittest.mock.patch.object(UM.Settings.InstanceContainer.InstanceContainer, "duplicate", duplicate): + result = legacy_profile_reader.read(os.path.join(os.path.dirname(__file__), file_name)) + + assert len(result) == 1 + + # Let's see what's inside the actual output file that we generated. + parser = configparser.ConfigParser() + parser.read_string(intercepted_data) + assert parser["general"]["definition"] == "mocked_quality_definition" + assert parser["general"]["version"] == "4" # Yes, before we upgraded. + assert parser["general"]["name"] == "Imported Legacy Profile" # Because we overwrote uniqueName. + assert parser["metadata"]["type"] == "quality_changes" + assert parser["metadata"]["quality_type"] == "normal" + assert parser["metadata"]["position"] == "0" + assert parser["metadata"]["setting_version"] == "5" # Yes, before we upgraded. \ No newline at end of file diff --git a/plugins/LegacyProfileReader/tests/normal_case.ini b/plugins/LegacyProfileReader/tests/normal_case.ini new file mode 100644 index 0000000000..213444d2d3 --- /dev/null +++ b/plugins/LegacyProfileReader/tests/normal_case.ini @@ -0,0 +1,7 @@ +[profile] +foo = bar +boo = far +fill_overlap = 3 + +[alterations] +some = values diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 1a1ea92d10..11ee610bec 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -32,7 +32,8 @@ class PostProcessingPlugin(QObject, Extension): def __init__(self, parent = None) -> None: QObject.__init__(self, parent) Extension.__init__(self) - self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup) + self.setMenuName(i18n_catalog.i18nc("@item:inmenu", "Post Processing")) + self.addMenuItem(i18n_catalog.i18nc("@item:inmenu", "Modify G-Code"), self.showPopup) self._view = None # Loaded scripts are all scripts that can be used diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py index aa5626b7f2..a31facf75a 100644 --- a/plugins/Toolbox/src/PackagesModel.py +++ b/plugins/Toolbox/src/PackagesModel.py @@ -12,7 +12,7 @@ from UM.Qt.ListModel import ListModel from .ConfigsModel import ConfigsModel -## Model that holds cura packages. By setting the filter property the instances held by this model can be changed. +## Model that holds Cura packages. By setting the filter property the instances held by this model can be changed. class PackagesModel(ListModel): def __init__(self, parent = None): super().__init__(parent) @@ -70,7 +70,7 @@ class PackagesModel(ListModel): # Links is a list of dictionaries with "title" and "url". Convert this list into a dict so it's easier # to process. - link_list = package['data']['links'] if 'links' in package['data'] else [] + link_list = package["data"]["links"] if "links" in package["data"] else [] links_dict = {d["title"]: d["url"] for d in link_list} if "author_id" not in package["author"] or "display_name" not in package["author"]: diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 43e1f5b3d9..562a964f01 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -172,18 +172,18 @@ class Toolbox(QObject, Extension): self._cloud_api_version = self._getCloudAPIVersion() self._cloud_api_root = self._getCloudAPIRoot() self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format( - cloud_api_root=self._cloud_api_root, - cloud_api_version=self._cloud_api_version, - sdk_version=self._sdk_version + cloud_api_root = self._cloud_api_root, + cloud_api_version = self._cloud_api_version, + sdk_version = self._sdk_version ) self._request_urls = { - "authors": QUrl("{base_url}/authors".format(base_url=self._api_url)), - "packages": QUrl("{base_url}/packages".format(base_url=self._api_url)), - "plugins_showcase": QUrl("{base_url}/showcase".format(base_url=self._api_url)), - "plugins_available": QUrl("{base_url}/packages?package_type=plugin".format(base_url=self._api_url)), - "materials_showcase": QUrl("{base_url}/showcase".format(base_url=self._api_url)), - "materials_available": QUrl("{base_url}/packages?package_type=material".format(base_url=self._api_url)), - "materials_generic": QUrl("{base_url}/packages?package_type=material&tags=generic".format(base_url=self._api_url)) + "authors": QUrl("{base_url}/authors".format(base_url = self._api_url)), + "packages": QUrl("{base_url}/packages".format(base_url = self._api_url)), + "plugins_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)), + "plugins_available": QUrl("{base_url}/packages?package_type=plugin".format(base_url = self._api_url)), + "materials_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)), + "materials_available": QUrl("{base_url}/packages?package_type=material".format(base_url = self._api_url)), + "materials_generic": QUrl("{base_url}/packages?package_type=material&tags=generic".format(base_url = self._api_url)) } # Get the API root for the packages API depending on Cura version settings. @@ -209,11 +209,11 @@ class Toolbox(QObject, Extension): # Get the packages version depending on Cura version settings. def _getSDKVersion(self) -> Union[int, str]: if not hasattr(cura, "CuraVersion"): - return self._plugin_registry.APIVersion + return self._application.getAPIVersion().getMajor() if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore - return self._plugin_registry.APIVersion + return self._application.getAPIVersion().getMajor() if not cura.CuraVersion.CuraSDKVersion: # type: ignore - return self._plugin_registry.APIVersion + return self._application.getAPIVersion().getMajor() return cura.CuraVersion.CuraSDKVersion # type: ignore @pyqtSlot() @@ -299,7 +299,7 @@ class Toolbox(QObject, Extension): for plugin_id in old_plugin_ids: # Neither the installed packages nor the packages that are scheduled to remove are old plugins if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids: - Logger.log('i', 'Found a plugin that was installed with the old plugin browser: %s', plugin_id) + Logger.log("i", "Found a plugin that was installed with the old plugin browser: %s", plugin_id) old_metadata = self._plugin_registry.getMetaData(plugin_id) new_metadata = self._convertPluginMetadata(old_metadata) @@ -511,7 +511,10 @@ class Toolbox(QObject, Extension): # version, we also need to check if the current one has a lower SDK version. If so, this package should also # be upgradable. elif remote_version == local_version: - can_upgrade = local_package.get("sdk_version", 0) < remote_package.get("sdk_version", 0) + # First read sdk_version_semver. If that doesn't exist, read just sdk_version (old version system). + remote_sdk_version = Version(remote_package.get("sdk_version_semver", remote_package.get("sdk_version", 0))) + local_sdk_version = Version(local_package.get("sdk_version_semver", local_package.get("sdk_version", 0))) + can_upgrade = local_sdk_version < remote_sdk_version return can_upgrade diff --git a/resources/definitions/fdmextruder.def.json b/resources/definitions/fdmextruder.def.json index 19c9e92d18..cb49b1e128 100644 --- a/resources/definitions/fdmextruder.def.json +++ b/resources/definitions/fdmextruder.def.json @@ -78,7 +78,7 @@ "machine_extruder_start_code": { "label": "Extruder Start G-Code", - "description": "Start g-code to execute whenever turning the extruder on.", + "description": "Start g-code to execute when switching to this extruder.", "type": "str", "default_value": "", "settable_per_mesh": false, @@ -124,7 +124,7 @@ "machine_extruder_end_code": { "label": "Extruder End G-Code", - "description": "End g-code to execute whenever turning the extruder off.", + "description": "End g-code to execute when switching away from this extruder.", "type": "str", "default_value": "", "settable_per_mesh": false, diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 5c446e0b6f..c015ab8ccb 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2406,7 +2406,7 @@ "switch_extruder_retraction_amount": { "label": "Nozzle Switch Retraction Distance", - "description": "The amount of retraction: Set at 0 for no retraction at all. This should generally be the same as the length of the heat zone.", + "description": "The amount of retraction when switching extruders. Set to 0 for no retraction at all. This should generally be the same as the length of the heat zone.", "type": "float", "unit": "mm", "enabled": "retraction_enable", @@ -4139,6 +4139,20 @@ "limit_to_extruder": "support_infill_extruder_nr", "settable_per_mesh": false }, + "minimum_support_area": + { + "label": "Minimum Support Area", + "description": "Minimum area size for support polygons. Polygons which have an area smaller than this value will not be generated.", + "unit": "mm²", + "type": "float", + "default_value": 0.0, + "minimum_value": "0", + "enabled": "support_enable", + "limit_to_extruder": "support_infill_extruder_nr", + "settable_per_mesh": true, + "fabricate_enabled": true, + "intermediate_enabled": true + }, "support_interface_enable": { "label": "Enable Support Interface", @@ -4378,6 +4392,50 @@ } } }, + "minimum_interface_area": + { + "label": "Minimum Support Interface Area", + "description": "Minimum area size for support interface polygons. Polygons which have an area smaller than this value will not be generated.", + "unit": "mm²", + "type": "float", + "default_value": 1.0, + "minimum_value": "0", + "minimum_value_warning": "minimum_support_area", + "limit_to_extruder": "support_interface_extruder_nr", + "enabled": "support_interface_enable and support_enable", + "settable_per_mesh": true, + "children": + { + "minimum_roof_area": + { + "label": "Minimum Support Roof Area", + "description": "Minimum area size for the roofs of the support. Polygons which have an area smaller than this value will not be generated.", + "unit": "mm²", + "type": "float", + "default_value": 1.0, + "value": "extruderValue(support_roof_extruder_nr, 'minimum_interface_area')", + "minimum_value": "0", + "minimum_value_warning": "minimum_support_area", + "limit_to_extruder": "support_roof_extruder_nr", + "enabled": "support_roof_enable and support_enable", + "settable_per_mesh": true + }, + "minimum_bottom_area": + { + "label": "Minimum Support Floor Area", + "description": "Minimum area size for the floors of the support. Polygons which have an area smaller than this value will not be generated.", + "unit": "mm²", + "type": "float", + "default_value": 1.0, + "value": "extruderValue(support_bottom_extruder_nr, 'minimum_interface_area')", + "minimum_value": "0", + "minimum_value_warning": "minimum_support_area", + "limit_to_extruder": "support_bottom_extruder_nr", + "enabled": "support_bottom_enable and support_enable", + "settable_per_mesh": true + } + } + }, "support_interface_offset": { "label": "Support Interface Horizontal Expansion", @@ -4398,7 +4456,6 @@ "description": "Amount of offset applied to the roofs of the support.", "unit": "mm", "type": "float", - "minimum_value": "0", "default_value": 0.0, "value": "extruderValue(support_roof_extruder_nr, 'support_interface_offset')", "maximum_value": "extruderValue(support_extruder_nr, 'support_offset')", @@ -4413,7 +4470,6 @@ "description": "Amount of offset applied to the floors of the support.", "unit": "mm", "type": "float", - "minimum_value": "0", "default_value": 0.0, "value": "extruderValue(support_bottom_extruder_nr, 'support_interface_offset')", "maximum_value": "extruderValue(support_extruder_nr, 'support_offset')", diff --git a/resources/quality/fabtotum/fabtotum_abs_fast.inst.cfg b/resources/quality/fabtotum/fabtotum_abs_fast.inst.cfg index f2628e16df..95e8b93b36 100644 --- a/resources/quality/fabtotum/fabtotum_abs_fast.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_abs_fast.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = fast weight = -1 -material = fabtotum_abs +material = generic_abs [values] adhesion_type = raft diff --git a/resources/quality/fabtotum/fabtotum_abs_high.inst.cfg b/resources/quality/fabtotum/fabtotum_abs_high.inst.cfg index d750eb98a3..baedf0ed2b 100644 --- a/resources/quality/fabtotum/fabtotum_abs_high.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_abs_high.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = high weight = 1 -material = fabtotum_abs +material = generic_abs [values] adhesion_type = raft diff --git a/resources/quality/fabtotum/fabtotum_abs_normal.inst.cfg b/resources/quality/fabtotum/fabtotum_abs_normal.inst.cfg index 7db9682c8a..58933486ee 100644 --- a/resources/quality/fabtotum/fabtotum_abs_normal.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_abs_normal.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = normal weight = 0 -material = fabtotum_abs +material = generic_abs [values] adhesion_type = raft diff --git a/resources/quality/fabtotum/fabtotum_nylon_fast.inst.cfg b/resources/quality/fabtotum/fabtotum_nylon_fast.inst.cfg index b6629d1fac..00f0737227 100644 --- a/resources/quality/fabtotum/fabtotum_nylon_fast.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_nylon_fast.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = fast weight = -1 -material = fabtotum_nylon +material = generic_nylon [values] adhesion_type = raft diff --git a/resources/quality/fabtotum/fabtotum_nylon_high.inst.cfg b/resources/quality/fabtotum/fabtotum_nylon_high.inst.cfg index f819f2bbd5..bd7f32c9ba 100644 --- a/resources/quality/fabtotum/fabtotum_nylon_high.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_nylon_high.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = high weight = 1 -material = fabtotum_nylon +material = generic_nylon [values] adhesion_type = raft diff --git a/resources/quality/fabtotum/fabtotum_nylon_normal.inst.cfg b/resources/quality/fabtotum/fabtotum_nylon_normal.inst.cfg index 8e0ed4f93c..6a450e7ffe 100644 --- a/resources/quality/fabtotum/fabtotum_nylon_normal.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_nylon_normal.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = normal weight = 0 -material = fabtotum_nylon +material = generic_nylon [values] adhesion_type = raft diff --git a/resources/quality/fabtotum/fabtotum_pla_fast.inst.cfg b/resources/quality/fabtotum/fabtotum_pla_fast.inst.cfg index dce262216a..afac0b0884 100644 --- a/resources/quality/fabtotum/fabtotum_pla_fast.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_pla_fast.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = fast weight = -1 -material = fabtotum_pla +material = generic_pla [values] adhesion_type = skirt diff --git a/resources/quality/fabtotum/fabtotum_pla_high.inst.cfg b/resources/quality/fabtotum/fabtotum_pla_high.inst.cfg index 2dce693235..89dc6d9b33 100644 --- a/resources/quality/fabtotum/fabtotum_pla_high.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_pla_high.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = high weight = 1 -material = fabtotum_pla +material = generic_pla [values] adhesion_type = skirt diff --git a/resources/quality/fabtotum/fabtotum_pla_normal.inst.cfg b/resources/quality/fabtotum/fabtotum_pla_normal.inst.cfg index 01f730c2e8..e5496a13d4 100644 --- a/resources/quality/fabtotum/fabtotum_pla_normal.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_pla_normal.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = normal weight = 0 -material = fabtotum_pla +material = generic_pla [values] adhesion_type = skirt diff --git a/resources/quality/fabtotum/fabtotum_tpu_fast.inst.cfg b/resources/quality/fabtotum/fabtotum_tpu_fast.inst.cfg index be8d2e3e3a..7917c92514 100644 --- a/resources/quality/fabtotum/fabtotum_tpu_fast.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_tpu_fast.inst.cfg @@ -6,7 +6,7 @@ name = Fast Quality [metadata] type = quality setting_version = 5 -material = fabtotum_tpu +material = generic_tpu quality_type = fast weight = -1 diff --git a/resources/quality/fabtotum/fabtotum_tpu_high.inst.cfg b/resources/quality/fabtotum/fabtotum_tpu_high.inst.cfg index 2dcaa15bd3..1c31967d79 100644 --- a/resources/quality/fabtotum/fabtotum_tpu_high.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_tpu_high.inst.cfg @@ -6,7 +6,7 @@ name = High Quality [metadata] type = quality setting_version = 5 -material = fabtotum_tpu +material = generic_tpu quality_type = high weight = 1 diff --git a/resources/quality/fabtotum/fabtotum_tpu_normal.inst.cfg b/resources/quality/fabtotum/fabtotum_tpu_normal.inst.cfg index e680038e26..0a3821f953 100644 --- a/resources/quality/fabtotum/fabtotum_tpu_normal.inst.cfg +++ b/resources/quality/fabtotum/fabtotum_tpu_normal.inst.cfg @@ -6,7 +6,7 @@ name = Normal Quality [metadata] type = quality setting_version = 5 -material = fabtotum_TPU +material = generic_tpu quality_type = normal weight = 0 diff --git a/resources/quality/zyyx/zyyx_agile_pro_flex_fast.inst.cfg b/resources/quality/zyyx/zyyx_agile_pro_flex_fast.inst.cfg index c7d7faf575..188bdd25e5 100644 --- a/resources/quality/zyyx/zyyx_agile_pro_flex_fast.inst.cfg +++ b/resources/quality/zyyx/zyyx_agile_pro_flex_fast.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = fast weight = 1 -material = zyyx_pro_flex +material = generic_tpu [values] layer_height = 0.3 diff --git a/resources/quality/zyyx/zyyx_agile_pro_flex_fine.inst.cfg b/resources/quality/zyyx/zyyx_agile_pro_flex_fine.inst.cfg index 1e0cc16df0..6654889c10 100644 --- a/resources/quality/zyyx/zyyx_agile_pro_flex_fine.inst.cfg +++ b/resources/quality/zyyx/zyyx_agile_pro_flex_fine.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = fine weight = 3 -material = zyyx_pro_flex +material = generic_tpu [values] layer_height = 0.12 diff --git a/resources/quality/zyyx/zyyx_agile_pro_flex_normal.inst.cfg b/resources/quality/zyyx/zyyx_agile_pro_flex_normal.inst.cfg index 8f7be7b481..f56355100c 100644 --- a/resources/quality/zyyx/zyyx_agile_pro_flex_normal.inst.cfg +++ b/resources/quality/zyyx/zyyx_agile_pro_flex_normal.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = normal weight = 2 -material = zyyx_pro_flex +material = generic_tpu [values] layer_height = 0.2 diff --git a/resources/quality/zyyx/zyyx_agile_pro_pla_fast.inst.cfg b/resources/quality/zyyx/zyyx_agile_pro_pla_fast.inst.cfg index 7ac7dc89b5..7ae4be06b0 100644 --- a/resources/quality/zyyx/zyyx_agile_pro_pla_fast.inst.cfg +++ b/resources/quality/zyyx/zyyx_agile_pro_pla_fast.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = fast weight = 1 -material = zyyx_pro_pla +material = generic_pla [values] layer_height = 0.3 diff --git a/resources/quality/zyyx/zyyx_agile_pro_pla_fine.inst.cfg b/resources/quality/zyyx/zyyx_agile_pro_pla_fine.inst.cfg index 98033908f6..64c7d4afc8 100644 --- a/resources/quality/zyyx/zyyx_agile_pro_pla_fine.inst.cfg +++ b/resources/quality/zyyx/zyyx_agile_pro_pla_fine.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = fine weight = 3 -material = zyyx_pro_pla +material = generic_pla [values] layer_height = 0.1 diff --git a/resources/quality/zyyx/zyyx_agile_pro_pla_normal.inst.cfg b/resources/quality/zyyx/zyyx_agile_pro_pla_normal.inst.cfg index b694bd9172..dbdd600ece 100644 --- a/resources/quality/zyyx/zyyx_agile_pro_pla_normal.inst.cfg +++ b/resources/quality/zyyx/zyyx_agile_pro_pla_normal.inst.cfg @@ -8,7 +8,7 @@ setting_version = 5 type = quality quality_type = normal weight = 2 -material = zyyx_pro_pla +material = generic_pla [values] layer_height = 0.2