From c2c96faf5fcbad942f8cf257e75c94a623ac5eaa Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 28 May 2020 17:13:44 +0200 Subject: [PATCH] Convert remaining doxygen to rst --- cmake/mod_bundled_packages_json.py | 22 +- cura/Operations/PlatformPhysicsOperation.py | 3 +- .../SetBuildPlateNumberOperation.py | 3 +- cura/Operations/SetParentOperation.py | 36 +- cura/PrinterOutput/FirmwareUpdater.py | 3 +- .../Models/ExtruderConfigurationModel.py | 9 +- .../Models/ExtruderOutputModel.py | 18 +- .../Models/PrinterConfigurationModel.py | 12 +- .../Models/PrinterOutputModel.py | 18 +- cura/PrinterOutput/NetworkMJPGImage.py | 3 +- .../NetworkedPrinterOutputDevice.py | 82 +- cura/PrinterOutput/Peripheral.py | 20 +- cura/PrinterOutput/PrinterOutputDevice.py | 51 +- cura/ReaderWriters/ProfileReader.py | 16 +- cura/ReaderWriters/ProfileWriter.py | 34 +- cura/Scene/BuildPlateDecorator.py | 3 +- cura/Scene/ConvexHullDecorator.py | 89 +- cura/Scene/ConvexHullNode.py | 10 +- cura/Scene/CuraSceneController.py | 3 +- cura/Scene/CuraSceneNode.py | 24 +- cura/Scene/ZOffsetDecorator.py | 3 +- cura/Settings/ContainerManager.py | 136 +- cura/Settings/CuraContainerRegistry.py | 1549 +++++++++-------- cura/Settings/CuraContainerStack.py | 284 +-- cura/Settings/CuraStackBuilder.py | 79 +- cura/Settings/Exceptions.py | 12 +- cura/Settings/ExtruderManager.py | 160 +- cura/Settings/ExtruderStack.py | 61 +- cura/Settings/GlobalStack.py | 91 +- cura/Settings/IntentManager.py | 88 +- cura/Settings/MachineManager.py | 194 ++- cura/Settings/MachineNameValidator.py | 23 +- cura/Settings/SetObjectExtruderOperation.py | 4 +- cura/Settings/SettingInheritanceManager.py | 14 +- cura/Settings/SettingOverrideDecorator.py | 65 +- cura/UI/MachineActionManager.py | 66 +- cura/UI/ObjectsModel.py | 3 +- cura/UI/PrintInformation.py | 24 +- cura/Utils/Decorators.py | 14 +- tests/Machines/TestMachineNode.py | 31 +- tests/Machines/TestQualityNode.py | 2 +- tests/Machines/TestVariantNode.py | 41 +- tests/Settings/MockContainer.py | 122 +- tests/Settings/TestCuraContainerRegistry.py | 6 +- tests/Settings/TestDefinitionContainer.py | 71 +- tests/Settings/TestExtruderStack.py | 52 +- tests/Settings/TestGlobalStack.py | 73 +- tests/Settings/TestProfiles.py | 6 +- tests/TestArrange.py | 87 +- 49 files changed, 2163 insertions(+), 1657 deletions(-) diff --git a/cmake/mod_bundled_packages_json.py b/cmake/mod_bundled_packages_json.py index 6423591f57..66038b30e2 100755 --- a/cmake/mod_bundled_packages_json.py +++ b/cmake/mod_bundled_packages_json.py @@ -11,11 +11,13 @@ import os import sys -## Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths. -# -# \param work_dir The directory to look for JSON files recursively. -# \return A list of JSON files in absolute paths that are found in the given directory. def find_json_files(work_dir: str) -> list: + """Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths. + + :param work_dir: The directory to look for JSON files recursively. + :return: A list of JSON files in absolute paths that are found in the given directory. + """ + json_file_list = [] for root, dir_names, file_names in os.walk(work_dir): for file_name in file_names: @@ -24,12 +26,14 @@ def find_json_files(work_dir: str) -> list: return json_file_list -## Removes the given entries from the given JSON file. The file will modified in-place. -# -# \param file_path The JSON file to modify. -# \param entries A list of strings as entries to remove. -# \return None def remove_entries_from_json_file(file_path: str, entries: list) -> None: + """Removes the given entries from the given JSON file. The file will modified in-place. + + :param file_path: The JSON file to modify. + :param entries: A list of strings as entries to remove. + :return: None + """ + try: with open(file_path, "r", encoding = "utf-8") as f: package_dict = json.load(f, object_hook = collections.OrderedDict) diff --git a/cura/Operations/PlatformPhysicsOperation.py b/cura/Operations/PlatformPhysicsOperation.py index 0d69320eec..e433b67a7b 100644 --- a/cura/Operations/PlatformPhysicsOperation.py +++ b/cura/Operations/PlatformPhysicsOperation.py @@ -6,8 +6,9 @@ from UM.Operations.GroupedOperation import GroupedOperation from UM.Scene.SceneNode import SceneNode -## A specialised operation designed specifically to modify the previous operation. class PlatformPhysicsOperation(Operation): + """A specialised operation designed specifically to modify the previous operation.""" + def __init__(self, node: SceneNode, translation: Vector) -> None: super().__init__() self._node = node diff --git a/cura/Operations/SetBuildPlateNumberOperation.py b/cura/Operations/SetBuildPlateNumberOperation.py index fd48cf47d9..8a5bdeb442 100644 --- a/cura/Operations/SetBuildPlateNumberOperation.py +++ b/cura/Operations/SetBuildPlateNumberOperation.py @@ -7,8 +7,9 @@ from UM.Operations.Operation import Operation from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -## Simple operation to set the buildplate number of a scenenode. class SetBuildPlateNumberOperation(Operation): + """Simple operation to set the buildplate number of a scenenode.""" + def __init__(self, node: SceneNode, build_plate_nr: int) -> None: super().__init__() self._node = node diff --git a/cura/Operations/SetParentOperation.py b/cura/Operations/SetParentOperation.py index 6d603c1d82..52bef4aabc 100644 --- a/cura/Operations/SetParentOperation.py +++ b/cura/Operations/SetParentOperation.py @@ -6,31 +6,37 @@ from UM.Scene.SceneNode import SceneNode from UM.Operations import Operation - -## An operation that parents a scene node to another scene node. class SetParentOperation(Operation.Operation): - ## Initialises this SetParentOperation. - # - # \param node The node which will be reparented. - # \param parent_node The node which will be the parent. + """An operation that parents a scene node to another scene node.""" + def __init__(self, node: SceneNode, parent_node: Optional[SceneNode]) -> None: + """Initialises this SetParentOperation. + + :param node: The node which will be reparented. + :param parent_node: The node which will be the parent. + """ + super().__init__() self._node = node self._parent = parent_node self._old_parent = node.getParent() # To restore the previous parent in case of an undo. - ## Undoes the set-parent operation, restoring the old parent. def undo(self) -> None: + """Undoes the set-parent operation, restoring the old parent.""" + self._set_parent(self._old_parent) - ## Re-applies the set-parent operation. def redo(self) -> None: + """Re-applies the set-parent operation.""" + self._set_parent(self._parent) - ## Sets the parent of the node while applying transformations to the world-transform of the node stays the same. - # - # \param new_parent The new parent. Note: this argument can be None, which would hide the node from the scene. def _set_parent(self, new_parent: Optional[SceneNode]) -> None: + """Sets the parent of the node while applying transformations to the world-transform of the node stays the same. + + :param new_parent: The new parent. Note: this argument can be None, which would hide the node from the scene. + """ + if new_parent: current_parent = self._node.getParent() if current_parent: @@ -56,8 +62,10 @@ class SetParentOperation(Operation.Operation): self._node.setParent(new_parent) - ## Returns a programmer-readable representation of this operation. - # - # \return A programmer-readable representation of this operation. def __repr__(self) -> str: + """Returns a programmer-readable representation of this operation. + + :return: A programmer-readable representation of this operation. + """ + return "SetParentOperation(node = {0}, parent_node={1})".format(self._node, self._parent) diff --git a/cura/PrinterOutput/FirmwareUpdater.py b/cura/PrinterOutput/FirmwareUpdater.py index 80269b97a3..2794bf5c65 100644 --- a/cura/PrinterOutput/FirmwareUpdater.py +++ b/cura/PrinterOutput/FirmwareUpdater.py @@ -44,8 +44,9 @@ class FirmwareUpdater(QObject): def _updateFirmware(self) -> None: raise NotImplementedError("_updateFirmware needs to be implemented") - ## Cleanup after a succesful update def _cleanupAfterUpdate(self) -> None: + """Cleanup after a succesful update""" + # Clean up for next attempt. self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True, name = "FirmwareUpdateThread") self._firmware_file = "" diff --git a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py index 4a1cf4916f..b80a652163 100644 --- a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py +++ b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py @@ -47,10 +47,13 @@ class ExtruderConfigurationModel(QObject): def hotendID(self) -> Optional[str]: return self._hotend_id - ## This method is intended to indicate whether the configuration is valid or not. - # The method checks if the mandatory fields are or not set - # At this moment is always valid since we allow to have empty material and variants. def isValid(self) -> bool: + """This method is intended to indicate whether the configuration is valid or not. + + The method checks if the mandatory fields are or not set + At this moment is always valid since we allow to have empty material and variants. + """ + return True def __str__(self) -> str: diff --git a/cura/PrinterOutput/Models/ExtruderOutputModel.py b/cura/PrinterOutput/Models/ExtruderOutputModel.py index 889e140312..9e74e9520c 100644 --- a/cura/PrinterOutput/Models/ExtruderOutputModel.py +++ b/cura/PrinterOutput/Models/ExtruderOutputModel.py @@ -54,8 +54,9 @@ class ExtruderOutputModel(QObject): def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]) -> None: self._extruder_configuration.setMaterial(material) - ## Update the hotend temperature. This only changes it locally. def updateHotendTemperature(self, temperature: float) -> None: + """Update the hotend temperature. This only changes it locally.""" + if self._hotend_temperature != temperature: self._hotend_temperature = temperature self.hotendTemperatureChanged.emit() @@ -65,9 +66,10 @@ class ExtruderOutputModel(QObject): self._target_hotend_temperature = temperature self.targetHotendTemperatureChanged.emit() - ## Set the target hotend temperature. This ensures that it's actually sent to the remote. @pyqtSlot(float) def setTargetHotendTemperature(self, temperature: float) -> None: + """Set the target hotend temperature. This ensures that it's actually sent to the remote.""" + self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature) self.updateTargetHotendTemperature(temperature) @@ -101,13 +103,15 @@ class ExtruderOutputModel(QObject): def isPreheating(self) -> bool: return self._is_preheating - ## Pre-heats the extruder before printer. - # - # \param temperature The temperature to heat the extruder to, in degrees - # Celsius. - # \param duration How long the bed should stay warm, in seconds. @pyqtSlot(float, float) def preheatHotend(self, temperature: float, duration: float) -> None: + """Pre-heats the extruder before printer. + + :param temperature: The temperature to heat the extruder to, in degrees + Celsius. + :param duration: How long the bed should stay warm, in seconds. + """ + self._printer._controller.preheatHotend(self, temperature, duration) @pyqtSlot() diff --git a/cura/PrinterOutput/Models/PrinterConfigurationModel.py b/cura/PrinterOutput/Models/PrinterConfigurationModel.py index 52c7b6f960..c5aa949ff3 100644 --- a/cura/PrinterOutput/Models/PrinterConfigurationModel.py +++ b/cura/PrinterOutput/Models/PrinterConfigurationModel.py @@ -48,9 +48,11 @@ class PrinterConfigurationModel(QObject): def buildplateConfiguration(self) -> str: return self._buildplate_configuration - ## This method is intended to indicate whether the configuration is valid or not. - # The method checks if the mandatory fields are or not set def isValid(self) -> bool: + """This method is intended to indicate whether the configuration is valid or not. + + The method checks if the mandatory fields are or not set + """ if not self._extruder_configurations: return False for configuration in self._extruder_configurations: @@ -97,9 +99,11 @@ class PrinterConfigurationModel(QObject): return True - ## The hash function is used to compare and create unique sets. The configuration is unique if the configuration - # of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same. def __hash__(self): + """The hash function is used to compare and create unique sets. The configuration is unique if the configuration + + of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same. + """ extruder_hash = hash(0) first_extruder = None for configuration in self._extruder_configurations: diff --git a/cura/PrinterOutput/Models/PrinterOutputModel.py b/cura/PrinterOutput/Models/PrinterOutputModel.py index 37135bf663..8b716a1958 100644 --- a/cura/PrinterOutput/Models/PrinterOutputModel.py +++ b/cura/PrinterOutput/Models/PrinterOutputModel.py @@ -163,13 +163,15 @@ class PrinterOutputModel(QObject): def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None: self._controller.moveHead(self, x, y, z, speed) - ## Pre-heats the heated bed of the printer. - # - # \param temperature The temperature to heat the bed to, in degrees - # Celsius. - # \param duration How long the bed should stay warm, in seconds. @pyqtSlot(float, float) def preheatBed(self, temperature: float, duration: float) -> None: + """Pre-heats the heated bed of the printer. + + :param temperature: The temperature to heat the bed to, in degrees + Celsius. + :param duration: How long the bed should stay warm, in seconds. + """ + self._controller.preheatBed(self, temperature, duration) @pyqtSlot() @@ -200,8 +202,9 @@ class PrinterOutputModel(QObject): self._unique_name = unique_name self.nameChanged.emit() - ## Update the bed temperature. This only changes it locally. def updateBedTemperature(self, temperature: float) -> None: + """Update the bed temperature. This only changes it locally.""" + if self._bed_temperature != temperature: self._bed_temperature = temperature self.bedTemperatureChanged.emit() @@ -211,9 +214,10 @@ class PrinterOutputModel(QObject): self._target_bed_temperature = temperature self.targetBedTemperatureChanged.emit() - ## Set the target bed temperature. This ensures that it's actually sent to the remote. @pyqtSlot(float) def setTargetBedTemperature(self, temperature: float) -> None: + """Set the target bed temperature. This ensures that it's actually sent to the remote.""" + self._controller.setTargetBedTemperature(self, temperature) self.updateTargetBedTemperature(temperature) diff --git a/cura/PrinterOutput/NetworkMJPGImage.py b/cura/PrinterOutput/NetworkMJPGImage.py index 42132a7880..0bfcfab764 100644 --- a/cura/PrinterOutput/NetworkMJPGImage.py +++ b/cura/PrinterOutput/NetworkMJPGImage.py @@ -32,8 +32,9 @@ class NetworkMJPGImage(QQuickPaintedItem): self.setAntialiasing(True) - ## Ensure that close gets called when object is destroyed def __del__(self) -> None: + """Ensure that close gets called when object is destroyed""" + self.stop() diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 60be5bc8f3..e57e461dde 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -84,8 +84,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def _compressGCode(self) -> Optional[bytes]: self._compressing_gcode = True - ## Mash the data into single string max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line. + """Mash the data into single string""" file_data_bytes_list = [] batched_lines = [] batched_lines_count = 0 @@ -145,9 +145,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request - ## This method was only available privately before, but it was actually called from SendMaterialJob.py. - # We now have a public equivalent as well. We did not remove the private one as plugins might be using that. def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: + """This method was only available privately before, but it was actually called from SendMaterialJob.py. + + We now have a public equivalent as well. We did not remove the private one as plugins might be using that. + """ return self._createFormPart(content_header, data, content_type) def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: @@ -163,8 +165,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): part.setBody(data) return part - ## Convenience function to get the username, either from the cloud or from the OS. def _getUserName(self) -> str: + """Convenience function to get the username, either from the cloud or from the OS.""" + # check first if we are logged in with the Ultimaker Account account = CuraApplication.getInstance().getCuraAPI().account # type: Account if account and account.isLoggedIn: @@ -187,15 +190,17 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._createNetworkManager() assert (self._manager is not None) - ## Sends a put request to the given path. - # \param url: The path after the API prefix. - # \param data: The data to be sent in the body - # \param content_type: The content type of the body data. - # \param on_finished: The function to call when the response is received. - # \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. def put(self, url: str, data: Union[str, bytes], content_type: Optional[str] = "application/json", on_finished: Optional[Callable[[QNetworkReply], None]] = None, on_progress: Optional[Callable[[int, int], None]] = None) -> None: + """Sends a put request to the given path. + + :param url: The path after the API prefix. + :param data: The data to be sent in the body + :param content_type: The content type of the body data. + :param on_finished: The function to call when the response is received. + :param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. + """ self._validateManager() request = self._createEmptyRequest(url, content_type = content_type) @@ -212,10 +217,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if on_progress is not None: reply.uploadProgress.connect(on_progress) - ## Sends a delete request to the given path. - # \param url: The path after the API prefix. - # \param on_finished: The function to be call when the response is received. def delete(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: + """Sends a delete request to the given path. + + :param url: The path after the API prefix. + :param on_finished: The function to be call when the response is received. + """ self._validateManager() request = self._createEmptyRequest(url) @@ -228,10 +235,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): reply = self._manager.deleteResource(request) self._registerOnFinishedCallback(reply, on_finished) - ## Sends a get request to the given path. - # \param url: The path after the API prefix. - # \param on_finished: The function to be call when the response is received. def get(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: + """Sends a get request to the given path. + + :param url: The path after the API prefix. + :param on_finished: The function to be call when the response is received. + """ self._validateManager() request = self._createEmptyRequest(url) @@ -244,14 +253,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): reply = self._manager.get(request) self._registerOnFinishedCallback(reply, on_finished) - ## Sends a post request to the given path. - # \param url: The path after the API prefix. - # \param data: The data to be sent in the body - # \param on_finished: The function to call when the response is received. - # \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. def post(self, url: str, data: Union[str, bytes], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Optional[Callable[[int, int], None]] = None) -> None: + + """Sends a post request to the given path. + + :param url: The path after the API prefix. + :param data: The data to be sent in the body + :param on_finished: The function to call when the response is received. + :param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. + """ + self._validateManager() request = self._createEmptyRequest(url) @@ -318,10 +331,13 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if on_finished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished - ## This method checks if the name of the group stored in the definition container is correct. - # After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group - # then all the container stacks are updated, both the current and the hidden ones. def _checkCorrectGroupName(self, device_id: str, group_name: str) -> None: + """This method checks if the name of the group stored in the definition container is correct. + + After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group + then all the container stacks are updated, both the current and the hidden ones. + """ + global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() active_machine_network_name = CuraApplication.getInstance().getMachineManager().activeMachineNetworkKey() if global_container_stack and device_id == active_machine_network_name: @@ -366,32 +382,38 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def getProperties(self): return self._properties - ## Get the unique key of this machine - # \return key String containing the key of the machine. @pyqtProperty(str, constant = True) def key(self) -> str: + """Get the unique key of this machine + + :return: key String containing the key of the machine. + """ return self._id - ## The IP address of the printer. @pyqtProperty(str, constant = True) def address(self) -> str: + """The IP address of the printer.""" + return self._properties.get(b"address", b"").decode("utf-8") - ## Name of the printer (as returned from the ZeroConf properties) @pyqtProperty(str, constant = True) def name(self) -> str: + """Name of the printer (as returned from the ZeroConf properties)""" + return self._properties.get(b"name", b"").decode("utf-8") - ## Firmware version (as returned from the ZeroConf properties) @pyqtProperty(str, constant = True) def firmwareVersion(self) -> str: + """Firmware version (as returned from the ZeroConf properties)""" + return self._properties.get(b"firmware_version", b"").decode("utf-8") @pyqtProperty(str, constant = True) def printerType(self) -> str: return self._properties.get(b"printer_type", b"Unknown").decode("utf-8") - ## IP adress of this printer @pyqtProperty(str, constant = True) def ipAddress(self) -> str: + """IP adress of this printer""" + return self._address diff --git a/cura/PrinterOutput/Peripheral.py b/cura/PrinterOutput/Peripheral.py index 2693b82c36..e9a283ba2b 100644 --- a/cura/PrinterOutput/Peripheral.py +++ b/cura/PrinterOutput/Peripheral.py @@ -2,15 +2,19 @@ # Cura is released under the terms of the LGPLv3 or higher. -## Data class that represents a peripheral for a printer. -# -# Output device plug-ins may specify that the printer has a certain set of -# peripherals. This set is then possibly shown in the interface of the monitor -# stage. class Peripheral: - ## Constructs the peripheral. - # \param type A unique ID for the type of peripheral. - # \param name A human-readable name for the peripheral. + """Data class that represents a peripheral for a printer. + + Output device plug-ins may specify that the printer has a certain set of + peripherals. This set is then possibly shown in the interface of the monitor + stage. + """ + def __init__(self, peripheral_type: str, name: str) -> None: + """Constructs the peripheral. + + :param peripheral_type: A unique ID for the type of peripheral. + :param name: A human-readable name for the peripheral. + """ self.type = peripheral_type self.name = name diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index 0e0ad488b1..9b10e6abec 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -24,8 +24,9 @@ if MYPY: i18n_catalog = i18nCatalog("cura") -## The current processing state of the backend. class ConnectionState(IntEnum): + """The current processing state of the backend.""" + Closed = 0 Connecting = 1 Connected = 2 @@ -40,17 +41,19 @@ class ConnectionType(IntEnum): CloudConnection = 3 -## Printer output device adds extra interface options on top of output device. -# -# The assumption is made the printer is a FDM printer. -# -# Note that a number of settings are marked as "final". This is because decorators -# are not inherited by children. To fix this we use the private counter part of those -# functions to actually have the implementation. -# -# For all other uses it should be used in the same way as a "regular" OutputDevice. @signalemitter class PrinterOutputDevice(QObject, OutputDevice): + """Printer output device adds extra interface options on top of output device. + + The assumption is made the printer is a FDM printer. + + Note that a number of settings are marked as "final". This is because decorators + are not inherited by children. To fix this we use the private counter part of those + functions to actually have the implementation. + + For all other uses it should be used in the same way as a "regular" OutputDevice. + """ + printersChanged = pyqtSignal() connectionStateChanged = pyqtSignal(str) @@ -184,26 +187,30 @@ class PrinterOutputDevice(QObject, OutputDevice): if self._monitor_item is None: self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self}) - ## Attempt to establish connection def connect(self) -> None: + """Attempt to establish connection""" + self.setConnectionState(ConnectionState.Connecting) self._update_timer.start() - ## Attempt to close the connection def close(self) -> None: + """Attempt to close the connection""" + self._update_timer.stop() self.setConnectionState(ConnectionState.Closed) - ## Ensure that close gets called when object is destroyed def __del__(self) -> None: + """Ensure that close gets called when object is destroyed""" + self.close() @pyqtProperty(bool, notify = acceptsCommandsChanged) def acceptsCommands(self) -> bool: return self._accepts_commands - ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands def _setAcceptsCommands(self, accepts_commands: bool) -> None: + """Set a flag to signal the UI that the printer is not (yet) ready to receive commands""" + if self._accepts_commands != accepts_commands: self._accepts_commands = accepts_commands @@ -241,16 +248,20 @@ class PrinterOutputDevice(QObject, OutputDevice): # At this point there may be non-updated configurations self._updateUniqueConfigurations() - ## Set the device firmware name - # - # \param name The name of the firmware. def _setFirmwareName(self, name: str) -> None: + """Set the device firmware name + + :param name: The name of the firmware. + """ + self._firmware_name = name - ## Get the name of device firmware - # - # This name can be used to define device type def getFirmwareName(self) -> Optional[str]: + """Get the name of device firmware + + This name can be used to define device type + """ + return self._firmware_name def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]: diff --git a/cura/ReaderWriters/ProfileReader.py b/cura/ReaderWriters/ProfileReader.py index 460fce823e..3d80411713 100644 --- a/cura/ReaderWriters/ProfileReader.py +++ b/cura/ReaderWriters/ProfileReader.py @@ -10,15 +10,19 @@ class NoProfileException(Exception): pass -## A type of plug-ins that reads profiles from a file. -# -# The profile is then stored as instance container of the type user profile. class ProfileReader(PluginObject): + """A type of plug-ins that reads profiles from a file. + + The profile is then stored as instance container of the type user profile. + """ + def __init__(self): super().__init__() - ## Read profile data from a file and return a filled profile. - # - # \return \type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles. def read(self, file_name): + """Read profile data from a file and return a filled profile. + + :return: :type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles. + """ + raise NotImplementedError("Profile reader plug-in was not correctly implemented. The read function was not implemented.") diff --git a/cura/ReaderWriters/ProfileWriter.py b/cura/ReaderWriters/ProfileWriter.py index 5f81dc28c3..0dd787335e 100644 --- a/cura/ReaderWriters/ProfileWriter.py +++ b/cura/ReaderWriters/ProfileWriter.py @@ -3,23 +3,29 @@ from UM.PluginObject import PluginObject -## Base class for profile writer plugins. -# -# This class defines a write() function to write profiles to files with. + class ProfileWriter(PluginObject): - ## Initialises the profile writer. - # - # This currently doesn't do anything since the writer is basically static. + """Base class for profile writer plugins. + + This class defines a write() function to write profiles to files with. + """ + def __init__(self): + """Initialises the profile writer. + + This currently doesn't do anything since the writer is basically static. + """ + super().__init__() - ## Writes a profile to the specified file path. - # - # The profile writer may write its own file format to the specified file. - # - # \param path \type{string} The file to output to. - # \param profiles \type{Profile} or \type{List} The profile(s) to write to the file. - # \return \code True \endcode if the writing was successful, or \code - # False \endcode if it wasn't. def write(self, path, profiles): + """Writes a profile to the specified file path. + + The profile writer may write its own file format to the specified file. + + :param path: :type{string} The file to output to. + :param profiles: :type{Profile} or :type{List} The profile(s) to write to the file. + :return: True if the writing was successful, or False if it wasn't. + """ + raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.") diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index cff9f88f62..9dd9d3dc24 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -2,8 +2,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from cura.Scene.CuraSceneNode import CuraSceneNode -## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator. class BuildPlateDecorator(SceneNodeDecorator): + """Make a SceneNode build plate aware CuraSceneNode objects all have this decorator.""" + def __init__(self, build_plate_number: int = -1) -> None: super().__init__() self._build_plate_number = build_plate_number diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index b5f5fb4540..3dee409761 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -23,9 +23,12 @@ if TYPE_CHECKING: from UM.Math.Matrix import Matrix -## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. -# If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. class ConvexHullDecorator(SceneNodeDecorator): + """The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. + + If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. + """ + def __init__(self) -> None: super().__init__() @@ -74,13 +77,16 @@ class ConvexHullDecorator(SceneNodeDecorator): self._onChanged() - ## Force that a new (empty) object is created upon copy. def __deepcopy__(self, memo): + """Force that a new (empty) object is created upon copy.""" + return ConvexHullDecorator() - ## The polygon representing the 2D adhesion area. - # If no adhesion is used, the regular convex hull is returned def getAdhesionArea(self) -> Optional[Polygon]: + """The polygon representing the 2D adhesion area. + + If no adhesion is used, the regular convex hull is returned + """ if self._node is None: return None @@ -90,9 +96,11 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._add2DAdhesionMargin(hull) - ## Get the unmodified 2D projected convex hull of the node (if any) - # In case of one-at-a-time, this includes adhesion and head+fans clearance def getConvexHull(self) -> Optional[Polygon]: + """Get the unmodified 2D projected convex hull of the node (if any) + + In case of one-at-a-time, this includes adhesion and head+fans clearance + """ if self._node is None: return None if self._node.callDecoration("isNonPrintingMesh"): @@ -108,9 +116,11 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._compute2DConvexHull() - ## For one at the time this is the convex hull of the node with the full head size - # In case of printing all at once this is None. def getConvexHullHeadFull(self) -> Optional[Polygon]: + """For one at the time this is the convex hull of the node with the full head size + + In case of printing all at once this is None. + """ if self._node is None: return None @@ -126,10 +136,12 @@ class ConvexHullDecorator(SceneNodeDecorator): return False return bool(parent.callDecoration("isGroup")) - ## Get convex hull of the object + head size - # In case of printing all at once this is None. - # For one at the time this is area with intersection of mirrored head def getConvexHullHead(self) -> Optional[Polygon]: + """Get convex hull of the object + head size + + In case of printing all at once this is None. + For one at the time this is area with intersection of mirrored head + """ if self._node is None: return None if self._node.callDecoration("isNonPrintingMesh"): @@ -142,10 +154,12 @@ class ConvexHullDecorator(SceneNodeDecorator): return head_with_fans_with_adhesion_margin return None - ## Get convex hull of the node - # In case of printing all at once this None?? - # For one at the time this is the area without the head. def getConvexHullBoundary(self) -> Optional[Polygon]: + """Get convex hull of the node + + In case of printing all at once this None?? + For one at the time this is the area without the head. + """ if self._node is None: return None @@ -157,10 +171,12 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._compute2DConvexHull() return None - ## Get the buildplate polygon where will be printed - # In case of printing all at once this is the same as convex hull (no individual adhesion) - # For one at the time this includes the adhesion area def getPrintingArea(self) -> Optional[Polygon]: + """Get the buildplate polygon where will be printed + + In case of printing all at once this is the same as convex hull (no individual adhesion) + For one at the time this includes the adhesion area + """ if self._isSingularOneAtATimeNode(): # In one-at-a-time mode, every printed object gets it's own adhesion printing_area = self.getAdhesionArea() @@ -168,8 +184,9 @@ class ConvexHullDecorator(SceneNodeDecorator): printing_area = self.getConvexHull() return printing_area - ## The same as recomputeConvexHull, but using a timer if it was set. def recomputeConvexHullDelayed(self) -> None: + """The same as recomputeConvexHull, but using a timer if it was set.""" + if self._recompute_convex_hull_timer is not None: self._recompute_convex_hull_timer.start() else: @@ -325,9 +342,11 @@ class ConvexHullDecorator(SceneNodeDecorator): return convex_hull.getMinkowskiHull(head_and_fans) return None - ## Compensate given 2D polygon with adhesion margin - # \return 2D polygon with added margin def _add2DAdhesionMargin(self, poly: Polygon) -> Polygon: + """Compensate given 2D polygon with adhesion margin + + :return: 2D polygon with added margin + """ if not self._global_stack: return Polygon() # Compensate for raft/skirt/brim @@ -358,12 +377,14 @@ class ConvexHullDecorator(SceneNodeDecorator): poly = poly.getMinkowskiHull(extra_margin_polygon) return poly - ## Offset the convex hull with settings that influence the collision area. - # - # \param convex_hull Polygon of the original convex hull. - # \return New Polygon instance that is offset with everything that - # influences the collision area. def _offsetHull(self, convex_hull: Polygon) -> Polygon: + """Offset the convex hull with settings that influence the collision area. + + :param convex_hull: Polygon of the original convex hull. + :return: New Polygon instance that is offset with everything that + influences the collision area. + """ + horizontal_expansion = max( self._getSettingProperty("xy_offset", "value"), self._getSettingProperty("xy_offset_layer_0", "value") @@ -409,8 +430,9 @@ class ConvexHullDecorator(SceneNodeDecorator): self._onChanged() - ## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property). def _getSettingProperty(self, setting_key: str, prop: str = "value") -> Any: + """Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).""" + if self._global_stack is None or self._node is None: return None per_mesh_stack = self._node.callDecoration("getStack") @@ -430,16 +452,18 @@ class ConvexHullDecorator(SceneNodeDecorator): # Limit_to_extruder is set. The global stack handles this then return self._global_stack.getProperty(setting_key, prop) - ## Returns True if node is a descendant or the same as the root node. def __isDescendant(self, root: "SceneNode", node: Optional["SceneNode"]) -> bool: + """Returns True if node is a descendant or the same as the root node.""" + if node is None: return False if root is node: return True return self.__isDescendant(root, node.getParent()) - ## True if print_sequence is one_at_a_time and _node is not part of a group def _isSingularOneAtATimeNode(self) -> bool: + """True if print_sequence is one_at_a_time and _node is not part of a group""" + if self._node is None: return False return self._global_stack is not None \ @@ -450,7 +474,8 @@ class ConvexHullDecorator(SceneNodeDecorator): "adhesion_type", "raft_margin", "print_sequence", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"] - ## Settings that change the convex hull. - # - # If these settings change, the convex hull should be recalculated. _influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh"} + """Settings that change the convex hull. + + If these settings change, the convex hull should be recalculated. + """ diff --git a/cura/Scene/ConvexHullNode.py b/cura/Scene/ConvexHullNode.py index da2713a522..cd0951cba6 100644 --- a/cura/Scene/ConvexHullNode.py +++ b/cura/Scene/ConvexHullNode.py @@ -18,11 +18,13 @@ if TYPE_CHECKING: class ConvexHullNode(SceneNode): shader = None # To prevent the shader from being re-built over and over again, only load it once. - ## Convex hull node is a special type of scene node that is used to display an area, to indicate the - # location an object uses on the buildplate. This area (or area's in case of one at a time printing) is - # then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded - # to represent the raft as well. def __init__(self, node: SceneNode, hull: Optional[Polygon], thickness: float, parent: Optional[SceneNode] = None) -> None: + """Convex hull node is a special type of scene node that is used to display an area, to indicate the + + location an object uses on the buildplate. This area (or area's in case of one at a time printing) is + then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded + to represent the raft as well. + """ super().__init__(parent) self.setCalculateBoundingBox(False) diff --git a/cura/Scene/CuraSceneController.py b/cura/Scene/CuraSceneController.py index 36d9e68c8f..2fd05db87a 100644 --- a/cura/Scene/CuraSceneController.py +++ b/cura/Scene/CuraSceneController.py @@ -72,9 +72,10 @@ class CuraSceneController(QObject): max_build_plate = max(build_plate_number, max_build_plate) return max_build_plate - ## Either select or deselect an item @pyqtSlot(int) def changeSelection(self, index): + """Either select or deselect an item""" + modifiers = QApplication.keyboardModifiers() ctrl_is_active = modifiers & Qt.ControlModifier shift_is_active = modifiers & Qt.ShiftModifier diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index eb609def5a..b9f2279414 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -15,9 +15,11 @@ from cura.Settings.ExtruderStack import ExtruderStack # For typing. from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings. -## Scene nodes that are models are only seen when selecting the corresponding build plate -# Note that many other nodes can just be UM SceneNode objects. class CuraSceneNode(SceneNode): + """Scene nodes that are models are only seen when selecting the corresponding build plate + + Note that many other nodes can just be UM SceneNode objects. + """ def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None: super().__init__(parent = parent, visible = visible, name = name) if not no_setting_override: @@ -36,9 +38,11 @@ class CuraSceneNode(SceneNode): def isSelectable(self) -> bool: return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate - ## Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned - # TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded def getPrintingExtruder(self) -> Optional[ExtruderStack]: + """Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned + + TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded + """ global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack is None: return None @@ -69,8 +73,9 @@ class CuraSceneNode(SceneNode): # This point should never be reached return None - ## Return the color of the material used to print this model def getDiffuseColor(self) -> List[float]: + """Return the color of the material used to print this model""" + printing_extruder = self.getPrintingExtruder() material_color = "#808080" # Fallback color @@ -86,8 +91,9 @@ class CuraSceneNode(SceneNode): 1.0 ] - ## Return if any area collides with the convex hull of this scene node def collidesWithAreas(self, areas: List[Polygon]) -> bool: + """Return if any area collides with the convex hull of this scene node""" + convex_hull = self.callDecoration("getPrintingArea") if convex_hull: if not convex_hull.isValid(): @@ -101,8 +107,9 @@ class CuraSceneNode(SceneNode): return True return False - ## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box def _calculateAABB(self) -> None: + """Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box""" + self._aabb = None if self._mesh_data: self._aabb = self._mesh_data.getExtents(self.getWorldTransformation()) @@ -122,8 +129,9 @@ class CuraSceneNode(SceneNode): else: self._aabb = self._aabb + child.getBoundingBox() - ## Taken from SceneNode, but replaced SceneNode with CuraSceneNode def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode": + """Taken from SceneNode, but replaced SceneNode with CuraSceneNode""" + copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later copy.setTransformation(self.getLocalTransformation()) copy.setMeshData(self._mesh_data) diff --git a/cura/Scene/ZOffsetDecorator.py b/cura/Scene/ZOffsetDecorator.py index b35b17a412..1f1f5a9b1f 100644 --- a/cura/Scene/ZOffsetDecorator.py +++ b/cura/Scene/ZOffsetDecorator.py @@ -1,8 +1,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator -## A decorator that stores the amount an object has been moved below the platform. class ZOffsetDecorator(SceneNodeDecorator): + """A decorator that stores the amount an object has been moved below the platform.""" + def __init__(self) -> None: super().__init__() self._z_offset = 0. diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 4d972ba87e..29be16dcce 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -33,12 +33,14 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## Manager class that contains common actions to deal with containers in Cura. -# -# This is primarily intended as a class to be able to perform certain actions -# from within QML. We want to be able to trigger things like removing a container -# when a certain action happens. This can be done through this class. class ContainerManager(QObject): + """Manager class that contains common actions to deal with containers in Cura. + + This is primarily intended as a class to be able to perform certain actions + from within QML. We want to be able to trigger things like removing a container + when a certain action happens. This can be done through this class. + """ + def __init__(self, application: "CuraApplication") -> None: if ContainerManager.__instance is not None: @@ -67,21 +69,23 @@ class ContainerManager(QObject): return "" return str(result) - ## Set a metadata entry of the specified container. - # - # This will set the specified entry of the container's metadata to the specified - # value. Note that entries containing dictionaries can have their entries changed - # by using "/" as a separator. For example, to change an entry "foo" in a - # dictionary entry "bar", you can specify "bar/foo" as entry name. - # - # \param container_node \type{ContainerNode} - # \param entry_name \type{str} The name of the metadata entry to change. - # \param entry_value The new value of the entry. - # - # TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this. - # Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want? @pyqtSlot("QVariant", str, str) def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool: + """Set a metadata entry of the specified container. + + This will set the specified entry of the container's metadata to the specified + value. Note that entries containing dictionaries can have their entries changed + by using "/" as a separator. For example, to change an entry "foo" in a + dictionary entry "bar", you can specify "bar/foo" as entry name. + + :param container_node: :type{ContainerNode} + :param entry_name: :type{str} The name of the metadata entry to change. + :param entry_value: The new value of the entry. + + TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this. + Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want? + """ + if container_node.container is None: Logger.log("w", "Container node {0} doesn't have a container.".format(container_node.container_id)) return False @@ -124,18 +128,20 @@ class ContainerManager(QObject): def makeUniqueName(self, original_name: str) -> str: return cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().uniqueName(original_name) - ## Get a list of string that can be used as name filters for a Qt File Dialog - # - # This will go through the list of available container types and generate a list of strings - # out of that. The strings are formatted as "description (*.extension)" and can be directly - # passed to a nameFilters property of a Qt File Dialog. - # - # \param type_name Which types of containers to list. These types correspond to the "type" - # key of the plugin metadata. - # - # \return A string list with name filters. @pyqtSlot(str, result = "QStringList") def getContainerNameFilters(self, type_name: str) -> List[str]: + """Get a list of string that can be used as name filters for a Qt File Dialog + + This will go through the list of available container types and generate a list of strings + out of that. The strings are formatted as "description (*.extension)" and can be directly + passed to a nameFilters property of a Qt File Dialog. + + :param type_name: Which types of containers to list. These types correspond to the "type" + key of the plugin metadata. + + :return: A string list with name filters. + """ + if not self._container_name_filters: self._updateContainerNameFilters() @@ -147,17 +153,18 @@ class ContainerManager(QObject): filters.append("All Files (*)") return filters - ## Export a container to a file - # - # \param container_id The ID of the container to export - # \param file_type The type of file to save as. Should be in the form of "description (*.extension, *.ext)" - # \param file_url_or_string The URL where to save the file. - # - # \return A dictionary containing a key "status" with a status code and a key "message" with a message - # explaining the status. - # The status code can be one of "error", "cancelled", "success" @pyqtSlot(str, str, QUrl, result = "QVariantMap") def exportContainer(self, container_id: str, file_type: str, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: + """Export a container to a file + + :param container_id: The ID of the container to export + :param file_type: The type of file to save as. Should be in the form of "description (*.extension, *.ext)" + :param file_url_or_string: The URL where to save the file. + + :return: A dictionary containing a key "status" with a status code and a key "message" with a message + explaining the status. The status code can be one of "error", "cancelled", "success" + """ + if not container_id or not file_type or not file_url_or_string: return {"status": "error", "message": "Invalid arguments"} @@ -214,14 +221,16 @@ class ContainerManager(QObject): return {"status": "success", "message": "Successfully exported container", "path": file_url} - ## Imports a profile from a file - # - # \param file_url A URL that points to the file to import. - # - # \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key - # containing a message for the user @pyqtSlot(QUrl, result = "QVariantMap") def importMaterialContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: + """Imports a profile from a file + + :param file_url: A URL that points to the file to import. + + :return: :type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key + containing a message for the user + """ + if not file_url_or_string: return {"status": "error", "message": "Invalid path"} @@ -266,14 +275,16 @@ class ContainerManager(QObject): return {"status": "success", "message": "Successfully imported container {0}".format(container.getName())} - ## Update the current active quality changes container with the settings from the user container. - # - # This will go through the active global stack and all active extruder stacks and merge the changes from the user - # container into the quality_changes container. After that, the user container is cleared. - # - # \return \type{bool} True if successful, False if not. @pyqtSlot(result = bool) def updateQualityChanges(self) -> bool: + """Update the current active quality changes container with the settings from the user container. + + This will go through the active global stack and all active extruder stacks and merge the changes from the user + container into the quality_changes container. After that, the user container is cleared. + + :return: :type{bool} True if successful, False if not. + """ + application = cura.CuraApplication.CuraApplication.getInstance() global_stack = application.getMachineManager().activeMachine if not global_stack: @@ -313,9 +324,10 @@ class ContainerManager(QObject): return True - ## Clear the top-most (user) containers of the active stacks. @pyqtSlot() def clearUserContainers(self) -> None: + """Clear the top-most (user) containers of the active stacks.""" + machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager() machine_manager.blurSettings.emit() @@ -335,25 +347,28 @@ class ContainerManager(QObject): for container in send_emits_containers: container.sendPostponedEmits() - ## Get a list of materials that have the same GUID as the reference material - # - # \param material_node The node representing the material for which to get - # the same GUID. - # \param exclude_self Whether to include the name of the material you - # provided. - # \return A list of names of materials with the same GUID. @pyqtSlot("QVariant", bool, result = "QStringList") def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]: + """Get a list of materials that have the same GUID as the reference material + + :param material_node: The node representing the material for which to get + the same GUID. + :param exclude_self: Whether to include the name of the material you provided. + :return: A list of names of materials with the same GUID. + """ + same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(GUID = material_node.guid) if exclude_self: return list({meta["name"] for meta in same_guid if meta["base_file"] != material_node.base_file}) else: return list({meta["name"] for meta in same_guid}) - ## Unlink a material from all other materials by creating a new GUID - # \param material_id \type{str} the id of the material to create a new GUID for. @pyqtSlot("QVariant") def unlinkMaterial(self, material_node: "MaterialNode") -> None: + """Unlink a material from all other materials by creating a new GUID + + :param material_id: :type{str} the id of the material to create a new GUID for. + """ # Get the material group if material_node.container is None: # Failed to lazy-load this container. return @@ -428,9 +443,10 @@ class ContainerManager(QObject): name_filter = "{0} ({1})".format(mime_type.comment, suffix_list) 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) -> Dict[str, str]: + """Import single profile, file_url does not have to end with curaprofile""" + if not file_url.isValid(): return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)} path = file_url.toLocalFile() diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 0ef09a1fac..1ada51af59 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -1,765 +1,784 @@ -# Copyright (c) 2019 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import os -import re -import configparser - -from typing import Any, cast, Dict, Optional, List, Union -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 -from UM.Settings.SettingInstance import SettingInstance -from UM.Logger import Logger -from UM.Message import Message -from UM.Platform import Platform -from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with. -from UM.Resources import Resources -from UM.Util import parseBool -from cura.ReaderWriters.ProfileWriter import ProfileWriter - -from . import ExtruderStack -from . import GlobalStack - -import cura.CuraApplication -from cura.Settings.cura_empty_instance_containers import empty_quality_container -from cura.Machines.ContainerTree import ContainerTree -from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader - -from UM.i18n import i18nCatalog -catalog = i18nCatalog("cura") - - -class CuraContainerRegistry(ContainerRegistry): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack - # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack - # is added, we check to see if an extruder stack needs to be added. - self.containerAdded.connect(self._onContainerAdded) - - ## Overridden from ContainerRegistry - # - # Adds a container to the registry. - # - # This will also try to convert a ContainerStack to either Extruder or - # Global stack based on metadata information. - @override(ContainerRegistry) - def addContainer(self, container: ContainerInterface) -> None: - # Note: Intentional check with type() because we want to ignore subclasses - if type(container) == ContainerStack: - container = self._convertContainerStack(cast(ContainerStack, container)) - - if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()): - # Check against setting version of the definition. - required_setting_version = cura.CuraApplication.CuraApplication.SettingVersion - actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0)) - if required_setting_version != actual_setting_version: - Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version)) - return # Don't add. - - super().addContainer(container) - - ## Create a name that is not empty and unique - # \param container_type \type{string} Type of the container (machine, quality, ...) - # \param current_name \type{} Current name of the container, which may be an acceptable option - # \param new_name \type{string} Base name, which may not be unique - # \param fallback_name \type{string} Name to use when (stripped) new_name is empty - # \return \type{string} Name that is unique for the specified type and name/id - def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str: - new_name = new_name.strip() - num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name) - if num_check: - new_name = num_check.group(1) - if new_name == "": - new_name = fallback_name - - unique_name = new_name - i = 1 - # In case we are renaming, the current name of the container is also a valid end-result - while self._containerExists(container_type, unique_name) and unique_name != current_name: - i += 1 - unique_name = "%s #%d" % (new_name, i) - - return unique_name - - ## Check if a container with of a certain type and a certain name or id exists - # Both the id and the name are checked, because they may not be the same and it is better if they are both unique - # \param container_type \type{string} Type of the container (machine, quality, ...) - # \param container_name \type{string} Name to check - def _containerExists(self, container_type: str, container_name: str): - container_class = ContainerStack if container_type == "machine" else InstanceContainer - - return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \ - self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type) - - ## Exports an profile to a file - # - # \param container_list \type{list} the containers to export. This is not - # necessarily in any order! - # \param file_name \type{str} the full path and filename to export to. - # \param file_type \type{str} the file type with the format " (*.)" - # \return True if the export succeeded, false otherwise. - def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool: - # Parse the fileType to deduce what plugin can save the file format. - # fileType has the format " (*.)" - split = file_type.rfind(" (*.") # Find where the description ends and the extension starts. - if split < 0: # Not found. Invalid format. - Logger.log("e", "Invalid file format identifier %s", file_type) - return False - description = file_type[:split] - extension = file_type[split + 4:-1] # Leave out the " (*." and ")". - if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any. - file_name += "." + extension - - # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself. - if not Platform.isWindows(): - if os.path.exists(file_name): - result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), - catalog.i18nc("@label Don't translate the XML tag !", "The file {0} already exists. Are you sure you want to overwrite it?").format(file_name)) - if result == QMessageBox.No: - return False - - profile_writer = self._findProfileWriter(extension, description) - try: - if profile_writer is None: - raise Exception("Unable to find a profile writer") - success = profile_writer.write(file_name, container_list) - except Exception as e: - Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e)) - m = Message(catalog.i18nc("@info:status Don't translate the XML tags or !", "Failed to export profile to {0}: {1}", file_name, str(e)), - lifetime = 0, - title = catalog.i18nc("@info:title", "Error")) - m.show() - return False - if not success: - Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name) - m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Failed to export profile to {0}: Writer plugin reported failure.", file_name), - lifetime = 0, - title = catalog.i18nc("@info:title", "Error")) - m.show() - return False - m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Exported profile to {0}", file_name), - title = catalog.i18nc("@info:title", "Export succeeded")) - m.show() - return True - - ## Gets the plugin object matching the criteria - # \param extension - # \param description - # \return The plugin object matching the given extension and description. - def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]: - plugin_registry = PluginRegistry.getInstance() - for plugin_id, meta_data in self._getIOPlugins("profile_writer"): - for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write. - supported_extension = supported_type.get("extension", None) - if supported_extension == extension: # This plugin supports a file type with the same extension. - supported_description = supported_type.get("description", None) - if supported_description == description: # The description is also identical. Assume it's the same file type. - return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id)) - return None - - ## Imports a profile from a file - # - # \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 !", "Failed to import profile from {0}: {1}", file_name, "Invalid path")} - - global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() - if not global_stack: - 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)} - container_tree = ContainerTree.getInstance() - - machine_extruders = [] - for position in sorted(global_stack.extruders): - machine_extruders.append(global_stack.extruders[position]) - - plugin_registry = PluginRegistry.getInstance() - extension = file_name.split(".")[-1] - - for plugin_id, meta_data in self._getIOPlugins("profile_reader"): - if meta_data["profile_reader"][0]["extension"] != extension: - continue - 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: - return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "No custom profile to import in file {0}", file_name)} - except Exception as e: - # Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None. - Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e)) - return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}:", file_name) + "\n" + str(e) + ""} - - if profile_or_list: - # Ensure it is always a list of profiles - if not isinstance(profile_or_list, list): - profile_or_list = [profile_or_list] - - # First check if this profile is suitable for this machine - global_profile = None - extruder_profiles = [] - if len(profile_or_list) == 1: - global_profile = profile_or_list[0] - else: - for profile in profile_or_list: - if not profile.getMetaDataEntry("position"): - global_profile = profile - else: - extruder_profiles.append(profile) - extruder_profiles = sorted(extruder_profiles, key = lambda x: int(x.getMetaDataEntry("position"))) - profile_or_list = [global_profile] + extruder_profiles - - if not global_profile: - Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name) - 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)} - profile_definition = global_profile.getMetaDataEntry("definition") - - # Make sure we have a profile_definition in the file: - if profile_definition is None: - break - machine_definitions = self.findContainers(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_definitions[0] - - # Get the expected machine definition. - # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... - has_machine_quality = parseBool(machine_definition.getMetaDataEntry("has_machine_quality", "false")) - profile_definition = machine_definition.getMetaDataEntry("quality_definition", machine_definition.getId()) if has_machine_quality else "fdmprinter" - expected_machine_definition = container_tree.machines[global_stack.definition.getId()].quality_definition - - # And check if the profile_definition matches either one (showing error if not): - if profile_definition != expected_machine_definition: - Logger.log("d", "Profile {file_name} is for machine {profile_definition}, but the current active machine is {expected_machine_definition}. Changing profile's definition.".format(file_name = file_name, profile_definition = profile_definition, expected_machine_definition = expected_machine_definition)) - global_profile.setMetaDataEntry("definition", expected_machine_definition) - for extruder_profile in extruder_profiles: - extruder_profile.setMetaDataEntry("definition", expected_machine_definition) - - quality_name = global_profile.getName() - quality_type = global_profile.getMetaDataEntry("quality_type") - - name_seed = os.path.splitext(os.path.basename(file_name))[0] - new_name = self.uniqueName(name_seed) - - # Ensure it is always a list of profiles - if type(profile_or_list) is not list: - profile_or_list = [profile_or_list] - - # Make sure that there are also extruder stacks' quality_changes, not just one for the global stack - if len(profile_or_list) == 1: - global_profile = profile_or_list[0] - extruder_profiles = [] - for idx, extruder in enumerate(global_stack.extruders.values()): - profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1)) - profile = InstanceContainer(profile_id) - profile.setName(quality_name) - profile.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion) - profile.setMetaDataEntry("type", "quality_changes") - profile.setMetaDataEntry("definition", expected_machine_definition) - profile.setMetaDataEntry("quality_type", quality_type) - profile.setDirty(True) - if idx == 0: - # Move all per-extruder settings to the first extruder's quality_changes - for qc_setting_key in global_profile.getAllKeys(): - settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = global_profile.getProperty(qc_setting_key, "value") - - setting_definition = global_stack.getSettingDefinition(qc_setting_key) - 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) - - for profile in extruder_profiles: - profile_or_list.append(profile) - - # Import all profiles - profile_ids_added = [] # type: List[str] - for profile_index, profile in enumerate(profile_or_list): - if profile_index == 0: - # This is assumed to be the global profile - 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 - extruder_id = machine_extruders[profile_index - 1].definition.getId() - extruder_position = str(profile_index - 1) - if not profile.getMetaDataEntry("position"): - profile.setMetaDataEntry("position", extruder_position) - else: - profile.setMetaDataEntry("position", extruder_position) - profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_") - - else: # More extruders in the imported file than in the machine. - continue # Delete the additional profiles. - - result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) - if result is not None: - # Remove any profiles that did got added. - for profile_id in profile_ids_added: - self.removeContainer(profile_id) - - return {"status": "error", "message": catalog.i18nc( - "@info:status Don't translate the XML tag !", - "Failed to import profile from {0}:", - file_name) + " " + result} - profile_ids_added.append(profile.getId()) - return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} - - # This message is throw when the profile reader doesn't find any profile in the file - return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)} - - # If it hasn't returned by now, none of the plugins loaded the profile successfully. - return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)} - - @override(ContainerRegistry) - def load(self) -> None: - super().load() - self._registerSingleExtrusionMachinesExtruderStacks() - self._connectUpgradedExtruderStacksToMachines() - - ## Check if the metadata for a container is okay before adding it. - # - # This overrides the one from UM.Settings.ContainerRegistry because we - # also require that the setting_version is correct. - @override(ContainerRegistry) - def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool: - if metadata is None: - return False - if "setting_version" not in metadata: - return False - try: - if int(metadata["setting_version"]) != cura.CuraApplication.CuraApplication.SettingVersion: - return False - except ValueError: #Not parsable as int. - return False - return True - - ## Update an imported profile to match the current machine configuration. - # - # \param profile The profile to configure. - # \param id_seed The base ID for the profile. May be changed so it does not conflict with existing containers. - # \param new_name The new name for the profile. - # - # \return None if configuring was successful or an error message if an error occurred. - def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]: - profile.setDirty(True) # Ensure the profiles are correctly saved - - new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile")) - profile.setMetaDataEntry("id", new_id) - profile.setName(new_name) - - # Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile - # It also solves an issue with importing profiles from G-Codes - profile.setMetaDataEntry("id", new_id) - profile.setMetaDataEntry("definition", machine_definition_id) - - if "type" in profile.getMetaData(): - profile.setMetaDataEntry("type", "quality_changes") - else: - profile.setMetaDataEntry("type", "quality_changes") - - quality_type = profile.getMetaDataEntry("quality_type") - if not quality_type: - return catalog.i18nc("@info:status", "Profile is missing a quality type.") - - global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() - if global_stack is None: - return None - definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition - profile.setDefinition(definition_id) - - # Check to make sure the imported profile actually makes sense in context of the current configuration. - # This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as - # successfully imported but then fail to show up. - quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups() - # "not_supported" profiles can be imported. - if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict: - return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type) - - ContainerRegistry.getInstance().addContainer(profile) - - return None - - @override(ContainerRegistry) - def saveDirtyContainers(self) -> None: - # Lock file for "more" atomically loading and saving to/from config dir. - with self.lockFile(): - # Save base files first - for instance in self.findDirtyContainers(container_type=InstanceContainer): - if instance.getMetaDataEntry("removed"): - continue - if instance.getId() == instance.getMetaData().get("base_file"): - self.saveContainer(instance) - - for instance in self.findDirtyContainers(container_type=InstanceContainer): - if instance.getMetaDataEntry("removed"): - continue - self.saveContainer(instance) - - for stack in self.findContainerStacks(): - self.saveContainer(stack) - - ## Gets a list of profile writer plugins - # \return List of tuples of (plugin_id, meta_data). - def _getIOPlugins(self, io_type): - plugin_registry = PluginRegistry.getInstance() - active_plugin_ids = plugin_registry.getActivePlugins() - - result = [] - for plugin_id in active_plugin_ids: - meta_data = plugin_registry.getMetaData(plugin_id) - if io_type in meta_data: - result.append( (plugin_id, meta_data) ) - return result - - ## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack. - def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]: - assert type(container) == ContainerStack - - container_type = container.getMetaDataEntry("type") - if container_type not in ("extruder_train", "machine"): - # It is not an extruder or machine, so do nothing with the stack - return container - - Logger.log("d", "Converting ContainerStack {stack} to {type}", stack = container.getId(), type = container_type) - - if container_type == "extruder_train": - new_stack = ExtruderStack.ExtruderStack(container.getId()) - else: - new_stack = GlobalStack.GlobalStack(container.getId()) - - container_contents = container.serialize() - new_stack.deserialize(container_contents) - - # Delete the old configuration file so we do not get double stacks - if os.path.isfile(container.getPath()): - os.remove(container.getPath()) - - return new_stack - - def _registerSingleExtrusionMachinesExtruderStacks(self) -> None: - machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"}) - for machine in machines: - extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId()) - if not extruder_stacks: - self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder") - - def _onContainerAdded(self, container: ContainerInterface) -> None: - # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack - # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack - # is added, we check to see if an extruder stack needs to be added. - if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine": - return - - machine_extruder_trains = container.getMetaDataEntry("machine_extruder_trains") - if machine_extruder_trains is not None and machine_extruder_trains != {"0": "fdmextruder"}: - return - - extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId()) - if not extruder_stacks: - self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder") - - # - # new_global_quality_changes is optional. It is only used in project loading for a scenario like this: - # - override the current machine - # - create new for custom quality profile - # new_global_quality_changes is the new global quality changes container in this scenario. - # create_new_ids indicates if new unique ids must be created - # - def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True): - new_extruder_id = extruder_id - - application = cura.CuraApplication.CuraApplication.getInstance() - - extruder_definitions = self.findDefinitionContainers(id = new_extruder_id) - if not extruder_definitions: - Logger.log("w", "Could not find definition containers for extruder %s", new_extruder_id) - return - - extruder_definition = extruder_definitions[0] - unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id - - extruder_stack = ExtruderStack.ExtruderStack(unique_name) - extruder_stack.setName(extruder_definition.getName()) - extruder_stack.setDefinition(extruder_definition) - extruder_stack.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) - - # create a new definition_changes container for the extruder stack - definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings") if create_new_ids else extruder_stack.getId() + "_settings" - definition_changes_name = definition_changes_id - definition_changes = InstanceContainer(definition_changes_id, parent = application) - definition_changes.setName(definition_changes_name) - definition_changes.setMetaDataEntry("setting_version", application.SettingVersion) - definition_changes.setMetaDataEntry("type", "definition_changes") - definition_changes.setMetaDataEntry("definition", extruder_definition.getId()) - - # move definition_changes settings if exist - for setting_key in definition_changes.getAllKeys(): - if machine.definition.getProperty(setting_key, "settable_per_extruder"): - setting_value = machine.definitionChanges.getProperty(setting_key, "value") - if setting_value is not None: - # move it to the extruder stack's definition_changes - setting_definition = machine.getSettingDefinition(setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - definition_changes.addInstance(new_instance) - definition_changes.setDirty(True) - - machine.definitionChanges.removeInstance(setting_key, postpone_emit = True) - - self.addContainer(definition_changes) - extruder_stack.setDefinitionChanges(definition_changes) - - # create empty user changes container otherwise - user_container_id = self.uniqueName(extruder_stack.getId() + "_user") if create_new_ids else extruder_stack.getId() + "_user" - user_container_name = user_container_id - user_container = InstanceContainer(user_container_id, parent = application) - user_container.setName(user_container_name) - user_container.setMetaDataEntry("type", "user") - user_container.setMetaDataEntry("machine", machine.getId()) - user_container.setMetaDataEntry("setting_version", application.SettingVersion) - user_container.setDefinition(machine.definition.getId()) - user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position")) - - if machine.userChanges: - # For the newly created extruder stack, we need to move all "per-extruder" settings to the user changes - # container to the extruder stack. - for user_setting_key in machine.userChanges.getAllKeys(): - settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = machine.getProperty(user_setting_key, "value") - - setting_definition = machine.getSettingDefinition(user_setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - user_container.addInstance(new_instance) - user_container.setDirty(True) - - machine.userChanges.removeInstance(user_setting_key, postpone_emit = True) - - self.addContainer(user_container) - extruder_stack.setUserChanges(user_container) - - empty_variant = application.empty_variant_container - empty_material = application.empty_material_container - empty_quality = application.empty_quality_container - - if machine.variant.getId() not in ("empty", "empty_variant"): - variant = machine.variant - else: - variant = empty_variant - extruder_stack.variant = variant - - if machine.material.getId() not in ("empty", "empty_material"): - material = machine.material - else: - material = empty_material - extruder_stack.material = material - - if machine.quality.getId() not in ("empty", "empty_quality"): - quality = machine.quality - else: - quality = empty_quality - extruder_stack.quality = quality - - machine_quality_changes = machine.qualityChanges - if new_global_quality_changes is not None: - machine_quality_changes = new_global_quality_changes - - if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): - extruder_quality_changes_container = self.findInstanceContainers(name = machine_quality_changes.getName(), extruder = extruder_id) - if extruder_quality_changes_container: - extruder_quality_changes_container = extruder_quality_changes_container[0] - - quality_changes_id = extruder_quality_changes_container.getId() - extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] - else: - # Some extruder quality_changes containers can be created at runtime as files in the qualities - # folder. Those files won't be loaded in the registry immediately. So we also need to search - # the folder to see if the quality_changes exists. - extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) - if extruder_quality_changes_container: - quality_changes_id = extruder_quality_changes_container.getId() - extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) - extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] - else: - # If we still cannot find a quality changes container for the extruder, create a new one - container_name = machine_quality_changes.getName() - container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name) - extruder_quality_changes_container = InstanceContainer(container_id, parent = application) - extruder_quality_changes_container.setName(container_name) - extruder_quality_changes_container.setMetaDataEntry("type", "quality_changes") - extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion) - extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) - extruder_quality_changes_container.setMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type")) - extruder_quality_changes_container.setMetaDataEntry("intent_category", "default") # Intent categories weren't a thing back then. - extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId()) - - self.addContainer(extruder_quality_changes_container) - extruder_stack.qualityChanges = extruder_quality_changes_container - - if not extruder_quality_changes_container: - Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", - machine_quality_changes.getName(), extruder_stack.getId()) - else: - # Move all per-extruder settings to the extruder's quality changes - for qc_setting_key in machine_quality_changes.getAllKeys(): - settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = machine_quality_changes.getProperty(qc_setting_key, "value") - - setting_definition = machine.getSettingDefinition(qc_setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - extruder_quality_changes_container.addInstance(new_instance) - extruder_quality_changes_container.setDirty(True) - - machine_quality_changes.removeInstance(qc_setting_key, postpone_emit=True) - else: - extruder_stack.qualityChanges = self.findInstanceContainers(id = "empty_quality_changes")[0] - - self.addContainer(extruder_stack) - - # Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have - # per-extruder settings in the container for the machine instead of the extruder. - if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): - quality_changes_machine_definition_id = machine_quality_changes.getDefinition().getId() - else: - whole_machine_definition = machine.definition - machine_entry = machine.definition.getMetaDataEntry("machine") - if machine_entry is not None: - container_registry = ContainerRegistry.getInstance() - whole_machine_definition = container_registry.findDefinitionContainers(id = machine_entry)[0] - - quality_changes_machine_definition_id = "fdmprinter" - if whole_machine_definition.getMetaDataEntry("has_machine_quality"): - quality_changes_machine_definition_id = machine.definition.getMetaDataEntry("quality_definition", - whole_machine_definition.getId()) - qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id) - qc_groups = {} # map of qc names -> qc containers - for qc in qcs: - qc_name = qc.getName() - if qc_name not in qc_groups: - qc_groups[qc_name] = [] - qc_groups[qc_name].append(qc) - # Try to find from the quality changes cura directory too - quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) - if quality_changes_container: - qc_groups[qc_name].append(quality_changes_container) - - for qc_name, qc_list in qc_groups.items(): - qc_dict = {"global": None, "extruders": []} - for qc in qc_list: - extruder_position = qc.getMetaDataEntry("position") - if extruder_position is not None: - qc_dict["extruders"].append(qc) - else: - qc_dict["global"] = qc - if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1: - # Move per-extruder settings - for qc_setting_key in qc_dict["global"].getAllKeys(): - settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = qc_dict["global"].getProperty(qc_setting_key, "value") - - setting_definition = machine.getSettingDefinition(qc_setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - qc_dict["extruders"][0].addInstance(new_instance) - qc_dict["extruders"][0].setDirty(True) - - qc_dict["global"].removeInstance(qc_setting_key, postpone_emit=True) - - # Set next stack at the end - extruder_stack.setNextStack(machine) - - return extruder_stack - - def _findQualityChangesContainerInCuraFolder(self, name: str) -> Optional[InstanceContainer]: - quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer) - - instance_container = None - - for item in os.listdir(quality_changes_dir): - file_path = os.path.join(quality_changes_dir, item) - if not os.path.isfile(file_path): - continue - - parser = configparser.ConfigParser(interpolation = None) - try: - parser.read([file_path]) - except Exception: - # Skip, it is not a valid stack file - continue - - if not parser.has_option("general", "name"): - continue - - if parser["general"]["name"] == name: - # Load the container - container_id = os.path.basename(file_path).replace(".inst.cfg", "") - if self.findInstanceContainers(id = container_id): - # This container is already in the registry, skip it - continue - - instance_container = InstanceContainer(container_id) - with open(file_path, "r", encoding = "utf-8") as f: - serialized = f.read() - try: - instance_container.deserialize(serialized, file_path) - except ContainerFormatError: - Logger.logException("e", "Unable to deserialize InstanceContainer %s", file_path) - continue - self.addContainer(instance_container) - break - - return instance_container - - # Fix the extruders that were upgraded to ExtruderStack instances during addContainer. - # The stacks are now responsible for setting the next stack on deserialize. However, - # due to problems with loading order, some stacks may not have the proper next stack - # set after upgrading, because the proper global stack was not yet loaded. This method - # makes sure those extruders also get the right stack set. - def _connectUpgradedExtruderStacksToMachines(self) -> None: - extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack) - for extruder_stack in extruder_stacks: - if extruder_stack.getNextStack(): - # Has the right next stack, so ignore it. - continue - - machines = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack.getMetaDataEntry("machine", "")) - if machines: - extruder_stack.setNextStack(machines[0]) - else: - Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId()) - - # Override just for the type. - @classmethod - @override(ContainerRegistry) - def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry": - return cast(CuraContainerRegistry, super().getInstance(*args, **kwargs)) +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import os +import re +import configparser + +from typing import Any, cast, Dict, Optional, List, Union +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 +from UM.Settings.SettingInstance import SettingInstance +from UM.Logger import Logger +from UM.Message import Message +from UM.Platform import Platform +from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with. +from UM.Resources import Resources +from UM.Util import parseBool +from cura.ReaderWriters.ProfileWriter import ProfileWriter + +from . import ExtruderStack +from . import GlobalStack + +import cura.CuraApplication +from cura.Settings.cura_empty_instance_containers import empty_quality_container +from cura.Machines.ContainerTree import ContainerTree +from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader + +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") + + +class CuraContainerRegistry(ContainerRegistry): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack + # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack + # is added, we check to see if an extruder stack needs to be added. + self.containerAdded.connect(self._onContainerAdded) + + @override(ContainerRegistry) + def addContainer(self, container: ContainerInterface) -> None: + """Overridden from ContainerRegistry + + Adds a container to the registry. + + This will also try to convert a ContainerStack to either Extruder or + Global stack based on metadata information. + """ + + # Note: Intentional check with type() because we want to ignore subclasses + if type(container) == ContainerStack: + container = self._convertContainerStack(cast(ContainerStack, container)) + + if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()): + # Check against setting version of the definition. + required_setting_version = cura.CuraApplication.CuraApplication.SettingVersion + actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0)) + if required_setting_version != actual_setting_version: + Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version)) + return # Don't add. + + super().addContainer(container) + + def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str: + """Create a name that is not empty and unique + + :param container_type: :type{string} Type of the container (machine, quality, ...) + :param current_name: :type{} Current name of the container, which may be an acceptable option + :param new_name: :type{string} Base name, which may not be unique + :param fallback_name: :type{string} Name to use when (stripped) new_name is empty + :return: :type{string} Name that is unique for the specified type and name/id + """ + new_name = new_name.strip() + num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name) + if num_check: + new_name = num_check.group(1) + if new_name == "": + new_name = fallback_name + + unique_name = new_name + i = 1 + # In case we are renaming, the current name of the container is also a valid end-result + while self._containerExists(container_type, unique_name) and unique_name != current_name: + i += 1 + unique_name = "%s #%d" % (new_name, i) + + return unique_name + + def _containerExists(self, container_type: str, container_name: str): + """Check if a container with of a certain type and a certain name or id exists + + Both the id and the name are checked, because they may not be the same and it is better if they are both unique + :param container_type: :type{string} Type of the container (machine, quality, ...) + :param container_name: :type{string} Name to check + """ + container_class = ContainerStack if container_type == "machine" else InstanceContainer + + return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \ + self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type) + + def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool: + """Exports an profile to a file + + :param container_list: :type{list} the containers to export. This is not + necessarily in any order! + :param file_name: :type{str} the full path and filename to export to. + :param file_type: :type{str} the file type with the format " (*.)" + :return: True if the export succeeded, false otherwise. + """ + + # Parse the fileType to deduce what plugin can save the file format. + # fileType has the format " (*.)" + split = file_type.rfind(" (*.") # Find where the description ends and the extension starts. + if split < 0: # Not found. Invalid format. + Logger.log("e", "Invalid file format identifier %s", file_type) + return False + description = file_type[:split] + extension = file_type[split + 4:-1] # Leave out the " (*." and ")". + if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any. + file_name += "." + extension + + # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself. + if not Platform.isWindows(): + if os.path.exists(file_name): + result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), + catalog.i18nc("@label Don't translate the XML tag !", "The file {0} already exists. Are you sure you want to overwrite it?").format(file_name)) + if result == QMessageBox.No: + return False + + profile_writer = self._findProfileWriter(extension, description) + try: + if profile_writer is None: + raise Exception("Unable to find a profile writer") + success = profile_writer.write(file_name, container_list) + except Exception as e: + Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e)) + m = Message(catalog.i18nc("@info:status Don't translate the XML tags or !", "Failed to export profile to {0}: {1}", file_name, str(e)), + lifetime = 0, + title = catalog.i18nc("@info:title", "Error")) + m.show() + return False + if not success: + Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name) + m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Failed to export profile to {0}: Writer plugin reported failure.", file_name), + lifetime = 0, + title = catalog.i18nc("@info:title", "Error")) + m.show() + return False + m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Exported profile to {0}", file_name), + title = catalog.i18nc("@info:title", "Export succeeded")) + m.show() + return True + + def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]: + """Gets the plugin object matching the criteria + + :param extension: + :param description: + :return: The plugin object matching the given extension and description. + """ + plugin_registry = PluginRegistry.getInstance() + for plugin_id, meta_data in self._getIOPlugins("profile_writer"): + for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write. + supported_extension = supported_type.get("extension", None) + if supported_extension == extension: # This plugin supports a file type with the same extension. + supported_description = supported_type.get("description", None) + if supported_description == description: # The description is also identical. Assume it's the same file type. + return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id)) + return None + + def importProfile(self, file_name: str) -> Dict[str, str]: + """Imports a profile from a file + + :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. + """ + + 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 !", "Failed to import profile from {0}: {1}", file_name, "Invalid path")} + + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() + if not global_stack: + 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)} + container_tree = ContainerTree.getInstance() + + machine_extruders = [] + for position in sorted(global_stack.extruders): + machine_extruders.append(global_stack.extruders[position]) + + plugin_registry = PluginRegistry.getInstance() + extension = file_name.split(".")[-1] + + for plugin_id, meta_data in self._getIOPlugins("profile_reader"): + if meta_data["profile_reader"][0]["extension"] != extension: + continue + 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: + return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "No custom profile to import in file {0}", file_name)} + except Exception as e: + # Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None. + Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e)) + return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}:", file_name) + "\n" + str(e) + ""} + + if profile_or_list: + # Ensure it is always a list of profiles + if not isinstance(profile_or_list, list): + profile_or_list = [profile_or_list] + + # First check if this profile is suitable for this machine + global_profile = None + extruder_profiles = [] + if len(profile_or_list) == 1: + global_profile = profile_or_list[0] + else: + for profile in profile_or_list: + if not profile.getMetaDataEntry("position"): + global_profile = profile + else: + extruder_profiles.append(profile) + extruder_profiles = sorted(extruder_profiles, key = lambda x: int(x.getMetaDataEntry("position"))) + profile_or_list = [global_profile] + extruder_profiles + + if not global_profile: + Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name) + 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)} + profile_definition = global_profile.getMetaDataEntry("definition") + + # Make sure we have a profile_definition in the file: + if profile_definition is None: + break + machine_definitions = self.findContainers(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_definitions[0] + + # Get the expected machine definition. + # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... + has_machine_quality = parseBool(machine_definition.getMetaDataEntry("has_machine_quality", "false")) + profile_definition = machine_definition.getMetaDataEntry("quality_definition", machine_definition.getId()) if has_machine_quality else "fdmprinter" + expected_machine_definition = container_tree.machines[global_stack.definition.getId()].quality_definition + + # And check if the profile_definition matches either one (showing error if not): + if profile_definition != expected_machine_definition: + Logger.log("d", "Profile {file_name} is for machine {profile_definition}, but the current active machine is {expected_machine_definition}. Changing profile's definition.".format(file_name = file_name, profile_definition = profile_definition, expected_machine_definition = expected_machine_definition)) + global_profile.setMetaDataEntry("definition", expected_machine_definition) + for extruder_profile in extruder_profiles: + extruder_profile.setMetaDataEntry("definition", expected_machine_definition) + + quality_name = global_profile.getName() + quality_type = global_profile.getMetaDataEntry("quality_type") + + name_seed = os.path.splitext(os.path.basename(file_name))[0] + new_name = self.uniqueName(name_seed) + + # Ensure it is always a list of profiles + if type(profile_or_list) is not list: + profile_or_list = [profile_or_list] + + # Make sure that there are also extruder stacks' quality_changes, not just one for the global stack + if len(profile_or_list) == 1: + global_profile = profile_or_list[0] + extruder_profiles = [] + for idx, extruder in enumerate(global_stack.extruders.values()): + profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1)) + profile = InstanceContainer(profile_id) + profile.setName(quality_name) + profile.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion) + profile.setMetaDataEntry("type", "quality_changes") + profile.setMetaDataEntry("definition", expected_machine_definition) + profile.setMetaDataEntry("quality_type", quality_type) + profile.setDirty(True) + if idx == 0: + # Move all per-extruder settings to the first extruder's quality_changes + for qc_setting_key in global_profile.getAllKeys(): + settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = global_profile.getProperty(qc_setting_key, "value") + + setting_definition = global_stack.getSettingDefinition(qc_setting_key) + 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) + + for profile in extruder_profiles: + profile_or_list.append(profile) + + # Import all profiles + profile_ids_added = [] # type: List[str] + for profile_index, profile in enumerate(profile_or_list): + if profile_index == 0: + # This is assumed to be the global profile + 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 + extruder_id = machine_extruders[profile_index - 1].definition.getId() + extruder_position = str(profile_index - 1) + if not profile.getMetaDataEntry("position"): + profile.setMetaDataEntry("position", extruder_position) + else: + profile.setMetaDataEntry("position", extruder_position) + profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_") + + else: # More extruders in the imported file than in the machine. + continue # Delete the additional profiles. + + result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) + if result is not None: + # Remove any profiles that did got added. + for profile_id in profile_ids_added: + self.removeContainer(profile_id) + + return {"status": "error", "message": catalog.i18nc( + "@info:status Don't translate the XML tag !", + "Failed to import profile from {0}:", + file_name) + " " + result} + profile_ids_added.append(profile.getId()) + return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} + + # This message is throw when the profile reader doesn't find any profile in the file + return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)} + + # If it hasn't returned by now, none of the plugins loaded the profile successfully. + return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)} + + @override(ContainerRegistry) + def load(self) -> None: + super().load() + self._registerSingleExtrusionMachinesExtruderStacks() + self._connectUpgradedExtruderStacksToMachines() + + @override(ContainerRegistry) + def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool: + """Check if the metadata for a container is okay before adding it. + + This overrides the one from UM.Settings.ContainerRegistry because we + also require that the setting_version is correct. + """ + + if metadata is None: + return False + if "setting_version" not in metadata: + return False + try: + if int(metadata["setting_version"]) != cura.CuraApplication.CuraApplication.SettingVersion: + return False + except ValueError: #Not parsable as int. + return False + return True + + def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]: + """Update an imported profile to match the current machine configuration. + + :param profile: The profile to configure. + :param id_seed: The base ID for the profile. May be changed so it does not conflict with existing containers. + :param new_name: The new name for the profile. + + :return: None if configuring was successful or an error message if an error occurred. + """ + + profile.setDirty(True) # Ensure the profiles are correctly saved + + new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile")) + profile.setMetaDataEntry("id", new_id) + profile.setName(new_name) + + # Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile + # It also solves an issue with importing profiles from G-Codes + profile.setMetaDataEntry("id", new_id) + profile.setMetaDataEntry("definition", machine_definition_id) + + if "type" in profile.getMetaData(): + profile.setMetaDataEntry("type", "quality_changes") + else: + profile.setMetaDataEntry("type", "quality_changes") + + quality_type = profile.getMetaDataEntry("quality_type") + if not quality_type: + return catalog.i18nc("@info:status", "Profile is missing a quality type.") + + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() + if global_stack is None: + return None + definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition + profile.setDefinition(definition_id) + + # Check to make sure the imported profile actually makes sense in context of the current configuration. + # This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as + # successfully imported but then fail to show up. + quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups() + # "not_supported" profiles can be imported. + if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict: + return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type) + + ContainerRegistry.getInstance().addContainer(profile) + + return None + + @override(ContainerRegistry) + def saveDirtyContainers(self) -> None: + # Lock file for "more" atomically loading and saving to/from config dir. + with self.lockFile(): + # Save base files first + for instance in self.findDirtyContainers(container_type=InstanceContainer): + if instance.getMetaDataEntry("removed"): + continue + if instance.getId() == instance.getMetaData().get("base_file"): + self.saveContainer(instance) + + for instance in self.findDirtyContainers(container_type=InstanceContainer): + if instance.getMetaDataEntry("removed"): + continue + self.saveContainer(instance) + + for stack in self.findContainerStacks(): + self.saveContainer(stack) + + def _getIOPlugins(self, io_type): + """Gets a list of profile writer plugins + + :return: List of tuples of (plugin_id, meta_data). + """ + plugin_registry = PluginRegistry.getInstance() + active_plugin_ids = plugin_registry.getActivePlugins() + + result = [] + for plugin_id in active_plugin_ids: + meta_data = plugin_registry.getMetaData(plugin_id) + if io_type in meta_data: + result.append( (plugin_id, meta_data) ) + return result + + def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]: + """Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.""" + + assert type(container) == ContainerStack + + container_type = container.getMetaDataEntry("type") + if container_type not in ("extruder_train", "machine"): + # It is not an extruder or machine, so do nothing with the stack + return container + + Logger.log("d", "Converting ContainerStack {stack} to {type}", stack = container.getId(), type = container_type) + + if container_type == "extruder_train": + new_stack = ExtruderStack.ExtruderStack(container.getId()) + else: + new_stack = GlobalStack.GlobalStack(container.getId()) + + container_contents = container.serialize() + new_stack.deserialize(container_contents) + + # Delete the old configuration file so we do not get double stacks + if os.path.isfile(container.getPath()): + os.remove(container.getPath()) + + return new_stack + + def _registerSingleExtrusionMachinesExtruderStacks(self) -> None: + machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"}) + for machine in machines: + extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId()) + if not extruder_stacks: + self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder") + + def _onContainerAdded(self, container: ContainerInterface) -> None: + # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack + # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack + # is added, we check to see if an extruder stack needs to be added. + if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine": + return + + machine_extruder_trains = container.getMetaDataEntry("machine_extruder_trains") + if machine_extruder_trains is not None and machine_extruder_trains != {"0": "fdmextruder"}: + return + + extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId()) + if not extruder_stacks: + self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder") + + # + # new_global_quality_changes is optional. It is only used in project loading for a scenario like this: + # - override the current machine + # - create new for custom quality profile + # new_global_quality_changes is the new global quality changes container in this scenario. + # create_new_ids indicates if new unique ids must be created + # + def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True): + new_extruder_id = extruder_id + + application = cura.CuraApplication.CuraApplication.getInstance() + + extruder_definitions = self.findDefinitionContainers(id = new_extruder_id) + if not extruder_definitions: + Logger.log("w", "Could not find definition containers for extruder %s", new_extruder_id) + return + + extruder_definition = extruder_definitions[0] + unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id + + extruder_stack = ExtruderStack.ExtruderStack(unique_name) + extruder_stack.setName(extruder_definition.getName()) + extruder_stack.setDefinition(extruder_definition) + extruder_stack.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) + + # create a new definition_changes container for the extruder stack + definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings") if create_new_ids else extruder_stack.getId() + "_settings" + definition_changes_name = definition_changes_id + definition_changes = InstanceContainer(definition_changes_id, parent = application) + definition_changes.setName(definition_changes_name) + definition_changes.setMetaDataEntry("setting_version", application.SettingVersion) + definition_changes.setMetaDataEntry("type", "definition_changes") + definition_changes.setMetaDataEntry("definition", extruder_definition.getId()) + + # move definition_changes settings if exist + for setting_key in definition_changes.getAllKeys(): + if machine.definition.getProperty(setting_key, "settable_per_extruder"): + setting_value = machine.definitionChanges.getProperty(setting_key, "value") + if setting_value is not None: + # move it to the extruder stack's definition_changes + setting_definition = machine.getSettingDefinition(setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + definition_changes.addInstance(new_instance) + definition_changes.setDirty(True) + + machine.definitionChanges.removeInstance(setting_key, postpone_emit = True) + + self.addContainer(definition_changes) + extruder_stack.setDefinitionChanges(definition_changes) + + # create empty user changes container otherwise + user_container_id = self.uniqueName(extruder_stack.getId() + "_user") if create_new_ids else extruder_stack.getId() + "_user" + user_container_name = user_container_id + user_container = InstanceContainer(user_container_id, parent = application) + user_container.setName(user_container_name) + user_container.setMetaDataEntry("type", "user") + user_container.setMetaDataEntry("machine", machine.getId()) + user_container.setMetaDataEntry("setting_version", application.SettingVersion) + user_container.setDefinition(machine.definition.getId()) + user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position")) + + if machine.userChanges: + # For the newly created extruder stack, we need to move all "per-extruder" settings to the user changes + # container to the extruder stack. + for user_setting_key in machine.userChanges.getAllKeys(): + settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = machine.getProperty(user_setting_key, "value") + + setting_definition = machine.getSettingDefinition(user_setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + user_container.addInstance(new_instance) + user_container.setDirty(True) + + machine.userChanges.removeInstance(user_setting_key, postpone_emit = True) + + self.addContainer(user_container) + extruder_stack.setUserChanges(user_container) + + empty_variant = application.empty_variant_container + empty_material = application.empty_material_container + empty_quality = application.empty_quality_container + + if machine.variant.getId() not in ("empty", "empty_variant"): + variant = machine.variant + else: + variant = empty_variant + extruder_stack.variant = variant + + if machine.material.getId() not in ("empty", "empty_material"): + material = machine.material + else: + material = empty_material + extruder_stack.material = material + + if machine.quality.getId() not in ("empty", "empty_quality"): + quality = machine.quality + else: + quality = empty_quality + extruder_stack.quality = quality + + machine_quality_changes = machine.qualityChanges + if new_global_quality_changes is not None: + machine_quality_changes = new_global_quality_changes + + if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): + extruder_quality_changes_container = self.findInstanceContainers(name = machine_quality_changes.getName(), extruder = extruder_id) + if extruder_quality_changes_container: + extruder_quality_changes_container = extruder_quality_changes_container[0] + + quality_changes_id = extruder_quality_changes_container.getId() + extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] + else: + # Some extruder quality_changes containers can be created at runtime as files in the qualities + # folder. Those files won't be loaded in the registry immediately. So we also need to search + # the folder to see if the quality_changes exists. + extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) + if extruder_quality_changes_container: + quality_changes_id = extruder_quality_changes_container.getId() + extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) + extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] + else: + # If we still cannot find a quality changes container for the extruder, create a new one + container_name = machine_quality_changes.getName() + container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name) + extruder_quality_changes_container = InstanceContainer(container_id, parent = application) + extruder_quality_changes_container.setName(container_name) + extruder_quality_changes_container.setMetaDataEntry("type", "quality_changes") + extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion) + extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) + extruder_quality_changes_container.setMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type")) + extruder_quality_changes_container.setMetaDataEntry("intent_category", "default") # Intent categories weren't a thing back then. + extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId()) + + self.addContainer(extruder_quality_changes_container) + extruder_stack.qualityChanges = extruder_quality_changes_container + + if not extruder_quality_changes_container: + Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", + machine_quality_changes.getName(), extruder_stack.getId()) + else: + # Move all per-extruder settings to the extruder's quality changes + for qc_setting_key in machine_quality_changes.getAllKeys(): + settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = machine_quality_changes.getProperty(qc_setting_key, "value") + + setting_definition = machine.getSettingDefinition(qc_setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + extruder_quality_changes_container.addInstance(new_instance) + extruder_quality_changes_container.setDirty(True) + + machine_quality_changes.removeInstance(qc_setting_key, postpone_emit=True) + else: + extruder_stack.qualityChanges = self.findInstanceContainers(id = "empty_quality_changes")[0] + + self.addContainer(extruder_stack) + + # Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have + # per-extruder settings in the container for the machine instead of the extruder. + if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): + quality_changes_machine_definition_id = machine_quality_changes.getDefinition().getId() + else: + whole_machine_definition = machine.definition + machine_entry = machine.definition.getMetaDataEntry("machine") + if machine_entry is not None: + container_registry = ContainerRegistry.getInstance() + whole_machine_definition = container_registry.findDefinitionContainers(id = machine_entry)[0] + + quality_changes_machine_definition_id = "fdmprinter" + if whole_machine_definition.getMetaDataEntry("has_machine_quality"): + quality_changes_machine_definition_id = machine.definition.getMetaDataEntry("quality_definition", + whole_machine_definition.getId()) + qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id) + qc_groups = {} # map of qc names -> qc containers + for qc in qcs: + qc_name = qc.getName() + if qc_name not in qc_groups: + qc_groups[qc_name] = [] + qc_groups[qc_name].append(qc) + # Try to find from the quality changes cura directory too + quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) + if quality_changes_container: + qc_groups[qc_name].append(quality_changes_container) + + for qc_name, qc_list in qc_groups.items(): + qc_dict = {"global": None, "extruders": []} + for qc in qc_list: + extruder_position = qc.getMetaDataEntry("position") + if extruder_position is not None: + qc_dict["extruders"].append(qc) + else: + qc_dict["global"] = qc + if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1: + # Move per-extruder settings + for qc_setting_key in qc_dict["global"].getAllKeys(): + settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = qc_dict["global"].getProperty(qc_setting_key, "value") + + setting_definition = machine.getSettingDefinition(qc_setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + qc_dict["extruders"][0].addInstance(new_instance) + qc_dict["extruders"][0].setDirty(True) + + qc_dict["global"].removeInstance(qc_setting_key, postpone_emit=True) + + # Set next stack at the end + extruder_stack.setNextStack(machine) + + return extruder_stack + + def _findQualityChangesContainerInCuraFolder(self, name: str) -> Optional[InstanceContainer]: + quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer) + + instance_container = None + + for item in os.listdir(quality_changes_dir): + file_path = os.path.join(quality_changes_dir, item) + if not os.path.isfile(file_path): + continue + + parser = configparser.ConfigParser(interpolation = None) + try: + parser.read([file_path]) + except Exception: + # Skip, it is not a valid stack file + continue + + if not parser.has_option("general", "name"): + continue + + if parser["general"]["name"] == name: + # Load the container + container_id = os.path.basename(file_path).replace(".inst.cfg", "") + if self.findInstanceContainers(id = container_id): + # This container is already in the registry, skip it + continue + + instance_container = InstanceContainer(container_id) + with open(file_path, "r", encoding = "utf-8") as f: + serialized = f.read() + try: + instance_container.deserialize(serialized, file_path) + except ContainerFormatError: + Logger.logException("e", "Unable to deserialize InstanceContainer %s", file_path) + continue + self.addContainer(instance_container) + break + + return instance_container + + # Fix the extruders that were upgraded to ExtruderStack instances during addContainer. + # The stacks are now responsible for setting the next stack on deserialize. However, + # due to problems with loading order, some stacks may not have the proper next stack + # set after upgrading, because the proper global stack was not yet loaded. This method + # makes sure those extruders also get the right stack set. + def _connectUpgradedExtruderStacksToMachines(self) -> None: + extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack) + for extruder_stack in extruder_stacks: + if extruder_stack.getNextStack(): + # Has the right next stack, so ignore it. + continue + + machines = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack.getMetaDataEntry("machine", "")) + if machines: + extruder_stack.setNextStack(machines[0]) + else: + Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId()) + + # Override just for the type. + @classmethod + @override(ContainerRegistry) + def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry": + return cast(CuraContainerRegistry, super().getInstance(*args, **kwargs)) diff --git a/cura/Settings/CuraContainerStack.py b/cura/Settings/CuraContainerStack.py index 1455e140a8..1551e46ef2 100755 --- a/cura/Settings/CuraContainerStack.py +++ b/cura/Settings/CuraContainerStack.py @@ -18,25 +18,27 @@ from cura.Settings import cura_empty_instance_containers from . import Exceptions -## Base class for Cura related stacks that want to enforce certain containers are available. -# -# This class makes sure that the stack has the following containers set: user changes, quality -# changes, quality, material, variant, definition changes and finally definition. Initially, -# these will be equal to the empty instance container. -# -# The container types are determined based on the following criteria: -# - user: An InstanceContainer with the metadata entry "type" set to "user". -# - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes". -# - quality: An InstanceContainer with the metadata entry "type" set to "quality". -# - material: An InstanceContainer with the metadata entry "type" set to "material". -# - variant: An InstanceContainer with the metadata entry "type" set to "variant". -# - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes". -# - definition: A DefinitionContainer. -# -# Internally, this class ensures the mentioned containers are always there and kept in a specific order. -# This also means that operations on the stack that modifies the container ordering is prohibited and -# will raise an exception. class CuraContainerStack(ContainerStack): + """Base class for Cura related stacks that want to enforce certain containers are available. + + This class makes sure that the stack has the following containers set: user changes, quality + changes, quality, material, variant, definition changes and finally definition. Initially, + these will be equal to the empty instance container. + + The container types are determined based on the following criteria: + - user: An InstanceContainer with the metadata entry "type" set to "user". + - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes". + - quality: An InstanceContainer with the metadata entry "type" set to "quality". + - material: An InstanceContainer with the metadata entry "type" set to "material". + - variant: An InstanceContainer with the metadata entry "type" set to "variant". + - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes". + - definition: A DefinitionContainer. + + Internally, this class ensures the mentioned containers are always there and kept in a specific order. + This also means that operations on the stack that modifies the container ordering is prohibited and + will raise an exception. + """ + def __init__(self, container_id: str) -> None: super().__init__(container_id) @@ -61,101 +63,131 @@ class CuraContainerStack(ContainerStack): # This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted. pyqtContainersChanged = pyqtSignal() - ## Set the user changes container. - # - # \param new_user_changes The new user changes container. It is expected to have a "type" metadata entry with the value "user". def setUserChanges(self, new_user_changes: InstanceContainer) -> None: + """Set the user changes container. + + :param new_user_changes: The new user changes container. It is expected to have a "type" metadata entry with the value "user". + """ + self.replaceContainer(_ContainerIndexes.UserChanges, new_user_changes) - ## Get the user changes container. - # - # \return The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged) def userChanges(self) -> InstanceContainer: + """Get the user changes container. + + :return: The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.UserChanges]) - ## Set the quality changes container. - # - # \param new_quality_changes The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes". def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None: + """Set the quality changes container. + + :param new_quality_changes: The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes". + """ + self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit) - ## Get the quality changes container. - # - # \return The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged) def qualityChanges(self) -> InstanceContainer: + """Get the quality changes container. + + :return: The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges]) - ## Set the intent container. - # - # \param new_intent The new intent container. It is expected to have a "type" metadata entry with the value "intent". def setIntent(self, new_intent: InstanceContainer, postpone_emit: bool = False) -> None: + """Set the intent container. + + :param new_intent: The new intent container. It is expected to have a "type" metadata entry with the value "intent". + """ + self.replaceContainer(_ContainerIndexes.Intent, new_intent, postpone_emit = postpone_emit) - ## Get the quality container. - # - # \return The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setIntent, notify = pyqtContainersChanged) def intent(self) -> InstanceContainer: + """Get the quality container. + + :return: The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Intent]) - ## Set the quality container. - # - # \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality". def setQuality(self, new_quality: InstanceContainer, postpone_emit: bool = False) -> None: + """Set the quality container. + + :param new_quality: The new quality container. It is expected to have a "type" metadata entry with the value "quality". + """ + self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit) - ## Get the quality container. - # - # \return The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged) def quality(self) -> InstanceContainer: + """Get the quality container. + + :return: The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Quality]) - ## Set the material container. - # - # \param new_material The new material container. It is expected to have a "type" metadata entry with the value "material". def setMaterial(self, new_material: InstanceContainer, postpone_emit: bool = False) -> None: + """Set the material container. + + :param new_material: The new material container. It is expected to have a "type" metadata entry with the value "material". + """ + self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit) - ## Get the material container. - # - # \return The material container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged) def material(self) -> InstanceContainer: + """Get the material container. + + :return: The material container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Material]) - ## Set the variant container. - # - # \param new_variant The new variant container. It is expected to have a "type" metadata entry with the value "variant". def setVariant(self, new_variant: InstanceContainer) -> None: + """Set the variant container. + + :param new_variant: The new variant container. It is expected to have a "type" metadata entry with the value "variant". + """ + self.replaceContainer(_ContainerIndexes.Variant, new_variant) - ## Get the variant container. - # - # \return The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged) def variant(self) -> InstanceContainer: + """Get the variant container. + + :return: The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Variant]) - ## Set the definition changes container. - # - # \param new_definition_changes The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes". def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None: + """Set the definition changes container. + + :param new_definition_changes: The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes". + """ + self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes) - ## Get the definition changes container. - # - # \return The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged) def definitionChanges(self) -> InstanceContainer: + """Get the definition changes container. + + :return: The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.DefinitionChanges]) - ## Set the definition container. - # - # \param new_definition The new definition container. It is expected to have a "type" metadata entry with the value "definition". def setDefinition(self, new_definition: DefinitionContainerInterface) -> None: + """Set the definition container. + + :param new_definition: The new definition container. It is expected to have a "type" metadata entry with the value "definition". + """ + self.replaceContainer(_ContainerIndexes.Definition, new_definition) def getDefinition(self) -> "DefinitionContainer": @@ -171,14 +203,16 @@ class CuraContainerStack(ContainerStack): def getTop(self) -> "InstanceContainer": return self.userChanges - ## Check whether the specified setting has a 'user' value. - # - # A user value here is defined as the setting having a value in either - # the UserChanges or QualityChanges container. - # - # \return True if the setting has a user value, False if not. @pyqtSlot(str, result = bool) def hasUserValue(self, key: str) -> bool: + """Check whether the specified setting has a 'user' value. + + A user value here is defined as the setting having a value in either + the UserChanges or QualityChanges container. + + :return: True if the setting has a user value, False if not. + """ + if self._containers[_ContainerIndexes.UserChanges].hasProperty(key, "value"): return True @@ -187,51 +221,61 @@ class CuraContainerStack(ContainerStack): return False - ## Set a property of a setting. - # - # This will set a property of a specified setting. Since the container stack does not contain - # any settings itself, it is required to specify a container to set the property on. The target - # container is matched by container type. - # - # \param key The key of the setting to set. - # \param property_name The name of the property to set. - # \param new_value The new value to set the property to. def setProperty(self, key: str, property_name: str, property_value: Any, container: "ContainerInterface" = None, set_from_cache: bool = False) -> None: + """Set a property of a setting. + + This will set a property of a specified setting. Since the container stack does not contain + any settings itself, it is required to specify a container to set the property on. The target + container is matched by container type. + + :param key: The key of the setting to set. + :param property_name: The name of the property to set. + :param new_value: The new value to set the property to. + """ + container_index = _ContainerIndexes.UserChanges self._containers[container_index].setProperty(key, property_name, property_value, container, set_from_cache) - ## Overridden from ContainerStack - # - # Since we have a fixed order of containers in the stack and this method would modify the container - # ordering, we disallow this operation. @override(ContainerStack) def addContainer(self, container: ContainerInterface) -> None: + """Overridden from ContainerStack + + Since we have a fixed order of containers in the stack and this method would modify the container + ordering, we disallow this operation. + """ + raise Exceptions.InvalidOperationError("Cannot add a container to Global stack") - ## Overridden from ContainerStack - # - # Since we have a fixed order of containers in the stack and this method would modify the container - # ordering, we disallow this operation. @override(ContainerStack) def insertContainer(self, index: int, container: ContainerInterface) -> None: + """Overridden from ContainerStack + + Since we have a fixed order of containers in the stack and this method would modify the container + ordering, we disallow this operation. + """ + raise Exceptions.InvalidOperationError("Cannot insert a container into Global stack") - ## Overridden from ContainerStack - # - # Since we have a fixed order of containers in the stack and this method would modify the container - # ordering, we disallow this operation. @override(ContainerStack) def removeContainer(self, index: int = 0) -> None: + """Overridden from ContainerStack + + Since we have a fixed order of containers in the stack and this method would modify the container + ordering, we disallow this operation. + """ + raise Exceptions.InvalidOperationError("Cannot remove a container from Global stack") - ## Overridden from ContainerStack - # - # Replaces the container at the specified index with another container. - # This version performs checks to make sure the new container has the expected metadata and type. - # - # \throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type. @override(ContainerStack) def replaceContainer(self, index: int, container: ContainerInterface, postpone_emit: bool = False) -> None: + """Overridden from ContainerStack + + Replaces the container at the specified index with another container. + This version performs checks to make sure the new container has the expected metadata and type. + + :throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type. + """ + expected_type = _ContainerIndexes.IndexTypeMap[index] if expected_type == "definition": if not isinstance(container, DefinitionContainer): @@ -245,16 +289,18 @@ class CuraContainerStack(ContainerStack): super().replaceContainer(index, container, postpone_emit) - ## Overridden from ContainerStack - # - # This deserialize will make sure the internal list of containers matches with what we expect. - # It will first check to see if the container at a certain index already matches with what we - # expect. If it does not, it will search for a matching container with the correct type. Should - # no container with the correct type be found, it will use the empty container. - # - # \throws InvalidContainerStackError Raised when no definition can be found for the stack. @override(ContainerStack) def deserialize(self, serialized: str, file_name: Optional[str] = None) -> str: + """Overridden from ContainerStack + + This deserialize will make sure the internal list of containers matches with what we expect. + It will first check to see if the container at a certain index already matches with what we + expect. If it does not, it will search for a matching container with the correct type. Should + no container with the correct type be found, it will use the empty container. + + :raise InvalidContainerStackError: Raised when no definition can be found for the stack. + """ + # update the serialized data first serialized = super().deserialize(serialized, file_name) @@ -298,10 +344,9 @@ class CuraContainerStack(ContainerStack): ## TODO; Deserialize the containers. return serialized - ## protected: - - # Helper to make sure we emit a PyQt signal on container changes. def _onContainersChanged(self, container: Any) -> None: + """Helper to make sure we emit a PyQt signal on container changes.""" + Application.getInstance().callLater(self.pyqtContainersChanged.emit) # Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine @@ -309,16 +354,18 @@ class CuraContainerStack(ContainerStack): def _getMachineDefinition(self) -> DefinitionContainer: return self.definition - ## Find the ID that should be used when searching for instance containers for a specified definition. - # - # This handles the situation where the definition specifies we should use a different definition when - # searching for instance containers. - # - # \param machine_definition The definition to find the "quality definition" for. - # - # \return The ID of the definition container to use when searching for instance containers. @classmethod def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str: + """Find the ID that should be used when searching for instance containers for a specified definition. + + This handles the situation where the definition specifies we should use a different definition when + searching for instance containers. + + :param machine_definition: The definition to find the "quality definition" for. + + :return: The ID of the definition container to use when searching for instance containers. + """ + quality_definition = machine_definition.getMetaDataEntry("quality_definition") if not quality_definition: return machine_definition.id #type: ignore @@ -330,17 +377,18 @@ class CuraContainerStack(ContainerStack): return cls._findInstanceContainerDefinitionId(definitions[0]) - ## getProperty for extruder positions, with translation from -1 to default extruder number def getExtruderPositionValueWithDefault(self, key): + """getProperty for extruder positions, with translation from -1 to default extruder number""" + value = self.getProperty(key, "value") if value == -1: value = int(Application.getInstance().getMachineManager().defaultExtruderPosition) return value -## private: -# Private helper class to keep track of container positions and their types. class _ContainerIndexes: + """Private helper class to keep track of container positions and their types.""" + UserChanges = 0 QualityChanges = 1 Intent = 2 diff --git a/cura/Settings/CuraStackBuilder.py b/cura/Settings/CuraStackBuilder.py index 257af78ecc..5dc32f6e24 100644 --- a/cura/Settings/CuraStackBuilder.py +++ b/cura/Settings/CuraStackBuilder.py @@ -13,17 +13,20 @@ from .GlobalStack import GlobalStack from .ExtruderStack import ExtruderStack -## Contains helper functions to create new machines. class CuraStackBuilder: + """Contains helper functions to create new machines.""" + - ## Create a new instance of a machine. - # - # \param name The name of the new machine. - # \param definition_id The ID of the machine definition to use. - # - # \return The new global stack or None if an error occurred. @classmethod def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]: + """Create a new instance of a machine. + + :param name: The name of the new machine. + :param definition_id: The ID of the machine definition to use. + + :return: The new global stack or None if an error occurred. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() @@ -71,12 +74,14 @@ class CuraStackBuilder: return new_global_stack - ## Create a default Extruder Stack - # - # \param global_stack The global stack this extruder refers to. - # \param extruder_position The position of the current extruder. @classmethod def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None: + """Create a default Extruder Stack + + :param global_stack: The global stack this extruder refers to. + :param extruder_position: The position of the current extruder. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() @@ -120,17 +125,6 @@ class CuraStackBuilder: registry.addContainer(new_extruder) - ## Create a new Extruder stack - # - # \param new_stack_id The ID of the new stack. - # \param extruder_definition The definition to base the new stack on. - # \param machine_definition_id The ID of the machine definition to use for the user container. - # \param position The position the extruder occupies in the machine. - # \param variant_container The variant selected for the current extruder. - # \param material_container The material selected for the current extruder. - # \param quality_container The quality selected for the current extruder. - # - # \return A new Extruder stack instance with the specified parameters. @classmethod def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface, machine_definition_id: str, @@ -139,6 +133,19 @@ class CuraStackBuilder: material_container: "InstanceContainer", quality_container: "InstanceContainer") -> ExtruderStack: + """Create a new Extruder stack + + :param new_stack_id: The ID of the new stack. + :param extruder_definition: The definition to base the new stack on. + :param machine_definition_id: The ID of the machine definition to use for the user container. + :param position: The position the extruder occupies in the machine. + :param variant_container: The variant selected for the current extruder. + :param material_container: The material selected for the current extruder. + :param quality_container: The quality selected for the current extruder. + + :return: A new Extruder stack instance with the specified parameters. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() @@ -167,29 +174,23 @@ class CuraStackBuilder: return stack - ## Create a new Global stack - # - # \param new_stack_id The ID of the new stack. - # \param definition The definition to base the new stack on. - # \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm" - # - # \return A new Global stack instance with the specified parameters. - - ## Create a new Global stack - # - # \param new_stack_id The ID of the new stack. - # \param definition The definition to base the new stack on. - # \param variant_container The variant selected for the current stack. - # \param material_container The material selected for the current stack. - # \param quality_container The quality selected for the current stack. - # - # \return A new Global stack instance with the specified parameters. @classmethod def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, variant_container: "InstanceContainer", material_container: "InstanceContainer", quality_container: "InstanceContainer") -> GlobalStack: + """Create a new Global stack + + :param new_stack_id: The ID of the new stack. + :param definition: The definition to base the new stack on. + :param variant_container: The variant selected for the current stack. + :param material_container: The material selected for the current stack. + :param quality_container: The quality selected for the current stack. + + :return: A new Global stack instance with the specified parameters. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() diff --git a/cura/Settings/Exceptions.py b/cura/Settings/Exceptions.py index 0a869cf922..fbb130417c 100644 --- a/cura/Settings/Exceptions.py +++ b/cura/Settings/Exceptions.py @@ -2,21 +2,25 @@ # Cura is released under the terms of the LGPLv3 or higher. -## Raised when trying to perform an operation like add on a stack that does not allow that. class InvalidOperationError(Exception): + """Raised when trying to perform an operation like add on a stack that does not allow that.""" + pass -## Raised when trying to replace a container with a container that does not have the expected type. class InvalidContainerError(Exception): + """Raised when trying to replace a container with a container that does not have the expected type.""" + pass -## Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders. class TooManyExtrudersError(Exception): + """Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders.""" + pass -## Raised when an extruder has no next stack set. class NoGlobalStackError(Exception): + """Raised when an extruder has no next stack set.""" + pass diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 4610e6a454..f9ffde4872 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -19,13 +19,15 @@ if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack -## Manages all existing extruder stacks. -# -# This keeps a list of extruder stacks for each machine. class ExtruderManager(QObject): + """Manages all existing extruder stacks. + + This keeps a list of extruder stacks for each machine. + """ - ## Registers listeners and such to listen to changes to the extruders. def __init__(self, parent = None): + """Registers listeners and such to listen to changes to the extruders.""" + if ExtruderManager.__instance is not None: raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) ExtruderManager.__instance = self @@ -43,20 +45,22 @@ class ExtruderManager(QObject): Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) - ## Signal to notify other components when the list of extruders for a machine definition changes. extrudersChanged = pyqtSignal(QVariant) + """Signal to notify other components when the list of extruders for a machine definition changes.""" - ## Notify when the user switches the currently active extruder. activeExtruderChanged = pyqtSignal() + """Notify when the user switches the currently active extruder.""" - ## Gets the unique identifier of the currently active extruder stack. - # - # The currently active extruder stack is the stack that is currently being - # edited. - # - # \return The unique ID of the currently active extruder stack. @pyqtProperty(str, notify = activeExtruderChanged) def activeExtruderStackId(self) -> Optional[str]: + """Gets the unique identifier of the currently active extruder stack. + + The currently active extruder stack is the stack that is currently being + edited. + + :return: The unique ID of the currently active extruder stack. + """ + if not self._application.getGlobalContainerStack(): return None # No active machine, so no active extruder. try: @@ -64,9 +68,10 @@ class ExtruderManager(QObject): except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong. return None - ## Gets a dict with the extruder stack ids with the extruder number as the key. @pyqtProperty("QVariantMap", notify = extrudersChanged) def extruderIds(self) -> Dict[str, str]: + """Gets a dict with the extruder stack ids with the extruder number as the key.""" + extruder_stack_ids = {} # type: Dict[str, str] global_container_stack = self._application.getGlobalContainerStack() @@ -75,11 +80,13 @@ class ExtruderManager(QObject): return extruder_stack_ids - ## Changes the active extruder by index. - # - # \param index The index of the new active extruder. @pyqtSlot(int) def setActiveExtruderIndex(self, index: int) -> None: + """Changes the active extruder by index. + + :param index: The index of the new active extruder. + """ + if self._active_extruder_index != index: self._active_extruder_index = index self.activeExtruderChanged.emit() @@ -88,12 +95,13 @@ class ExtruderManager(QObject): def activeExtruderIndex(self) -> int: return self._active_extruder_index - ## Emitted whenever the selectedObjectExtruders property changes. selectedObjectExtrudersChanged = pyqtSignal() + """Emitted whenever the selectedObjectExtruders property changes.""" - ## Provides a list of extruder IDs used by the current selected objects. @pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged) def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]: + """Provides a list of extruder IDs used by the current selected objects.""" + if not self._selected_object_extruders: object_extruders = set() @@ -122,11 +130,13 @@ class ExtruderManager(QObject): return self._selected_object_extruders - ## Reset the internal list used for the selectedObjectExtruders property - # - # This will trigger a recalculation of the extruders used for the - # selection. def resetSelectedObjectExtruders(self) -> None: + """Reset the internal list used for the selectedObjectExtruders property + + This will trigger a recalculation of the extruders used for the + selection. + """ + self._selected_object_extruders = [] self.selectedObjectExtrudersChanged.emit() @@ -134,8 +144,9 @@ class ExtruderManager(QObject): def getActiveExtruderStack(self) -> Optional["ExtruderStack"]: return self.getExtruderStack(self.activeExtruderIndex) - ## Get an extruder stack by index def getExtruderStack(self, index) -> Optional["ExtruderStack"]: + """Get an extruder stack by index""" + global_container_stack = self._application.getGlobalContainerStack() if global_container_stack: if global_container_stack.getId() in self._extruder_trains: @@ -162,12 +173,14 @@ class ExtruderManager(QObject): if changed: self.extrudersChanged.emit(machine_id) - ## Gets a property of a setting for all extruders. - # - # \param setting_key \type{str} The setting to get the property of. - # \param property \type{str} The property to get. - # \return \type{List} the list of results def getAllExtruderSettings(self, setting_key: str, prop: str) -> List[Any]: + """Gets a property of a setting for all extruders. + + :param setting_key: :type{str} The setting to get the property of. + :param prop: :type{str} The property to get. + :return: :type{List} the list of results + """ + result = [] for extruder_stack in self.getActiveExtruderStacks(): @@ -182,17 +195,19 @@ class ExtruderManager(QObject): else: return value - ## Gets the extruder stacks that are actually being used at the moment. - # - # An extruder stack is being used if it is the extruder to print any mesh - # with, or if it is the support infill extruder, the support interface - # extruder, or the bed adhesion extruder. - # - # If there are no extruders, this returns the global stack as a singleton - # list. - # - # \return A list of extruder stacks. def getUsedExtruderStacks(self) -> List["ExtruderStack"]: + """Gets the extruder stacks that are actually being used at the moment. + + An extruder stack is being used if it is the extruder to print any mesh + with, or if it is the support infill extruder, the support interface + extruder, or the bed adhesion extruder. + + If there are no extruders, this returns the global stack as a singleton + list. + + :return: A list of extruder stacks. + """ + global_stack = self._application.getGlobalContainerStack() container_registry = ContainerRegistry.getInstance() @@ -277,11 +292,13 @@ class ExtruderManager(QObject): Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids) return [] - ## Get the extruder that the print will start with. - # - # This should mirror the implementation in CuraEngine of - # ``FffGcodeWriter::getStartExtruder()``. def getInitialExtruderNr(self) -> int: + """Get the extruder that the print will start with. + + This should mirror the implementation in CuraEngine of + ``FffGcodeWriter::getStartExtruder()``. + """ + application = cura.CuraApplication.CuraApplication.getInstance() global_stack = application.getGlobalContainerStack() @@ -296,28 +313,34 @@ class ExtruderManager(QObject): # REALLY no adhesion? Use the first used extruder. return self.getUsedExtruderStacks()[0].getProperty("extruder_nr", "value") - ## Removes the container stack and user profile for the extruders for a specific machine. - # - # \param machine_id The machine to remove the extruders for. def removeMachineExtruders(self, machine_id: str) -> None: + """Removes the container stack and user profile for the extruders for a specific machine. + + :param machine_id: The machine to remove the extruders for. + """ + for extruder in self.getMachineExtruders(machine_id): ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId()) ContainerRegistry.getInstance().removeContainer(extruder.getId()) if machine_id in self._extruder_trains: del self._extruder_trains[machine_id] - ## Returns extruders for a specific machine. - # - # \param machine_id The machine to get the extruders of. def getMachineExtruders(self, machine_id: str) -> List["ExtruderStack"]: + """Returns extruders for a specific machine. + + :param machine_id: The machine to get the extruders of. + """ + if machine_id not in self._extruder_trains: return [] return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]] - ## Returns the list of active extruder stacks, taking into account the machine extruder count. - # - # \return \type{List[ContainerStack]} a list of def getActiveExtruderStacks(self) -> List["ExtruderStack"]: + """Returns the list of active extruder stacks, taking into account the machine extruder count. + + :return: :type{List[ContainerStack]} a list of + """ + global_stack = self._application.getGlobalContainerStack() if not global_stack: return [] @@ -329,8 +352,9 @@ class ExtruderManager(QObject): self.resetSelectedObjectExtruders() - ## Adds the extruders to the selected machine. def addMachineExtruders(self, global_stack: GlobalStack) -> None: + """Adds the extruders to the selected machine.""" + extruders_changed = False container_registry = ContainerRegistry.getInstance() global_stack_id = global_stack.getId() @@ -396,26 +420,30 @@ class ExtruderManager(QObject): raise IndexError(msg) extruder_stack_0.definition = extruder_definition - ## Get all extruder values for a certain setting. - # - # This is exposed to qml for display purposes - # - # \param key The key of the setting to retrieve values for. - # - # \return String representing the extruder values @pyqtSlot(str, result="QVariant") def getInstanceExtruderValues(self, key: str) -> List: + """Get all extruder values for a certain setting. + + This is exposed to qml for display purposes + + :param key: The key of the setting to retrieve values for. + + :return: String representing the extruder values + """ + return self._application.getCuraFormulaFunctions().getValuesInAllExtruders(key) - ## Get the resolve value or value for a given key - # - # This is the effective value for a given key, it is used for values in the global stack. - # This is exposed to SettingFunction to use in value functions. - # \param key The key of the setting to get the value of. - # - # \return The effective value @staticmethod def getResolveOrValue(key: str) -> Any: + """Get the resolve value or value for a given key + + This is the effective value for a given key, it is used for values in the global stack. + This is exposed to SettingFunction to use in value functions. + :param key: The key of the setting to get the value of. + + :return: The effective value + """ + global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) resolved_value = global_stack.getProperty(key, "value") diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index 5d4b3e38b1..7369838baa 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -22,10 +22,9 @@ if TYPE_CHECKING: from cura.Settings.GlobalStack import GlobalStack -## Represents an Extruder and its related containers. -# -# class ExtruderStack(CuraContainerStack): + """Represents an Extruder and its related containers.""" + def __init__(self, container_id: str) -> None: super().__init__(container_id) @@ -35,11 +34,13 @@ class ExtruderStack(CuraContainerStack): enabledChanged = pyqtSignal() - ## Overridden from ContainerStack - # - # This will set the next stack and ensure that we register this stack as an extruder. @override(ContainerStack) def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None: + """Overridden from ContainerStack + + This will set the next stack and ensure that we register this stack as an extruder. + """ + super().setNextStack(stack) stack.addExtruder(self) self.setMetaDataEntry("machine", stack.id) @@ -71,11 +72,13 @@ class ExtruderStack(CuraContainerStack): compatibleMaterialDiameterChanged = pyqtSignal() - ## Return the filament diameter that the machine requires. - # - # If the machine has no requirement for the diameter, -1 is returned. - # \return The filament diameter for the printer def getCompatibleMaterialDiameter(self) -> float: + """Return the filament diameter that the machine requires. + + If the machine has no requirement for the diameter, -1 is returned. + :return: The filament diameter for the printer + """ + context = PropertyEvaluationContext(self) context.context["evaluate_from_container_index"] = _ContainerIndexes.Variant @@ -97,31 +100,35 @@ class ExtruderStack(CuraContainerStack): approximateMaterialDiameterChanged = pyqtSignal() - ## Return the approximate filament diameter that the machine requires. - # - # The approximate material diameter is the material diameter rounded to - # the nearest millimetre. - # - # If the machine has no requirement for the diameter, -1 is returned. - # - # \return The approximate filament diameter for the printer def getApproximateMaterialDiameter(self) -> float: + """Return the approximate filament diameter that the machine requires. + + The approximate material diameter is the material diameter rounded to + the nearest millimetre. + + If the machine has no requirement for the diameter, -1 is returned. + + :return: The approximate filament diameter for the printer + """ + return round(self.getCompatibleMaterialDiameter()) approximateMaterialDiameter = pyqtProperty(float, fget = getApproximateMaterialDiameter, notify = approximateMaterialDiameterChanged) - ## Overridden from ContainerStack - # - # It will perform a few extra checks when trying to get properties. - # - # The two extra checks it currently does is to ensure a next stack is set and to bypass - # the extruder when the property is not settable per extruder. - # - # \throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without - # having a next stack set. @override(ContainerStack) def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: + """Overridden from ContainerStack + + It will perform a few extra checks when trying to get properties. + + The two extra checks it currently does is to ensure a next stack is set and to bypass + the extruder when the property is not settable per extruder. + + :throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without + having a next stack set. + """ + if not self._next_stack: raise Exceptions.NoGlobalStackError("Extruder {id} is missing the next stack!".format(id = self.id)) diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index d3a8842aa3..e020221187 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -29,9 +29,9 @@ if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack -## Represents the Global or Machine stack and its related containers. -# class GlobalStack(CuraContainerStack): + """Represents the Global or Machine stack and its related containers.""" + def __init__(self, container_id: str) -> None: super().__init__(container_id) @@ -58,12 +58,14 @@ class GlobalStack(CuraContainerStack): extrudersChanged = pyqtSignal() configuredConnectionTypesChanged = pyqtSignal() - ## Get the list of extruders of this stack. - # - # \return The extruders registered with this stack. @pyqtProperty("QVariantMap", notify = extrudersChanged) @deprecated("Please use extruderList instead.", "4.4") def extruders(self) -> Dict[str, "ExtruderStack"]: + """Get the list of extruders of this stack. + + :return: The extruders registered with this stack. + """ + return self._extruders @pyqtProperty("QVariantList", notify = extrudersChanged) @@ -86,16 +88,18 @@ class GlobalStack(CuraContainerStack): def getLoadingPriority(cls) -> int: return 2 - ## The configured connection types can be used to find out if the global - # stack is configured to be connected with a printer, without having to - # know all the details as to how this is exactly done (and without - # actually setting the stack to be active). - # - # This data can then in turn also be used when the global stack is active; - # If we can't get a network connection, but it is configured to have one, - # we can display a different icon to indicate the difference. @pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged) def configuredConnectionTypes(self) -> List[int]: + """The configured connection types can be used to find out if the global + + stack is configured to be connected with a printer, without having to + know all the details as to how this is exactly done (and without + actually setting the stack to be active). + + This data can then in turn also be used when the global stack is active; + If we can't get a network connection, but it is configured to have one, + we can display a different icon to indicate the difference. + """ # Requesting it from the metadata actually gets them as strings (as that's what you get from serializing). # But we do want them returned as a list of ints (so the rest of the code can directly compare) connection_types = self.getMetaDataEntry("connection_type", "").split(",") @@ -122,16 +126,18 @@ class GlobalStack(CuraContainerStack): ConnectionType.CloudConnection.value] return has_remote_connection - ## \sa configuredConnectionTypes def addConfiguredConnectionType(self, connection_type: int) -> None: + """:sa configuredConnectionTypes""" + configured_connection_types = self.configuredConnectionTypes if connection_type not in configured_connection_types: # Store the values as a string. configured_connection_types.append(connection_type) self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types])) - ## \sa configuredConnectionTypes def removeConfiguredConnectionType(self, connection_type: int) -> None: + """:sa configuredConnectionTypes""" + configured_connection_types = self.configuredConnectionTypes if connection_type in configured_connection_types: # Store the values as a string. @@ -163,13 +169,15 @@ class GlobalStack(CuraContainerStack): def preferred_output_file_formats(self) -> str: return self.getMetaDataEntry("file_formats") - ## Add an extruder to the list of extruders of this stack. - # - # \param extruder The extruder to add. - # - # \throws Exceptions.TooManyExtrudersError Raised when trying to add an extruder while we - # already have the maximum number of extruders. def addExtruder(self, extruder: ContainerStack) -> None: + """Add an extruder to the list of extruders of this stack. + + :param extruder: The extruder to add. + + :raise Exceptions.TooManyExtrudersError: Raised when trying to add an extruder while we + already have the maximum number of extruders. + """ + position = extruder.getMetaDataEntry("position") if position is None: Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id) @@ -183,19 +191,21 @@ class GlobalStack(CuraContainerStack): self.extrudersChanged.emit() Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position) - ## Overridden from ContainerStack - # - # This will return the value of the specified property for the specified setting, - # unless the property is "value" and that setting has a "resolve" function set. - # When a resolve is set, it will instead try and execute the resolve first and - # then fall back to the normal "value" property. - # - # \param key The setting key to get the property of. - # \param property_name The property to get the value of. - # - # \return The value of the property for the specified setting, or None if not found. @override(ContainerStack) def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: + """Overridden from ContainerStack + + This will return the value of the specified property for the specified setting, + unless the property is "value" and that setting has a "resolve" function set. + When a resolve is set, it will instead try and execute the resolve first and + then fall back to the normal "value" property. + + :param key: The setting key to get the property of. + :param property_name: The property to get the value of. + + :return: The value of the property for the specified setting, or None if not found. + """ + if not self.definition.findDefinitions(key = key): return None @@ -235,11 +245,13 @@ class GlobalStack(CuraContainerStack): context.popContainer() return result - ## Overridden from ContainerStack - # - # This will simply raise an exception since the Global stack cannot have a next stack. @override(ContainerStack) def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None: + """Overridden from ContainerStack + + This will simply raise an exception since the Global stack cannot have a next stack. + """ + raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!") # protected: @@ -267,9 +279,11 @@ class GlobalStack(CuraContainerStack): return True - ## Perform some sanity checks on the global stack - # Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1 def isValid(self) -> bool: + """Perform some sanity checks on the global stack + + Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1 + """ container_registry = ContainerRegistry.getInstance() extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId()) @@ -299,9 +313,10 @@ class GlobalStack(CuraContainerStack): def hasVariantBuildplates(self) -> bool: return parseBool(self.getMetaDataEntry("has_variant_buildplates", False)) - ## Get default firmware file name if one is specified in the firmware @pyqtSlot(result = str) def getDefaultFirmwareName(self) -> str: + """Get default firmware file name if one is specified in the firmware""" + machine_has_heated_bed = self.getProperty("machine_heated_bed", "value") baudrate = 250000 diff --git a/cura/Settings/IntentManager.py b/cura/Settings/IntentManager.py index 10ea8dff6a..9f636c9cc1 100644 --- a/cura/Settings/IntentManager.py +++ b/cura/Settings/IntentManager.py @@ -15,29 +15,32 @@ if TYPE_CHECKING: from UM.Settings.InstanceContainer import InstanceContainer -## Front-end for querying which intents are available for a certain -# configuration. class IntentManager(QObject): + """Front-end for querying which intents are available for a certain configuration. + """ __instance = None - ## This class is a singleton. @classmethod def getInstance(cls): + """This class is a singleton.""" + if not cls.__instance: cls.__instance = IntentManager() return cls.__instance intentCategoryChanged = pyqtSignal() #Triggered when we switch categories. - ## Gets the metadata dictionaries of all intent profiles for a given - # configuration. - # - # \param definition_id ID of the printer. - # \param nozzle_name Name of the nozzle. - # \param material_base_file The base_file of the material. - # \return A list of metadata dictionaries matching the search criteria, or - # an empty list if nothing was found. def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]: + """Gets the metadata dictionaries of all intent profiles for a given + + configuration. + + :param definition_id: ID of the printer. + :param nozzle_name: Name of the nozzle. + :param material_base_file: The base_file of the material. + :return: A list of metadata dictionaries matching the search criteria, or + an empty list if nothing was found. + """ intent_metadatas = [] # type: List[Dict[str, Any]] try: materials = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials @@ -53,28 +56,32 @@ class IntentManager(QObject): intent_metadatas.append(intent_node.getMetadata()) return intent_metadatas - ## Collects and returns all intent categories available for the given - # parameters. Note that the 'default' category is always available. - # - # \param definition_id ID of the printer. - # \param nozzle_name Name of the nozzle. - # \param material_id ID of the material. - # \return A set of intent category names. def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]: + """Collects and returns all intent categories available for the given + + parameters. Note that the 'default' category is always available. + + :param definition_id: ID of the printer. + :param nozzle_name: Name of the nozzle. + :param material_id: ID of the material. + :return: A set of intent category names. + """ categories = set() for intent in self.intentMetadatas(definition_id, nozzle_id, material_id): categories.add(intent["intent_category"]) categories.add("default") #The "empty" intent is not an actual profile specific to the configuration but we do want it to appear in the categories list. return list(categories) - ## List of intents to be displayed in the interface. - # - # For the interface this will have to be broken up into the different - # intent categories. That is up to the model there. - # - # \return A list of tuples of intent_category and quality_type. The actual - # instance may vary per extruder. def getCurrentAvailableIntents(self) -> List[Tuple[str, str]]: + """List of intents to be displayed in the interface. + + For the interface this will have to be broken up into the different + intent categories. That is up to the model there. + + :return: A list of tuples of intent_category and quality_type. The actual + instance may vary per extruder. + """ + application = cura.CuraApplication.CuraApplication.getInstance() global_stack = application.getGlobalContainerStack() if global_stack is None: @@ -100,16 +107,18 @@ class IntentManager(QObject): result.add((intent_metadata["intent_category"], intent_metadata["quality_type"])) return list(result) - ## List of intent categories available in either of the extruders. - # - # This is purposefully inconsistent with the way that the quality types - # are listed. The quality types will show all quality types available in - # the printer using any configuration. This will only list the intent - # categories that are available using the current configuration (but the - # union over the extruders). - # \return List of all categories in the current configurations of all - # extruders. def currentAvailableIntentCategories(self) -> List[str]: + """List of intent categories available in either of the extruders. + + This is purposefully inconsistent with the way that the quality types + are listed. The quality types will show all quality types available in + the printer using any configuration. This will only list the intent + categories that are available using the current configuration (but the + union over the extruders). + :return: List of all categories in the current configurations of all + extruders. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return ["default"] @@ -123,10 +132,12 @@ class IntentManager(QObject): final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id)) return list(final_intent_categories) - ## The intent that gets selected by default when no intent is available for - # the configuration, an extruder can't match the intent that the user - # selects, or just when creating a new printer. def getDefaultIntent(self) -> "InstanceContainer": + """The intent that gets selected by default when no intent is available for + + the configuration, an extruder can't match the intent that the user + selects, or just when creating a new printer. + """ return empty_intent_container @pyqtProperty(str, notify = intentCategoryChanged) @@ -137,9 +148,10 @@ class IntentManager(QObject): return "" return active_extruder_stack.intent.getMetaDataEntry("intent_category", "") - ## Apply intent on the stacks. @pyqtSlot(str, str) def selectIntent(self, intent_category: str, quality_type: str) -> None: + """Apply intent on the stacks.""" + Logger.log("i", "Attempting to set intent_category to [%s] and quality type to [%s]", intent_category, quality_type) old_intent_category = self.currentIntentCategory application = cura.CuraApplication.CuraApplication.getInstance() diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 2866e3a494..e56a3e38f3 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -215,8 +215,9 @@ class MachineManager(QObject): return set() return general_definition_containers[0].getAllKeys() - ## Triggered when the global container stack is changed in CuraApplication. def _onGlobalContainerChanged(self) -> None: + """Triggered when the global container stack is changed in CuraApplication.""" + if self._global_container_stack: try: self._global_container_stack.containersChanged.disconnect(self._onContainersChanged) @@ -338,12 +339,15 @@ class MachineManager(QObject): Logger.log("w", "An extruder has an unknown material, switching it to the preferred material") self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material) - ## Given a definition id, return the machine with this id. - # Optional: add a list of keys and values to filter the list of machines with the given definition id - # \param definition_id \type{str} definition id that needs to look for - # \param metadata_filter \type{dict} list of metadata keys and values used for filtering @staticmethod def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]: + """Given a definition id, return the machine with this id. + + Optional: add a list of keys and values to filter the list of machines with the given definition id + :param definition_id: :type{str} definition id that needs to look for + :param metadata_filter: :type{dict} list of metadata keys and values used for filtering + """ + if metadata_filter is None: metadata_filter = {} machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) @@ -397,9 +401,10 @@ class MachineManager(QObject): Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start)) return False - ## Check if the global_container has instances in the user container @pyqtProperty(bool, notify = activeStackValueChanged) def hasUserSettings(self) -> bool: + """Check if the global_container has instances in the user container""" + if not self._global_container_stack: return False @@ -422,10 +427,12 @@ class MachineManager(QObject): num_user_settings += stack.getTop().getNumInstances() return num_user_settings - ## Delete a user setting from the global stack and all extruder stacks. - # \param key \type{str} the name of the key to delete @pyqtSlot(str) def clearUserSettingAllCurrentStacks(self, key: str) -> None: + """Delete a user setting from the global stack and all extruder stacks. + + :param key: :type{str} the name of the key to delete + """ Logger.log("i", "Clearing the setting [%s] from all stacks", key) if not self._global_container_stack: return @@ -454,11 +461,13 @@ class MachineManager(QObject): for container in send_emits_containers: container.sendPostponedEmits() - ## Check if none of the stacks contain error states - # Note that the _stacks_have_errors is cached due to performance issues - # Calling _checkStack(s)ForErrors on every change is simply too expensive @pyqtProperty(bool, notify = stacksValidationChanged) def stacksHaveErrors(self) -> bool: + """Check if none of the stacks contain error states + + Note that the _stacks_have_errors is cached due to performance issues + Calling _checkStack(s)ForErrors on every change is simply too expensive + """ return bool(self._stacks_have_errors) @pyqtProperty(str, notify = globalContainerChanged) @@ -528,14 +537,16 @@ class MachineManager(QObject): return material.getId() return "" - ## Gets the layer height of the currently active quality profile. - # - # This is indicated together with the name of the active quality profile. - # - # \return The layer height of the currently active quality profile. If - # there is no quality profile, this returns the default layer height. @pyqtProperty(float, notify = activeQualityGroupChanged) def activeQualityLayerHeight(self) -> float: + """Gets the layer height of the currently active quality profile. + + This is indicated together with the name of the active quality profile. + + :return: The layer height of the currently active quality profile. If + there is no quality profile, this returns the default layer height. + """ + if not self._global_container_stack: return 0 value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = self._global_container_stack.qualityChanges.getId()) @@ -605,13 +616,15 @@ class MachineManager(QObject): return result - ## Returns whether there is anything unsupported in the current set-up. - # - # The current set-up signifies the global stack and all extruder stacks, - # so this indicates whether there is any container in any of the container - # stacks that is not marked as supported. @pyqtProperty(bool, notify = activeQualityChanged) def isCurrentSetupSupported(self) -> bool: + """Returns whether there is anything unsupported in the current set-up. + + The current set-up signifies the global stack and all extruder stacks, + so this indicates whether there is any container in any of the container + stacks that is not marked as supported. + """ + if not self._global_container_stack: return False for stack in [self._global_container_stack] + self._global_container_stack.extruderList: @@ -622,9 +635,10 @@ class MachineManager(QObject): return False return True - ## Copy the value of the setting of the current extruder to all other extruders as well as the global container. @pyqtSlot(str) def copyValueToExtruders(self, key: str) -> None: + """Copy the value of the setting of the current extruder to all other extruders as well as the global container.""" + if self._active_container_stack is None or self._global_container_stack is None: return new_value = self._active_container_stack.getProperty(key, "value") @@ -634,9 +648,10 @@ class MachineManager(QObject): if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value: extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved - ## Copy the value of all manually changed settings of the current extruder to all other extruders. @pyqtSlot() def copyAllValuesToExtruders(self) -> None: + """Copy the value of all manually changed settings of the current extruder to all other extruders.""" + if self._active_container_stack is None or self._global_container_stack is None: return @@ -648,19 +663,23 @@ class MachineManager(QObject): # Check if the value has to be replaced extruder_stack.userChanges.setProperty(key, "value", new_value) - ## Get the Definition ID to use to select quality profiles for the currently active machine - # \returns DefinitionID (string) if found, empty string otherwise @pyqtProperty(str, notify = globalContainerChanged) def activeQualityDefinitionId(self) -> str: + """Get the Definition ID to use to select quality profiles for the currently active machine + + :returns: DefinitionID (string) if found, empty string otherwise + """ global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if not global_stack: return "" return ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition - ## Gets how the active definition calls variants - # Caveat: per-definition-variant-title is currently not translated (though the fallback is) @pyqtProperty(str, notify = globalContainerChanged) def activeDefinitionVariantsName(self) -> str: + """Gets how the active definition calls variants + + Caveat: per-definition-variant-title is currently not translated (though the fallback is) + """ fallback_title = catalog.i18nc("@label", "Nozzle") if self._global_container_stack: return self._global_container_stack.definition.getMetaDataEntry("variants_name", fallback_title) @@ -708,9 +727,10 @@ class MachineManager(QObject): # This reuses the method and remove all printers recursively self.removeMachine(hidden_containers[0].getId()) - ## The selected buildplate is compatible if it is compatible with all the materials in all the extruders @pyqtProperty(bool, notify = activeMaterialChanged) def variantBuildplateCompatible(self) -> bool: + """The selected buildplate is compatible if it is compatible with all the materials in all the extruders""" + if not self._global_container_stack: return True @@ -727,10 +747,12 @@ class MachineManager(QObject): return buildplate_compatible - ## The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible - # for the other material but the buildplate is still usable @pyqtProperty(bool, notify = activeMaterialChanged) def variantBuildplateUsable(self) -> bool: + """The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible + + for the other material but the buildplate is still usable + """ if not self._global_container_stack: return True @@ -751,11 +773,13 @@ class MachineManager(QObject): return result - ## Get the Definition ID of a machine (specified by ID) - # \param machine_id string machine id to get the definition ID of - # \returns DefinitionID if found, None otherwise @pyqtSlot(str, result = str) def getDefinitionByMachineId(self, machine_id: str) -> Optional[str]: + """Get the Definition ID of a machine (specified by ID) + + :param machine_id: string machine id to get the definition ID of + :returns: DefinitionID if found, None otherwise + """ containers = CuraContainerRegistry.getInstance().findContainerStacks(id = machine_id) if containers: return containers[0].definition.getId() @@ -786,8 +810,9 @@ class MachineManager(QObject): Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value) return result - ## Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed def correctExtruderSettings(self) -> None: + """Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed""" + if self._global_container_stack is None: return for setting_key in self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.userChanges): @@ -803,9 +828,11 @@ class MachineManager(QObject): title = catalog.i18nc("@info:title", "Settings updated")) caution_message.show() - ## Set the amount of extruders on the active machine (global stack) - # \param extruder_count int the number of extruders to set def setActiveMachineExtruderCount(self, extruder_count: int) -> None: + """Set the amount of extruders on the active machine (global stack) + + :param extruder_count: int the number of extruders to set + """ if self._global_container_stack is None: return extruder_manager = self._application.getExtruderManager() @@ -902,9 +929,10 @@ class MachineManager(QObject): def defaultExtruderPosition(self) -> str: return self._default_extruder_position - ## This will fire the propertiesChanged for all settings so they will be updated in the front-end @pyqtSlot() def forceUpdateAllSettings(self) -> None: + """This will fire the propertiesChanged for all settings so they will be updated in the front-end""" + if self._global_container_stack is None: return with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): @@ -945,11 +973,13 @@ class MachineManager(QObject): def _onMaterialNameChanged(self) -> None: self.activeMaterialChanged.emit() - ## Get the signals that signal that the containers changed for all stacks. - # - # This includes the global stack and all extruder stacks. So if any - # container changed anywhere. def _getContainerChangedSignals(self) -> List[Signal]: + """Get the signals that signal that the containers changed for all stacks. + + This includes the global stack and all extruder stacks. So if any + container changed anywhere. + """ + if self._global_container_stack is None: return [] return [s.containersChanged for s in self._global_container_stack.extruderList + [self._global_container_stack]] @@ -962,18 +992,21 @@ class MachineManager(QObject): container = extruder.userChanges container.setProperty(setting_name, property_name, property_value) - ## Reset all setting properties of a setting for all extruders. - # \param setting_name The ID of the setting to reset. @pyqtSlot(str) def resetSettingForAllExtruders(self, setting_name: str) -> None: + """Reset all setting properties of a setting for all extruders. + + :param setting_name: The ID of the setting to reset. + """ if self._global_container_stack is None: return for extruder in self._global_container_stack.extruderList: container = extruder.userChanges container.removeInstance(setting_name) - ## Update _current_root_material_id when the current root material was changed. def _onRootMaterialChanged(self) -> None: + """Update _current_root_material_id when the current root material was changed.""" + self._current_root_material_id = {} changed = False @@ -1135,8 +1168,9 @@ class MachineManager(QObject): return False return True - ## Update current quality type and machine after setting material def _updateQualityWithMaterial(self, *args: Any) -> None: + """Update current quality type and machine after setting material""" + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return @@ -1177,8 +1211,9 @@ class MachineManager(QObject): current_quality_type, quality_type) self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True) - ## Update the current intent after the quality changed def _updateIntentWithQuality(self): + """Update the current intent after the quality changed""" + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return @@ -1205,12 +1240,14 @@ class MachineManager(QObject): category = current_category self.setIntentByCategory(category) - ## Update the material profile in the current stacks when the variant is - # changed. - # \param position The extruder stack to update. If provided with None, all - # extruder stacks will be updated. @pyqtSlot() def updateMaterialWithVariant(self, position: Optional[str] = None) -> None: + """Update the material profile in the current stacks when the variant is + + changed. + :param position: The extruder stack to update. If provided with None, all + extruder stacks will be updated. + """ if self._global_container_stack is None: return if position is None: @@ -1245,10 +1282,12 @@ class MachineManager(QObject): material_node = nozzle_node.preferredMaterial(approximate_material_diameter) self._setMaterial(position_item, material_node) - ## Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new - # instance with the same network key. @pyqtSlot(str) def switchPrinterType(self, machine_name: str) -> None: + """Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new + + instance with the same network key. + """ # Don't switch if the user tries to change to the same type of printer if self._global_container_stack is None or self._global_container_stack.definition.name == machine_name: return @@ -1400,10 +1439,12 @@ class MachineManager(QObject): material_node = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials[root_material_id] self.setMaterial(position, material_node) - ## Global_stack: if you want to provide your own global_stack instead of the current active one - # if you update an active machine, special measures have to be taken. @pyqtSlot(str, "QVariant") def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None: + """Global_stack: if you want to provide your own global_stack instead of the current active one + + if you update an active machine, special measures have to be taken. + """ if global_stack is not None and global_stack != self._global_container_stack: global_stack.extruders[position].material = container_node.container return @@ -1449,10 +1490,12 @@ class MachineManager(QObject): # Get all the quality groups for this global stack and filter out by quality_type self.setQualityGroup(ContainerTree.getInstance().getCurrentQualityGroups()[quality_type]) - ## Optionally provide global_stack if you want to use your own - # The active global_stack is treated differently. @pyqtSlot(QObject) def setQualityGroup(self, quality_group: "QualityGroup", no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None: + """Optionally provide global_stack if you want to use your own + + The active global_stack is treated differently. + """ if global_stack is not None and global_stack != self._global_container_stack: if quality_group is None: Logger.log("e", "Could not set quality group because quality group is None") @@ -1514,15 +1557,17 @@ class MachineManager(QObject): return {"main": main_part, "suffix": suffix_part} - ## Change the intent category of the current printer. - # - # All extruders can change their profiles. If an intent profile is - # available with the desired intent category, that one will get chosen. - # Otherwise the intent profile will be left to the empty profile, which - # represents the "default" intent category. - # \param intent_category The intent category to change to. @pyqtSlot(str) def setIntentByCategory(self, intent_category: str) -> None: + """Change the intent category of the current printer. + + All extruders can change their profiles. If an intent profile is + available with the desired intent category, that one will get chosen. + Otherwise the intent profile will be left to the empty profile, which + represents the "default" intent category. + :param intent_category: The intent category to change to. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return @@ -1554,21 +1599,25 @@ class MachineManager(QObject): else: # No intent had the correct category. extruder.intent = empty_intent_container - ## Get the currently activated quality group. - # - # If no printer is added yet or the printer doesn't have quality profiles, - # this returns ``None``. - # \return The currently active quality group. def activeQualityGroup(self) -> Optional["QualityGroup"]: + """Get the currently activated quality group. + + If no printer is added yet or the printer doesn't have quality profiles, + this returns ``None``. + :return: The currently active quality group. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if not global_stack or global_stack.quality == empty_quality_container: return None return ContainerTree.getInstance().getCurrentQualityGroups().get(self.activeQualityType) - ## Get the name of the active quality group. - # \return The name of the active quality group. @pyqtProperty(str, notify = activeQualityGroupChanged) def activeQualityGroupName(self) -> str: + """Get the name of the active quality group. + + :return: The name of the active quality group. + """ quality_group = self.activeQualityGroup() if quality_group is None: return "" @@ -1641,9 +1690,10 @@ class MachineManager(QObject): self.updateMaterialWithVariant(None) self._updateQualityWithMaterial() - ## This function will translate any printer type name to an abbreviated printer type name @pyqtSlot(str, result = str) def getAbbreviatedMachineName(self, machine_type_name: str) -> str: + """This function will translate any printer type name to an abbreviated printer type name""" + abbr_machine = "" for word in re.findall(r"[\w']+", machine_type_name): if word.lower() == "ultimaker": diff --git a/cura/Settings/MachineNameValidator.py b/cura/Settings/MachineNameValidator.py index acdda4b0a0..2bb614f093 100644 --- a/cura/Settings/MachineNameValidator.py +++ b/cura/Settings/MachineNameValidator.py @@ -10,10 +10,13 @@ from UM.Resources import Resources from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.InstanceContainer import InstanceContainer -## Are machine names valid? -# -# Performs checks based on the length of the name. + class MachineNameValidator(QObject): + """Are machine names valid? + + Performs checks based on the length of the name. + """ + def __init__(self, parent = None): super().__init__(parent) @@ -32,12 +35,13 @@ class MachineNameValidator(QObject): validationChanged = pyqtSignal() - ## Check if a specified machine name is allowed. - # - # \param name The machine name to check. - # \return ``QValidator.Invalid`` if it's disallowed, or - # ``QValidator.Acceptable`` if it's allowed. def validate(self, name): + """Check if a specified machine name is allowed. + + :param name: The machine name to check. + :return: ``QValidator.Invalid`` if it's disallowed, or ``QValidator.Acceptable`` if it's allowed. + """ + #Check for file name length of the current settings container (which is the longest file we're saving with the name). try: filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax @@ -50,9 +54,10 @@ class MachineNameValidator(QObject): return QValidator.Acceptable #All checks succeeded. - ## Updates the validation state of a machine name text field. @pyqtSlot(str) def updateValidation(self, new_name): + """Updates the validation state of a machine name text field.""" + is_valid = self.validate(new_name) if is_valid == QValidator.Acceptable: self.validation_regex = "^.*$" #Matches anything. diff --git a/cura/Settings/SetObjectExtruderOperation.py b/cura/Settings/SetObjectExtruderOperation.py index 25c1c6b759..63227c58e3 100644 --- a/cura/Settings/SetObjectExtruderOperation.py +++ b/cura/Settings/SetObjectExtruderOperation.py @@ -6,8 +6,10 @@ from UM.Operations.Operation import Operation from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -## Simple operation to set the extruder a certain object should be printed with. + class SetObjectExtruderOperation(Operation): + """Simple operation to set the extruder a certain object should be printed with.""" + def __init__(self, node: SceneNode, extruder_id: str) -> None: self._node = node self._extruder_id = extruder_id diff --git a/cura/Settings/SettingInheritanceManager.py b/cura/Settings/SettingInheritanceManager.py index 7db579bf3f..6179e76ab7 100644 --- a/cura/Settings/SettingInheritanceManager.py +++ b/cura/Settings/SettingInheritanceManager.py @@ -45,9 +45,10 @@ class SettingInheritanceManager(QObject): settingsWithIntheritanceChanged = pyqtSignal() - ## Get the keys of all children settings with an override. @pyqtSlot(str, result = "QStringList") def getChildrenKeysWithOverride(self, key: str) -> List[str]: + """Get the keys of all children settings with an override.""" + if self._global_container_stack is None: return [] definitions = self._global_container_stack.definition.findDefinitions(key=key) @@ -163,8 +164,9 @@ class SettingInheritanceManager(QObject): def settingsWithInheritanceWarning(self) -> List[str]: return self._settings_with_inheritance_warning - ## Check if a setting has an inheritance function that is overwritten def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool: + """Check if a setting has an inheritance function that is overwritten""" + has_setting_function = False if not stack: stack = self._active_container_stack @@ -177,17 +179,19 @@ class SettingInheritanceManager(QObject): containers = [] # type: List[ContainerInterface] - ## Check if the setting has a user state. If not, it is never overwritten. has_user_state = stack.getProperty(key, "state") == InstanceState.User + """Check if the setting has a user state. If not, it is never overwritten.""" + if not has_user_state: return False - ## If a setting is not enabled, don't label it as overwritten (It's never visible anyway). + # If a setting is not enabled, don't label it as overwritten (It's never visible anyway). if not stack.getProperty(key, "enabled"): return False - ## Also check if the top container is not a setting function (this happens if the inheritance is restored). user_container = stack.getTop() + """Also check if the top container is not a setting function (this happens if the inheritance is restored).""" + if user_container and isinstance(user_container.getProperty(key, "value"), SettingFunction): return False diff --git a/cura/Settings/SettingOverrideDecorator.py b/cura/Settings/SettingOverrideDecorator.py index 03b4c181dd..d48bff042f 100644 --- a/cura/Settings/SettingOverrideDecorator.py +++ b/cura/Settings/SettingOverrideDecorator.py @@ -15,21 +15,24 @@ from UM.Application import Application from cura.Settings.PerObjectContainerStack import PerObjectContainerStack from cura.Settings.ExtruderManager import ExtruderManager -## A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding -# the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by -# this stack still resolve. @signalemitter class SettingOverrideDecorator(SceneNodeDecorator): - ## Event indicating that the user selected a different extruder. + """A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding + + the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by + this stack still resolve. + """ activeExtruderChanged = Signal() + """Event indicating that the user selected a different extruder.""" - ## Non-printing meshes - # - # If these settings are True for any mesh, the mesh does not need a convex hull, - # and is sent to the slicer regardless of whether it fits inside the build volume. - # Note that Support Mesh is not in here because it actually generates - # g-code in the volume of the mesh. _non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"} + """Non-printing meshes + + If these settings are True for any mesh, the mesh does not need a convex hull, + and is sent to the slicer regardless of whether it fits inside the build volume. + Note that Support Mesh is not in here because it actually generates + g-code in the volume of the mesh. + """ _non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"} def __init__(self): @@ -56,11 +59,11 @@ class SettingOverrideDecorator(SceneNodeDecorator): return "SettingOverrideInstanceContainer-%s" % uuid.uuid1() def __deepcopy__(self, memo): - ## Create a fresh decorator object deep_copy = SettingOverrideDecorator() + """Create a fresh decorator object""" - ## Copy the instance instance_container = copy.deepcopy(self._stack.getContainer(0), memo) + """Copy the instance""" # A unique name must be added, or replaceContainer will not replace it instance_container.setMetaDataEntry("id", self._generateUniqueName()) @@ -78,22 +81,28 @@ class SettingOverrideDecorator(SceneNodeDecorator): return deep_copy - ## Gets the currently active extruder to print this object with. - # - # \return An extruder's container stack. def getActiveExtruder(self): + """Gets the currently active extruder to print this object with. + + :return: An extruder's container stack. + """ + return self._extruder_stack - ## Gets the signal that emits if the active extruder changed. - # - # This can then be accessed via a decorator. def getActiveExtruderChangedSignal(self): + """Gets the signal that emits if the active extruder changed. + + This can then be accessed via a decorator. + """ + return self.activeExtruderChanged - ## Gets the currently active extruders position - # - # \return An extruder's position, or None if no position info is available. def getActiveExtruderPosition(self): + """Gets the currently active extruders position + + :return: An extruder's position, or None if no position info is available. + """ + # for support_meshes, always use the support_extruder if self.getStack().getProperty("support_mesh", "value"): global_container_stack = Application.getInstance().getGlobalContainerStack() @@ -126,9 +135,11 @@ class SettingOverrideDecorator(SceneNodeDecorator): Application.getInstance().getBackend().needsSlicing() Application.getInstance().getBackend().tickle() - ## Makes sure that the stack upon which the container stack is placed is - # kept up to date. def _updateNextStack(self): + """Makes sure that the stack upon which the container stack is placed is + + kept up to date. + """ if self._extruder_stack: extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack) if extruder_stack: @@ -147,10 +158,12 @@ class SettingOverrideDecorator(SceneNodeDecorator): else: self._stack.setNextStack(Application.getInstance().getGlobalContainerStack()) - ## Changes the extruder with which to print this node. - # - # \param extruder_stack_id The new extruder stack to print with. def setActiveExtruder(self, extruder_stack_id): + """Changes the extruder with which to print this node. + + :param extruder_stack_id: The new extruder stack to print with. + """ + self._extruder_stack = extruder_stack_id self._updateNextStack() ExtruderManager.getInstance().resetSelectedObjectExtruders() diff --git a/cura/UI/MachineActionManager.py b/cura/UI/MachineActionManager.py index 6efd3217a1..25234fd43f 100644 --- a/cura/UI/MachineActionManager.py +++ b/cura/UI/MachineActionManager.py @@ -15,13 +15,15 @@ if TYPE_CHECKING: from cura.MachineAction import MachineAction -## Raised when trying to add an unknown machine action as a required action class UnknownMachineActionError(Exception): + """Raised when trying to add an unknown machine action as a required action""" + pass -## Raised when trying to add a machine action that does not have an unique key. class NotUniqueMachineActionError(Exception): + """Raised when trying to add a machine action that does not have an unique key.""" + pass @@ -71,9 +73,11 @@ class MachineActionManager(QObject): self._definition_ids_with_default_actions_added.add(definition_id) Logger.log("i", "Default machine actions added for machine definition [%s]", definition_id) - ## Add a required action to a machine - # Raises an exception when the action is not recognised. def addRequiredAction(self, definition_id: str, action_key: str) -> None: + """Add a required action to a machine + + Raises an exception when the action is not recognised. + """ if action_key in self._machine_actions: if definition_id in self._required_actions: if self._machine_actions[action_key] not in self._required_actions[definition_id]: @@ -83,8 +87,9 @@ class MachineActionManager(QObject): else: raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id)) - ## Add a supported action to a machine. def addSupportedAction(self, definition_id: str, action_key: str) -> None: + """Add a supported action to a machine.""" + if action_key in self._machine_actions: if definition_id in self._supported_actions: if self._machine_actions[action_key] not in self._supported_actions[definition_id]: @@ -94,8 +99,9 @@ class MachineActionManager(QObject): else: Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id) - ## Add an action to the first start list of a machine. def addFirstStartAction(self, definition_id: str, action_key: str) -> None: + """Add an action to the first start list of a machine.""" + if action_key in self._machine_actions: if definition_id in self._first_start_actions: self._first_start_actions[definition_id].append(self._machine_actions[action_key]) @@ -104,57 +110,69 @@ class MachineActionManager(QObject): else: Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id) - ## Add a (unique) MachineAction - # if the Key of the action is not unique, an exception is raised. def addMachineAction(self, action: "MachineAction") -> None: + """Add a (unique) MachineAction + + if the Key of the action is not unique, an exception is raised. + """ if action.getKey() not in self._machine_actions: self._machine_actions[action.getKey()] = action else: raise NotUniqueMachineActionError("MachineAction with key %s was already added. Actions must have unique keys.", action.getKey()) - ## Get all actions supported by given machine - # \param definition_id The ID of the definition you want the supported actions of - # \returns set of supported actions. @pyqtSlot(str, result = "QVariantList") def getSupportedActions(self, definition_id: str) -> List["MachineAction"]: + """Get all actions supported by given machine + + :param definition_id: The ID of the definition you want the supported actions of + :returns: set of supported actions. + """ if definition_id in self._supported_actions: return list(self._supported_actions[definition_id]) else: return list() - ## Get all actions required by given machine - # \param definition_id The ID of the definition you want the required actions of - # \returns set of required actions. def getRequiredActions(self, definition_id: str) -> List["MachineAction"]: + """Get all actions required by given machine + + :param definition_id: The ID of the definition you want the required actions of + :returns: set of required actions. + """ if definition_id in self._required_actions: return self._required_actions[definition_id] else: return list() - ## Get all actions that need to be performed upon first start of a given machine. - # Note that contrary to required / supported actions a list is returned (as it could be required to run the same - # action multiple times). - # \param definition_id The ID of the definition that you want to get the "on added" actions for. - # \returns List of actions. @pyqtSlot(str, result = "QVariantList") def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]: + """Get all actions that need to be performed upon first start of a given machine. + + Note that contrary to required / supported actions a list is returned (as it could be required to run the same + action multiple times). + :param definition_id: The ID of the definition that you want to get the "on added" actions for. + :returns: List of actions. + """ if definition_id in self._first_start_actions: return self._first_start_actions[definition_id] else: return [] - ## Remove Machine action from manager - # \param action to remove def removeMachineAction(self, action: "MachineAction") -> None: + """Remove Machine action from manager + + :param action: to remove + """ try: del self._machine_actions[action.getKey()] except KeyError: Logger.log("w", "Trying to remove MachineAction (%s) that was already removed", action.getKey()) - ## Get MachineAction by key - # \param key String of key to select - # \return Machine action if found, None otherwise def getMachineAction(self, key: str) -> Optional["MachineAction"]: + """Get MachineAction by key + + :param key: String of key to select + :return: Machine action if found, None otherwise + """ if key in self._machine_actions: return self._machine_actions[key] else: diff --git a/cura/UI/ObjectsModel.py b/cura/UI/ObjectsModel.py index 659732e895..1383476665 100644 --- a/cura/UI/ObjectsModel.py +++ b/cura/UI/ObjectsModel.py @@ -31,8 +31,9 @@ class _NodeInfo: self.is_group = is_group # type: bool -## Keep track of all objects in the project class ObjectsModel(ListModel): + """Keep track of all objects in the project""" + NameRole = Qt.UserRole + 1 SelectedRole = Qt.UserRole + 2 OutsideAreaRole = Qt.UserRole + 3 diff --git a/cura/UI/PrintInformation.py b/cura/UI/PrintInformation.py index c39314dc02..cbe0900ec0 100644 --- a/cura/UI/PrintInformation.py +++ b/cura/UI/PrintInformation.py @@ -21,11 +21,13 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## A class for processing and the print times per build plate as well as managing the job name -# -# This class also mangles the current machine name and the filename of the first loaded mesh into a job name. -# This job name is requested by the JobSpecs qml file. class PrintInformation(QObject): + """A class for processing and the print times per build plate as well as managing the job name + + This class also mangles the current machine name and the filename of the first loaded mesh into a job name. + This job name is requested by the JobSpecs qml file. + """ + UNTITLED_JOB_NAME = "Untitled" @@ -380,10 +382,12 @@ class PrintInformation(QObject): def baseName(self): return self._base_name - ## Created an acronym-like abbreviated machine name from the currently - # active machine name. - # Called each time the global stack is switched. def _defineAbbreviatedMachineName(self) -> None: + """Created an acronym-like abbreviated machine name from the currently active machine name. + + Called each time the global stack is switched. + """ + global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: self._abbr_machine = "" @@ -392,8 +396,9 @@ class PrintInformation(QObject): self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name) - ## Utility method that strips accents from characters (eg: â -> a) def _stripAccents(self, to_strip: str) -> str: + """Utility method that strips accents from characters (eg: â -> a)""" + return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn') @pyqtSlot(result = "QVariantMap") @@ -431,6 +436,7 @@ class PrintInformation(QObject): return self._change_timer.start() - ## Listen to scene changes to check if we need to reset the print information def _onSceneChanged(self) -> None: + """Listen to scene changes to check if we need to reset the print information""" + self.setToZeroPrintInformation(self._active_build_plate) diff --git a/cura/Utils/Decorators.py b/cura/Utils/Decorators.py index 9275ee6ce9..7be718c51c 100644 --- a/cura/Utils/Decorators.py +++ b/cura/Utils/Decorators.py @@ -11,13 +11,15 @@ from typing import Callable SEMANTIC_VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+(\.[0-9]+)?$") -## Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported -# APIs, meaning that those APIs should be versioned and maintained. -# -# \param since_version The earliest version since when this API becomes supported. This means that since this version, -# this API function is supposed to behave the same. This parameter is not used. It's just a -# documentation. def api(since_version: str) -> Callable: + """Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported + + APIs, meaning that those APIs should be versioned and maintained. + + :param since_version: The earliest version since when this API becomes supported. This means that since this version, + this API function is supposed to behave the same. This parameter is not used. It's just a + documentation. + """ # Make sure that APi versions are semantic versions if not SEMANTIC_VERSION_REGEX.fullmatch(since_version): raise ValueError("API since_version [%s] is not a semantic version." % since_version) diff --git a/tests/Machines/TestMachineNode.py b/tests/Machines/TestMachineNode.py index 50d7bdafa0..3b0822770b 100644 --- a/tests/Machines/TestMachineNode.py +++ b/tests/Machines/TestMachineNode.py @@ -26,12 +26,14 @@ def container_registry(): result.findContainersMetadata = MagicMock(return_value = [metadata_dict]) return result -## Creates a machine node without anything underneath it. No sub-nodes. -# -# For testing stuff with machine nodes without testing _loadAll(). You'll need -# to add subnodes manually in your test. @pytest.fixture def empty_machine_node(): + """Creates a machine node without anything underneath it. No sub-nodes. + + For testing stuff with machine nodes without testing _loadAll(). You'll need + to add subnodes manually in your test. + """ + empty_container_registry = MagicMock() empty_container_registry.findContainersMetadata = MagicMock(return_value = [metadata_dict]) # Still contain the MachineNode's own metadata for the constructor. empty_container_registry.findInstanceContainersMetadata = MagicMock(return_value = []) @@ -77,9 +79,13 @@ def test_metadataProperties(container_registry): assert node.preferred_material == metadata_dict["preferred_material"] assert node.preferred_quality_type == metadata_dict["preferred_quality_type"] -## Test getting quality groups when there are quality profiles available for -# the requested configurations on two extruders. + def test_getQualityGroupsBothExtrudersAvailable(empty_machine_node): + """Test getting quality groups when there are quality profiles available for + + the requested configurations on two extruders. + """ + # Prepare a tree inside the machine node. extruder_0_node = MagicMock(quality_type = "quality_type_1") extruder_1_node = MagicMock(quality_type = "quality_type_1") # Same quality type, so this is the one that can be returned. @@ -121,12 +127,15 @@ def test_getQualityGroupsBothExtrudersAvailable(empty_machine_node): assert result["quality_type_1"].name == global_node.getMetaDataEntry("name", "Unnamed Profile") assert result["quality_type_1"].quality_type == "quality_type_1" -## Test the "is_available" flag on quality groups. -# -# If a profile is available for a quality type on an extruder but not on all -# extruders, there should be a quality group for it but it should not be made -# available. + def test_getQualityGroupsAvailability(empty_machine_node): + """Test the "is_available" flag on quality groups. + + If a profile is available for a quality type on an extruder but not on all + extruders, there should be a quality group for it but it should not be made + available. + """ + # Prepare a tree inside the machine node. extruder_0_both = MagicMock(quality_type = "quality_type_both") # This quality type is available for both extruders. extruder_1_both = MagicMock(quality_type = "quality_type_both") diff --git a/tests/Machines/TestQualityNode.py b/tests/Machines/TestQualityNode.py index ffe897d203..0501450b5d 100644 --- a/tests/Machines/TestQualityNode.py +++ b/tests/Machines/TestQualityNode.py @@ -6,7 +6,7 @@ import pytest from cura.Machines.QualityNode import QualityNode -## Metadata for hypothetical containers that get put in the registry. +# Metadata for hypothetical containers that get put in the registry. metadatas = [ { "id": "matching_intent", # Matches our query. diff --git a/tests/Machines/TestVariantNode.py b/tests/Machines/TestVariantNode.py index 9a0213ef99..96084001c1 100644 --- a/tests/Machines/TestVariantNode.py +++ b/tests/Machines/TestVariantNode.py @@ -49,12 +49,14 @@ def machine_node(): mocked_machine_node.preferred_material = "preferred_material" return mocked_machine_node -## Constructs a variant node without any subnodes. -# -# This is useful for performing tests on VariantNode without being dependent -# on how _loadAll works. @pytest.fixture def empty_variant_node(machine_node): + """Constructs a variant node without any subnodes. + + This is useful for performing tests on VariantNode without being dependent + on how _loadAll works. + """ + container_registry = MagicMock( findContainersMetadata = MagicMock(return_value = [{"name": "test variant name"}]) ) @@ -132,9 +134,12 @@ def test_materialAdded_update(container_registry, machine_node, metadata, change for key in changed_material_list: assert original_material_nodes[key] != variant_node.materials[key] -## Tests the preferred material when the exact base file is available in the -# materials list for this node. + def test_preferredMaterialExactMatch(empty_variant_node): + """Tests the preferred material when the exact base file is available in the + + materials list for this node. + """ empty_variant_node.materials = { "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "preferred_material": MagicMock(getMetaDataEntry = lambda x: 3) # Exact match. @@ -143,9 +148,12 @@ def test_preferredMaterialExactMatch(empty_variant_node): assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material"], "It should match exactly on this one since it's the preferred material." -## Tests the preferred material when a submaterial is available in the -# materials list for this node. + def test_preferredMaterialSubmaterial(empty_variant_node): + """Tests the preferred material when a submaterial is available in the + + materials list for this node. + """ empty_variant_node.materials = { "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "preferred_material_base_file_aa04": MagicMock(getMetaDataEntry = lambda x: 3) # This is a submaterial of the preferred material. @@ -154,9 +162,10 @@ def test_preferredMaterialSubmaterial(empty_variant_node): assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material_base_file_aa04"], "It should match on the submaterial just as well." -## Tests the preferred material matching on the approximate diameter of the -# filament. + def test_preferredMaterialDiameter(empty_variant_node): + """Tests the preferred material matching on the approximate diameter of the filament. + """ empty_variant_node.materials = { "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "preferred_material_wrong_diameter": MagicMock(getMetaDataEntry = lambda x: 2), # Approximate diameter is 2 instead of 3. @@ -166,18 +175,22 @@ def test_preferredMaterialDiameter(empty_variant_node): assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material_correct_diameter"], "It should match only on the material with correct diameter." -## Tests the preferred material matching on a different material if the -# diameter is wrong. + def test_preferredMaterialDiameterNoMatch(empty_variant_node): + """Tests the preferred material matching on a different material if the diameter is wrong.""" + empty_variant_node.materials = collections.OrderedDict() empty_variant_node.materials["some_different_material"] = MagicMock(getMetaDataEntry = lambda x: 3) # This one first so that it gets iterated over first. empty_variant_node.materials["preferred_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # Wrong diameter. assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["some_different_material"], "It should match on another material with the correct diameter if the preferred one is unavailable." -## Tests that the material diameter is considered more important to match than -# the preferred diameter. + def test_preferredMaterialDiameterPreference(empty_variant_node): + """Tests that the material diameter is considered more important to match than + the preferred diameter. + """ + empty_variant_node.materials = collections.OrderedDict() empty_variant_node.materials["some_different_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # This one first so that it gets iterated over first. empty_variant_node.materials["preferred_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # Matches on ID but not diameter. diff --git a/tests/Settings/MockContainer.py b/tests/Settings/MockContainer.py index 0400359154..8bb570bae1 100644 --- a/tests/Settings/MockContainer.py +++ b/tests/Settings/MockContainer.py @@ -5,18 +5,21 @@ import UM.PluginObject from UM.Signal import Signal -## Fake container class to add to the container registry. -# -# This allows us to test the container registry without testing the container -# class. If something is wrong in the container class it won't influence this -# test. - class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): - ## Initialise a new definition container. - # - # The container will have the specified ID and all metadata in the - # provided dictionary. + """Fake container class to add to the container registry. + + This allows us to test the container registry without testing the container + class. If something is wrong in the container class it won't influence this + test. + """ + def __init__(self, metadata = None): + """Initialise a new definition container. + + The container will have the specified ID and all metadata in the + provided dictionary. + """ + super().__init__() if metadata is None: self._metadata = {} @@ -24,55 +27,69 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): self._metadata = metadata self._plugin_id = "MockContainerPlugin" - ## Gets the ID that was provided at initialisation. - # - # \return The ID of the container. def getId(self): + """Gets the ID that was provided at initialisation. + + :return: The ID of the container. + """ + return self._metadata["id"] - ## Gets all metadata of this container. - # - # This returns the metadata dictionary that was provided in the - # constructor of this mock container. - # - # \return The metadata for this container. def getMetaData(self): + """Gets all metadata of this container. + + This returns the metadata dictionary that was provided in the + constructor of this mock container. + + :return: The metadata for this container. + """ + return self._metadata - ## Gets a metadata entry from the metadata dictionary. - # - # \param key The key of the metadata entry. - # \return The value of the metadata entry, or None if there is no such - # entry. def getMetaDataEntry(self, entry, default = None): + """Gets a metadata entry from the metadata dictionary. + + :param key: The key of the metadata entry. + :return: The value of the metadata entry, or None if there is no such + entry. + """ + if entry in self._metadata: return self._metadata[entry] return default - ## Gets a human-readable name for this container. - # \return The name from the metadata, or "MockContainer" if there was no - # name provided. def getName(self): + """Gets a human-readable name for this container. + + :return: The name from the metadata, or "MockContainer" if there was no + name provided. + """ return self._metadata.get("name", "MockContainer") - ## Get whether a container stack is enabled or not. - # \return Always returns True. @property def isEnabled(self): + """Get whether a container stack is enabled or not. + + :return: Always returns True. + """ return True - ## Get whether the container item is stored on a read only location in the filesystem. - # - # \return Always returns False def isReadOnly(self): + """Get whether the container item is stored on a read only location in the filesystem. + + :return: Always returns False + """ + return False - ## Mock get path def getPath(self): + """Mock get path""" + return "/path/to/the/light/side" - ## Mock set path def setPath(self, path): + """Mock set path""" + pass def getAllKeys(self): @@ -91,31 +108,38 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): return None - ## Get the value of a container item. - # - # Since this mock container cannot contain any items, it always returns - # None. - # - # \return Always returns None. def getValue(self, key): + """Get the value of a container item. + + Since this mock container cannot contain any items, it always returns None. + + :return: Always returns None. + """ + pass - ## Get whether the container item has a specific property. - # - # This method is not implemented in the mock container. def hasProperty(self, key, property_name): + """Get whether the container item has a specific property. + + This method is not implemented in the mock container. + """ + return key in self.items - ## Serializes the container to a string representation. - # - # This method is not implemented in the mock container. def serialize(self, ignored_metadata_keys = None): + """Serializes the container to a string representation. + + This method is not implemented in the mock container. + """ + raise NotImplementedError() - ## Deserializes the container from a string representation. - # - # This method is not implemented in the mock container. def deserialize(self, serialized, file_name: Optional[str] = None): + """Deserializes the container from a string representation. + + This method is not implemented in the mock container. + """ + raise NotImplementedError() @classmethod diff --git a/tests/Settings/TestCuraContainerRegistry.py b/tests/Settings/TestCuraContainerRegistry.py index 8bd6fe0ccb..df4a1df0a9 100644 --- a/tests/Settings/TestCuraContainerRegistry.py +++ b/tests/Settings/TestCuraContainerRegistry.py @@ -42,8 +42,9 @@ def test_createUniqueName(container_registry): assert container_registry.createUniqueName("user", "test", "", "nope") == "nope" -## Tests whether addContainer properly converts to ExtruderStack. def test_addContainerExtruderStack(container_registry, definition_container, definition_changes_container): + """Tests whether addContainer properly converts to ExtruderStack.""" + container_registry.addContainer(definition_container) container_registry.addContainer(definition_changes_container) @@ -61,8 +62,9 @@ def test_addContainerExtruderStack(container_registry, definition_container, def assert type(mock_super_add_container.call_args_list[0][0][0]) == ExtruderStack -## Tests whether addContainer properly converts to GlobalStack. def test_addContainerGlobalStack(container_registry, definition_container, definition_changes_container): + """Tests whether addContainer properly converts to GlobalStack.""" + container_registry.addContainer(definition_container) container_registry.addContainer(definition_changes_container) diff --git a/tests/Settings/TestDefinitionContainer.py b/tests/Settings/TestDefinitionContainer.py index 2f2b343338..f2a9c0d245 100644 --- a/tests/Settings/TestDefinitionContainer.py +++ b/tests/Settings/TestDefinitionContainer.py @@ -57,9 +57,10 @@ def test_noCategory(file_path): metadata = DefinitionContainer.deserializeMetadata(json, "test_container_id") assert "category" not in metadata[0] -## Tests all definition containers @pytest.mark.parametrize("file_path", machine_filepaths) def test_validateMachineDefinitionContainer(file_path, definition_container): + """Tests all definition containers""" + file_name = os.path.basename(file_path) if file_name == "fdmprinter.def.json" or file_name == "fdmextruder.def.json": return # Stop checking, these are root files. @@ -85,13 +86,15 @@ def assertIsDefinitionValid(definition_container, file_path): if "platform_texture" in metadata[0]: assert metadata[0]["platform_texture"] in all_images -## Tests whether setting values are not being hidden by parent containers. -# -# When a definition container defines a "default_value" but inherits from a -# definition that defines a "value", the "default_value" is ineffective. This -# test fails on those things. @pytest.mark.parametrize("file_path", definition_filepaths) def test_validateOverridingDefaultValue(file_path: str): + """Tests whether setting values are not being hidden by parent containers. + + When a definition container defines a "default_value" but inherits from a + definition that defines a "value", the "default_value" is ineffective. This + test fails on those things. + """ + with open(file_path, encoding = "utf-8") as f: doc = json.load(f) @@ -107,12 +110,14 @@ def test_validateOverridingDefaultValue(file_path: str): faulty_keys.add(key) assert not faulty_keys, "Unnecessary default_values for {faulty_keys} in {file_name}".format(faulty_keys = sorted(faulty_keys), file_name = file_path) # If there is a value in the parent settings, then the default_value is not effective. -## Get all settings and their properties from a definition we're inheriting -# from. -# \param definition_id The definition we're inheriting from. -# \return A dictionary of settings by key. Each setting is a dictionary of -# properties. + def getInheritedSettings(definition_id: str) -> Dict[str, Any]: + """Get all settings and their properties from a definition we're inheriting from. + + :param definition_id: The definition we're inheriting from. + :return: A dictionary of settings by key. Each setting is a dictionary of properties. + """ + definition_path = os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", definition_id + ".def.json") with open(definition_path, encoding = "utf-8") as f: doc = json.load(f) @@ -127,13 +132,15 @@ def getInheritedSettings(definition_id: str) -> Dict[str, Any]: return result -## Put all settings in the main dictionary rather than in children dicts. -# \param settings Nested settings. The keys are the setting IDs. The values -# are dictionaries of properties per setting, including the "children" -# property. -# \return A dictionary of settings by key. Each setting is a dictionary of -# properties. + def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]: + """Put all settings in the main dictionary rather than in children dicts. + + :param settings: Nested settings. The keys are the setting IDs. The values + are dictionaries of properties per setting, including the "children" property. + :return: A dictionary of settings by key. Each setting is a dictionary of properties. + """ + result = {} for entry, contents in settings.items(): if "children" in contents: @@ -142,12 +149,16 @@ def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]: result[entry] = contents return result -## Make one dictionary override the other. Nested dictionaries override each -# other in the same way. -# \param base A dictionary of settings that will get overridden by the other. -# \param overrides A dictionary of settings that will override the other. -# \return Combined setting data. + def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, Any]: + """Make one dictionary override the other. Nested dictionaries override each + + other in the same way. + :param base: A dictionary of settings that will get overridden by the other. + :param overrides: A dictionary of settings that will override the other. + :return: Combined setting data. + """ + result = {} result.update(base) for key, val in overrides.items(): @@ -161,21 +172,25 @@ def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, An result[key] = val return result -## Verifies that definition contains don't have an ID field. -# -# ID fields are legacy. They should not be used any more. This is legacy that -# people don't seem to be able to get used to. + @pytest.mark.parametrize("file_path", definition_filepaths) def test_noId(file_path: str): + """Verifies that definition contains don't have an ID field. + + ID fields are legacy. They should not be used any more. This is legacy that + people don't seem to be able to get used to. + """ + with open(file_path, encoding = "utf-8") as f: doc = json.load(f) assert "id" not in doc, "Definitions should not have an ID field." -## Verifies that extruders say that they work on the same extruder_nr as what -# is listed in their machine definition. + @pytest.mark.parametrize("file_path", extruder_filepaths) def test_extruderMatch(file_path: str): + """Verifies that extruders say that they work on the same extruder_nr as what is listed in their machine definition.""" + extruder_id = os.path.basename(file_path).split(".")[0] with open(file_path, encoding = "utf-8") as f: doc = json.load(f) diff --git a/tests/Settings/TestExtruderStack.py b/tests/Settings/TestExtruderStack.py index 73d5f583b3..e9f167b6a1 100644 --- a/tests/Settings/TestExtruderStack.py +++ b/tests/Settings/TestExtruderStack.py @@ -14,11 +14,13 @@ from cura.Settings.Exceptions import InvalidContainerError, InvalidOperationErro from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.cura_empty_instance_containers import empty_container -## Gets an instance container with a specified container type. -# -# \param container_type The type metadata for the instance container. -# \return An instance container instance. def getInstanceContainer(container_type) -> InstanceContainer: + """Gets an instance container with a specified container type. + + :param container_type: The type metadata for the instance container. + :return: An instance container instance. + """ + container = InstanceContainer(container_id = "InstanceContainer") container.setMetaDataEntry("type", container_type) return container @@ -32,10 +34,12 @@ class InstanceContainerSubClass(InstanceContainer): super().__init__(container_id = "SubInstanceContainer") self.setMetaDataEntry("type", container_type) -#############################START OF TEST CASES################################ +############################START OF TEST CASES################################ + -## Tests whether adding a container is properly forbidden. def test_addContainer(extruder_stack): + """Tests whether adding a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): extruder_stack.addContainer(unittest.mock.MagicMock()) @@ -164,8 +168,10 @@ def test_constrainDefinitionInvalid(container, extruder_stack): def test_constrainDefinitionValid(container, extruder_stack): extruder_stack.definition = container #Should not give an error. -## Tests whether deserialising completes the missing containers with empty ones. + def test_deserializeCompletesEmptyContainers(extruder_stack): + """Tests whether deserialising completes the missing containers with empty ones.""" + extruder_stack._containers = [DefinitionContainer(container_id = "definition"), extruder_stack.definitionChanges] #Set the internal state of this stack manually. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -179,8 +185,10 @@ def test_deserializeCompletesEmptyContainers(extruder_stack): continue assert extruder_stack.getContainer(container_type_index) == empty_container #All others need to be empty. -## Tests whether an instance container with the wrong type gets removed when deserialising. + def test_deserializeRemovesWrongInstanceContainer(extruder_stack): + """Tests whether an instance container with the wrong type gets removed when deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "wrong type") extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -189,8 +197,10 @@ def test_deserializeRemovesWrongInstanceContainer(extruder_stack): assert extruder_stack.quality == extruder_stack._empty_instance_container #Replaced with empty. -## Tests whether a container with the wrong class gets removed when deserialising. + def test_deserializeRemovesWrongContainerClass(extruder_stack): + """Tests whether a container with the wrong class gets removed when deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = DefinitionContainer(container_id = "wrong class") extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -199,16 +209,20 @@ def test_deserializeRemovesWrongContainerClass(extruder_stack): assert extruder_stack.quality == extruder_stack._empty_instance_container #Replaced with empty. -## Tests whether an instance container in the definition spot results in an error. + def test_deserializeWrongDefinitionClass(extruder_stack): + """Tests whether an instance container in the definition spot results in an error.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = getInstanceContainer(container_type = "definition") #Correct type but wrong class. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. with pytest.raises(UM.Settings.ContainerStack.InvalidContainerStackError): #Must raise an error that there is no definition container. extruder_stack.deserialize("") -## Tests whether an instance container with the wrong type is moved into the correct slot by deserialising. + def test_deserializeMoveInstanceContainer(extruder_stack): + """Tests whether an instance container with the wrong type is moved into the correct slot by deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "material") #Not in the correct spot. extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -218,8 +232,10 @@ def test_deserializeMoveInstanceContainer(extruder_stack): assert extruder_stack.quality == empty_container assert extruder_stack.material != empty_container -## Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising. + def test_deserializeMoveDefinitionContainer(extruder_stack): + """Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Material] = DefinitionContainer(container_id = "some definition") #Not in the correct spot. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -228,8 +244,10 @@ def test_deserializeMoveDefinitionContainer(extruder_stack): assert extruder_stack.material == empty_container assert extruder_stack.definition != empty_container -## Tests whether getProperty properly applies the stack-like behaviour on its containers. + def test_getPropertyFallThrough(global_stack, extruder_stack): + """Tests whether getProperty properly applies the stack-like behaviour on its containers.""" + # ExtruderStack.setNextStack calls registerExtruder for backward compatibility, but we do not need a complete extruder manager ExtruderManager._ExtruderManager__instance = unittest.mock.MagicMock() @@ -273,13 +291,17 @@ def test_getPropertyFallThrough(global_stack, extruder_stack): extruder_stack.userChanges = mock_layer_heights[container_indices.UserChanges] assert extruder_stack.getProperty("layer_height", "value") == container_indices.UserChanges -## Tests whether inserting a container is properly forbidden. + def test_insertContainer(extruder_stack): + """Tests whether inserting a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): extruder_stack.insertContainer(0, unittest.mock.MagicMock()) -## Tests whether removing a container is properly forbidden. + def test_removeContainer(extruder_stack): + """Tests whether removing a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): extruder_stack.removeContainer(unittest.mock.MagicMock()) diff --git a/tests/Settings/TestGlobalStack.py b/tests/Settings/TestGlobalStack.py index c1044c9de6..391013233d 100755 --- a/tests/Settings/TestGlobalStack.py +++ b/tests/Settings/TestGlobalStack.py @@ -16,11 +16,13 @@ import UM.Settings.SettingDefinition #To add settings to the definition. from cura.Settings.cura_empty_instance_containers import empty_container -## Gets an instance container with a specified container type. -# -# \param container_type The type metadata for the instance container. -# \return An instance container instance. def getInstanceContainer(container_type) -> InstanceContainer: + """Gets an instance container with a specified container type. + + :param container_type: The type metadata for the instance container. + :return: An instance container instance. + """ + container = InstanceContainer(container_id = "InstanceContainer") container.setMetaDataEntry("type", container_type) return container @@ -37,17 +39,19 @@ class InstanceContainerSubClass(InstanceContainer): self.setMetaDataEntry("type", container_type) -#############################START OF TEST CASES################################ +############################START OF TEST CASES################################ -## Tests whether adding a container is properly forbidden. def test_addContainer(global_stack): + """Tests whether adding a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.addContainer(unittest.mock.MagicMock()) -## Tests adding extruders to the global stack. def test_addExtruder(global_stack): + """Tests adding extruders to the global stack.""" + mock_definition = unittest.mock.MagicMock() mock_definition.getProperty = lambda key, property, context = None: 2 if key == "machine_extruder_count" and property == "value" else None @@ -213,9 +217,12 @@ def test_constrainDefinitionValid(container, global_stack): global_stack.definition = container #Should not give an error. -## Tests whether deserialising completes the missing containers with empty ones. The initial containers are just the -# definition and the definition_changes (that cannot be empty after CURA-5281) def test_deserializeCompletesEmptyContainers(global_stack): + """Tests whether deserialising completes the missing containers with empty ones. The initial containers are just the + + definition and the definition_changes (that cannot be empty after CURA-5281) + """ + global_stack._containers = [DefinitionContainer(container_id = "definition"), global_stack.definitionChanges] #Set the internal state of this stack manually. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -229,8 +236,9 @@ def test_deserializeCompletesEmptyContainers(global_stack): assert global_stack.getContainer(container_type_index) == empty_container #All others need to be empty. -## Tests whether an instance container with the wrong type gets removed when deserialising. def test_deserializeRemovesWrongInstanceContainer(global_stack): + """Tests whether an instance container with the wrong type gets removed when deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "wrong type") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -240,8 +248,9 @@ def test_deserializeRemovesWrongInstanceContainer(global_stack): assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty. -## Tests whether a container with the wrong class gets removed when deserialising. def test_deserializeRemovesWrongContainerClass(global_stack): + """Tests whether a container with the wrong class gets removed when deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = DefinitionContainer(container_id = "wrong class") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -251,8 +260,9 @@ def test_deserializeRemovesWrongContainerClass(global_stack): assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty. -## Tests whether an instance container in the definition spot results in an error. def test_deserializeWrongDefinitionClass(global_stack): + """Tests whether an instance container in the definition spot results in an error.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = getInstanceContainer(container_type = "definition") #Correct type but wrong class. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -260,8 +270,9 @@ def test_deserializeWrongDefinitionClass(global_stack): global_stack.deserialize("") -## Tests whether an instance container with the wrong type is moved into the correct slot by deserialising. def test_deserializeMoveInstanceContainer(global_stack): + """Tests whether an instance container with the wrong type is moved into the correct slot by deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "material") #Not in the correct spot. global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -272,8 +283,9 @@ def test_deserializeMoveInstanceContainer(global_stack): assert global_stack.material != empty_container -## Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising. def test_deserializeMoveDefinitionContainer(global_stack): + """Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Material] = DefinitionContainer(container_id = "some definition") #Not in the correct spot. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -283,8 +295,9 @@ def test_deserializeMoveDefinitionContainer(global_stack): assert global_stack.definition != empty_container -## Tests whether getProperty properly applies the stack-like behaviour on its containers. def test_getPropertyFallThrough(global_stack): + """Tests whether getProperty properly applies the stack-like behaviour on its containers.""" + #A few instance container mocks to put in the stack. mock_layer_heights = {} #For each container type, a mock container that defines layer height to something unique. mock_no_settings = {} #For each container type, a mock container that has no settings at all. @@ -326,8 +339,9 @@ def test_getPropertyFallThrough(global_stack): assert global_stack.getProperty("layer_height", "value") == container_indexes.UserChanges -## In definitions, test whether having no resolve allows us to find the value. def test_getPropertyNoResolveInDefinition(global_stack): + """In definitions, test whether having no resolve allows us to find the value.""" + value = unittest.mock.MagicMock() #Just sets the value for bed temperature. value.getProperty = lambda key, property, context = None: 10 if (key == "material_bed_temperature" and property == "value") else None @@ -336,8 +350,9 @@ def test_getPropertyNoResolveInDefinition(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 10 #No resolve, so fall through to value. -## In definitions, when the value is asked and there is a resolve function, it must get the resolve first. def test_getPropertyResolveInDefinition(global_stack): + """In definitions, when the value is asked and there is a resolve function, it must get the resolve first.""" + resolve_and_value = unittest.mock.MagicMock() #Sets the resolve and value for bed temperature. resolve_and_value.getProperty = lambda key, property, context = None: (7.5 if property == "resolve" else 5) if (key == "material_bed_temperature" and property in ("resolve", "value")) else None #7.5 resolve, 5 value. @@ -346,8 +361,9 @@ def test_getPropertyResolveInDefinition(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 7.5 #Resolve wins in the definition. -## In instance containers, when the value is asked and there is a resolve function, it must get the value first. def test_getPropertyResolveInInstance(global_stack): + """In instance containers, when the value is asked and there is a resolve function, it must get the value first.""" + container_indices = cura.Settings.CuraContainerStack._ContainerIndexes instance_containers = {} for container_type in container_indices.IndexTypeMap: @@ -373,8 +389,9 @@ def test_getPropertyResolveInInstance(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 5 -## Tests whether the value in instances gets evaluated before the resolve in definitions. def test_getPropertyInstancesBeforeResolve(global_stack): + """Tests whether the value in instances gets evaluated before the resolve in definitions.""" + def getValueProperty(key, property, context = None): if key != "material_bed_temperature": return None @@ -404,8 +421,9 @@ def test_getPropertyInstancesBeforeResolve(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 10 -## Tests whether the hasUserValue returns true for settings that are changed in the user-changes container. def test_hasUserValueUserChanges(global_stack): + """Tests whether the hasUserValue returns true for settings that are changed in the user-changes container.""" + container = unittest.mock.MagicMock() container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "user") container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. @@ -416,8 +434,9 @@ def test_hasUserValueUserChanges(global_stack): assert not global_stack.hasUserValue("") -## Tests whether the hasUserValue returns true for settings that are changed in the quality-changes container. def test_hasUserValueQualityChanges(global_stack): + """Tests whether the hasUserValue returns true for settings that are changed in the quality-changes container.""" + container = unittest.mock.MagicMock() container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality_changes") container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. @@ -428,8 +447,9 @@ def test_hasUserValueQualityChanges(global_stack): assert not global_stack.hasUserValue("") -## Tests whether a container in some other place on the stack is correctly not recognised as user value. def test_hasNoUserValue(global_stack): + """Tests whether a container in some other place on the stack is correctly not recognised as user value.""" + container = unittest.mock.MagicMock() container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality") container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. @@ -438,20 +458,23 @@ def test_hasNoUserValue(global_stack): assert not global_stack.hasUserValue("layer_height") #However this container is quality, so it's not a user value. -## Tests whether inserting a container is properly forbidden. def test_insertContainer(global_stack): + """Tests whether inserting a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.insertContainer(0, unittest.mock.MagicMock()) -## Tests whether removing a container is properly forbidden. def test_removeContainer(global_stack): + """Tests whether removing a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.removeContainer(unittest.mock.MagicMock()) -## Tests whether changing the next stack is properly forbidden. def test_setNextStack(global_stack): + """Tests whether changing the next stack is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.setNextStack(unittest.mock.MagicMock()) diff --git a/tests/Settings/TestProfiles.py b/tests/Settings/TestProfiles.py index cf26ad7020..fba57c5eea 100644 --- a/tests/Settings/TestProfiles.py +++ b/tests/Settings/TestProfiles.py @@ -61,9 +61,10 @@ variant_filepaths = collectAllVariants() intent_filepaths = collectAllIntents() -## Attempt to load all the quality profiles. @pytest.mark.parametrize("file_name", quality_filepaths) def test_validateQualityProfiles(file_name): + """Attempt to load all the quality profiles.""" + try: with open(file_name, encoding = "utf-8") as data: serialized = data.read() @@ -114,9 +115,10 @@ def test_validateIntentProfiles(file_name): # File can't be read, header sections missing, whatever the case, this shouldn't happen! assert False, "Got an exception while reading the file {file_name}: {err}".format(file_name = file_name, err = str(e)) -## Attempt to load all the variant profiles. @pytest.mark.parametrize("file_name", variant_filepaths) def test_validateVariantProfiles(file_name): + """Attempt to load all the variant profiles.""" + try: with open(file_name, encoding = "utf-8") as data: serialized = data.read() diff --git a/tests/TestArrange.py b/tests/TestArrange.py index a00b544936..f37e48f19c 100755 --- a/tests/TestArrange.py +++ b/tests/TestArrange.py @@ -6,36 +6,43 @@ import numpy from cura.Arranging.Arrange import Arrange from cura.Arranging.ShapeArray import ShapeArray -## Triangle of area 12 def gimmeTriangle(): + """Triangle of area 12""" + return numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32) -## Boring square def gimmeSquare(): + """Boring square""" + return numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32) -## Triangle of area 12 def gimmeShapeArray(scale = 1.0): + """Triangle of area 12""" + vertices = gimmeTriangle() shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) return shape_arr -## Boring square def gimmeShapeArraySquare(scale = 1.0): + """Boring square""" + vertices = gimmeSquare() shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) return shape_arr -## Smoke test for Arrange def test_smoke_arrange(): + """Smoke test for Arrange""" + Arrange.create(fixed_nodes = []) -## Smoke test for ShapeArray def test_smoke_ShapeArray(): + """Smoke test for ShapeArray""" + gimmeShapeArray() -## Test ShapeArray def test_ShapeArray(): + """Test ShapeArray""" + scale = 1 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -44,8 +51,9 @@ def test_ShapeArray(): count = len(numpy.where(shape_arr.arr == 1)[0]) assert count >= 10 # should approach 12 -## Test ShapeArray with scaling def test_ShapeArray_scaling(): + """Test ShapeArray with scaling""" + scale = 2 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -54,8 +62,9 @@ def test_ShapeArray_scaling(): count = len(numpy.where(shape_arr.arr == 1)[0]) assert count >= 40 # should approach 2*2*12 = 48 -## Test ShapeArray with scaling def test_ShapeArray_scaling2(): + """Test ShapeArray with scaling""" + scale = 0.5 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -64,8 +73,9 @@ def test_ShapeArray_scaling2(): count = len(numpy.where(shape_arr.arr == 1)[0]) assert count >= 1 # should approach 3, but it can be inaccurate due to pixel rounding -## Test centerFirst def test_centerFirst(): + """Test centerFirst""" + ar = Arrange(300, 300, 150, 150, scale = 1) ar.centerFirst() assert ar._priority[150][150] < ar._priority[170][150] @@ -75,8 +85,9 @@ def test_centerFirst(): assert ar._priority[150][150] < ar._priority[150][130] assert ar._priority[150][150] < ar._priority[130][130] -## Test centerFirst def test_centerFirst_rectangular(): + """Test centerFirst""" + ar = Arrange(400, 300, 200, 150, scale = 1) ar.centerFirst() assert ar._priority[150][200] < ar._priority[150][220] @@ -86,15 +97,17 @@ def test_centerFirst_rectangular(): assert ar._priority[150][200] < ar._priority[130][200] assert ar._priority[150][200] < ar._priority[130][180] -## Test centerFirst def test_centerFirst_rectangular2(): + """Test centerFirst""" + ar = Arrange(10, 20, 5, 10, scale = 1) ar.centerFirst() assert ar._priority[10][5] < ar._priority[10][7] -## Test backFirst def test_backFirst(): + """Test backFirst""" + ar = Arrange(300, 300, 150, 150, scale = 1) ar.backFirst() assert ar._priority[150][150] < ar._priority[170][150] @@ -102,8 +115,9 @@ def test_backFirst(): assert ar._priority[150][150] > ar._priority[130][150] assert ar._priority[150][150] > ar._priority[130][130] -## See if the result of bestSpot has the correct form def test_smoke_bestSpot(): + """See if the result of bestSpot has the correct form""" + ar = Arrange(30, 30, 15, 15, scale = 1) ar.centerFirst() @@ -114,8 +128,9 @@ def test_smoke_bestSpot(): assert hasattr(best_spot, "penalty_points") assert hasattr(best_spot, "priority") -## Real life test def test_bestSpot(): + """Real life test""" + ar = Arrange(16, 16, 8, 8, scale = 1) ar.centerFirst() @@ -131,8 +146,9 @@ def test_bestSpot(): assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location ar.place(best_spot.x, best_spot.y, shape_arr) -## Real life test rectangular build plate def test_bestSpot_rectangular_build_plate(): + """Real life test rectangular build plate""" + ar = Arrange(16, 40, 8, 20, scale = 1) ar.centerFirst() @@ -164,8 +180,9 @@ def test_bestSpot_rectangular_build_plate(): best_spot_x = ar.bestSpot(shape_arr) ar.place(best_spot_x.x, best_spot_x.y, shape_arr) -## Real life test def test_bestSpot_scale(): + """Real life test""" + scale = 0.5 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -182,8 +199,9 @@ def test_bestSpot_scale(): assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location ar.place(best_spot.x, best_spot.y, shape_arr) -## Real life test def test_bestSpot_scale_rectangular(): + """Real life test""" + scale = 0.5 ar = Arrange(16, 40, 8, 20, scale = scale) ar.centerFirst() @@ -205,8 +223,9 @@ def test_bestSpot_scale_rectangular(): best_spot = ar.bestSpot(shape_arr_square) ar.place(best_spot.x, best_spot.y, shape_arr_square) -## Try to place an object and see if something explodes def test_smoke_place(): + """Try to place an object and see if something explodes""" + ar = Arrange(30, 30, 15, 15) ar.centerFirst() @@ -216,8 +235,9 @@ def test_smoke_place(): ar.place(0, 0, shape_arr) assert numpy.any(ar._occupied) -## See of our center has less penalty points than out of the center def test_checkShape(): + """See of our center has less penalty points than out of the center""" + ar = Arrange(30, 30, 15, 15) ar.centerFirst() @@ -228,8 +248,9 @@ def test_checkShape(): assert points2 > points assert points3 > points -## See of our center has less penalty points than out of the center def test_checkShape_rectangular(): + """See of our center has less penalty points than out of the center""" + ar = Arrange(20, 30, 10, 15) ar.centerFirst() @@ -240,8 +261,9 @@ def test_checkShape_rectangular(): assert points2 > points assert points3 > points -## Check that placing an object on occupied place returns None. def test_checkShape_place(): + """Check that placing an object on occupied place returns None.""" + ar = Arrange(30, 30, 15, 15) ar.centerFirst() @@ -252,8 +274,9 @@ def test_checkShape_place(): assert points2 is None -## Test the whole sequence def test_smoke_place_objects(): + """Test the whole sequence""" + ar = Arrange(20, 20, 10, 10, scale = 1) ar.centerFirst() shape_arr = gimmeShapeArray() @@ -268,26 +291,30 @@ def test_compare_occupied_and_priority_tables(): ar.centerFirst() assert ar._priority.shape == ar._occupied.shape -## Polygon -> array def test_arrayFromPolygon(): + """Polygon -> array""" + vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) array = ShapeArray.arrayFromPolygon([5, 5], vertices) assert numpy.any(array) -## Polygon -> array def test_arrayFromPolygon2(): + """Polygon -> array""" + vertices = numpy.array([[-3, 1], [3, 1], [2, -3]]) array = ShapeArray.arrayFromPolygon([5, 5], vertices) assert numpy.any(array) -## Polygon -> array def test_fromPolygon(): + """Polygon -> array""" + vertices = numpy.array([[0, 0.5], [0, 0], [0.5, 0]]) array = ShapeArray.fromPolygon(vertices, scale=0.5) assert numpy.any(array.arr) -## Line definition -> array with true/false def test_check(): + """Line definition -> array with true/false""" + base_array = numpy.zeros([5, 5], dtype=float) p1 = numpy.array([0, 0]) p2 = numpy.array([4, 4]) @@ -296,8 +323,9 @@ def test_check(): assert check_array[3][0] assert not check_array[0][3] -## Line definition -> array with true/false def test_check2(): + """Line definition -> array with true/false""" + base_array = numpy.zeros([5, 5], dtype=float) p1 = numpy.array([0, 3]) p2 = numpy.array([4, 3]) @@ -306,8 +334,9 @@ def test_check2(): assert not check_array[3][0] assert check_array[3][4] -## Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM def test_parts_of_fromNode(): + """Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM""" + from UM.Math.Polygon import Polygon p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32)) offset = 1