diff --git a/cura/API/Backups.py b/cura/API/Backups.py index a2423bd798..5964557264 100644 --- a/cura/API/Backups.py +++ b/cura/API/Backups.py @@ -1,5 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Tuple, Optional + from cura.Backups.BackupsManager import BackupsManager @@ -17,7 +19,7 @@ class Backups: ## Create a new back-up using the BackupsManager. # \return Tuple containing a ZIP file with the back-up data and a dict # with metadata about the back-up. - def createBackup(self) -> (bytes, dict): + def createBackup(self) -> Tuple[Optional[bytes], Optional[dict]]: return self.manager.createBackup() ## Restore a back-up using the BackupsManager. diff --git a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py index 6abc553a4b..8bbc2bf132 100644 --- a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py @@ -20,14 +20,14 @@ from typing import List ## Do arrangements on multiple build plates (aka builtiplexer) class ArrangeArray: - def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]): + def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]) -> None: self._x = x self._y = y self._fixed_nodes = fixed_nodes self._count = 0 self._first_empty = None self._has_empty = False - self._arrange = [] + self._arrange = [] # type: List[Arrange] def _update_first_empty(self): for i, a in enumerate(self._arrange): @@ -48,16 +48,17 @@ class ArrangeArray: return self._count def get(self, index): + print(self._arrange) return self._arrange[index] def getFirstEmpty(self): - if not self._is_empty: + if not self._has_empty: self.add() return self._arrange[self._first_empty] class ArrangeObjectsAllBuildPlatesJob(Job): - def __init__(self, nodes: List[SceneNode], min_offset = 8): + def __init__(self, nodes: List[SceneNode], min_offset = 8) -> None: super().__init__() self._nodes = nodes self._min_offset = min_offset diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index 5e982582fd..ce11556b5b 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -20,7 +20,7 @@ from typing import List class ArrangeObjectsJob(Job): - def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8): + def __init__(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode], min_offset = 8) -> None: super().__init__() self._nodes = nodes self._fixed_nodes = fixed_nodes diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 70807a96d7..f935aa6af5 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -28,12 +28,12 @@ class Backup: # Re-use translation catalog. catalog = i18nCatalog("cura") - def __init__(self, zip_file: bytes = None, meta_data: dict = None): + def __init__(self, zip_file: bytes = None, meta_data: dict = None) -> None: self.zip_file = zip_file # type: Optional[bytes] self.meta_data = meta_data # type: Optional[dict] ## Create a back-up from the current user config folder. - def makeFromCurrent(self) -> (bool, Optional[str]): + def makeFromCurrent(self) -> None: cura_release = CuraApplication.getInstance().getVersion() version_data_dir = Resources.getDataStoragePath() @@ -54,6 +54,8 @@ class Backup: # Create an empty buffer and write the archive to it. buffer = io.BytesIO() archive = self._makeArchive(buffer, version_data_dir) + if archive is None: + return files = archive.namelist() # Count the metadata items. We do this in a rather naive way at the moment. diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index 850b0a2edc..bc560a8dd9 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -1,6 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional +from typing import Optional, Tuple from UM.Logger import Logger from cura.Backups.Backup import Backup @@ -18,7 +18,7 @@ class BackupsManager: ## Get a back-up of the current configuration. # \return A tuple containing a ZipFile (the actual back-up) and a dict # containing some metadata (like version). - def createBackup(self) -> (Optional[bytes], Optional[dict]): + def createBackup(self) -> Tuple[Optional[bytes], Optional[dict]]: self._disableAutoSave() backup = Backup() backup.makeFromCurrent() diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 8544438f3a..1ddc41717e 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -1,12 +1,12 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import QObject, QUrl from PyQt5.QtGui import QDesktopServices -from UM.FlameProfiler import pyqtSlot +from typing import List, TYPE_CHECKING from UM.Event import CallFunctionEvent -from UM.Application import Application +from UM.FlameProfiler import pyqtSlot from UM.Math.Vector import Vector from UM.Scene.Selection import Selection from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator @@ -14,6 +14,7 @@ from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation from UM.Operations.TranslateOperation import TranslateOperation +import cura.CuraApplication from cura.Operations.SetParentOperation import SetParentOperation from cura.MultiplyObjectsJob import MultiplyObjectsJob from cura.Settings.SetObjectExtruderOperation import SetObjectExtruderOperation @@ -23,28 +24,30 @@ from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOper from UM.Logger import Logger +if TYPE_CHECKING: + from UM.Scene.SceneNode import SceneNode class CuraActions(QObject): - def __init__(self, parent = None): + def __init__(self, parent: QObject = None) -> None: super().__init__(parent) @pyqtSlot() - def openDocumentation(self): + def openDocumentation(self) -> None: # Starting a web browser from a signal handler connected to a menu will crash on windows. # So instead, defer the call to the next run of the event loop, since that does work. # Note that weirdly enough, only signal handlers that open a web browser fail like that. event = CallFunctionEvent(self._openUrl, [QUrl("http://ultimaker.com/en/support/software")], {}) - Application.getInstance().functionEvent(event) + cura.CuraApplication.CuraApplication.getInstance().functionEvent(event) @pyqtSlot() - def openBugReportPage(self): + def openBugReportPage(self) -> None: event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {}) - Application.getInstance().functionEvent(event) + cura.CuraApplication.CuraApplication.getInstance().functionEvent(event) ## Reset camera position and direction to default @pyqtSlot() def homeCamera(self) -> None: - scene = Application.getInstance().getController().getScene() + scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene() camera = scene.getActiveCamera() camera.setPosition(Vector(-80, 250, 700)) camera.setPerspective(True) @@ -72,17 +75,17 @@ class CuraActions(QObject): # \param count The number of times to multiply the selection. @pyqtSlot(int) def multiplySelection(self, count: int) -> None: - min_offset = Application.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors + min_offset = cura.CuraApplication.CuraApplication.getInstance().getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors job = MultiplyObjectsJob(Selection.getAllSelectedObjects(), count, min_offset = max(min_offset, 8)) job.start() ## Delete all selected objects. @pyqtSlot() def deleteSelection(self) -> None: - if not Application.getInstance().getController().getToolsEnabled(): + if not cura.CuraApplication.CuraApplication.getInstance().getController().getToolsEnabled(): return - removed_group_nodes = [] + removed_group_nodes = [] #type: List[SceneNode] op = GroupedOperation() nodes = Selection.getAllSelectedObjects() for node in nodes: @@ -96,7 +99,7 @@ class CuraActions(QObject): op.addOperation(RemoveSceneNodeOperation(group_node)) # Reset the print information - Application.getInstance().getController().getScene().sceneChanged.emit(node) + cura.CuraApplication.CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node) op.push() @@ -111,7 +114,7 @@ class CuraActions(QObject): for node in Selection.getAllSelectedObjects(): # If the node is a group, apply the active extruder to all children of the group. if node.callDecoration("isGroup"): - for grouped_node in BreadthFirstIterator(node): + for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if grouped_node.callDecoration("getActiveExtruder") == extruder_id: continue @@ -143,7 +146,7 @@ class CuraActions(QObject): Logger.log("d", "Setting build plate number... %d" % build_plate_nr) operation = GroupedOperation() - root = Application.getInstance().getController().getScene().getRoot() + root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot() nodes_to_change = [] for node in Selection.getAllSelectedObjects(): @@ -151,7 +154,7 @@ class CuraActions(QObject): while parent_node.getParent() != root: parent_node = parent_node.getParent() - for single_node in BreadthFirstIterator(parent_node): + for single_node in BreadthFirstIterator(parent_node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. nodes_to_change.append(single_node) if not nodes_to_change: @@ -164,5 +167,5 @@ class CuraActions(QObject): Selection.clear() - def _openUrl(self, url): + def _openUrl(self, url: QUrl) -> None: QDesktopServices.openUrl(url) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 8a3d84d728..b404e841b7 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -2,7 +2,6 @@ # Cura is released under the terms of the LGPLv3 or higher. import copy -import json import os import sys import time @@ -14,7 +13,8 @@ from PyQt5.QtGui import QColor, QIcon from PyQt5.QtWidgets import QMessageBox from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType -from UM.Qt.QtApplication import QtApplication +from typing import cast, TYPE_CHECKING + from UM.Scene.SceneNode import SceneNode from UM.Scene.Camera import Camera from UM.Math.Vector import Vector @@ -28,6 +28,8 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Mesh.ReadMeshJob import ReadMeshJob from UM.Logger import Logger from UM.Preferences import Preferences +from UM.Qt.QtApplication import QtApplication #The class we're inheriting from. +from UM.View.SelectionPass import SelectionPass #For typing. from UM.Scene.Selection import Selection from UM.Scene.GroupDecorator import GroupDecorator from UM.Settings.ContainerStack import ContainerStack @@ -109,14 +111,12 @@ from UM.FlameProfiler import pyqtSlot numpy.seterr(all = "ignore") -MYPY = False -if not MYPY: - try: - from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode - except ImportError: - CuraVersion = "master" # [CodeStyle: Reflecting imported value] - CuraBuildType = "" - CuraDebugMode = False +try: + from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode +except ImportError: + CuraVersion = "master" # [CodeStyle: Reflecting imported value] + CuraBuildType = "" + CuraDebugMode = False class CuraApplication(QtApplication): @@ -1720,7 +1720,7 @@ class CuraApplication(QtApplication): def _onContextMenuRequested(self, x: float, y: float) -> None: # Ensure we select the object if we request a context menu over an object without having a selection. if not Selection.hasSelection(): - node = self.getController().getScene().findObject(self.getRenderer().getRenderPass("selection").getIdAtPosition(x, y)) + node = self.getController().getScene().findObject(cast(SelectionPass, self.getRenderer().getRenderPass("selection")).getIdAtPosition(x, y)) if node: while(node.getParent() and node.getParent().callDecoration("isGroup")): node = node.getParent() diff --git a/cura/Machines/ContainerNode.py b/cura/Machines/ContainerNode.py index 44e2d6875d..944579e354 100644 --- a/cura/Machines/ContainerNode.py +++ b/cura/Machines/ContainerNode.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional +from typing import Optional, Any, Dict, Union, TYPE_CHECKING from collections import OrderedDict @@ -9,6 +9,9 @@ from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from UM.Logger import Logger from UM.Settings.InstanceContainer import InstanceContainer +if TYPE_CHECKING: + from cura.Machines.QualityGroup import QualityGroup + ## # A metadata / container combination. Use getContainer() to get the container corresponding to the metadata. @@ -23,10 +26,16 @@ from UM.Settings.InstanceContainer import InstanceContainer class ContainerNode: __slots__ = ("metadata", "container", "children_map") - def __init__(self, metadata: Optional[dict] = None): + def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None: self.metadata = metadata self.container = None - self.children_map = OrderedDict() + self.children_map = OrderedDict() #type: OrderedDict[str, Union[QualityGroup, ContainerNode]] + + ## Get an entry value from the metadata + def getMetaDataEntry(self, entry: str, default: Any = None) -> Any: + if self.metadata is None: + return default + return self.metadata.get(entry, default) def getChildNode(self, child_key: str) -> Optional["ContainerNode"]: return self.children_map.get(child_key) @@ -50,4 +59,4 @@ class ContainerNode: return self.container def __str__(self) -> str: - return "%s[%s]" % (self.__class__.__name__, self.metadata.get("id")) + return "%s[%s]" % (self.__class__.__name__, self.getMetaDataEntry("id")) diff --git a/cura/Machines/MaterialGroup.py b/cura/Machines/MaterialGroup.py index 93c8a227a8..b57e0e1808 100644 --- a/cura/Machines/MaterialGroup.py +++ b/cura/Machines/MaterialGroup.py @@ -18,10 +18,10 @@ from cura.Machines.MaterialNode import MaterialNode #For type checking. class MaterialGroup: __slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list") - def __init__(self, name: str, root_material_node: MaterialNode): + def __init__(self, name: str, root_material_node: MaterialNode) -> None: self.name = name self.is_read_only = False - self.root_material_node = root_material_node + self.root_material_node = root_material_node # type: MaterialNode self.derived_material_node_list = [] #type: List[MaterialNode] def __str__(self) -> str: diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py index ff666f392d..49ea10cf03 100644 --- a/cura/Machines/MaterialManager.py +++ b/cura/Machines/MaterialManager.py @@ -4,6 +4,7 @@ from collections import defaultdict, OrderedDict import copy import uuid +from typing import Dict from typing import Optional, TYPE_CHECKING from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot @@ -263,7 +264,7 @@ class MaterialManager(QObject): # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # def getAvailableMaterials(self, machine_definition: "DefinitionContainer", extruder_variant_name: Optional[str], - diameter: float) -> dict: + diameter: float) -> Dict[str, MaterialNode]: # round the diameter to get the approximate diameter rounded_diameter = str(round(diameter)) if rounded_diameter not in self._diameter_machine_variant_material_map: @@ -288,7 +289,7 @@ class MaterialManager(QObject): # 3. generic material (for fdmprinter) machine_exclude_materials = machine_definition.getMetaDataEntry("exclude_materials", []) - material_id_metadata_dict = dict() + material_id_metadata_dict = dict() # type: Dict[str, MaterialNode] for node in nodes_to_check: if node is not None: # Only exclude the materials that are explicitly specified in the "exclude_materials" field. @@ -434,7 +435,7 @@ class MaterialManager(QObject): nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list for node in nodes_to_remove: - self._container_registry.removeContainer(node.metadata["id"]) + self._container_registry.removeContainer(node.getMetaDataEntry("id", "")) # # Methods for GUI @@ -445,22 +446,27 @@ class MaterialManager(QObject): # @pyqtSlot("QVariant", str) def setMaterialName(self, material_node: "MaterialNode", name: str): - root_material_id = material_node.metadata["base_file"] + root_material_id = material_node.getMetaDataEntry("base_file") + if root_material_id is None: + return if self._container_registry.isReadOnly(root_material_id): Logger.log("w", "Cannot set name of read-only container %s.", root_material_id) return material_group = self.getMaterialGroup(root_material_id) if material_group: - material_group.root_material_node.getContainer().setName(name) + container = material_group.root_material_node.getContainer() + if container: + container.setName(name) # # Removes the given material. # @pyqtSlot("QVariant") def removeMaterial(self, material_node: "MaterialNode"): - root_material_id = material_node.metadata["base_file"] - self.removeMaterialByRootId(root_material_id) + root_material_id = material_node.getMetaDataEntry("base_file") + if root_material_id is not None: + self.removeMaterialByRootId(root_material_id) # # Creates a duplicate of a material, which has the same GUID and base_file metadata. @@ -539,6 +545,10 @@ class MaterialManager(QObject): root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter) material_group = self.getMaterialGroup(root_material_id) + if not material_group: # This should never happen + Logger.log("w", "Cannot get the material group of %s.", root_material_id) + return "" + # Create a new ID & container to hold the data. new_id = self._container_registry.uniqueName("custom_material") new_metadata = {"name": catalog.i18nc("@label", "Custom Material"), diff --git a/cura/Machines/MaterialNode.py b/cura/Machines/MaterialNode.py index fde11186c2..04423d7b2c 100644 --- a/cura/Machines/MaterialNode.py +++ b/cura/Machines/MaterialNode.py @@ -1,7 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - -from typing import Optional +from typing import Optional, Dict from .ContainerNode import ContainerNode @@ -15,7 +14,6 @@ from .ContainerNode import ContainerNode class MaterialNode(ContainerNode): __slots__ = ("material_map", "children_map") - def __init__(self, metadata: Optional[dict] = None): + def __init__(self, metadata: Optional[dict] = None) -> None: super().__init__(metadata = metadata) - self.material_map = {} # material_root_id -> material_node - self.children_map = {} # mapping for the child nodes + self.material_map = {} # type: Dict[str, MaterialNode] # material_root_id -> material_node diff --git a/cura/Machines/Models/QualityProfilesDropDownMenuModel.py b/cura/Machines/Models/QualityProfilesDropDownMenuModel.py index d8c4b668cf..02de23b10a 100644 --- a/cura/Machines/Models/QualityProfilesDropDownMenuModel.py +++ b/cura/Machines/Models/QualityProfilesDropDownMenuModel.py @@ -83,7 +83,7 @@ class QualityProfilesDropDownMenuModel(ListModel): self.setItems(item_list) - def _fetchLayerHeight(self, quality_group: "QualityGroup"): + def _fetchLayerHeight(self, quality_group: "QualityGroup") -> float: global_stack = self._machine_manager.activeMachine if not self._layer_height_unit: unit = global_stack.definition.getProperty("layer_height", "unit") @@ -94,10 +94,12 @@ class QualityProfilesDropDownMenuModel(ListModel): default_layer_height = global_stack.definition.getProperty("layer_height", "value") # Get layer_height from the quality profile for the GlobalStack + if quality_group.node_for_global is None: + return float(default_layer_height) container = quality_group.node_for_global.getContainer() layer_height = default_layer_height - if container.hasProperty("layer_height", "value"): + if container and container.hasProperty("layer_height", "value"): layer_height = container.getProperty("layer_height", "value") else: # Look for layer_height in the GlobalStack from material -> definition diff --git a/cura/Machines/QualityChangesGroup.py b/cura/Machines/QualityChangesGroup.py index 5ff5af3657..2d0e655ed8 100644 --- a/cura/Machines/QualityChangesGroup.py +++ b/cura/Machines/QualityChangesGroup.py @@ -1,22 +1,27 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import TYPE_CHECKING + from UM.Application import Application from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from .QualityGroup import QualityGroup +if TYPE_CHECKING: + from cura.Machines.QualityNode import QualityNode + class QualityChangesGroup(QualityGroup): - def __init__(self, name: str, quality_type: str, parent = None): + def __init__(self, name: str, quality_type: str, parent = None) -> None: super().__init__(name, quality_type, parent) self._container_registry = Application.getInstance().getContainerRegistry() def addNode(self, node: "QualityNode"): - extruder_position = node.metadata.get("position") + extruder_position = node.getMetaDataEntry("position") if extruder_position is None and self.node_for_global is not None or extruder_position in self.nodes_for_extruders: #We would be overwriting another node. - ConfigurationErrorMessage.getInstance().addFaultyContainers(node.metadata["id"]) + ConfigurationErrorMessage.getInstance().addFaultyContainers(node.getMetaDataEntry("id")) return if extruder_position is None: #Then we're a global quality changes profile. diff --git a/cura/Machines/QualityGroup.py b/cura/Machines/QualityGroup.py index 02096cfb36..90ef63af51 100644 --- a/cura/Machines/QualityGroup.py +++ b/cura/Machines/QualityGroup.py @@ -1,10 +1,10 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Dict, Optional, List +from typing import Dict, Optional, List, Set from PyQt5.QtCore import QObject, pyqtSlot - +from cura.Machines.ContainerNode import ContainerNode # # A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used. @@ -21,11 +21,11 @@ from PyQt5.QtCore import QObject, pyqtSlot # class QualityGroup(QObject): - def __init__(self, name: str, quality_type: str, parent = None): + def __init__(self, name: str, quality_type: str, parent = None) -> None: super().__init__(parent) self.name = name - self.node_for_global = None # type: Optional["QualityGroup"] - self.nodes_for_extruders = {} # type: Dict[int, "QualityGroup"] + self.node_for_global = None # type: Optional[ContainerNode] + self.nodes_for_extruders = {} # type: Dict[int, ContainerNode] self.quality_type = quality_type self.is_available = False @@ -33,15 +33,17 @@ class QualityGroup(QObject): def getName(self) -> str: return self.name - def getAllKeys(self) -> set: - result = set() + def getAllKeys(self) -> Set[str]: + result = set() #type: Set[str] for node in [self.node_for_global] + list(self.nodes_for_extruders.values()): if node is None: continue - result.update(node.getContainer().getAllKeys()) + container = node.getContainer() + if container: + result.update(container.getAllKeys()) return result - def getAllNodes(self) -> List["QualityGroup"]: + def getAllNodes(self) -> List[ContainerNode]: result = [] if self.node_for_global is not None: result.append(self.node_for_global) diff --git a/cura/Machines/QualityManager.py b/cura/Machines/QualityManager.py index cb0a2f5922..8033057f77 100644 --- a/cura/Machines/QualityManager.py +++ b/cura/Machines/QualityManager.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Optional, cast from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot @@ -90,7 +90,7 @@ class QualityManager(QObject): if definition_id not in self._machine_variant_material_quality_type_to_quality_dict: self._machine_variant_material_quality_type_to_quality_dict[definition_id] = QualityNode() - machine_node = self._machine_variant_material_quality_type_to_quality_dict[definition_id] + machine_node = cast(QualityNode, self._machine_variant_material_quality_type_to_quality_dict[definition_id]) if is_global_quality: # For global qualities, save data in the machine node @@ -102,7 +102,7 @@ class QualityManager(QObject): # too. if variant_name not in machine_node.children_map: machine_node.children_map[variant_name] = QualityNode() - variant_node = machine_node.children_map[variant_name] + variant_node = cast(QualityNode, machine_node.children_map[variant_name]) if root_material_id is None: # If only variant_name is specified but material is not, add the quality/quality_changes metadata @@ -114,7 +114,7 @@ class QualityManager(QObject): # material node. if root_material_id not in variant_node.children_map: variant_node.children_map[root_material_id] = QualityNode() - material_node = variant_node.children_map[root_material_id] + material_node = cast(QualityNode, variant_node.children_map[root_material_id]) material_node.addQualityMetadata(quality_type, metadata) @@ -123,7 +123,7 @@ class QualityManager(QObject): if root_material_id is not None: if root_material_id not in machine_node.children_map: machine_node.children_map[root_material_id] = QualityNode() - material_node = machine_node.children_map[root_material_id] + material_node = cast(QualityNode, machine_node.children_map[root_material_id]) material_node.addQualityMetadata(quality_type, metadata) @@ -351,7 +351,7 @@ class QualityManager(QObject): def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup"): Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name) for node in quality_changes_group.getAllNodes(): - self._container_registry.removeContainer(node.metadata["id"]) + self._container_registry.removeContainer(node.getMetaDataEntry("id")) # # Rename a set of quality changes containers. Returns the new name. @@ -365,7 +365,9 @@ class QualityManager(QObject): new_name = self._container_registry.uniqueName(new_name) for node in quality_changes_group.getAllNodes(): - node.getContainer().setName(new_name) + container = node.getContainer() + if container: + container.setName(new_name) quality_changes_group.name = new_name diff --git a/cura/Machines/QualityNode.py b/cura/Machines/QualityNode.py index a30e219da3..f384ee7825 100644 --- a/cura/Machines/QualityNode.py +++ b/cura/Machines/QualityNode.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional +from typing import Optional, Dict, cast from .ContainerNode import ContainerNode from .QualityChangesGroup import QualityChangesGroup @@ -12,9 +12,9 @@ from .QualityChangesGroup import QualityChangesGroup # class QualityNode(ContainerNode): - def __init__(self, metadata: Optional[dict] = None): + def __init__(self, metadata: Optional[dict] = None) -> None: super().__init__(metadata = metadata) - self.quality_type_map = {} # quality_type -> QualityNode for InstanceContainer + self.quality_type_map = {} # type: Dict[str, QualityNode] # quality_type -> QualityNode for InstanceContainer def addQualityMetadata(self, quality_type: str, metadata: dict): if quality_type not in self.quality_type_map: @@ -32,4 +32,4 @@ class QualityNode(ContainerNode): if name not in quality_type_node.children_map: quality_type_node.children_map[name] = QualityChangesGroup(name, quality_type) quality_changes_group = quality_type_node.children_map[name] - quality_changes_group.addNode(QualityNode(metadata)) + cast(QualityChangesGroup, quality_changes_group).addNode(QualityNode(metadata)) diff --git a/cura/PickingPass.py b/cura/PickingPass.py index 2a1abe8f63..bfe465ff39 100644 --- a/cura/PickingPass.py +++ b/cura/PickingPass.py @@ -1,6 +1,9 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from UM.Application import Application + +from typing import Optional, TYPE_CHECKING + +from UM.Qt.QtApplication import QtApplication from UM.Math.Vector import Vector from UM.Resources import Resources @@ -10,19 +13,21 @@ from UM.View.RenderBatch import RenderBatch from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +if TYPE_CHECKING: + from UM.View.GL.ShaderProgram import ShaderProgram ## A RenderPass subclass that renders a the distance of selectable objects from the active camera to a texture. # The texture is used to map a 2d location (eg the mouse location) to a world space position # # Note that in order to increase precision, the 24 bit depth value is encoded into all three of the R,G & B channels class PickingPass(RenderPass): - def __init__(self, width: int, height: int): + def __init__(self, width: int, height: int) -> None: super().__init__("picking", width, height) - self._renderer = Application.getInstance().getRenderer() + self._renderer = QtApplication.getInstance().getRenderer() - self._shader = None - self._scene = Application.getInstance().getController().getScene() + self._shader = None #type: Optional[ShaderProgram] + self._scene = QtApplication.getInstance().getController().getScene() def render(self) -> None: if not self._shader: @@ -37,7 +42,7 @@ class PickingPass(RenderPass): batch = RenderBatch(self._shader) # Fill up the batch with objects that can be sliced. ` - for node in DepthFirstIterator(self._scene.getRoot()): + for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): batch.addItem(node.getWorldTransformation(), node.getMeshData()) diff --git a/cura/PreviewPass.py b/cura/PreviewPass.py index 436e2719b7..befb52ee5e 100644 --- a/cura/PreviewPass.py +++ b/cura/PreviewPass.py @@ -1,5 +1,8 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. + +from typing import Optional, TYPE_CHECKING + from UM.Application import Application from UM.Resources import Resources @@ -10,7 +13,8 @@ from UM.View.RenderBatch import RenderBatch from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator -from typing import Optional +if TYPE_CHECKING: + from UM.View.GL.ShaderProgram import ShaderProgram MYPY = False if MYPY: @@ -33,16 +37,16 @@ def prettier_color(color_list): # # This is useful to get a preview image of a scene taken from a different location as the active camera. class PreviewPass(RenderPass): - def __init__(self, width: int, height: int): + def __init__(self, width: int, height: int) -> None: super().__init__("preview", width, height, 0) self._camera = None # type: Optional[Camera] self._renderer = Application.getInstance().getRenderer() - self._shader = None - self._non_printing_shader = None - self._support_mesh_shader = None + self._shader = None #type: Optional[ShaderProgram] + self._non_printing_shader = None #type: Optional[ShaderProgram] + self._support_mesh_shader = None #type: Optional[ShaderProgram] self._scene = Application.getInstance().getController().getScene() # Set the camera to be used by this render pass @@ -53,20 +57,23 @@ class PreviewPass(RenderPass): def render(self) -> None: if not self._shader: self._shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "overhang.shader")) - self._shader.setUniformValue("u_overhangAngle", 1.0) - self._shader.setUniformValue("u_ambientColor", [0.1, 0.1, 0.1, 1.0]) - self._shader.setUniformValue("u_specularColor", [0.6, 0.6, 0.6, 1.0]) - self._shader.setUniformValue("u_shininess", 20.0) + if self._shader: + self._shader.setUniformValue("u_overhangAngle", 1.0) + self._shader.setUniformValue("u_ambientColor", [0.1, 0.1, 0.1, 1.0]) + self._shader.setUniformValue("u_specularColor", [0.6, 0.6, 0.6, 1.0]) + self._shader.setUniformValue("u_shininess", 20.0) if not self._non_printing_shader: - self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader")) - self._non_printing_shader.setUniformValue("u_diffuseColor", [0.5, 0.5, 0.5, 0.5]) - self._non_printing_shader.setUniformValue("u_opacity", 0.6) + if self._non_printing_shader: + self._non_printing_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "transparent_object.shader")) + self._non_printing_shader.setUniformValue("u_diffuseColor", [0.5, 0.5, 0.5, 0.5]) + self._non_printing_shader.setUniformValue("u_opacity", 0.6) if not self._support_mesh_shader: self._support_mesh_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "striped.shader")) - self._support_mesh_shader.setUniformValue("u_vertical_stripes", True) - self._support_mesh_shader.setUniformValue("u_width", 5.0) + if self._support_mesh_shader: + self._support_mesh_shader.setUniformValue("u_vertical_stripes", True) + self._support_mesh_shader.setUniformValue("u_width", 5.0) self._gl.glClearColor(0.0, 0.0, 0.0, 0.0) self._gl.glClear(self._gl.GL_COLOR_BUFFER_BIT | self._gl.GL_DEPTH_BUFFER_BIT) @@ -75,8 +82,8 @@ class PreviewPass(RenderPass): batch = RenderBatch(self._shader) batch_support_mesh = RenderBatch(self._support_mesh_shader) - # Fill up the batch with objects that can be sliced. ` - for node in DepthFirstIterator(self._scene.getRoot()): + # Fill up the batch with objects that can be sliced. + for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible(): per_mesh_stack = node.callDecoration("getStack") if node.callDecoration("isNonThumbnailVisibleMesh"): diff --git a/cura/PrinterOutput/ExtruderOutputModel.py b/cura/PrinterOutput/ExtruderOutputModel.py index 75b9cc98ac..0726662c6c 100644 --- a/cura/PrinterOutput/ExtruderOutputModel.py +++ b/cura/PrinterOutput/ExtruderOutputModel.py @@ -4,10 +4,9 @@ from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel -from typing import Optional +from typing import Optional, TYPE_CHECKING -MYPY = False -if MYPY: +if TYPE_CHECKING: from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel @@ -20,12 +19,12 @@ class ExtruderOutputModel(QObject): extruderConfigurationChanged = pyqtSignal() isPreheatingChanged = pyqtSignal() - def __init__(self, printer: "PrinterOutputModel", position, parent=None): + def __init__(self, printer: "PrinterOutputModel", position, parent=None) -> None: super().__init__(parent) self._printer = printer self._position = position - self._target_hotend_temperature = 0 - self._hotend_temperature = 0 + self._target_hotend_temperature = 0 # type: float + self._hotend_temperature = 0 # type: float self._hotend_id = "" self._active_material = None # type: Optional[MaterialOutputModel] self._extruder_configuration = ExtruderConfigurationModel() @@ -47,7 +46,7 @@ class ExtruderOutputModel(QObject): return False @pyqtProperty(QObject, notify = activeMaterialChanged) - def activeMaterial(self) -> "MaterialOutputModel": + def activeMaterial(self) -> Optional["MaterialOutputModel"]: return self._active_material def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]): diff --git a/cura/PrinterOutput/GenericOutputController.py b/cura/PrinterOutput/GenericOutputController.py index dd1448a329..e6310e5bff 100644 --- a/cura/PrinterOutput/GenericOutputController.py +++ b/cura/PrinterOutput/GenericOutputController.py @@ -1,13 +1,15 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import TYPE_CHECKING + from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from PyQt5.QtCore import QTimer -MYPY = False -if MYPY: +if TYPE_CHECKING: from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel + from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel class GenericOutputController(PrinterOutputController): diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 97702ba08b..017fd0f0ed 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -1,17 +1,18 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from UM.Application import Application +from UM.FileHandler.FileHandler import FileHandler #For typing. from UM.Logger import Logger +from UM.Scene.SceneNode import SceneNode #For typing. +from cura.CuraApplication import CuraApplication from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState -from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply -from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, pyqtSignal, QUrl, QCoreApplication +from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator +from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication from time import time -from typing import Callable, Any, Optional, Dict, Tuple +from typing import Any, Callable, Dict, List, Optional from enum import IntEnum -from typing import List import os # To get the username import gzip @@ -27,20 +28,20 @@ class AuthState(IntEnum): class NetworkedPrinterOutputDevice(PrinterOutputDevice): authenticationStateChanged = pyqtSignal() - def __init__(self, device_id, address: str, properties, parent = None) -> None: + def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], parent: QObject = None) -> None: super().__init__(device_id = device_id, parent = parent) - self._manager = None # type: QNetworkAccessManager - self._last_manager_create_time = None # type: float + self._manager = None # type: Optional[QNetworkAccessManager] + self._last_manager_create_time = None # type: Optional[float] self._recreate_network_manager_time = 30 self._timeout_time = 10 # After how many seconds of no response should a timeout occur? - self._last_response_time = None # type: float - self._last_request_time = None # type: float + self._last_response_time = None # type: Optional[float] + self._last_request_time = None # type: Optional[float] self._api_prefix = "" self._address = address self._properties = properties - self._user_agent = "%s/%s " % (Application.getInstance().getApplicationName(), Application.getInstance().getVersion()) + self._user_agent = "%s/%s " % (CuraApplication.getInstance().getApplicationName(), CuraApplication.getInstance().getVersion()) self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]] self._authentication_state = AuthState.NotAuthenticated @@ -67,16 +68,16 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._printer_type = value break - def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs) -> None: + def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: raise NotImplementedError("requestWrite needs to be implemented") - def setAuthenticationState(self, authentication_state) -> None: + def setAuthenticationState(self, authentication_state: AuthState) -> None: if self._authentication_state != authentication_state: self._authentication_state = authentication_state self.authenticationStateChanged.emit() - @pyqtProperty(int, notify=authenticationStateChanged) - def authenticationState(self) -> int: + @pyqtProperty(int, notify = authenticationStateChanged) + def authenticationState(self) -> AuthState: return self._authentication_state def _compressDataAndNotifyQt(self, data_to_append: str) -> bytes: @@ -121,7 +122,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._compressing_gcode = False return b"".join(file_data_bytes_list) - def _update(self) -> bool: + def _update(self) -> None: if self._last_response_time: time_since_last_response = time() - self._last_response_time else: @@ -144,16 +145,16 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if time_since_last_response > self._recreate_network_manager_time: if self._last_manager_create_time is None: self._createNetworkManager() - if time() - self._last_manager_create_time > self._recreate_network_manager_time: + elif time() - self._last_manager_create_time > self._recreate_network_manager_time: self._createNetworkManager() + assert(self._manager is not None) elif self._connection_state == ConnectionState.closed: # Go out of timeout. - self.setConnectionState(self._connection_state_before_timeout) - self._connection_state_before_timeout = None + if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here + self.setConnectionState(self._connection_state_before_timeout) + self._connection_state_before_timeout = None - return True - - def _createEmptyRequest(self, target, content_type: Optional[str] = "application/json") -> QNetworkRequest: + def _createEmptyRequest(self, target: str, content_type: Optional[str] = "application/json") -> QNetworkRequest: url = QUrl("http://" + self._address + self._api_prefix + target) request = QNetworkRequest(url) if content_type is not None: @@ -161,7 +162,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request - def _createFormPart(self, content_header, data, content_type = None) -> QHttpPart: + def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: part = QHttpPart() if not content_header.startswith("form-data;"): @@ -187,33 +188,33 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if reply in self._kept_alive_multiparts: del self._kept_alive_multiparts[reply] - def put(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: + def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) self._last_request_time = time() reply = self._manager.put(request, data.encode()) - self._registerOnFinishedCallback(reply, onFinished) + self._registerOnFinishedCallback(reply, on_finished) - def get(self, target: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: + def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) self._last_request_time = time() reply = self._manager.get(request) - self._registerOnFinishedCallback(reply, onFinished) + self._registerOnFinishedCallback(reply, on_finished) - def post(self, target: str, data: str, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None: + def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target) self._last_request_time = time() reply = self._manager.post(request, data) - if onProgress is not None: - reply.uploadProgress.connect(onProgress) - self._registerOnFinishedCallback(reply, onFinished) + if on_progress is not None: + reply.uploadProgress.connect(on_progress) + self._registerOnFinishedCallback(reply, on_finished) - def postFormWithParts(self, target:str, parts: List[QHttpPart], onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None: + def postFormWithParts(self, target:str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply: if self._manager is None: self._createNetworkManager() request = self._createEmptyRequest(target, content_type=None) @@ -227,20 +228,20 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._kept_alive_multiparts[reply] = multi_post_part - if onProgress is not None: - reply.uploadProgress.connect(onProgress) - self._registerOnFinishedCallback(reply, onFinished) + if on_progress is not None: + reply.uploadProgress.connect(on_progress) + self._registerOnFinishedCallback(reply, on_finished) return reply - def postForm(self, target: str, header_data: str, body_data: bytes, onFinished: Optional[Callable[[Any, QNetworkReply], None]], onProgress: Callable = None) -> None: + def postForm(self, target: str, header_data: str, body_data: bytes, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: post_part = QHttpPart() post_part.setHeader(QNetworkRequest.ContentDispositionHeader, header_data) post_part.setBody(body_data) - self.postFormWithParts(target, [post_part], onFinished, onProgress) + self.postFormWithParts(target, [post_part], on_finished, on_progress) - def _onAuthenticationRequired(self, reply, authenticator) -> None: + def _onAuthenticationRequired(self, reply: QNetworkReply, authenticator: QAuthenticator) -> None: Logger.log("w", "Request to {url} required authentication, which was not implemented".format(url = reply.url().toString())) def _createNetworkManager(self) -> None: @@ -255,11 +256,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._manager.authenticationRequired.connect(self._onAuthenticationRequired) if self._properties.get(b"temporary", b"false") != b"true": - Application.getInstance().getMachineManager().checkCorrectGroupName(self.getId(), self.name) + CuraApplication.getInstance().getMachineManager().checkCorrectGroupName(self.getId(), self.name) - def _registerOnFinishedCallback(self, reply: QNetworkReply, onFinished: Optional[Callable[[Any, QNetworkReply], None]]) -> None: - if onFinished is not None: - self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = onFinished + def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: + if on_finished is not None: + self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished def __handleOnFinished(self, reply: QNetworkReply) -> None: # Due to garbage collection, we need to cache certain bits of post operations. @@ -296,30 +297,30 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): ## Get the unique key of this machine # \return key String containing the key of the machine. - @pyqtProperty(str, constant=True) + @pyqtProperty(str, constant = True) def key(self) -> str: return self._id ## The IP address of the printer. - @pyqtProperty(str, constant=True) + @pyqtProperty(str, constant = True) def address(self) -> str: return self._properties.get(b"address", b"").decode("utf-8") ## Name of the printer (as returned from the ZeroConf properties) - @pyqtProperty(str, constant=True) + @pyqtProperty(str, constant = True) def name(self) -> str: return self._properties.get(b"name", b"").decode("utf-8") ## Firmware version (as returned from the ZeroConf properties) - @pyqtProperty(str, constant=True) + @pyqtProperty(str, constant = True) def firmwareVersion(self) -> str: return self._properties.get(b"firmware_version", b"").decode("utf-8") - @pyqtProperty(str, constant=True) + @pyqtProperty(str, constant = True) def printerType(self) -> str: return self._printer_type - ## IPadress of this printer - @pyqtProperty(str, constant=True) + ## IP adress of this printer + @pyqtProperty(str, constant = True) def ipAddress(self) -> str: return self._address diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py index 92376ad1dd..b77600f85c 100644 --- a/cura/PrinterOutput/PrintJobOutputModel.py +++ b/cura/PrinterOutput/PrintJobOutputModel.py @@ -2,9 +2,9 @@ # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot -from typing import Optional -MYPY = False -if MYPY: +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel @@ -18,7 +18,7 @@ class PrintJobOutputModel(QObject): assignedPrinterChanged = pyqtSignal() ownerChanged = pyqtSignal() - def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None): + def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None: super().__init__(parent) self._output_controller = output_controller self._state = "" diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 928a882c8c..6fafa368bb 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -27,7 +27,7 @@ class PrinterOutputModel(QObject): cameraChanged = pyqtSignal() configurationChanged = pyqtSignal() - def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = ""): + def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None: super().__init__(parent) self._bed_temperature = -1 # Use -1 for no heated bed. self._target_bed_temperature = 0 diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 98ba4a19a8..67f02415e4 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -1,17 +1,20 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. + from UM.Decorators import deprecated from UM.i18n import i18nCatalog from UM.OutputDevice.OutputDevice import OutputDevice -from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal, QVariant +from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal from PyQt5.QtWidgets import QMessageBox from UM.Logger import Logger +from UM.FileHandler.FileHandler import FileHandler #For typing. +from UM.Scene.SceneNode import SceneNode #For typing. from UM.Signal import signalemitter -from UM.Application import Application +from UM.Qt.QtApplication import QtApplication from enum import IntEnum # For the connection state tracking. -from typing import List, Optional +from typing import Callable, List, Optional MYPY = False if MYPY: @@ -20,6 +23,16 @@ if MYPY: i18n_catalog = i18nCatalog("cura") + +## The current processing state of the backend. +class ConnectionState(IntEnum): + closed = 0 + connecting = 1 + connected = 2 + busy = 3 + error = 4 + + ## Printer output device adds extra interface options on top of output device. # # The assumption is made the printer is a FDM printer. @@ -47,38 +60,37 @@ class PrinterOutputDevice(QObject, OutputDevice): # Signal to indicate that the configuration of one of the printers has changed. uniqueConfigurationsChanged = pyqtSignal() - def __init__(self, device_id, parent = None): + def __init__(self, device_id: str, parent: QObject = None) -> None: super().__init__(device_id = device_id, parent = parent) self._printers = [] # type: List[PrinterOutputModel] self._unique_configurations = [] # type: List[ConfigurationModel] - self._monitor_view_qml_path = "" - self._monitor_component = None - self._monitor_item = None + self._monitor_view_qml_path = "" #type: str + self._monitor_component = None #type: Optional[QObject] + self._monitor_item = None #type: Optional[QObject] - self._control_view_qml_path = "" - self._control_component = None - self._control_item = None + self._control_view_qml_path = "" #type: str + self._control_component = None #type: Optional[QObject] + self._control_item = None #type: Optional[QObject] - self._qml_context = None - self._accepts_commands = False + self._accepts_commands = False #type: bool - self._update_timer = QTimer() + self._update_timer = QTimer() #type: QTimer self._update_timer.setInterval(2000) # TODO; Add preference for update interval self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) - self._connection_state = ConnectionState.closed + self._connection_state = ConnectionState.closed #type: ConnectionState - self._firmware_name = None - self._address = "" - self._connection_text = "" + self._firmware_name = None #type: Optional[str] + self._address = "" #type: str + self._connection_text = "" #type: str self.printersChanged.connect(self._onPrintersChanged) - Application.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations) + QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations) @pyqtProperty(str, notify = connectionTextChanged) - def address(self): + def address(self) -> str: return self._address def setConnectionText(self, connection_text): @@ -87,36 +99,36 @@ class PrinterOutputDevice(QObject, OutputDevice): self.connectionTextChanged.emit() @pyqtProperty(str, constant=True) - def connectionText(self): + def connectionText(self) -> str: return self._connection_text - def materialHotendChangedMessage(self, callback): + def materialHotendChangedMessage(self, callback: Callable[[int], None]) -> None: Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'") callback(QMessageBox.Yes) - def isConnected(self): + def isConnected(self) -> bool: return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error - def setConnectionState(self, connection_state): + def setConnectionState(self, connection_state: ConnectionState) -> None: if self._connection_state != connection_state: self._connection_state = connection_state self.connectionStateChanged.emit(self._id) @pyqtProperty(str, notify = connectionStateChanged) - def connectionState(self): + def connectionState(self) -> ConnectionState: return self._connection_state - def _update(self): + def _update(self) -> None: pass - def _getPrinterByKey(self, key) -> Optional["PrinterOutputModel"]: + def _getPrinterByKey(self, key: str) -> Optional["PrinterOutputModel"]: for printer in self._printers: if printer.key == key: return printer return None - def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): + def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: raise NotImplementedError("requestWrite needs to be implemented") @pyqtProperty(QObject, notify = printersChanged) @@ -126,11 +138,11 @@ class PrinterOutputDevice(QObject, OutputDevice): return None @pyqtProperty("QVariantList", notify = printersChanged) - def printers(self): + def printers(self) -> List["PrinterOutputModel"]: return self._printers - @pyqtProperty(QObject, constant=True) - def monitorItem(self): + @pyqtProperty(QObject, constant = True) + def monitorItem(self) -> QObject: # Note that we specifically only check if the monitor component is created. # It could be that it failed to actually create the qml item! If we check if the item was created, it will try to # create the item (and fail) every time. @@ -138,49 +150,49 @@ class PrinterOutputDevice(QObject, OutputDevice): self._createMonitorViewFromQML() return self._monitor_item - @pyqtProperty(QObject, constant=True) - def controlItem(self): + @pyqtProperty(QObject, constant = True) + def controlItem(self) -> QObject: if not self._control_component: self._createControlViewFromQML() return self._control_item - def _createControlViewFromQML(self): + def _createControlViewFromQML(self) -> None: if not self._control_view_qml_path: return if self._control_item is None: - self._control_item = Application.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self}) + self._control_item = QtApplication.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self}) - def _createMonitorViewFromQML(self): + def _createMonitorViewFromQML(self) -> None: if not self._monitor_view_qml_path: return if self._monitor_item is None: - self._monitor_item = Application.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self}) + self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self}) ## Attempt to establish connection - def connect(self): + def connect(self) -> None: self.setConnectionState(ConnectionState.connecting) self._update_timer.start() ## Attempt to close the connection - def close(self): + def close(self) -> None: self._update_timer.stop() self.setConnectionState(ConnectionState.closed) ## Ensure that close gets called when object is destroyed - def __del__(self): + def __del__(self) -> None: self.close() - @pyqtProperty(bool, notify=acceptsCommandsChanged) - def acceptsCommands(self): + @pyqtProperty(bool, notify = acceptsCommandsChanged) + def acceptsCommands(self) -> bool: return self._accepts_commands @deprecated("Please use the protected function instead", "3.2") - def setAcceptsCommands(self, accepts_commands): + def setAcceptsCommands(self, accepts_commands: bool) -> None: self._setAcceptsCommands(accepts_commands) ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands - def _setAcceptsCommands(self, accepts_commands): + def _setAcceptsCommands(self, accepts_commands: bool) -> None: if self._accepts_commands != accepts_commands: self._accepts_commands = accepts_commands @@ -188,15 +200,15 @@ class PrinterOutputDevice(QObject, OutputDevice): # Returns the unique configurations of the printers within this output device @pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged) - def uniqueConfigurations(self): + def uniqueConfigurations(self) -> List["ConfigurationModel"]: return self._unique_configurations - def _updateUniqueConfigurations(self): + def _updateUniqueConfigurations(self) -> None: self._unique_configurations = list(set([printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None])) self._unique_configurations.sort(key = lambda k: k.printerType) self.uniqueConfigurationsChanged.emit() - def _onPrintersChanged(self): + def _onPrintersChanged(self) -> None: for printer in self._printers: printer.configurationChanged.connect(self._updateUniqueConfigurations) @@ -205,21 +217,12 @@ class PrinterOutputDevice(QObject, OutputDevice): ## Set the device firmware name # - # \param name \type{str} The name of the firmware. - def _setFirmwareName(self, name): + # \param name The name of the firmware. + def _setFirmwareName(self, name: str) -> None: self._firmware_name = name ## Get the name of device firmware # # This name can be used to define device type - def getFirmwareName(self): - return self._firmware_name - - -## The current processing state of the backend. -class ConnectionState(IntEnum): - closed = 0 - connecting = 1 - connected = 2 - busy = 3 - error = 4 + def getFirmwareName(self) -> Optional[str]: + return self._firmware_name \ No newline at end of file diff --git a/cura/Scene/CuraSceneController.py b/cura/Scene/CuraSceneController.py index 749c5257a2..4b19271538 100644 --- a/cura/Scene/CuraSceneController.py +++ b/cura/Scene/CuraSceneController.py @@ -16,7 +16,7 @@ from UM.Signal import Signal class CuraSceneController(QObject): activeBuildPlateChanged = Signal() - def __init__(self, objects_model: ObjectsModel, multi_build_plate_model: MultiBuildPlateModel): + def __init__(self, objects_model: ObjectsModel, multi_build_plate_model: MultiBuildPlateModel) -> None: super().__init__() self._objects_model = objects_model diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index 92f1d839fb..259c273329 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -1,40 +1,47 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. + from copy import deepcopy -from typing import List, Optional +from typing import cast, Dict, List, Optional from UM.Application import Application from UM.Math.AxisAlignedBox import AxisAlignedBox +from UM.Math.Polygon import Polygon #For typing. from UM.Scene.SceneNode import SceneNode +from UM.Scene.SceneNodeDecorator import SceneNodeDecorator #To cast the deepcopy of every decorator back to SceneNodeDecorator. -from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator - +import cura.CuraApplication #To get the build plate. +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): - def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False): + 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: self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled self._outside_buildarea = False - def setOutsideBuildArea(self, new_value): + def setOutsideBuildArea(self, new_value: bool) -> None: self._outside_buildarea = new_value - def isOutsideBuildArea(self): + def isOutsideBuildArea(self) -> bool: return self._outside_buildarea or self.callDecoration("getBuildPlateNumber") < 0 - def isVisible(self): - return super().isVisible() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getMultiBuildPlateModel().activeBuildPlate + def isVisible(self) -> bool: + return super().isVisible() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate def isSelectable(self) -> bool: - return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == Application.getInstance().getMultiBuildPlateModel().activeBuildPlate + 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): + def getPrintingExtruder(self) -> Optional[ExtruderStack]: global_container_stack = Application.getInstance().getGlobalContainerStack() + if global_container_stack is None: + return None + per_mesh_stack = self.callDecoration("getStack") extruders = list(global_container_stack.extruders.values()) @@ -79,17 +86,17 @@ class CuraSceneNode(SceneNode): ] ## Return if the provided bbox collides with the bbox of this scene node - def collidesWithBbox(self, check_bbox): + def collidesWithBbox(self, check_bbox: AxisAlignedBox) -> bool: bbox = self.getBoundingBox() - - # Mark the node as outside the build volume if the bounding box test fails. - if check_bbox.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection: - return True + if bbox is not None: + # Mark the node as outside the build volume if the bounding box test fails. + if check_bbox.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection: + return True return False ## Return if any area collides with the convex hull of this scene node - def collidesWithArea(self, areas): + def collidesWithArea(self, areas: List[Polygon]) -> bool: convex_hull = self.callDecoration("getConvexHull") if convex_hull: if not convex_hull.isValid(): @@ -104,8 +111,7 @@ class CuraSceneNode(SceneNode): return False ## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box - def _calculateAABB(self): - aabb = None + def _calculateAABB(self) -> None: if self._mesh_data: aabb = self._mesh_data.getExtents(self.getWorldTransformation()) else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0) @@ -123,18 +129,18 @@ class CuraSceneNode(SceneNode): self._aabb = aabb ## Taken from SceneNode, but replaced SceneNode with CuraSceneNode - def __deepcopy__(self, memo): + def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode": copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later copy.setTransformation(self.getLocalTransformation()) copy.setMeshData(self._mesh_data) - copy.setVisible(deepcopy(self._visible, memo)) - copy._selectable = deepcopy(self._selectable, memo) - copy._name = deepcopy(self._name, memo) + copy.setVisible(cast(bool, deepcopy(self._visible, memo))) + copy._selectable = cast(bool, deepcopy(self._selectable, memo)) + copy._name = cast(str, deepcopy(self._name, memo)) for decorator in self._decorators: - copy.addDecorator(deepcopy(decorator, memo)) + copy.addDecorator(cast(SceneNodeDecorator, deepcopy(decorator, memo))) for child in self._children: - copy.addChild(deepcopy(child, memo)) + copy.addChild(cast(SceneNode, deepcopy(child, memo))) self.calculateBoundingBoxMesh() return copy diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 0ba4963886..238f4dfc32 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -468,7 +468,7 @@ class ContainerManager(QObject): container_list = [n.getContainer() for n in quality_changes_group.getAllNodes() if n.getContainer() is not None] self._container_registry.exportQualityProfile(container_list, path, file_type) - __instance = None + __instance = None # type: ContainerManager @classmethod def getInstance(cls, *args, **kwargs) -> "ContainerManager": diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index f5036078be..6cbb3036f8 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -5,7 +5,7 @@ import os import re import configparser -from typing import Optional +from typing import cast, Optional from PyQt5.QtWidgets import QMessageBox @@ -26,7 +26,7 @@ from UM.Resources import Resources from . import ExtruderStack from . import GlobalStack -from cura.CuraApplication import CuraApplication +import cura.CuraApplication from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch from cura.ReaderWriters.ProfileReader import NoProfileException @@ -57,7 +57,7 @@ class CuraContainerRegistry(ContainerRegistry): if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()): # Check against setting version of the definition. - required_setting_version = CuraApplication.SettingVersion + 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)) @@ -260,7 +260,7 @@ class CuraContainerRegistry(ContainerRegistry): profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1)) profile = InstanceContainer(profile_id) profile.setName(quality_name) - profile.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) + profile.addMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion) profile.addMetaDataEntry("type", "quality_changes") profile.addMetaDataEntry("definition", expected_machine_definition) profile.addMetaDataEntry("quality_type", quality_type) @@ -356,13 +356,15 @@ class CuraContainerRegistry(ContainerRegistry): return catalog.i18nc("@info:status", "Profile is missing a quality type.") global_stack = Application.getInstance().getGlobalContainerStack() + if global_stack is None: + return None definition_id = getMachineDefinitionIDForQualitySearch(global_stack.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_manager = CuraApplication.getInstance()._quality_manager + quality_manager = cura.CuraApplication.CuraApplication.getInstance()._quality_manager quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack) if 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) @@ -465,7 +467,7 @@ class CuraContainerRegistry(ContainerRegistry): def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True): new_extruder_id = extruder_id - application = CuraApplication.getInstance() + application = cura.CuraApplication.CuraApplication.getInstance() extruder_definitions = self.findDefinitionContainers(id = new_extruder_id) if not extruder_definitions: @@ -485,7 +487,7 @@ class CuraContainerRegistry(ContainerRegistry): definition_changes_name = definition_changes_id definition_changes = InstanceContainer(definition_changes_id, parent = application) definition_changes.setName(definition_changes_name) - definition_changes.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) + definition_changes.addMetaDataEntry("setting_version", application.SettingVersion) definition_changes.addMetaDataEntry("type", "definition_changes") definition_changes.addMetaDataEntry("definition", extruder_definition.getId()) @@ -514,7 +516,7 @@ class CuraContainerRegistry(ContainerRegistry): user_container.setName(user_container_name) user_container.addMetaDataEntry("type", "user") user_container.addMetaDataEntry("machine", machine.getId()) - user_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) + user_container.addMetaDataEntry("setting_version", application.SettingVersion) user_container.setDefinition(machine.definition.getId()) user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position")) @@ -587,7 +589,7 @@ class CuraContainerRegistry(ContainerRegistry): extruder_quality_changes_container = InstanceContainer(container_id, parent = application) extruder_quality_changes_container.setName(container_name) extruder_quality_changes_container.addMetaDataEntry("type", "quality_changes") - extruder_quality_changes_container.addMetaDataEntry("setting_version", CuraApplication.SettingVersion) + extruder_quality_changes_container.addMetaDataEntry("setting_version", application.SettingVersion) extruder_quality_changes_container.addMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) extruder_quality_changes_container.addMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type")) extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId()) @@ -675,7 +677,7 @@ class CuraContainerRegistry(ContainerRegistry): return extruder_stack def _findQualityChangesContainerInCuraFolder(self, name): - quality_changes_dir = Resources.getPath(CuraApplication.ResourceTypes.QualityChangesInstanceContainer) + quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer) instance_container = None @@ -731,3 +733,9 @@ class CuraContainerRegistry(ContainerRegistry): 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)) \ No newline at end of file diff --git a/cura/Settings/CuraContainerStack.py b/cura/Settings/CuraContainerStack.py index e1f89eb725..0cee696a8d 100755 --- a/cura/Settings/CuraContainerStack.py +++ b/cura/Settings/CuraContainerStack.py @@ -1,15 +1,12 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import os.path - -from typing import Any, Optional - +from typing import Any, cast, List, Optional, Union from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject -from UM.FlameProfiler import pyqtSlot from UM.Application import Application from UM.Decorators import override +from UM.FlameProfiler import pyqtSlot from UM.Logger import Logger from UM.Settings.ContainerStack import ContainerStack, InvalidContainerStackError from UM.Settings.InstanceContainer import InstanceContainer @@ -39,19 +36,19 @@ from . import Exceptions # This also means that operations on the stack that modifies the container ordering is prohibited and # will raise an exception. class CuraContainerStack(ContainerStack): - def __init__(self, container_id: str): + def __init__(self, container_id: str) -> None: super().__init__(container_id) - self._container_registry = ContainerRegistry.getInstance() + self._container_registry = ContainerRegistry.getInstance() #type: ContainerRegistry - self._empty_instance_container = self._container_registry.getEmptyInstanceContainer() + self._empty_instance_container = self._container_registry.getEmptyInstanceContainer() #type: InstanceContainer - self._empty_quality_changes = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0] - self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0] - self._empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0] - self._empty_variant = self._container_registry.findInstanceContainers(id = "empty_variant")[0] + self._empty_quality_changes = self._container_registry.findInstanceContainers(id = "empty_quality_changes")[0] #type: InstanceContainer + self._empty_quality = self._container_registry.findInstanceContainers(id = "empty_quality")[0] #type: InstanceContainer + self._empty_material = self._container_registry.findInstanceContainers(id = "empty_material")[0] #type: InstanceContainer + self._empty_variant = self._container_registry.findInstanceContainers(id = "empty_variant")[0] #type: InstanceContainer - self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] + self._containers = [self._empty_instance_container for i in range(len(_ContainerIndexes.IndexTypeMap))] #type: List[Union[InstanceContainer, DefinitionContainer]] self._containers[_ContainerIndexes.QualityChanges] = self._empty_quality_changes self._containers[_ContainerIndexes.Quality] = self._empty_quality self._containers[_ContainerIndexes.Material] = self._empty_material @@ -76,7 +73,7 @@ class CuraContainerStack(ContainerStack): # \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: - return self._containers[_ContainerIndexes.UserChanges] + return cast(InstanceContainer, self._containers[_ContainerIndexes.UserChanges]) ## Set the quality changes container. # @@ -89,12 +86,12 @@ class CuraContainerStack(ContainerStack): # \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: - return self._containers[_ContainerIndexes.QualityChanges] + return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges]) ## 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 = False) -> None: + def setQuality(self, new_quality: InstanceContainer, postpone_emit: bool = False) -> None: self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit) ## Get the quality container. @@ -102,12 +99,12 @@ class CuraContainerStack(ContainerStack): # \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: - return self._containers[_ContainerIndexes.Quality] + 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 = False) -> None: + def setMaterial(self, new_material: InstanceContainer, postpone_emit: bool = False) -> None: self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit) ## Get the material container. @@ -115,7 +112,7 @@ class CuraContainerStack(ContainerStack): # \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: - return self._containers[_ContainerIndexes.Material] + return cast(InstanceContainer, self._containers[_ContainerIndexes.Material]) ## Set the variant container. # @@ -128,7 +125,7 @@ class CuraContainerStack(ContainerStack): # \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: - return self._containers[_ContainerIndexes.Variant] + return cast(InstanceContainer, self._containers[_ContainerIndexes.Variant]) ## Set the definition changes container. # @@ -141,7 +138,7 @@ class CuraContainerStack(ContainerStack): # \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: - return self._containers[_ContainerIndexes.DefinitionChanges] + return cast(InstanceContainer, self._containers[_ContainerIndexes.DefinitionChanges]) ## Set the definition container. # @@ -154,7 +151,7 @@ class CuraContainerStack(ContainerStack): # \return The definition container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(QObject, fset = setDefinition, notify = pyqtContainersChanged) def definition(self) -> DefinitionContainer: - return self._containers[_ContainerIndexes.Definition] + return cast(DefinitionContainer, self._containers[_ContainerIndexes.Definition]) @override(ContainerStack) def getBottom(self) -> "DefinitionContainer": @@ -189,13 +186,9 @@ class CuraContainerStack(ContainerStack): # \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. - # \param target_container The type of the container to set the property of. Defaults to "user". - def setProperty(self, key: str, property_name: str, new_value: Any, target_container: str = "user") -> None: - container_index = _ContainerIndexes.TypeIndexMap.get(target_container, -1) - if container_index != -1: - self._containers[container_index].setProperty(key, property_name, new_value) - else: - raise IndexError("Invalid target container {type}".format(type = target_container)) + def setProperty(self, key: str, property_name: str, property_value: Any, container: "ContainerInterface" = None, set_from_cache: bool = False) -> None: + container_index = _ContainerIndexes.UserChanges + self._containers[container_index].setProperty(key, property_name, property_value, container, set_from_cache) ## Overridden from ContainerStack # @@ -310,15 +303,15 @@ class CuraContainerStack(ContainerStack): # # \return The ID of the definition container to use when searching for instance containers. @classmethod - def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainer) -> str: + def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str: quality_definition = machine_definition.getMetaDataEntry("quality_definition") if not quality_definition: - return machine_definition.id + return machine_definition.id #type: ignore definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = quality_definition) if not definitions: - Logger.log("w", "Unable to find parent definition {parent} for machine {machine}", parent = quality_definition, machine = machine_definition.id) - return machine_definition.id + Logger.log("w", "Unable to find parent definition {parent} for machine {machine}", parent = quality_definition, machine = machine_definition.id) #type: ignore + return machine_definition.id #type: ignore return cls._findInstanceContainerDefinitionId(definitions[0]) diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 25690fcbde..a773bccc7e 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -1,10 +1,10 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt. from UM.FlameProfiler import pyqtSlot -from UM.Application import Application # To get the global container stack to find the current machine. +import cura.CuraApplication #To get the global container stack to find the current machine. from UM.Logger import Logger from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode @@ -16,7 +16,7 @@ from UM.Settings.SettingInstance import SettingInstance from UM.Settings.ContainerStack import ContainerStack from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext -from typing import Optional, List, TYPE_CHECKING, Union +from typing import Optional, List, TYPE_CHECKING, Union, Dict if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack @@ -36,14 +36,13 @@ class ExtruderManager(QObject): super().__init__(parent) - self._application = Application.getInstance() + self._application = cura.CuraApplication.CuraApplication.getInstance() self._extruder_trains = {} # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders. self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack self._selected_object_extruders = [] self._addCurrentMachineExtruders() - #Application.getInstance().globalContainerStackChanged.connect(self._globalContainerStackChanged) Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) ## Signal to notify other components when the list of extruders for a machine definition changes. @@ -60,42 +59,47 @@ class ExtruderManager(QObject): # \return The unique ID of the currently active extruder stack. @pyqtProperty(str, notify = activeExtruderChanged) def activeExtruderStackId(self) -> Optional[str]: - if not Application.getInstance().getGlobalContainerStack(): + if not self._application.getGlobalContainerStack(): return None # No active machine, so no active extruder. try: - return self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId() + return self._extruder_trains[self._application.getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId() 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 ## Return extruder count according to extruder trains. @pyqtProperty(int, notify = extrudersChanged) def extruderCount(self): - if not Application.getInstance().getGlobalContainerStack(): + if not self._application.getGlobalContainerStack(): return 0 # No active machine, so no extruders. try: - return len(self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]) + return len(self._extruder_trains[self._application.getGlobalContainerStack().getId()]) except KeyError: return 0 ## Gets a dict with the extruder stack ids with the extruder number as the key. @pyqtProperty("QVariantMap", notify = extrudersChanged) - def extruderIds(self): + def extruderIds(self) -> Dict[str, str]: extruder_stack_ids = {} - global_stack_id = Application.getInstance().getGlobalContainerStack().getId() + global_container_stack = self._application.getGlobalContainerStack() + if global_container_stack: + global_stack_id = global_container_stack.getId() - if global_stack_id in self._extruder_trains: - for position in self._extruder_trains[global_stack_id]: - extruder_stack_ids[position] = self._extruder_trains[global_stack_id][position].getId() + if global_stack_id in self._extruder_trains: + for position in self._extruder_trains[global_stack_id]: + extruder_stack_ids[position] = self._extruder_trains[global_stack_id][position].getId() return extruder_stack_ids @pyqtSlot(str, result = str) def getQualityChangesIdByExtruderStackId(self, extruder_stack_id: str) -> str: - for position in self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()]: - extruder = self._extruder_trains[Application.getInstance().getGlobalContainerStack().getId()][position] - if extruder.getId() == extruder_stack_id: - return extruder.qualityChanges.getId() + global_container_stack = self._application.getGlobalContainerStack() + if global_container_stack is not None: + for position in self._extruder_trains[global_container_stack.getId()]: + extruder = self._extruder_trains[global_container_stack.getId()][position] + if extruder.getId() == extruder_stack_id: + return extruder.qualityChanges.getId() + return "" ## Changes the active extruder by index. # @@ -132,7 +136,7 @@ class ExtruderManager(QObject): selected_nodes = [] for node in Selection.getAllSelectedObjects(): if node.callDecoration("isGroup"): - for grouped_node in BreadthFirstIterator(node): + for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if grouped_node.callDecoration("isGroup"): continue @@ -141,7 +145,7 @@ class ExtruderManager(QObject): selected_nodes.append(node) # Then, figure out which nodes are used by those selected nodes. - global_stack = Application.getInstance().getGlobalContainerStack() + global_stack = self._application.getGlobalContainerStack() current_extruder_trains = self._extruder_trains.get(global_stack.getId()) for node in selected_nodes: extruder = node.callDecoration("getActiveExtruder") @@ -164,7 +168,7 @@ class ExtruderManager(QObject): @pyqtSlot(result = QObject) def getActiveExtruderStack(self) -> Optional["ExtruderStack"]: - global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack = self._application.getGlobalContainerStack() if global_container_stack: if global_container_stack.getId() in self._extruder_trains: @@ -175,7 +179,7 @@ class ExtruderManager(QObject): ## Get an extruder stack by index def getExtruderStack(self, index) -> Optional["ExtruderStack"]: - global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack = self._application.getGlobalContainerStack() if global_container_stack: if global_container_stack.getId() in self._extruder_trains: if str(index) in self._extruder_trains[global_container_stack.getId()]: @@ -186,7 +190,9 @@ class ExtruderManager(QObject): def getExtruderStacks(self) -> List["ExtruderStack"]: result = [] for i in range(self.extruderCount): - result.append(self.getExtruderStack(i)) + stack = self.getExtruderStack(i) + if stack: + result.append(stack) return result def registerExtruder(self, extruder_train, machine_id): @@ -252,14 +258,14 @@ class ExtruderManager(QObject): support_bottom_enabled = False support_roof_enabled = False - scene_root = Application.getInstance().getController().getScene().getRoot() + scene_root = self._application.getController().getScene().getRoot() # If no extruders are registered in the extruder manager yet, return an empty array if len(self.extruderIds) == 0: return [] # Get the extruders of all printable meshes in the scene - meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()] + meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for mesh in meshes: extruder_stack_id = mesh.callDecoration("getActiveExtruder") if not extruder_stack_id: @@ -301,10 +307,10 @@ class ExtruderManager(QObject): # The platform adhesion extruder. Not used if using none. if global_stack.getProperty("adhesion_type", "value") != "none": - extruder_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value")) - if extruder_nr == "-1": - extruder_nr = Application.getInstance().getMachineManager().defaultExtruderPosition - used_extruder_stack_ids.add(self.extruderIds[extruder_nr]) + extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value")) + if extruder_str_nr == "-1": + extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition + used_extruder_stack_ids.add(self.extruderIds[extruder_str_nr]) try: return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids] @@ -335,7 +341,7 @@ class ExtruderManager(QObject): # The first element is the global container stack, followed by any extruder stacks. # \return \type{List[ContainerStack]} def getActiveGlobalAndExtruderStacks(self) -> Optional[List[Union["ExtruderStack", "GlobalStack"]]]: - global_stack = Application.getInstance().getGlobalContainerStack() + global_stack = self._application.getGlobalContainerStack() if not global_stack: return None @@ -347,7 +353,7 @@ class ExtruderManager(QObject): # # \return \type{List[ContainerStack]} a list of def getActiveExtruderStacks(self) -> List["ExtruderStack"]: - global_stack = Application.getInstance().getGlobalContainerStack() + global_stack = self._application.getGlobalContainerStack() if not global_stack: return [] @@ -471,7 +477,7 @@ class ExtruderManager(QObject): # If no extruder has the value, the list will contain the global value. @staticmethod def getExtruderValues(key): - global_stack = Application.getInstance().getGlobalContainerStack() + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() result = [] for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()): @@ -506,7 +512,7 @@ class ExtruderManager(QObject): # If no extruder has the value, the list will contain the global value. @staticmethod def getDefaultExtruderValues(key): - global_stack = Application.getInstance().getGlobalContainerStack() + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() context = PropertyEvaluationContext(global_stack) context.context["evaluate_from_container_index"] = 1 # skip the user settings container context.context["override_operators"] = { @@ -539,7 +545,7 @@ class ExtruderManager(QObject): ## Return the default extruder position from the machine manager @staticmethod def getDefaultExtruderPosition() -> str: - return Application.getInstance().getMachineManager().defaultExtruderPosition + return cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition ## Get all extruder values for a certain setting. # @@ -564,7 +570,7 @@ class ExtruderManager(QObject): @staticmethod def getExtruderValue(extruder_index, key): if extruder_index == -1: - extruder_index = int(Application.getInstance().getMachineManager().defaultExtruderPosition) + extruder_index = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition) extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index) if extruder: @@ -573,7 +579,7 @@ class ExtruderManager(QObject): value = value(extruder) else: # Just a value from global. - value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value") + value = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().getProperty(key, "value") return value @@ -602,7 +608,7 @@ class ExtruderManager(QObject): if isinstance(value, SettingFunction): value = value(extruder, context = context) else: # Just a value from global. - value = Application.getInstance().getGlobalContainerStack().getProperty(key, "value", context = context) + value = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().getProperty(key, "value", context = context) return value @@ -615,7 +621,7 @@ class ExtruderManager(QObject): # \return The effective value @staticmethod def getResolveOrValue(key): - global_stack = Application.getInstance().getGlobalContainerStack() + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() resolved_value = global_stack.getProperty(key, "value") return resolved_value @@ -629,7 +635,7 @@ class ExtruderManager(QObject): # \return The effective value @staticmethod def getDefaultResolveOrValue(key): - global_stack = Application.getInstance().getGlobalContainerStack() + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() context = PropertyEvaluationContext(global_stack) context.context["evaluate_from_container_index"] = 1 # skip the user settings container context.context["override_operators"] = { @@ -642,7 +648,7 @@ class ExtruderManager(QObject): return resolved_value - __instance = None + __instance = None # type: ExtruderManager @classmethod def getInstance(cls, *args, **kwargs) -> "ExtruderManager": diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index b3f7d529a2..4445563e00 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -1,11 +1,10 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Any, TYPE_CHECKING, Optional +from typing import Any, Dict, TYPE_CHECKING, Optional from PyQt5.QtCore import pyqtProperty, pyqtSignal -from UM.Application import Application from UM.Decorators import override from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase from UM.Settings.ContainerStack import ContainerStack @@ -13,6 +12,8 @@ from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.Interfaces import ContainerInterface, PropertyEvaluationContext from UM.Util import parseBool +import cura.CuraApplication + from . import Exceptions from .CuraContainerStack import CuraContainerStack, _ContainerIndexes from .ExtruderManager import ExtruderManager @@ -25,7 +26,7 @@ if TYPE_CHECKING: # # class ExtruderStack(CuraContainerStack): - def __init__(self, container_id: str): + def __init__(self, container_id: str) -> None: super().__init__(container_id) self.addMetaDataEntry("type", "extruder_train") # For backward compatibility @@ -50,14 +51,14 @@ class ExtruderStack(CuraContainerStack): def getNextStack(self) -> Optional["GlobalStack"]: return super().getNextStack() - def setEnabled(self, enabled): + def setEnabled(self, enabled: bool) -> None: if "enabled" not in self._metadata: self.addMetaDataEntry("enabled", "True") self.setMetaDataEntry("enabled", str(enabled)) self.enabledChanged.emit() @pyqtProperty(bool, notify = enabledChanged) - def isEnabled(self): + def isEnabled(self) -> bool: return parseBool(self.getMetaDataEntry("enabled", "True")) @classmethod @@ -113,7 +114,7 @@ class ExtruderStack(CuraContainerStack): limit_to_extruder = super().getProperty(key, "limit_to_extruder", context) if limit_to_extruder is not None: if limit_to_extruder == -1: - limit_to_extruder = int(Application.getInstance().getMachineManager().defaultExtruderPosition) + limit_to_extruder = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition) limit_to_extruder = str(limit_to_extruder) if (limit_to_extruder is not None and limit_to_extruder != "-1") and self.getMetaDataEntry("position") != str(limit_to_extruder): if str(limit_to_extruder) in self.getNextStack().extruders: @@ -142,7 +143,7 @@ class ExtruderStack(CuraContainerStack): if stacks: self.setNextStack(stacks[0]) - def _onPropertiesChanged(self, key, properties): + def _onPropertiesChanged(self, key: str, properties: Dict[str, Any]) -> None: # When there is a setting that is not settable per extruder that depends on a value from a setting that is, # we do not always get properly informed that we should re-evaluate the setting. So make sure to indicate # something changed for those settings. diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index 6d300954c2..6552e43073 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -1,29 +1,30 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from collections import defaultdict import threading -from typing import Any, Dict, Optional - +from typing import Any, Dict, Optional, Set, TYPE_CHECKING from PyQt5.QtCore import pyqtProperty -from UM.Application import Application from UM.Decorators import override - from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase from UM.Settings.ContainerStack import ContainerStack from UM.Settings.SettingInstance import InstanceState from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.Interfaces import PropertyEvaluationContext from UM.Logger import Logger +import cura.CuraApplication from . import Exceptions from .CuraContainerStack import CuraContainerStack +if TYPE_CHECKING: + from cura.Settings.ExtruderStack import ExtruderStack + ## Represents the Global or Machine stack and its related containers. # class GlobalStack(CuraContainerStack): - def __init__(self, container_id: str): + def __init__(self, container_id: str) -> None: super().__init__(container_id) self.addMetaDataEntry("type", "machine") # For backward compatibility @@ -34,7 +35,7 @@ class GlobalStack(CuraContainerStack): # and if so, to bypass the resolve to prevent an infinite recursion that would occur # if the resolve function tried to access the same property it is a resolve for. # Per thread we have our own resolving_settings, or strange things sometimes occur. - self._resolving_settings = defaultdict(set) # keys are thread names + self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names ## Get the list of extruders of this stack. # @@ -94,6 +95,7 @@ class GlobalStack(CuraContainerStack): context.pushContainer(self) # Handle the "resolve" property. + #TODO: Why the hell does this involve threading? if self._shouldResolve(key, property_name, context): current_thread = threading.current_thread() self._resolving_settings[current_thread.name].add(key) @@ -106,7 +108,7 @@ class GlobalStack(CuraContainerStack): limit_to_extruder = super().getProperty(key, "limit_to_extruder", context) if limit_to_extruder is not None: if limit_to_extruder == -1: - limit_to_extruder = int(Application.getInstance().getMachineManager().defaultExtruderPosition) + limit_to_extruder = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition) limit_to_extruder = str(limit_to_extruder) if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in self._extruders: if super().getProperty(key, "settable_per_extruder", context): @@ -155,7 +157,7 @@ class GlobalStack(CuraContainerStack): ## 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): + def isValid(self) -> bool: container_registry = ContainerRegistry.getInstance() extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId()) diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 1039085cf3..f5b04bd23a 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -1,10 +1,9 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import collections import time -#Type hinting. -from typing import List, Dict, TYPE_CHECKING, Optional +from typing import Any, Callable, List, Dict, TYPE_CHECKING, Optional from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator @@ -15,20 +14,22 @@ from UM.Signal import Signal from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer from UM.FlameProfiler import pyqtSlot from UM import Util - -from UM.Application import Application from UM.Logger import Logger from UM.Message import Message -from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.SettingFunction import SettingFunction from UM.Signal import postponeSignals, CompressTechnique +import cura.CuraApplication +from cura.Machines.ContainerNode import ContainerNode #For typing. +from cura.Machines.QualityChangesGroup import QualityChangesGroup #For typing. +from cura.Machines.QualityGroup import QualityGroup #For typing. from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch from cura.PrinterOutputDevice import PrinterOutputDevice from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel +from cura.Settings.CuraContainerRegistry import CuraContainerRegistry from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderStack import ExtruderStack @@ -40,29 +41,31 @@ catalog = i18nCatalog("cura") if TYPE_CHECKING: from cura.Settings.CuraContainerStack import CuraContainerStack from cura.Settings.GlobalStack import GlobalStack + from cura.Machines.MaterialManager import MaterialManager + from cura.Machines.QualityManager import QualityManager + from cura.Machines.VariantManager import VariantManager class MachineManager(QObject): - - def __init__(self, parent = None): + def __init__(self, parent: QObject = None) -> None: super().__init__(parent) self._active_container_stack = None # type: Optional[ExtruderManager] self._global_container_stack = None # type: Optional[GlobalStack] self._current_root_material_id = {} # type: Dict[str, str] - self._current_quality_group = None - self._current_quality_changes_group = None + self._current_quality_group = None # type: Optional[QualityGroup] + self._current_quality_changes_group = None # type: Optional[QualityChangesGroup] self._default_extruder_position = "0" # to be updated when extruders are switched on and off - self.machine_extruder_material_update_dict = collections.defaultdict(list) + self.machine_extruder_material_update_dict = collections.defaultdict(list) #type: Dict[str, List[Callable[[], None]]] - self._instance_container_timer = QTimer() + self._instance_container_timer = QTimer() #type: QTimer self._instance_container_timer.setInterval(250) self._instance_container_timer.setSingleShot(True) self._instance_container_timer.timeout.connect(self.__emitChangedSignals) - self._application = Application.getInstance() + self._application = cura.CuraApplication.CuraApplication.getInstance() #type: cura.CuraApplication.CuraApplication self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged) self._application.getContainerRegistry().containerLoadComplete.connect(self._onContainersChanged) @@ -74,14 +77,14 @@ class MachineManager(QObject): self.globalContainerChanged.connect(self.activeQualityChangesGroupChanged) self.globalContainerChanged.connect(self.activeQualityGroupChanged) - self._stacks_have_errors = None # type:Optional[bool] + self._stacks_have_errors = None # type: Optional[bool] - self._empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer() - self._empty_definition_changes_container = ContainerRegistry.getInstance().findContainers(id = "empty_definition_changes")[0] - self._empty_variant_container = ContainerRegistry.getInstance().findContainers(id = "empty_variant")[0] - self._empty_material_container = ContainerRegistry.getInstance().findContainers(id = "empty_material")[0] - self._empty_quality_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality")[0] - self._empty_quality_changes_container = ContainerRegistry.getInstance().findContainers(id = "empty_quality_changes")[0] + self._empty_container = CuraContainerRegistry.getInstance().getEmptyInstanceContainer() #type: InstanceContainer + self._empty_definition_changes_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_definition_changes")[0] #type: InstanceContainer + self._empty_variant_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_variant")[0] #type: InstanceContainer + self._empty_material_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_material")[0] #type: InstanceContainer + self._empty_quality_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_quality")[0] #type: InstanceContainer + self._empty_quality_changes_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_quality_changes")[0] #type: InstanceContainer self._onGlobalContainerChanged() @@ -99,8 +102,6 @@ class MachineManager(QObject): self._application.getPreferences().addPreference("cura/active_machine", "") - self._global_event_keys = set() - self._printer_output_devices = [] # type: List[PrinterOutputDevice] self._application.getOutputDeviceManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) # There might already be some output devices by the time the signal is connected @@ -116,15 +117,15 @@ class MachineManager(QObject): self._material_incompatible_message = Message(catalog.i18nc("@info:status", "The selected material is incompatible with the selected machine or configuration."), - title = catalog.i18nc("@info:title", "Incompatible Material")) + title = catalog.i18nc("@info:title", "Incompatible Material")) #type: Message - containers = ContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) + containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) #type: List[InstanceContainer] if containers: containers[0].nameChanged.connect(self._onMaterialNameChanged) - self._material_manager = self._application.getMaterialManager() - self._variant_manager = self._application.getVariantManager() - self._quality_manager = self._application.getQualityManager() + self._material_manager = self._application.getMaterialManager() #type: MaterialManager + self._variant_manager = self._application.getVariantManager() #type: VariantManager + self._quality_manager = self._application.getQualityManager() #type: QualityManager # When the materials lookup table gets updated, it can mean that a material has its name changed, which should # be reflected on the GUI. This signal emission makes sure that it happens. @@ -164,7 +165,7 @@ class MachineManager(QObject): def setInitialActiveMachine(self) -> None: active_machine_id = self._application.getPreferences().getValue("cura/active_machine") - if active_machine_id != "" and ContainerRegistry.getInstance().findContainerStacksMetadata(id = active_machine_id): + if active_machine_id != "" and CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = active_machine_id): # An active machine was saved, so restore it. self.setActiveMachine(active_machine_id) @@ -215,7 +216,7 @@ class MachineManager(QObject): @pyqtProperty(int, constant=True) def totalNumberOfSettings(self) -> int: - return len(ContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0].getAllKeys()) + return len(CuraContainerRegistry.getInstance().findDefinitionContainers(id = "fdmprinter")[0].getAllKeys()) def _onGlobalContainerChanged(self) -> None: if self._global_container_stack: @@ -355,7 +356,7 @@ class MachineManager(QObject): def setActiveMachine(self, stack_id: str) -> None: self.blurSettings.emit() # Ensure no-one has focus. - container_registry = ContainerRegistry.getInstance() + container_registry = CuraContainerRegistry.getInstance() containers = container_registry.findContainerStacks(id = stack_id) if not containers: @@ -381,7 +382,7 @@ class MachineManager(QObject): # \param metadata_filter \type{dict} list of metadata keys and values used for filtering @staticmethod def getMachine(definition_id: str, metadata_filter: Dict[str, str] = None) -> Optional["GlobalStack"]: - machines = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) + machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) for machine in machines: if machine.definition.getId() == definition_id: return machine @@ -625,11 +626,13 @@ class MachineManager(QObject): ## Check if a container is read_only @pyqtSlot(str, result = bool) def isReadOnly(self, container_id: str) -> bool: - return ContainerRegistry.getInstance().isReadOnly(container_id) + return CuraContainerRegistry.getInstance().isReadOnly(container_id) ## 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: + if self._active_container_stack is None or self._global_container_stack is None: + return new_value = self._active_container_stack.getProperty(key, "value") extruder_stacks = [stack for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())] @@ -641,6 +644,8 @@ class MachineManager(QObject): ## Copy the value of all manually changed settings of the current extruder to all other extruders. @pyqtSlot() def copyAllValuesToExtruders(self) -> None: + if self._active_container_stack is None or self._global_container_stack is None: + return extruder_stacks = list(self._global_container_stack.extruders.values()) for extruder_stack in extruder_stacks: if extruder_stack != self._active_container_stack: @@ -704,7 +709,7 @@ class MachineManager(QObject): @pyqtSlot(str, str) def renameMachine(self, machine_id: str, new_name: str) -> None: - container_registry = ContainerRegistry.getInstance() + container_registry = CuraContainerRegistry.getInstance() machine_stack = container_registry.findContainerStacks(id = machine_id) if machine_stack: new_name = container_registry.createUniqueName("machine", machine_stack[0].getName(), new_name, machine_stack[0].definition.getName()) @@ -718,23 +723,23 @@ class MachineManager(QObject): # activate a new machine before removing a machine because this is safer if activate_new_machine: - machine_stacks = ContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine") + machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine") other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id] if other_machine_stacks: self.setActiveMachine(other_machine_stacks[0]["id"]) - metadata = ContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0] + metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0] network_key = metadata["um_network_key"] if "um_network_key" in metadata else None ExtruderManager.getInstance().removeMachineExtruders(machine_id) - containers = ContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id) + containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id) for container in containers: - ContainerRegistry.getInstance().removeContainer(container["id"]) - ContainerRegistry.getInstance().removeContainer(machine_id) + CuraContainerRegistry.getInstance().removeContainer(container["id"]) + CuraContainerRegistry.getInstance().removeContainer(machine_id) # If the printer that is being removed is a network printer, the hidden printers have to be also removed if network_key: metadata_filter = {"um_network_key": network_key} - hidden_containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) + hidden_containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) if hidden_containers: # This reuses the method and remove all printers recursively self.removeMachine(hidden_containers[0].getId()) @@ -802,14 +807,17 @@ class MachineManager(QObject): ## Get the Definition ID of a machine (specified by ID) # \param machine_id string machine id to get the definition ID of - # \returns DefinitionID (string) if found, None otherwise + # \returns DefinitionID if found, None otherwise @pyqtSlot(str, result = str) - def getDefinitionByMachineId(self, machine_id: str) -> str: - containers = ContainerRegistry.getInstance().findContainerStacks(id = machine_id) + def getDefinitionByMachineId(self, machine_id: str) -> Optional[str]: + containers = CuraContainerRegistry.getInstance().findContainerStacks(id = machine_id) if containers: return containers[0].definition.getId() + return None def getIncompatibleSettingsOnEnabledExtruders(self, container: InstanceContainer) -> List[str]: + if self._global_container_stack is None: + return [] extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") result = [] # type: List[str] for setting_instance in container.findInstances(): @@ -834,6 +842,8 @@ class MachineManager(QObject): ## Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed def correctExtruderSettings(self) -> None: + if self._global_container_stack is None: + return for setting_key in self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.userChanges): self._global_container_stack.userChanges.removeInstance(setting_key) add_user_changes = self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.qualityChanges) @@ -851,6 +861,8 @@ class MachineManager(QObject): ## 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: + if self._global_container_stack is None: + return extruder_manager = self._application.getExtruderManager() definition_changes_container = self._global_container_stack.definitionChanges @@ -869,7 +881,7 @@ class MachineManager(QObject): # Check to see if any objects are set to print with an extruder that will no longer exist root_node = self._application.getController().getScene().getRoot() - for node in DepthFirstIterator(root_node): + for node in DepthFirstIterator(root_node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.getMeshData(): extruder_nr = node.callDecoration("getActiveExtruderPosition") @@ -884,7 +896,7 @@ class MachineManager(QObject): global_user_container = self._global_container_stack.userChanges # Make sure extruder_stacks exists - extruder_stacks = [] + extruder_stacks = [] #type: List[ExtruderStack] if previous_extruder_count == 1: extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() @@ -912,6 +924,8 @@ class MachineManager(QObject): return extruder def updateDefaultExtruder(self) -> None: + if self._global_container_stack is None: + return extruder_items = sorted(self._global_container_stack.extruders.items()) old_position = self._default_extruder_position new_default_position = "0" @@ -924,6 +938,8 @@ class MachineManager(QObject): self.extruderChanged.emit() def updateNumberExtrudersEnabled(self) -> None: + if self._global_container_stack is None: + return definition_changes_container = self._global_container_stack.definitionChanges machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") extruder_count = 0 @@ -936,6 +952,8 @@ class MachineManager(QObject): @pyqtProperty(int, notify = numberExtrudersEnabledChanged) def numberExtrudersEnabled(self) -> int: + if self._global_container_stack is None: + return 1 return self._global_container_stack.definitionChanges.getProperty("extruders_enabled_count", "value") @pyqtProperty(str, notify = extruderChanged) @@ -945,6 +963,8 @@ class MachineManager(QObject): ## This will fire the propertiesChanged for all settings so they will be updated in the front-end @pyqtSlot() def forceUpdateAllSettings(self) -> None: + if self._global_container_stack is None: + return with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): property_names = ["value", "resolve", "validationState"] for container in [self._global_container_stack] + list(self._global_container_stack.extruders.values()): @@ -954,8 +974,9 @@ class MachineManager(QObject): @pyqtSlot(int, bool) def setExtruderEnabled(self, position: int, enabled: bool) -> None: extruder = self.getExtruder(position) - if not extruder: + if not extruder or self._global_container_stack is None: Logger.log("w", "Could not find extruder on position %s", position) + return extruder.setEnabled(enabled) self.updateDefaultExtruder() @@ -991,6 +1012,8 @@ class MachineManager(QObject): @pyqtSlot(str, str, str) def setSettingForAllExtruders(self, setting_name: str, property_name: str, property_value: str) -> None: + if self._global_container_stack is None: + return for key, extruder in self._global_container_stack.extruders.items(): container = extruder.userChanges container.setProperty(setting_name, property_name, property_value) @@ -999,6 +1022,8 @@ class MachineManager(QObject): # \param setting_name The ID of the setting to reset. @pyqtSlot(str) def resetSettingForAllExtruders(self, setting_name: str) -> None: + if self._global_container_stack is None: + return for key, extruder in self._global_container_stack.extruders.items(): container = extruder.userChanges container.removeInstance(setting_name) @@ -1041,6 +1066,8 @@ class MachineManager(QObject): # for all stacks in the currently active machine. # def _setEmptyQuality(self) -> None: + if self._global_container_stack is None: + return self._current_quality_group = None self._current_quality_changes_group = None self._global_container_stack.quality = self._empty_quality_container @@ -1052,12 +1079,14 @@ class MachineManager(QObject): self.activeQualityGroupChanged.emit() self.activeQualityChangesGroupChanged.emit() - def _setQualityGroup(self, quality_group, empty_quality_changes: bool = True) -> None: + def _setQualityGroup(self, quality_group: Optional[QualityGroup], empty_quality_changes: bool = True) -> None: + if self._global_container_stack is None: + return if quality_group is None: self._setEmptyQuality() return - if quality_group.node_for_global.getContainer() is None: + if quality_group.node_for_global is None or quality_group.node_for_global.getContainer() is None: return for node in quality_group.nodes_for_extruders.values(): if node.getContainer() is None: @@ -1081,14 +1110,15 @@ class MachineManager(QObject): self.activeQualityGroupChanged.emit() self.activeQualityChangesGroupChanged.emit() - def _fixQualityChangesGroupToNotSupported(self, quality_changes_group): + def _fixQualityChangesGroupToNotSupported(self, quality_changes_group: QualityChangesGroup) -> None: nodes = [quality_changes_group.node_for_global] + list(quality_changes_group.nodes_for_extruders.values()) containers = [n.getContainer() for n in nodes if n is not None] for container in containers: - container.setMetaDataEntry("quality_type", "not_supported") + if container: + container.setMetaDataEntry("quality_type", "not_supported") quality_changes_group.quality_type = "not_supported" - def _setQualityChangesGroup(self, quality_changes_group): + def _setQualityChangesGroup(self, quality_changes_group: QualityChangesGroup) -> None: if self._global_container_stack is None: return #Can't change that. quality_type = quality_changes_group.quality_type @@ -1132,21 +1162,25 @@ class MachineManager(QObject): self.activeQualityGroupChanged.emit() self.activeQualityChangesGroupChanged.emit() - def _setVariantNode(self, position, container_node): - if container_node.getContainer() is None: + def _setVariantNode(self, position: str, container_node: ContainerNode) -> None: + if container_node.getContainer() is None or self._global_container_stack is None: return self._global_container_stack.extruders[position].variant = container_node.getContainer() self.activeVariantChanged.emit() - def _setGlobalVariant(self, container_node): + def _setGlobalVariant(self, container_node: ContainerNode) -> None: + if self._global_container_stack is None: + return self._global_container_stack.variant = container_node.getContainer() if not self._global_container_stack.variant: self._global_container_stack.variant = self._application.empty_variant_container - def _setMaterial(self, position, container_node = None): + def _setMaterial(self, position: str, container_node: ContainerNode = None) -> None: + if self._global_container_stack is None: + return if container_node and container_node.getContainer(): self._global_container_stack.extruders[position].material = container_node.getContainer() - root_material_id = container_node.metadata["base_file"] + root_material_id = container_node.getMetaDataEntry("base_file", None) else: self._global_container_stack.extruders[position].material = self._empty_material_container root_material_id = None @@ -1155,18 +1189,19 @@ class MachineManager(QObject): self._current_root_material_id[position] = root_material_id self.rootMaterialChanged.emit() - def activeMaterialsCompatible(self): + def activeMaterialsCompatible(self) -> bool: # check material - variant compatibility - if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)): - for position, extruder in self._global_container_stack.extruders.items(): - if extruder.isEnabled and not extruder.material.getMetaDataEntry("compatible"): - return False - if not extruder.material.getMetaDataEntry("compatible"): - return False + if self._global_container_stack is not None: + if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)): + for position, extruder in self._global_container_stack.extruders.items(): + if extruder.isEnabled and not extruder.material.getMetaDataEntry("compatible"): + return False + if not extruder.material.getMetaDataEntry("compatible"): + return False return True ## Update current quality type and machine after setting material - def _updateQualityWithMaterial(self, *args): + def _updateQualityWithMaterial(self, *args: Any) -> None: if self._global_container_stack is None: return Logger.log("i", "Updating quality/quality_changes due to material change") @@ -1205,7 +1240,7 @@ class MachineManager(QObject): current_quality_type, quality_type) self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True) - def updateMaterialWithVariant(self, position: Optional[str]): + def updateMaterialWithVariant(self, position: Optional[str]) -> None: if self._global_container_stack is None: return if position is None: @@ -1213,8 +1248,8 @@ class MachineManager(QObject): else: position_list = [position] - for position in position_list: - extruder = self._global_container_stack.extruders[position] + for position_item in position_list: + extruder = self._global_container_stack.extruders[position_item] current_material_base_name = extruder.material.getMetaDataEntry("base_file") current_variant_name = None @@ -1232,28 +1267,28 @@ class MachineManager(QObject): material_diameter) if not candidate_materials: - self._setMaterial(position, container_node = None) + self._setMaterial(position_item, container_node = None) continue if current_material_base_name in candidate_materials: new_material = candidate_materials[current_material_base_name] - self._setMaterial(position, new_material) + self._setMaterial(position_item, new_material) continue # The current material is not available, find the preferred one material_node = self._material_manager.getDefaultMaterial(self._global_container_stack, current_variant_name) if material_node is not None: - self._setMaterial(position, material_node) + 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: # Don't switch if the user tries to change to the same type of printer - if self.activeMachineDefinitionName == machine_name: + if self._global_container_stack is None or self.self.activeMachineDefinitionName == machine_name: return # Get the definition id corresponding to this machine name - machine_definition_id = ContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId() + machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId() # Try to find a machine with the same network key new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey}) # If there is no machine, then create a new one and set it to the non-hidden instance @@ -1275,6 +1310,8 @@ class MachineManager(QObject): @pyqtSlot(QObject) def applyRemoteConfiguration(self, configuration: ConfigurationModel) -> None: + if self._global_container_stack is None: + return self.blurSettings.emit() with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): self.switchPrinterType(configuration.printerType) @@ -1309,7 +1346,7 @@ class MachineManager(QObject): ## Find all container stacks that has the pair 'key = value' in its metadata and replaces the value with 'new_value' def replaceContainersMetadata(self, key: str, value: str, new_value: str) -> None: - machines = ContainerRegistry.getInstance().findContainerStacks(type = "machine") + machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine") for machine in machines: if machine.getMetaDataEntry(key) == value: machine.setMetaDataEntry(key, new_value) @@ -1322,18 +1359,18 @@ class MachineManager(QObject): # Check if the connect_group_name is correct. If not, update all the containers connected to the same printer if self.activeMachineNetworkGroupName != group_name: metadata_filter = {"um_network_key": self.activeMachineNetworkKey} - containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) + containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) for container in containers: container.setMetaDataEntry("connect_group_name", group_name) ## This method checks if there is an instance connected to the given network_key def existNetworkInstances(self, network_key: str) -> bool: metadata_filter = {"um_network_key": network_key} - containers = ContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) + containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) return bool(containers) @pyqtSlot("QVariant") - def setGlobalVariant(self, container_node): + def setGlobalVariant(self, container_node: ContainerNode) -> None: self.blurSettings.emit() with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): self._setGlobalVariant(container_node) @@ -1341,7 +1378,9 @@ class MachineManager(QObject): self._updateQualityWithMaterial() @pyqtSlot(str, str) - def setMaterialById(self, position, root_material_id): + def setMaterialById(self, position: str, root_material_id: str) -> None: + if self._global_container_stack is None: + return machine_definition_id = self._global_container_stack.definition.id position = str(position) extruder_stack = self._global_container_stack.extruders[position] @@ -1364,12 +1403,14 @@ class MachineManager(QObject): @pyqtSlot(str, str) def setVariantByName(self, position: str, variant_name: str) -> None: + if self._global_container_stack is None: + return machine_definition_id = self._global_container_stack.definition.id variant_node = self._variant_manager.getVariantNode(machine_definition_id, variant_name) self.setVariant(position, variant_node) @pyqtSlot(str, "QVariant") - def setVariant(self, position: str, container_node): + def setVariant(self, position: str, container_node: ContainerNode) -> None: position = str(position) self.blurSettings.emit() with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): @@ -1391,7 +1432,7 @@ class MachineManager(QObject): self.setQualityGroup(quality_group) @pyqtSlot(QObject) - def setQualityGroup(self, quality_group, no_dialog = False): + def setQualityGroup(self, quality_group: QualityGroup, no_dialog: bool = False) -> None: self.blurSettings.emit() with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): self._setQualityGroup(quality_group) @@ -1401,11 +1442,11 @@ class MachineManager(QObject): self._application.discardOrKeepProfileChanges() @pyqtProperty(QObject, fset = setQualityGroup, notify = activeQualityGroupChanged) - def activeQualityGroup(self): + def activeQualityGroup(self) -> Optional[QualityGroup]: return self._current_quality_group @pyqtSlot(QObject) - def setQualityChangesGroup(self, quality_changes_group, no_dialog = False): + def setQualityChangesGroup(self, quality_changes_group: QualityChangesGroup, no_dialog: bool = False) -> None: self.blurSettings.emit() with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): self._setQualityChangesGroup(quality_changes_group) @@ -1415,18 +1456,20 @@ class MachineManager(QObject): self._application.discardOrKeepProfileChanges() @pyqtSlot() - def resetToUseDefaultQuality(self): + def resetToUseDefaultQuality(self) -> None: + if self._global_container_stack is None: + return with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): self._setQualityGroup(self._current_quality_group) for stack in [self._global_container_stack] + list(self._global_container_stack.extruders.values()): stack.userChanges.clear() @pyqtProperty(QObject, fset = setQualityChangesGroup, notify = activeQualityChangesGroupChanged) - def activeQualityChangesGroup(self): + def activeQualityChangesGroup(self) -> Optional[QualityChangesGroup]: return self._current_quality_changes_group @pyqtProperty(str, notify = activeQualityGroupChanged) - def activeQualityOrQualityChangesName(self): + def activeQualityOrQualityChangesName(self) -> str: name = self._empty_quality_container.getName() if self._current_quality_changes_group: name = self._current_quality_changes_group.name diff --git a/cura/Settings/PerObjectContainerStack.py b/cura/Settings/PerObjectContainerStack.py index 33111cbed7..9f4576317f 100644 --- a/cura/Settings/PerObjectContainerStack.py +++ b/cura/Settings/PerObjectContainerStack.py @@ -1,3 +1,6 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + from typing import Any, Optional from UM.Application import Application @@ -9,7 +12,6 @@ from .CuraContainerStack import CuraContainerStack class PerObjectContainerStack(CuraContainerStack): - @override(CuraContainerStack) def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: if context is None: @@ -20,7 +22,7 @@ class PerObjectContainerStack(CuraContainerStack): # Return the user defined value if present, otherwise, evaluate the value according to the default routine. if self.getContainer(0).hasProperty(key, property_name): - if self.getContainer(0)._instances[key].state == InstanceState.User: + if self.getContainer(0).getProperty(key, "state") == InstanceState.User: result = super().getProperty(key, property_name, context) context.popContainer() return result @@ -53,13 +55,13 @@ class PerObjectContainerStack(CuraContainerStack): return result @override(CuraContainerStack) - def setNextStack(self, stack: CuraContainerStack): + def setNextStack(self, stack: CuraContainerStack) -> None: super().setNextStack(stack) # trigger signal to re-evaluate all default settings - for key, instance in self.getContainer(0)._instances.items(): + for key in self.getContainer(0).getAllKeys(): # only evaluate default settings - if instance.state != InstanceState.Default: + if self.getContainer(0).getProperty(key, "state") != InstanceState.Default: continue self._collectPropertyChanges(key, "value") diff --git a/cura/Settings/SettingInheritanceManager.py b/cura/Settings/SettingInheritanceManager.py index 7cbfbcfda1..6d4176ebf1 100644 --- a/cura/Settings/SettingInheritanceManager.py +++ b/cura/Settings/SettingInheritanceManager.py @@ -1,5 +1,6 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import List from PyQt5.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal from UM.FlameProfiler import pyqtSlot @@ -13,6 +14,7 @@ from UM.Logger import Logger # speed settings. If all the children of print_speed have a single value override, changing the speed won't # actually do anything, as only the 'leaf' settings are used by the engine. from UM.Settings.ContainerStack import ContainerStack +from UM.Settings.Interfaces import ContainerInterface from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingInstance import InstanceState @@ -157,7 +159,7 @@ class SettingInheritanceManager(QObject): stack = self._active_container_stack if not stack: #No active container stack yet! return False - containers = [] + 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 diff --git a/cura/Settings/SimpleModeSettingsManager.py b/cura/Settings/SimpleModeSettingsManager.py index a337d8b04e..fce43243bd 100644 --- a/cura/Settings/SimpleModeSettingsManager.py +++ b/cura/Settings/SimpleModeSettingsManager.py @@ -39,12 +39,12 @@ class SimpleModeSettingsManager(QObject): global_stack = self._machine_manager.activeMachine # check user settings in the global stack - user_setting_keys.update(set(global_stack.userChanges.getAllKeys())) + user_setting_keys.update(global_stack.userChanges.getAllKeys()) # check user settings in the extruder stacks if global_stack.extruders: for extruder_stack in global_stack.extruders.values(): - user_setting_keys.update(set(extruder_stack.userChanges.getAllKeys())) + user_setting_keys.update(extruder_stack.userChanges.getAllKeys()) # remove settings that are visible in recommended (we don't show the reset button for those) for skip_key in self.__ignored_custom_setting_keys: @@ -70,12 +70,12 @@ class SimpleModeSettingsManager(QObject): global_stack = self._machine_manager.activeMachine # check quality changes settings in the global stack - quality_changes_keys.update(set(global_stack.qualityChanges.getAllKeys())) + quality_changes_keys.update(global_stack.qualityChanges.getAllKeys()) # check quality changes settings in the extruder stacks if global_stack.extruders: for extruder_stack in global_stack.extruders.values(): - quality_changes_keys.update(set(extruder_stack.qualityChanges.getAllKeys())) + quality_changes_keys.update(extruder_stack.qualityChanges.getAllKeys()) # check if the qualityChanges container is not empty (meaning it is a user created profile) has_quality_changes = len(quality_changes_keys) > 0 diff --git a/cura/SingleInstance.py b/cura/SingleInstance.py index a664204d79..8109123df5 100644 --- a/cura/SingleInstance.py +++ b/cura/SingleInstance.py @@ -7,12 +7,12 @@ from typing import List, Optional from PyQt5.QtNetwork import QLocalServer, QLocalSocket +from UM.Qt.QtApplication import QtApplication #For typing. from UM.Logger import Logger class SingleInstance: - - def __init__(self, application, files_to_open: Optional[List[str]]): + def __init__(self, application: QtApplication, files_to_open: Optional[List[str]]) -> None: self._application = application self._files_to_open = files_to_open @@ -61,17 +61,22 @@ class SingleInstance: def startServer(self) -> None: self._single_instance_server = QLocalServer() - self._single_instance_server.newConnection.connect(self._onClientConnected) - self._single_instance_server.listen("ultimaker-cura") + if self._single_instance_server: + self._single_instance_server.newConnection.connect(self._onClientConnected) + self._single_instance_server.listen("ultimaker-cura") + else: + Logger.log("e", "Single instance server was not created.") - def _onClientConnected(self): + def _onClientConnected(self) -> None: Logger.log("i", "New connection recevied on our single-instance server") - connection = self._single_instance_server.nextPendingConnection() + connection = None #type: Optional[QLocalSocket] + if self._single_instance_server: + connection = self._single_instance_server.nextPendingConnection() if connection is not None: connection.readyRead.connect(lambda c = connection: self.__readCommands(c)) - def __readCommands(self, connection): + def __readCommands(self, connection: QLocalSocket) -> None: line = connection.readLine() while len(line) != 0: # There is also a .canReadLine() try: diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index ecccfc77ac..3df3d9d4a2 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -4,7 +4,7 @@ from configparser import ConfigParser import zipfile import os -from typing import List, Tuple +from typing import Dict, List, Tuple import xml.etree.ElementTree as ET @@ -38,7 +38,7 @@ i18n_catalog = i18nCatalog("cura") class ContainerInfo: - def __init__(self, file_name: str, serialized: str, parser: ConfigParser): + def __init__(self, file_name: str, serialized: str, parser: ConfigParser) -> None: self.file_name = file_name self.serialized = serialized self.parser = parser @@ -47,14 +47,14 @@ class ContainerInfo: class QualityChangesInfo: - def __init__(self): + def __init__(self) -> None: self.name = None self.global_info = None - self.extruder_info_dict = {} + self.extruder_info_dict = {} # type: Dict[str, ContainerInfo] class MachineInfo: - def __init__(self): + def __init__(self) -> None: self.container_id = None self.name = None self.definition_id = None @@ -66,11 +66,11 @@ class MachineInfo: self.definition_changes_info = None self.user_changes_info = None - self.extruder_info_dict = {} + self.extruder_info_dict = {} # type: Dict[str, ExtruderInfo] class ExtruderInfo: - def __init__(self): + def __init__(self) -> None: self.position = None self.enabled = True self.variant_info = None @@ -82,7 +82,7 @@ class ExtruderInfo: ## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): - def __init__(self): + def __init__(self) -> None: super().__init__() MimeTypeDatabase.addMimeType( @@ -112,28 +112,26 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # - variant self._ignored_instance_container_types = {"quality", "variant"} - self._resolve_strategies = {} + self._resolve_strategies = {} # type: Dict[str, str] - self._id_mapping = {} + self._id_mapping = {} # type: Dict[str, str] # In Cura 2.5 and 2.6, the empty profiles used to have those long names self._old_empty_profile_id_dict = {"empty_%s" % k: "empty" for k in ["material", "variant"]} self._is_same_machine_type = False - self._old_new_materials = {} - self._materials_to_select = {} + self._old_new_materials = {} # type: Dict[str, str] self._machine_info = None def _clearState(self): self._is_same_machine_type = False self._id_mapping = {} self._old_new_materials = {} - self._materials_to_select = {} self._machine_info = None ## Get a unique name based on the old_id. This is different from directly calling the registry in that it caches results. # This has nothing to do with speed, but with getting consistent new naming for instances & objects. - def getNewId(self, old_id): + def getNewId(self, old_id: str): if old_id not in self._id_mapping: self._id_mapping[old_id] = self._container_registry.uniqueName(old_id) return self._id_mapping[old_id] @@ -671,7 +669,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): else: material_container = materials[0] old_material_root_id = material_container.getMetaDataEntry("base_file") - if not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only. + if old_material_root_id is not None and not self._container_registry.isReadOnly(old_material_root_id): # Only create new materials if they are not read only. to_deserialize_material = True if self._resolve_strategies["material"] == "override": diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index ff6333763a..8d54f475d6 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -91,7 +91,7 @@ class ThreeMFWriter(MeshWriter): # Handle per object settings (if any) stack = um_node.callDecoration("getStack") if stack is not None: - changed_setting_keys = set(stack.getTop().getAllKeys()) + changed_setting_keys = stack.getTop().getAllKeys() # Ensure that we save the extruder used for this object in a multi-extrusion setup if stack.getProperty("machine_extruder_count", "value") > 1: diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 3e66edc203..e7dca2ae3e 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -1,8 +1,14 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from collections import defaultdict +import os +from PyQt5.QtCore import QObject, QTimer, pyqtSlot +import sys +from time import time +from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING + from UM.Backend.Backend import Backend, BackendState -from UM.Application import Application from UM.Scene.SceneNode import SceneNode from UM.Signal import Signal from UM.Logger import Logger @@ -10,29 +16,30 @@ from UM.Message import Message from UM.PluginRegistry import PluginRegistry from UM.Resources import Resources from UM.Platform import Platform -from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Qt.Duration import DurationFormat -from PyQt5.QtCore import QObject, pyqtSlot +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from UM.Settings.Interfaces import DefinitionContainerInterface +from UM.Settings.SettingInstance import SettingInstance #For typing. +from UM.Tool import Tool #For typing. -from collections import defaultdict +from cura.CuraApplication import CuraApplication from cura.Settings.ExtruderManager import ExtruderManager -from . import ProcessSlicedLayersJob -from . import StartSliceJob - -import os -import sys -from time import time - -from PyQt5.QtCore import QTimer +from .ProcessSlicedLayersJob import ProcessSlicedLayersJob +from .StartSliceJob import StartSliceJob, StartJobResult import Arcus +if TYPE_CHECKING: + from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel + from cura.Machines.MachineErrorChecker import MachineErrorChecker + from UM.Scene.Scene import Scene + from UM.Settings.ContainerStack import ContainerStack + from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") class CuraEngineBackend(QObject, Backend): - backendError = Signal() ## Starts the back-end plug-in. @@ -40,16 +47,16 @@ class CuraEngineBackend(QObject, Backend): # This registers all the signal listeners and prepares for communication # with the back-end in general. # CuraEngineBackend is exposed to qml as well. - def __init__(self, parent = None): - super().__init__(parent = parent) + def __init__(self) -> None: + super().__init__() # Find out where the engine is located, and how it is called. # This depends on how Cura is packaged and which OS we are running on. executable_name = "CuraEngine" if Platform.isWindows(): executable_name += ".exe" default_engine_location = executable_name - if os.path.exists(os.path.join(Application.getInstallPrefix(), "bin", executable_name)): - default_engine_location = os.path.join(Application.getInstallPrefix(), "bin", executable_name) + if os.path.exists(os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)): + default_engine_location = os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name) if hasattr(sys, "frozen"): default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name) if Platform.isLinux() and not default_engine_location: @@ -61,9 +68,9 @@ class CuraEngineBackend(QObject, Backend): default_engine_location = execpath break - self._application = Application.getInstance() - self._multi_build_plate_model = None - self._machine_error_checker = None + self._application = CuraApplication.getInstance() #type: CuraApplication + self._multi_build_plate_model = None #type: MultiBuildPlateModel + self._machine_error_checker = None #type: MachineErrorChecker if not default_engine_location: raise EnvironmentError("Could not find CuraEngine") @@ -71,16 +78,16 @@ class CuraEngineBackend(QObject, Backend): Logger.log("i", "Found CuraEngine at: %s", default_engine_location) default_engine_location = os.path.abspath(default_engine_location) - Application.getInstance().getPreferences().addPreference("backend/location", default_engine_location) + self._application.getPreferences().addPreference("backend/location", default_engine_location) # Workaround to disable layer view processing if layer view is not active. - self._layer_view_active = False + self._layer_view_active = False #type: bool self._onActiveViewChanged() - self._stored_layer_data = [] - self._stored_optimized_layer_data = {} # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob + self._stored_layer_data = [] #type: List[Arcus.PythonMessage] + self._stored_optimized_layer_data = {} #type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob - self._scene = self._application.getController().getScene() + self._scene = self._application.getController().getScene() #type: Scene self._scene.sceneChanged.connect(self._onSceneChanged) # Triggers for auto-slicing. Auto-slicing is triggered as follows: @@ -91,7 +98,7 @@ class CuraEngineBackend(QObject, Backend): # If there is an error check, stop the auto-slicing timer, and only wait for the error check to be finished # to start the auto-slicing timer again. # - self._global_container_stack = None + self._global_container_stack = None #type: Optional[ContainerStack] # Listeners for receiving messages from the back-end. self._message_handlers["cura.proto.Layer"] = self._onLayerMessage @@ -102,39 +109,39 @@ class CuraEngineBackend(QObject, Backend): self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage - self._start_slice_job = None - self._start_slice_job_build_plate = None - self._slicing = False # Are we currently slicing? - self._restart = False # Back-end is currently restarting? - self._tool_active = False # If a tool is active, some tasks do not have to do anything - self._always_restart = True # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. - self._process_layers_job = None # The currently active job to process layers, or None if it is not processing layers. - self._build_plates_to_be_sliced = [] # what needs slicing? - self._engine_is_fresh = True # Is the newly started engine used before or not? + self._start_slice_job = None #type: Optional[StartSliceJob] + self._start_slice_job_build_plate = None #type: Optional[int] + self._slicing = False #type: bool # Are we currently slicing? + self._restart = False #type: bool # Back-end is currently restarting? + self._tool_active = False #type: bool # If a tool is active, some tasks do not have to do anything + self._always_restart = True #type: bool # Always restart the engine when starting a new slice. Don't keep the process running. TODO: Fix engine statelessness. + self._process_layers_job = None #type: Optional[ProcessSlicedLayersJob] # The currently active job to process layers, or None if it is not processing layers. + self._build_plates_to_be_sliced = [] #type: List[int] # what needs slicing? + self._engine_is_fresh = True #type: bool # Is the newly started engine used before or not? - self._backend_log_max_lines = 20000 # Maximum number of lines to buffer - self._error_message = None # Pop-up message that shows errors. - self._last_num_objects = defaultdict(int) # Count number of objects to see if there is something changed - self._postponed_scene_change_sources = [] # scene change is postponed (by a tool) + self._backend_log_max_lines = 20000 #type: int # Maximum number of lines to buffer + self._error_message = None #type: Message # Pop-up message that shows errors. + self._last_num_objects = defaultdict(int) #type: Dict[int, int] # Count number of objects to see if there is something changed + self._postponed_scene_change_sources = [] #type: List[SceneNode] # scene change is postponed (by a tool) - self._slice_start_time = None - self._is_disabled = False + self._slice_start_time = None #type: Optional[float] + self._is_disabled = False #type: bool - Application.getInstance().getPreferences().addPreference("general/auto_slice", False) + self._application.getPreferences().addPreference("general/auto_slice", False) - self._use_timer = False + self._use_timer = False #type: bool # When you update a setting and other settings get changed through inheritance, many propertyChanged signals are fired. # This timer will group them up, and only slice for the last setting changed signal. # TODO: Properly group propertyChanged signals by whether they are triggered by the same user interaction. - self._change_timer = QTimer() + self._change_timer = QTimer() #type: QTimer self._change_timer.setSingleShot(True) self._change_timer.setInterval(500) self.determineAutoSlicing() - Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged) + self._application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged) self._application.initializationFinished.connect(self.initialize) - def initialize(self): + def initialize(self) -> None: self._multi_build_plate_model = self._application.getMultiBuildPlateModel() self._application.getController().activeViewChanged.connect(self._onActiveViewChanged) @@ -160,16 +167,16 @@ class CuraEngineBackend(QObject, Backend): # # This function should terminate the engine process. # Called when closing the application. - def close(self): + def close(self) -> None: # Terminate CuraEngine if it is still running at this point self._terminate() ## Get the command that is used to call the engine. # This is useful for debugging and used to actually start the engine. # \return list of commands and args / parameters. - def getEngineCommand(self): + def getEngineCommand(self) -> List[str]: json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json") - return [Application.getInstance().getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""] + return [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""] ## Emitted when we get a message containing print duration and material amount. # This also implies the slicing has finished. @@ -184,13 +191,13 @@ class CuraEngineBackend(QObject, Backend): slicingCancelled = Signal() @pyqtSlot() - def stopSlicing(self): + def stopSlicing(self) -> None: self.backendStateChange.emit(BackendState.NotStarted) if self._slicing: # We were already slicing. Stop the old job. self._terminate() self._createSocket() - if self._process_layers_job: # We were processing layers. Stop that, the layers are going to change soon. + if self._process_layers_job is not None: # We were processing layers. Stop that, the layers are going to change soon. Logger.log("d", "Aborting process layers job...") self._process_layers_job.abort() self._process_layers_job = None @@ -200,12 +207,12 @@ class CuraEngineBackend(QObject, Backend): ## Manually triggers a reslice @pyqtSlot() - def forceSlice(self): + def forceSlice(self) -> None: self.markSliceAll() self.slice() ## Perform a slice of the scene. - def slice(self): + def slice(self) -> None: Logger.log("d", "Starting to slice...") self._slice_start_time = time() if not self._build_plates_to_be_sliced: @@ -218,10 +225,10 @@ class CuraEngineBackend(QObject, Backend): return if not hasattr(self._scene, "gcode_dict"): - self._scene.gcode_dict = {} + self._scene.gcode_dict = {} #type: ignore #Because we are creating the missing attribute here. # see if we really have to slice - active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate + active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate build_plate_to_be_sliced = self._build_plates_to_be_sliced.pop(0) Logger.log("d", "Going to slice build plate [%s]!" % build_plate_to_be_sliced) num_objects = self._numObjectsPerBuildPlate() @@ -230,14 +237,14 @@ class CuraEngineBackend(QObject, Backend): self._stored_optimized_layer_data[build_plate_to_be_sliced] = [] if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0: - self._scene.gcode_dict[build_plate_to_be_sliced] = [] + self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #Because we created this attribute above. Logger.log("d", "Build plate %s has no objects to be sliced, skipping", build_plate_to_be_sliced) if self._build_plates_to_be_sliced: self.slice() return - if Application.getInstance().getPrintInformation() and build_plate_to_be_sliced == active_build_plate: - Application.getInstance().getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced) + if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate: + self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced) if self._process is None: self._createSocket() @@ -247,14 +254,14 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(0.0) self.backendStateChange.emit(BackendState.NotStarted) - self._scene.gcode_dict[build_plate_to_be_sliced] = [] #[] indexed by build plate number + self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #[] indexed by build plate number self._slicing = True self.slicingStarted.emit() self.determineAutoSlicing() # Switch timer on or off if appropriate slice_message = self._socket.createMessage("cura.proto.Slice") - self._start_slice_job = StartSliceJob.StartSliceJob(slice_message) + self._start_slice_job = StartSliceJob(slice_message) self._start_slice_job_build_plate = build_plate_to_be_sliced self._start_slice_job.setBuildPlate(self._start_slice_job_build_plate) self._start_slice_job.start() @@ -262,7 +269,7 @@ class CuraEngineBackend(QObject, Backend): ## Terminate the engine process. # Start the engine process by calling _createSocket() - def _terminate(self): + def _terminate(self) -> None: self._slicing = False self._stored_layer_data = [] if self._start_slice_job_build_plate in self._stored_optimized_layer_data: @@ -274,7 +281,7 @@ class CuraEngineBackend(QObject, Backend): self.processingProgress.emit(0) Logger.log("d", "Attempting to kill the engine process") - if Application.getInstance().getUseExternalBackend(): + if self._application.getUseExternalBackend(): return if self._process is not None: @@ -295,7 +302,7 @@ class CuraEngineBackend(QObject, Backend): # bootstrapping of a slice job. # # \param job The start slice job that was just finished. - def _onStartSliceCompleted(self, job): + def _onStartSliceCompleted(self, job: StartSliceJob) -> None: if self._error_message: self._error_message.hide() @@ -303,13 +310,13 @@ class CuraEngineBackend(QObject, Backend): if self._start_slice_job is job: self._start_slice_job = None - if job.isCancelled() or job.getError() or job.getResult() == StartSliceJob.StartJobResult.Error: + if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error: self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) return - if job.getResult() == StartSliceJob.StartJobResult.MaterialIncompatible: - if Application.getInstance().platformActivity: + if job.getResult() == StartJobResult.MaterialIncompatible: + if self._application.platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() @@ -319,10 +326,10 @@ class CuraEngineBackend(QObject, Backend): self.backendStateChange.emit(BackendState.NotStarted) return - if job.getResult() == StartSliceJob.StartJobResult.SettingError: - if Application.getInstance().platformActivity: + if job.getResult() == StartJobResult.SettingError: + if self._application.platformActivity: extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) - error_keys = [] + error_keys = [] #type: List[str] for extruder in extruders: error_keys.extend(extruder.getErrorKeys()) if not extruders: @@ -330,7 +337,7 @@ class CuraEngineBackend(QObject, Backend): error_labels = set() for key in error_keys: for stack in [self._global_container_stack] + extruders: #Search all container stacks for the definition of this setting. Some are only in an extruder stack. - definitions = stack.getBottom().findDefinitions(key = key) + definitions = cast(DefinitionContainerInterface, stack.getBottom()).findDefinitions(key = key) if definitions: break #Found it! No need to continue search. else: #No stack has a definition for this setting. @@ -338,8 +345,7 @@ class CuraEngineBackend(QObject, Backend): continue error_labels.add(definitions[0].label) - error_labels = ", ".join(error_labels) - self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(error_labels), + self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(", ".join(error_labels)), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) @@ -348,29 +354,27 @@ class CuraEngineBackend(QObject, Backend): self.backendStateChange.emit(BackendState.NotStarted) return - elif job.getResult() == StartSliceJob.StartJobResult.ObjectSettingError: + elif job.getResult() == StartJobResult.ObjectSettingError: errors = {} - for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): + for node in DepthFirstIterator(self._application.getController().getScene().getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. stack = node.callDecoration("getStack") if not stack: continue for key in stack.getErrorKeys(): - definition = self._global_container_stack.getBottom().findDefinitions(key = key) + definition = cast(DefinitionContainerInterface, self._global_container_stack.getBottom()).findDefinitions(key = key) if not definition: Logger.log("e", "When checking settings for errors, unable to find definition for key {key} in per-object stack.".format(key = key)) continue - definition = definition[0] - errors[key] = definition.label - error_labels = ", ".join(errors.values()) - self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = error_labels), + errors[key] = definition[0].label + self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = ", ".join(errors.values())), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() self.backendStateChange.emit(BackendState.Error) self.backendError.emit(job) return - if job.getResult() == StartSliceJob.StartJobResult.BuildPlateError: - if Application.getInstance().platformActivity: + if job.getResult() == StartJobResult.BuildPlateError: + if self._application.platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() @@ -379,7 +383,7 @@ class CuraEngineBackend(QObject, Backend): else: self.backendStateChange.emit(BackendState.NotStarted) - if job.getResult() == StartSliceJob.StartJobResult.ObjectsWithDisabledExtruder: + if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder: self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because there are objects associated with disabled Extruder %s." % job.getMessage()), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() @@ -387,8 +391,8 @@ class CuraEngineBackend(QObject, Backend): self.backendError.emit(job) return - if job.getResult() == StartSliceJob.StartJobResult.NothingToSlice: - if Application.getInstance().platformActivity: + if job.getResult() == StartJobResult.NothingToSlice: + if self._application.platformActivity: self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."), title = catalog.i18nc("@info:title", "Unable to slice")) self._error_message.show() @@ -411,20 +415,20 @@ class CuraEngineBackend(QObject, Backend): # It disables when # - preference auto slice is off # - decorator isBlockSlicing is found (used in g-code reader) - def determineAutoSlicing(self): + def determineAutoSlicing(self) -> bool: enable_timer = True self._is_disabled = False - if not Application.getInstance().getPreferences().getValue("general/auto_slice"): + if not self._application.getPreferences().getValue("general/auto_slice"): enable_timer = False - for node in DepthFirstIterator(self._scene.getRoot()): + for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("isBlockSlicing"): enable_timer = False self.backendStateChange.emit(BackendState.Disabled) self._is_disabled = True gcode_list = node.callDecoration("getGCodeList") if gcode_list is not None: - self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list + self._scene.gcode_dict[node.callDecoration("getBuildPlateNumber")] = gcode_list #type: ignore #Because we generate this attribute dynamically. if self._use_timer == enable_timer: return self._use_timer @@ -437,9 +441,9 @@ class CuraEngineBackend(QObject, Backend): return False ## Return a dict with number of objects per build plate - def _numObjectsPerBuildPlate(self): - num_objects = defaultdict(int) - for node in DepthFirstIterator(self._scene.getRoot()): + def _numObjectsPerBuildPlate(self) -> Dict[int, int]: + num_objects = defaultdict(int) #type: Dict[int, int] + for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. # Only count sliceable objects if node.callDecoration("isSliceable"): build_plate_number = node.callDecoration("getBuildPlateNumber") @@ -451,7 +455,7 @@ class CuraEngineBackend(QObject, Backend): # This should start a slice if the scene is now ready to slice. # # \param source The scene node that was changed. - def _onSceneChanged(self, source): + def _onSceneChanged(self, source: SceneNode) -> None: if not isinstance(source, SceneNode): return @@ -506,8 +510,8 @@ class CuraEngineBackend(QObject, Backend): ## Called when an error occurs in the socket connection towards the engine. # # \param error The exception that occurred. - def _onSocketError(self, error): - if Application.getInstance().isShuttingDown(): + def _onSocketError(self, error: Arcus.Error) -> None: + if self._application.isShuttingDown(): return super()._onSocketError(error) @@ -521,19 +525,19 @@ class CuraEngineBackend(QObject, Backend): Logger.log("w", "A socket error caused the connection to be reset") ## Remove old layer data (if any) - def _clearLayerData(self, build_plate_numbers = set()): - for node in DepthFirstIterator(self._scene.getRoot()): + def _clearLayerData(self, build_plate_numbers: Set = None) -> None: + for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("getLayerData"): if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers: node.getParent().removeChild(node) - def markSliceAll(self): - for build_plate_number in range(Application.getInstance().getMultiBuildPlateModel().maxBuildPlate + 1): + def markSliceAll(self) -> None: + for build_plate_number in range(self._application.getMultiBuildPlateModel().maxBuildPlate + 1): if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) ## Convenient function: mark everything to slice, emit state and clear layer data - def needsSlicing(self): + def needsSlicing(self) -> None: self.stopSlicing() self.markSliceAll() self.processingProgress.emit(0.0) @@ -545,7 +549,7 @@ class CuraEngineBackend(QObject, Backend): ## A setting has changed, so check if we must reslice. # \param instance The setting instance that has changed. # \param property The property of the setting instance that has changed. - def _onSettingChanged(self, instance, property): + def _onSettingChanged(self, instance: SettingInstance, property: str) -> None: if property == "value": # Only reslice if the value has changed. self.needsSlicing() self._onChanged() @@ -554,7 +558,7 @@ class CuraEngineBackend(QObject, Backend): if self._use_timer: self._change_timer.stop() - def _onStackErrorCheckFinished(self): + def _onStackErrorCheckFinished(self) -> None: self.determineAutoSlicing() if self._is_disabled: return @@ -566,13 +570,13 @@ class CuraEngineBackend(QObject, Backend): ## Called when a sliced layer data message is received from the engine. # # \param message The protobuf message containing sliced layer data. - def _onLayerMessage(self, message): + def _onLayerMessage(self, message: Arcus.PythonMessage) -> None: self._stored_layer_data.append(message) ## Called when an optimized sliced layer data message is received from the engine. # # \param message The protobuf message containing sliced layer data. - def _onOptimizedLayerMessage(self, message): + def _onOptimizedLayerMessage(self, message: Arcus.PythonMessage) -> None: if self._start_slice_job_build_plate not in self._stored_optimized_layer_data: self._stored_optimized_layer_data[self._start_slice_job_build_plate] = [] self._stored_optimized_layer_data[self._start_slice_job_build_plate].append(message) @@ -580,11 +584,11 @@ class CuraEngineBackend(QObject, Backend): ## Called when a progress message is received from the engine. # # \param message The protobuf message containing the slicing progress. - def _onProgressMessage(self, message): + def _onProgressMessage(self, message: Arcus.PythonMessage) -> None: self.processingProgress.emit(message.amount) self.backendStateChange.emit(BackendState.Processing) - def _invokeSlice(self): + def _invokeSlice(self) -> None: if self._use_timer: # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, # otherwise business as usual @@ -600,17 +604,17 @@ class CuraEngineBackend(QObject, Backend): ## Called when the engine sends a message that slicing is finished. # # \param message The protobuf message signalling that slicing is finished. - def _onSlicingFinishedMessage(self, message): + def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None: self.backendStateChange.emit(BackendState.Done) self.processingProgress.emit(1.0) - gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] + gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically. for index, line in enumerate(gcode_list): - replaced = line.replace("{print_time}", str(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601))) - replaced = replaced.replace("{filament_amount}", str(Application.getInstance().getPrintInformation().materialLengths)) - replaced = replaced.replace("{filament_weight}", str(Application.getInstance().getPrintInformation().materialWeights)) - replaced = replaced.replace("{filament_cost}", str(Application.getInstance().getPrintInformation().materialCosts)) - replaced = replaced.replace("{jobname}", str(Application.getInstance().getPrintInformation().jobName)) + replaced = line.replace("{print_time}", str(self._application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601))) + replaced = replaced.replace("{filament_amount}", str(self._application.getPrintInformation().materialLengths)) + replaced = replaced.replace("{filament_weight}", str(self._application.getPrintInformation().materialWeights)) + replaced = replaced.replace("{filament_cost}", str(self._application.getPrintInformation().materialCosts)) + replaced = replaced.replace("{jobname}", str(self._application.getPrintInformation().jobName)) gcode_list[index] = replaced @@ -619,7 +623,7 @@ class CuraEngineBackend(QObject, Backend): Logger.log("d", "Number of models per buildplate: %s", dict(self._numObjectsPerBuildPlate())) # See if we need to process the sliced layers job. - active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate + active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate if ( self._layer_view_active and (self._process_layers_job is None or not self._process_layers_job.isRunning()) and @@ -641,25 +645,27 @@ class CuraEngineBackend(QObject, Backend): ## Called when a g-code message is received from the engine. # # \param message The protobuf message containing g-code, encoded as UTF-8. - def _onGCodeLayerMessage(self, message): - self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) + def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None: + self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically. ## Called when a g-code prefix message is received from the engine. # # \param message The protobuf message containing the g-code prefix, # encoded as UTF-8. - def _onGCodePrefixMessage(self, message): - self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) + def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None: + self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically. ## Creates a new socket connection. - def _createSocket(self): - super()._createSocket(os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto"))) + def _createSocket(self, protocol_file: str = None) -> None: + if not protocol_file: + protocol_file = os.path.abspath(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "Cura.proto")) + super()._createSocket(protocol_file) self._engine_is_fresh = True ## Called when anything has changed to the stuff that needs to be sliced. # # This indicates that we should probably re-slice soon. - def _onChanged(self, *args, **kwargs): + def _onChanged(self, *args: Any, **kwargs: Any) -> None: self.needsSlicing() if self._use_timer: # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, @@ -677,7 +683,7 @@ class CuraEngineBackend(QObject, Backend): # # \param message The protobuf message containing the print time per feature and # material amount per extruder - def _onPrintTimeMaterialEstimates(self, message): + def _onPrintTimeMaterialEstimates(self, message: Arcus.PythonMessage) -> None: material_amounts = [] for index in range(message.repeatedMessageCount("materialEstimates")): material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount) @@ -688,7 +694,7 @@ class CuraEngineBackend(QObject, Backend): ## Called for parsing message to retrieve estimated time per feature # # \param message The protobuf message containing the print time per feature - def _parseMessagePrintTimes(self, message): + def _parseMessagePrintTimes(self, message: Arcus.PythonMessage) -> Dict[str, float]: result = { "inset_0": message.time_inset_0, "inset_x": message.time_inset_x, @@ -705,7 +711,7 @@ class CuraEngineBackend(QObject, Backend): return result ## Called when the back-end connects to the front-end. - def _onBackendConnected(self): + def _onBackendConnected(self) -> None: if self._restart: self._restart = False self._onChanged() @@ -716,7 +722,7 @@ class CuraEngineBackend(QObject, Backend): # continuously slicing while the user is dragging some tool handle. # # \param tool The tool that the user is using. - def _onToolOperationStarted(self, tool): + def _onToolOperationStarted(self, tool: Tool) -> None: self._tool_active = True # Do not react on scene change self.disableTimer() # Restart engine as soon as possible, we know we want to slice afterwards @@ -729,7 +735,7 @@ class CuraEngineBackend(QObject, Backend): # This indicates that we can safely start slicing again. # # \param tool The tool that the user was using. - def _onToolOperationStopped(self, tool): + def _onToolOperationStopped(self, tool: Tool) -> None: self._tool_active = False # React on scene change again self.determineAutoSlicing() # Switch timer on if appropriate # Process all the postponed scene changes @@ -737,18 +743,17 @@ class CuraEngineBackend(QObject, Backend): source = self._postponed_scene_change_sources.pop(0) self._onSceneChanged(source) - def _startProcessSlicedLayersJob(self, build_plate_number): - self._process_layers_job = ProcessSlicedLayersJob.ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number]) + def _startProcessSlicedLayersJob(self, build_plate_number: int) -> None: + self._process_layers_job = ProcessSlicedLayersJob(self._stored_optimized_layer_data[build_plate_number]) self._process_layers_job.setBuildPlate(build_plate_number) self._process_layers_job.finished.connect(self._onProcessLayersFinished) self._process_layers_job.start() ## Called when the user changes the active view mode. - def _onActiveViewChanged(self): - application = Application.getInstance() - view = application.getController().getActiveView() + def _onActiveViewChanged(self) -> None: + view = self._application.getController().getActiveView() if view: - active_build_plate = application.getMultiBuildPlateModel().activeBuildPlate + active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate if view.getPluginId() == "SimulationView": # If switching to layer view, we should process the layers if that hasn't been done yet. self._layer_view_active = True # There is data and we're not slicing at the moment @@ -766,14 +771,14 @@ class CuraEngineBackend(QObject, Backend): ## Called when the back-end self-terminates. # # We should reset our state and start listening for new connections. - def _onBackendQuit(self): + def _onBackendQuit(self) -> None: if not self._restart: if self._process: Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) self._process = None ## Called when the global container stack changes - def _onGlobalStackChanged(self): + def _onGlobalStackChanged(self) -> None: if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged) self._global_container_stack.containersChanged.disconnect(self._onChanged) @@ -783,7 +788,7 @@ class CuraEngineBackend(QObject, Backend): extruder.propertyChanged.disconnect(self._onSettingChanged) extruder.containersChanged.disconnect(self._onChanged) - self._global_container_stack = Application.getInstance().getGlobalContainerStack() + self._global_container_stack = self._application.getGlobalContainerStack() if self._global_container_stack: self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed. @@ -794,26 +799,26 @@ class CuraEngineBackend(QObject, Backend): extruder.containersChanged.connect(self._onChanged) self._onChanged() - def _onProcessLayersFinished(self, job): + def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None: del self._stored_optimized_layer_data[job.getBuildPlate()] self._process_layers_job = None Logger.log("d", "See if there is more to slice(2)...") self._invokeSlice() ## Connect slice function to timer. - def enableTimer(self): + def enableTimer(self) -> None: if not self._use_timer: self._change_timer.timeout.connect(self.slice) self._use_timer = True ## Disconnect slice function from timer. # This means that slicing will not be triggered automatically - def disableTimer(self): + def disableTimer(self) -> None: if self._use_timer: self._use_timer = False self._change_timer.timeout.disconnect(self.slice) - def _onPreferencesChanged(self, preference): + def _onPreferencesChanged(self, preference: str) -> None: if preference != "general/auto_slice": return auto_slice = self.determineAutoSlicing() @@ -821,11 +826,11 @@ class CuraEngineBackend(QObject, Backend): self._change_timer.start() ## Tickle the backend so in case of auto slicing, it starts the timer. - def tickle(self): + def tickle(self) -> None: if self._use_timer: self._change_timer.start() - def _extruderChanged(self): + def _extruderChanged(self) -> None: for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1): if build_plate_number not in self._build_plates_to_be_sliced: self._build_plates_to_be_sliced.append(build_plate_number) diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index c5b222195f..78dd4eafd2 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -1,21 +1,25 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. import numpy from string import Formatter from enum import IntEnum import time +from typing import Any, Dict, List, Optional, Set import re +import Arcus #For typing. from UM.Job import Job -from UM.Application import Application from UM.Logger import Logger +from UM.Settings.ContainerStack import ContainerStack #For typing. +from UM.Settings.SettingRelation import SettingRelation #For typing. from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator - +from UM.Scene.Scene import Scene #For typing. from UM.Settings.Validator import ValidatorState from UM.Settings.SettingRelation import RelationType +from cura.CuraApplication import CuraApplication from cura.Scene.CuraSceneNode import CuraSceneNode from cura.OneAtATimeIterator import OneAtATimeIterator from cura.Settings.ExtruderManager import ExtruderManager @@ -35,19 +39,19 @@ class StartJobResult(IntEnum): ObjectsWithDisabledExtruder = 8 -## Formatter class that handles token expansion in start/end gcod +## Formatter class that handles token expansion in start/end gcode class GcodeStartEndFormatter(Formatter): - def get_value(self, key, args, kwargs): # [CodeStyle: get_value is an overridden function from the Formatter class] + def get_value(self, key: str, *args: str, **kwargs) -> str: #type: ignore # [CodeStyle: get_value is an overridden function from the Formatter class] # The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key), # and a default_extruder_nr to use when no extruder_nr is specified if isinstance(key, str): try: - extruder_nr = kwargs["default_extruder_nr"] + extruder_nr = int(kwargs["default_extruder_nr"]) except ValueError: extruder_nr = -1 - key_fragments = [fragment.strip() for fragment in key.split(',')] + key_fragments = [fragment.strip() for fragment in key.split(",")] if len(key_fragments) == 2: try: extruder_nr = int(key_fragments[1]) @@ -74,25 +78,25 @@ class GcodeStartEndFormatter(Formatter): ## Job class that builds up the message of scene data to send to CuraEngine. class StartSliceJob(Job): - def __init__(self, slice_message): + def __init__(self, slice_message: Arcus.PythonMessage) -> None: super().__init__() - self._scene = Application.getInstance().getController().getScene() - self._slice_message = slice_message - self._is_cancelled = False - self._build_plate_number = None + self._scene = CuraApplication.getInstance().getController().getScene() #type: Scene + self._slice_message = slice_message #type: Arcus.PythonMessage + self._is_cancelled = False #type: bool + self._build_plate_number = None #type: Optional[int] - self._all_extruders_settings = None # cache for all setting values from all stacks (global & extruder) for the current machine + self._all_extruders_settings = None #type: Optional[Dict[str, Any]] # cache for all setting values from all stacks (global & extruder) for the current machine - def getSliceMessage(self): + def getSliceMessage(self) -> Arcus.PythonMessage: return self._slice_message - def setBuildPlate(self, build_plate_number): + def setBuildPlate(self, build_plate_number: int) -> None: self._build_plate_number = build_plate_number ## Check if a stack has any errors. ## returns true if it has errors, false otherwise. - def _checkStackForErrors(self, stack): + def _checkStackForErrors(self, stack: ContainerStack) -> bool: if stack is None: return False @@ -105,28 +109,28 @@ class StartSliceJob(Job): return False ## Runs the job that initiates the slicing. - def run(self): + def run(self) -> None: if self._build_plate_number is None: self.setResult(StartJobResult.Error) return - stack = Application.getInstance().getGlobalContainerStack() + stack = CuraApplication.getInstance().getGlobalContainerStack() if not stack: self.setResult(StartJobResult.Error) return # Don't slice if there is a setting with an error value. - if Application.getInstance().getMachineManager().stacksHaveErrors: + if CuraApplication.getInstance().getMachineManager().stacksHaveErrors: self.setResult(StartJobResult.SettingError) return - if Application.getInstance().getBuildVolume().hasErrors(): + if CuraApplication.getInstance().getBuildVolume().hasErrors(): self.setResult(StartJobResult.BuildPlateError) return # Don't slice if the buildplate or the nozzle type is incompatible with the materials - if not Application.getInstance().getMachineManager().variantBuildplateCompatible and \ - not Application.getInstance().getMachineManager().variantBuildplateUsable: + if not CuraApplication.getInstance().getMachineManager().variantBuildplateCompatible and \ + not CuraApplication.getInstance().getMachineManager().variantBuildplateUsable: self.setResult(StartJobResult.MaterialIncompatible) return @@ -141,7 +145,7 @@ class StartSliceJob(Job): # Don't slice if there is a per object setting with an error value. - for node in DepthFirstIterator(self._scene.getRoot()): + for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if not isinstance(node, CuraSceneNode) or not node.isSelectable(): continue @@ -151,7 +155,7 @@ class StartSliceJob(Job): with self._scene.getSceneLock(): # Remove old layer data. - for node in DepthFirstIterator(self._scene.getRoot()): + for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number: node.getParent().removeChild(node) break @@ -159,7 +163,7 @@ class StartSliceJob(Job): # Get the objects in their groups to print. object_groups = [] if stack.getProperty("print_sequence", "value") == "one_at_a_time": - for node in OneAtATimeIterator(self._scene.getRoot()): + for node in OneAtATimeIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. temp_list = [] # Node can't be printed, so don't bother sending it. @@ -185,7 +189,7 @@ class StartSliceJob(Job): else: temp_list = [] has_printing_mesh = False - for node in DepthFirstIterator(self._scene.getRoot()): + for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None: per_object_stack = node.callDecoration("getStack") is_non_printing_mesh = False @@ -212,12 +216,12 @@ class StartSliceJob(Job): if temp_list: object_groups.append(temp_list) - extruders_enabled = {position: stack.isEnabled for position, stack in Application.getInstance().getGlobalContainerStack().extruders.items()} + extruders_enabled = {position: stack.isEnabled for position, stack in CuraApplication.getInstance().getGlobalContainerStack().extruders.items()} filtered_object_groups = [] has_model_with_disabled_extruders = False associated_disabled_extruders = set() for group in object_groups: - stack = Application.getInstance().getGlobalContainerStack() + stack = CuraApplication.getInstance().getGlobalContainerStack() skip_group = False for node in group: extruder_position = node.callDecoration("getActiveExtruderPosition") @@ -283,11 +287,11 @@ class StartSliceJob(Job): self.setResult(StartJobResult.Finished) - def cancel(self): + def cancel(self) -> None: super().cancel() self._is_cancelled = True - def isCancelled(self): + def isCancelled(self) -> bool: return self._is_cancelled ## Creates a dictionary of tokens to replace in g-code pieces. @@ -297,7 +301,7 @@ class StartSliceJob(Job): # with. # \return A dictionary of replacement tokens to the values they should be # replaced with. - def _buildReplacementTokens(self, stack) -> dict: + def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") @@ -310,7 +314,7 @@ class StartSliceJob(Job): result["date"] = time.strftime("%d-%m-%Y") result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))] - initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0] + initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value") result["initial_extruder_nr"] = initial_extruder_nr @@ -319,9 +323,9 @@ class StartSliceJob(Job): ## Replace setting tokens in a piece of g-code. # \param value A piece of g-code to replace tokens in. # \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack - def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1): + def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1) -> str: if not self._all_extruders_settings: - global_stack = Application.getInstance().getGlobalContainerStack() + global_stack = CuraApplication.getInstance().getGlobalContainerStack() # NB: keys must be strings for the string formatter self._all_extruders_settings = { @@ -343,7 +347,7 @@ class StartSliceJob(Job): return str(value) ## Create extruder message from stack - def _buildExtruderMessage(self, stack): + def _buildExtruderMessage(self, stack: ContainerStack) -> None: message = self._slice_message.addRepeatedMessage("extruders") message.id = int(stack.getMetaDataEntry("position")) @@ -370,7 +374,7 @@ class StartSliceJob(Job): # # The settings are taken from the global stack. This does not include any # per-extruder settings or per-object settings. - def _buildGlobalSettingsMessage(self, stack): + def _buildGlobalSettingsMessage(self, stack: ContainerStack) -> None: settings = self._buildReplacementTokens(stack) # Pre-compute material material_bed_temp_prepend and material_print_temp_prepend @@ -384,7 +388,7 @@ class StartSliceJob(Job): # Replace the setting tokens in start and end g-code. # Use values from the first used extruder by default so we get the expected temperatures - initial_extruder_stack = Application.getInstance().getExtruderManager().getUsedExtruderStacks()[0] + initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0] initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value") settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr) @@ -405,7 +409,7 @@ class StartSliceJob(Job): # # \param stack The global stack with all settings, from which to read the # limit_to_extruder property. - def _buildGlobalInheritsStackMessage(self, stack): + def _buildGlobalInheritsStackMessage(self, stack: ContainerStack) -> None: for key in stack.getAllKeys(): extruder_position = int(round(float(stack.getProperty(key, "limit_to_extruder")))) if extruder_position >= 0: # Set to a specific extruder. @@ -415,9 +419,9 @@ class StartSliceJob(Job): Job.yieldThread() ## Check if a node has per object settings and ensure that they are set correctly in the message - # \param node \type{SceneNode} Node to check. + # \param node Node to check. # \param message object_lists message to put the per object settings in - def _handlePerObjectSettings(self, node, message): + def _handlePerObjectSettings(self, node: CuraSceneNode, message: Arcus.PythonMessage): stack = node.callDecoration("getStack") # Check if the node has a stack attached to it and the stack has any settings in the top container. @@ -426,7 +430,7 @@ class StartSliceJob(Job): # Check all settings for relations, so we can also calculate the correct values for dependent settings. top_of_stack = stack.getTop() # Cache for efficiency. - changed_setting_keys = set(top_of_stack.getAllKeys()) + changed_setting_keys = top_of_stack.getAllKeys() # Add all relations to changed settings as well. for key in top_of_stack.getAllKeys(): @@ -455,9 +459,9 @@ class StartSliceJob(Job): Job.yieldThread() ## Recursive function to put all settings that require each other for value changes in a list - # \param relations_set \type{set} Set of keys (strings) of settings that are influenced + # \param relations_set Set of keys of settings that are influenced # \param relations list of relation objects that need to be checked. - def _addRelations(self, relations_set, relations): + def _addRelations(self, relations_set: Set[str], relations: List[SettingRelation]): for relation in filter(lambda r: r.role == "value" or r.role == "limit_to_extruder", relations): if relation.type == RelationType.RequiresTarget: continue diff --git a/plugins/GCodeGzWriter/GCodeGzWriter.py b/plugins/GCodeGzWriter/GCodeGzWriter.py index 06fafb5995..6ddecdb0bd 100644 --- a/plugins/GCodeGzWriter/GCodeGzWriter.py +++ b/plugins/GCodeGzWriter/GCodeGzWriter.py @@ -3,7 +3,7 @@ import gzip from io import StringIO, BufferedIOBase #To write the g-code to a temporary buffer, and for typing. -from typing import List +from typing import cast, List from UM.Logger import Logger from UM.Mesh.MeshWriter import MeshWriter #The class we're extending/implementing. @@ -32,7 +32,7 @@ class GCodeGzWriter(MeshWriter): #Get the g-code from the g-code writer. gcode_textio = StringIO() #We have to convert the g-code into bytes. - success = PluginRegistry.getInstance().getPluginObject("GCodeWriter").write(gcode_textio, None) + success = cast(MeshWriter, PluginRegistry.getInstance().getPluginObject("GCodeWriter")).write(gcode_textio, None) if not success: #Writing the g-code failed. Then I can also not write the gzipped g-code. return False diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index 696b3b180b..05f40b41e7 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -1,11 +1,9 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from UM.Application import Application from UM.Backend import Backend from UM.Job import Job from UM.Logger import Logger -from UM.Math.AxisAlignedBox import AxisAlignedBox from UM.Math.Vector import Vector from UM.Message import Message from cura.Scene.CuraSceneNode import CuraSceneNode @@ -13,7 +11,8 @@ from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") -from cura import LayerDataBuilder +from cura.CuraApplication import CuraApplication +from cura.LayerDataBuilder import LayerDataBuilder from cura.LayerDataDecorator import LayerDataDecorator from cura.LayerPolygon import LayerPolygon from cura.Scene.GCodeListDecorator import GCodeListDecorator @@ -23,16 +22,16 @@ import numpy import math import re from typing import Dict, List, NamedTuple, Optional, Union -from collections import namedtuple -Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", float)]) +PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])]) +Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])]) ## This parser is intended to interpret the common firmware codes among all the # different flavors class FlavorParser: def __init__(self) -> None: - Application.getInstance().hideMessageSignal.connect(self._onHideMessage) + CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage) self._cancelled = False self._message = None self._layer_number = 0 @@ -40,21 +39,21 @@ class FlavorParser: self._clearValues() self._scene_node = None # X, Y, Z position, F feedrate and E extruder values are stored - self._position = namedtuple('Position', ['x', 'y', 'z', 'f', 'e']) + self._position = Position self._is_layers_in_file = False # Does the Gcode have the layers comment? - self._extruder_offsets = {} # Offsets for multi extruders. key is index, value is [x-offset, y-offset] + self._extruder_offsets = {} # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset] self._current_layer_thickness = 0.2 # default self._filament_diameter = 2.85 # default - Application.getInstance().getPreferences().addPreference("gcodereader/show_caution", True) + CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True) def _clearValues(self) -> None: self._extruder_number = 0 - self._extrusion_length_offset = [0] + self._extrusion_length_offset = [0] # type: List[float] self._layer_type = LayerPolygon.Inset0Type self._layer_number = 0 - self._previous_z = 0 - self._layer_data_builder = LayerDataBuilder.LayerDataBuilder() + self._previous_z = 0 # type: float + self._layer_data_builder = LayerDataBuilder() self._is_absolute_positioning = True # It can be absolute (G90) or relative (G91) self._is_absolute_extrusion = True # It can become absolute (M82, default) or relative (M83) @@ -77,14 +76,14 @@ class FlavorParser: def _getInt(self, line: str, code: str) -> Optional[int]: value = self._getValue(line, code) try: - return int(value) + return int(value) # type: ignore except: return None def _getFloat(self, line: str, code: str) -> Optional[float]: value = self._getValue(line, code) try: - return float(value) + return float(value) # type: ignore except: return None @@ -92,10 +91,6 @@ class FlavorParser: if message == self._message: self._cancelled = True - @staticmethod - def _getNullBoundingBox() -> AxisAlignedBox: - return AxisAlignedBox(minimum=Vector(0, 0, 0), maximum=Vector(10, 10, 10)) - def _createPolygon(self, layer_thickness: float, path: List[List[Union[float, int]]], extruder_offsets: List[float]) -> bool: countvalid = 0 for point in path: @@ -169,7 +164,7 @@ class FlavorParser: return 0.35 return line_width - def _gCode0(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position: + def _gCode0(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: x, y, z, f, e = position if self._is_absolute_positioning: @@ -205,7 +200,7 @@ class FlavorParser: _gCode1 = _gCode0 ## Home the head. - def _gCode28(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position: + def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: return self._position( params.x if params.x is not None else position.x, params.y if params.y is not None else position.y, @@ -214,20 +209,20 @@ class FlavorParser: position.e) ## Set the absolute positioning - def _gCode90(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position: + def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: self._is_absolute_positioning = True self._is_absolute_extrusion = True return position ## Set the relative positioning - def _gCode91(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position: + def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: self._is_absolute_positioning = False self._is_absolute_extrusion = False return position ## Reset the current position to the values specified. # For example: G92 X10 will set the X to 10 without any physical motion. - def _gCode92(self, position: Position, params: Position, path: List[List[Union[float, int]]]) -> Position: + def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: if params.e is not None: # Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e @@ -260,7 +255,7 @@ class FlavorParser: f = float(item[1:]) / 60 if item[0] == "E": e = float(item[1:]) - params = self._position(x, y, z, f, e) + params = PositionOptional(x, y, z, f, e) return func(position, params, path) return position @@ -290,13 +285,10 @@ class FlavorParser: Logger.log("d", "Preparing to load GCode") self._cancelled = False # We obtain the filament diameter from the selected extruder to calculate line widths - global_stack = Application.getInstance().getGlobalContainerStack() + global_stack = CuraApplication.getInstance().getGlobalContainerStack() self._filament_diameter = global_stack.extruders[str(self._extruder_number)].getProperty("material_diameter", "value") scene_node = CuraSceneNode() - # Override getBoundingBox function of the sceneNode, as this node should return a bounding box, but there is no - # real data to calculate it from. - scene_node.getBoundingBox = self._getNullBoundingBox gcode_list = [] self._is_layers_in_file = False @@ -322,13 +314,14 @@ class FlavorParser: lifetime=0, title = catalog.i18nc("@info:title", "G-code Details")) + assert(self._message is not None) # use for typing purposes self._message.setProgress(0) self._message.show() Logger.log("d", "Parsing Gcode...") - current_position = self._position(0, 0, 0, 0, [0]) - current_path = [] + current_position = Position(0, 0, 0, 0, [0]) + current_path = [] #type: List[List[float]] min_layer_number = 0 negative_layers = 0 previous_layer = 0 @@ -443,9 +436,9 @@ class FlavorParser: scene_node.addDecorator(gcode_list_decorator) # gcode_dict stores gcode_lists for a number of build plates. - active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate + active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate gcode_dict = {active_build_plate_id: gcode_list} - Application.getInstance().getController().getScene().gcode_dict = gcode_dict + CuraApplication.getInstance().getController().getScene().gcode_dict = gcode_dict #type: ignore #Because gcode_dict is generated dynamically. Logger.log("d", "Finished parsing Gcode") self._message.hide() @@ -453,7 +446,7 @@ class FlavorParser: if self._layer_number == 0: Logger.log("w", "File doesn't contain any valid layers") - settings = Application.getInstance().getGlobalContainerStack() + settings = CuraApplication.getInstance().getGlobalContainerStack() if not settings.getProperty("machine_center_is_zero", "value"): machine_width = settings.getProperty("machine_width", "value") machine_depth = settings.getProperty("machine_depth", "value") @@ -461,7 +454,7 @@ class FlavorParser: Logger.log("d", "GCode loading finished") - if Application.getInstance().getPreferences().getValue("gcodereader/show_caution"): + if CuraApplication.getInstance().getPreferences().getValue("gcodereader/show_caution"): caution_message = Message(catalog.i18nc( "@info:generic", "Make sure the g-code is suitable for your printer and printer configuration before sending the file to it. The g-code representation may not be accurate."), @@ -470,7 +463,7 @@ class FlavorParser: caution_message.show() # The "save/print" button's state is bound to the backend state. - backend = Application.getInstance().getBackend() + backend = CuraApplication.getInstance().getBackend() backend.backendStateChange.emit(Backend.BackendState.Disabled) return scene_node diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index c01d48be4c..d334c66dbe 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -140,7 +140,7 @@ class GCodeWriter(MeshWriter): serialized = flat_global_container.serialize() data = {"global_quality": serialized} - all_setting_keys = set(flat_global_container.getAllKeys()) + all_setting_keys = flat_global_container.getAllKeys() for extruder in sorted(stack.extruders.values(), key = lambda k: int(k.getMetaDataEntry("position"))): extruder_quality = extruder.qualityChanges if extruder_quality.getId() == "empty_quality_changes": @@ -167,7 +167,7 @@ class GCodeWriter(MeshWriter): extruder_serialized = flat_extruder_quality.serialize() data.setdefault("extruder_quality", []).append(extruder_serialized) - all_setting_keys.update(set(flat_extruder_quality.getAllKeys())) + all_setting_keys.update(flat_extruder_quality.getAllKeys()) # Check if there is any profiles if not all_setting_keys: diff --git a/plugins/SupportEraser/SupportEraser.py b/plugins/SupportEraser/SupportEraser.py index 06d9fc3707..92b42f9abc 100644 --- a/plugins/SupportEraser/SupportEraser.py +++ b/plugins/SupportEraser/SupportEraser.py @@ -1,22 +1,17 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import os -import os.path - from PyQt5.QtCore import Qt, QTimer from PyQt5.QtWidgets import QApplication from UM.Math.Vector import Vector from UM.Tool import Tool -from UM.Application import Application from UM.Event import Event, MouseEvent - from UM.Mesh.MeshBuilder import MeshBuilder from UM.Scene.Selection import Selection -from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator -from cura.Scene.CuraSceneNode import CuraSceneNode +from cura.CuraApplication import CuraApplication +from cura.Scene.CuraSceneNode import CuraSceneNode from cura.PickingPass import PickingPass from UM.Operations.GroupedOperation import GroupedOperation @@ -26,8 +21,6 @@ from cura.Operations.SetParentOperation import SetParentOperation from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator -from UM.Scene.GroupDecorator import GroupDecorator -from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator from UM.Settings.SettingInstance import SettingInstance @@ -38,7 +31,7 @@ class SupportEraser(Tool): self._controller = self.getController() self._selection_pass = None - Application.getInstance().globalContainerStackChanged.connect(self._updateEnabled) + CuraApplication.getInstance().globalContainerStackChanged.connect(self._updateEnabled) # Note: if the selection is cleared with this tool active, there is no way to switch to # another tool than to reselect an object (by clicking it) because the tool buttons in the @@ -106,7 +99,7 @@ class SupportEraser(Tool): mesh.addCube(10,10,10) node.setMeshData(mesh.build()) - active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate + active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate node.addDecorator(BuildPlateDecorator(active_build_plate)) node.addDecorator(SliceableObjectDecorator()) @@ -126,7 +119,7 @@ class SupportEraser(Tool): op.push() node.setPosition(position, CuraSceneNode.TransformSpace.World) - Application.getInstance().getController().getScene().sceneChanged.emit(node) + CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node) def _removeEraserMesh(self, node: CuraSceneNode): parent = node.getParent() @@ -139,16 +132,16 @@ class SupportEraser(Tool): if parent and not Selection.isSelected(parent): Selection.add(parent) - Application.getInstance().getController().getScene().sceneChanged.emit(node) + CuraApplication.getInstance().getController().getScene().sceneChanged.emit(node) def _updateEnabled(self): plugin_enabled = False - global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() if global_container_stack: plugin_enabled = global_container_stack.getProperty("anti_overhang_mesh", "enabled") - Application.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled) + CuraApplication.getInstance().getController().toolEnabledChanged.emit(self._plugin_id, plugin_enabled) def _onSelectionChanged(self): # When selection is passed from one object to another object, first the selection is cleared diff --git a/plugins/Toolbox/__init__.py b/plugins/Toolbox/__init__.py index 2f8e192764..70c00ed07c 100644 --- a/plugins/Toolbox/__init__.py +++ b/plugins/Toolbox/__init__.py @@ -9,4 +9,4 @@ def getMetaData(): def register(app): - return {"extension": Toolbox.Toolbox()} + return {"extension": Toolbox.Toolbox(app)} diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 2f26e21d65..0d0060e48c 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -1,19 +1,20 @@ # Copyright (c) 2018 Ultimaker B.V. # Toolbox is released under the terms of the LGPLv3 or higher. -from typing import Dict, Optional, Union, Any +from typing import Dict, Optional, Union, Any, cast import json import os import tempfile import platform +from typing import List from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply -from UM.Application import Application from UM.Logger import Logger from UM.PluginRegistry import PluginRegistry from UM.Extension import Extension +from UM.Qt.ListModel import ListModel from UM.i18n import i18nCatalog from UM.Version import Version @@ -27,44 +28,39 @@ i18n_catalog = i18nCatalog("cura") ## The Toolbox class is responsible of communicating with the server through the API class Toolbox(QObject, Extension): + DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" #type: str + DEFAULT_CLOUD_API_VERSION = 1 #type: int - DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" - DEFAULT_CLOUD_API_VERSION = 1 + def __init__(self, application: CuraApplication) -> None: + super().__init__() - def __init__(self, parent=None) -> None: - super().__init__(parent) + self._application = application #type: CuraApplication - self._application = Application.getInstance() - self._package_manager = None - self._plugin_registry = Application.getInstance().getPluginRegistry() - - self._sdk_version = None - self._cloud_api_version = None - self._cloud_api_root = None - self._api_url = None + self._sdk_version = None # type: Optional[int] + self._cloud_api_version = None # type: Optional[int] + self._cloud_api_root = None # type: Optional[str] + self._api_url = None # type: Optional[str] # Network: - self._get_packages_request = None - self._get_showcase_request = None - self._download_request = None - self._download_reply = None - self._download_progress = 0 - self._is_downloading = False - self._network_manager = None + self._download_request = None #type: Optional[QNetworkRequest] + self._download_reply = None #type: Optional[QNetworkReply] + self._download_progress = 0 #type: float + self._is_downloading = False #type: bool + self._network_manager = None #type: Optional[QNetworkAccessManager] self._request_header = [ b"User-Agent", str.encode( "%s/%s (%s %s)" % ( - Application.getInstance().getApplicationName(), - Application.getInstance().getVersion(), + self._application.getApplicationName(), + self._application.getVersion(), platform.system(), platform.machine(), ) ) ] - self._request_urls = {} - self._to_update = [] # Package_ids that are waiting to be updated - self._old_plugin_ids = [] + self._request_urls = {} # type: Dict[str, QUrl] + self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated + self._old_plugin_ids = [] # type: List[str] # Data: self._metadata = { @@ -76,7 +72,7 @@ class Toolbox(QObject, Extension): "materials_showcase": [], "materials_available": [], "materials_installed": [] - } + } # type: Dict[str, List[Any]] # Models: self._models = { @@ -88,33 +84,33 @@ class Toolbox(QObject, Extension): "materials_showcase": AuthorsModel(self), "materials_available": PackagesModel(self), "materials_installed": PackagesModel(self) - } + } # type: Dict[str, ListModel] # These properties are for keeping track of the UI state: # ---------------------------------------------------------------------- # View category defines which filter to use, and therefore effectively # which category is currently being displayed. For example, possible # values include "plugin" or "material", but also "installed". - self._view_category = "plugin" + self._view_category = "plugin" #type: str # View page defines which type of page layout to use. For example, # possible values include "overview", "detail" or "author". - self._view_page = "loading" + self._view_page = "loading" #type: str # Active package refers to which package is currently being downloaded, # installed, or otherwise modified. - self._active_package = None + self._active_package = None # type: Optional[Dict[str, Any]] - self._dialog = None - self._restart_required = False + self._dialog = None #type: Optional[QObject] + self._restart_required = False #type: bool # variables for the license agreement dialog - self._license_dialog_plugin_name = "" - self._license_dialog_license_content = "" - self._license_dialog_plugin_file_location = "" - self._restart_dialog_message = "" + self._license_dialog_plugin_name = "" #type: str + self._license_dialog_license_content = "" #type: str + self._license_dialog_plugin_file_location = "" #type: str + self._restart_dialog_message = "" #type: str - Application.getInstance().initializationFinished.connect(self._onAppInitialized) + self._application.initializationFinished.connect(self._onAppInitialized) @@ -156,7 +152,8 @@ class Toolbox(QObject, Extension): # This is a plugin, so most of the components required are not ready when # this is initialized. Therefore, we wait until the application is ready. def _onAppInitialized(self) -> None: - self._package_manager = Application.getInstance().getPackageManager() + self._plugin_registry = self._application.getPluginRegistry() + self._package_manager = self._application.getPackageManager() self._sdk_version = self._getSDKVersion() self._cloud_api_version = self._getCloudAPIVersion() self._cloud_api_root = self._getCloudAPIRoot() @@ -178,38 +175,38 @@ class Toolbox(QObject, Extension): def _getCloudAPIRoot(self) -> str: if not hasattr(cura, "CuraVersion"): return self.DEFAULT_CLOUD_API_ROOT - if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): + if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore return self.DEFAULT_CLOUD_API_ROOT - if not cura.CuraVersion.CuraCloudAPIRoot: + if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore return self.DEFAULT_CLOUD_API_ROOT - return cura.CuraVersion.CuraCloudAPIRoot + return cura.CuraVersion.CuraCloudAPIRoot # type: ignore # Get the cloud API version from CuraVersion def _getCloudAPIVersion(self) -> int: if not hasattr(cura, "CuraVersion"): return self.DEFAULT_CLOUD_API_VERSION - if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): + if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore return self.DEFAULT_CLOUD_API_VERSION - if not cura.CuraVersion.CuraCloudAPIVersion: + if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore return self.DEFAULT_CLOUD_API_VERSION - return cura.CuraVersion.CuraCloudAPIVersion + return cura.CuraVersion.CuraCloudAPIVersion # type: ignore # Get the packages version depending on Cura version settings. def _getSDKVersion(self) -> int: if not hasattr(cura, "CuraVersion"): return self._plugin_registry.APIVersion - if not hasattr(cura.CuraVersion, "CuraSDKVersion"): + if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore return self._plugin_registry.APIVersion - if not cura.CuraVersion.CuraSDKVersion: + if not cura.CuraVersion.CuraSDKVersion: # type: ignore return self._plugin_registry.APIVersion - return cura.CuraVersion.CuraSDKVersion + return cura.CuraVersion.CuraSDKVersion # type: ignore @pyqtSlot() def browsePackages(self) -> None: # Create the network manager: # This was formerly its own function but really had no reason to be as # it was never called more than once ever. - if self._network_manager: + if self._network_manager is not None: self._network_manager.finished.disconnect(self._onRequestFinished) self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccessibleChanged) self._network_manager = QNetworkAccessManager() @@ -235,11 +232,11 @@ class Toolbox(QObject, Extension): def _createDialog(self, qml_name: str) -> Optional[QObject]: Logger.log("d", "Toolbox: Creating dialog [%s].", qml_name) path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "resources", "qml", qml_name) - dialog = Application.getInstance().createQmlComponent(path, {"toolbox": self}) + dialog = self._application.createQmlComponent(path, {"toolbox": self}) return dialog - def _convertPluginMetadata(self, plugin: dict) -> dict: + def _convertPluginMetadata(self, plugin: Dict[str, Any]) -> Dict[str, Any]: formatted = { "package_id": plugin["id"], "package_type": "plugin", @@ -257,7 +254,6 @@ class Toolbox(QObject, Extension): @pyqtSlot() def _updateInstalledModels(self) -> None: - # This is moved here to avoid code duplication and so that after installing plugins they get removed from the # list of old plugins old_plugin_ids = self._plugin_registry.getInstalledPlugins() @@ -265,7 +261,7 @@ class Toolbox(QObject, Extension): scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs() self._old_plugin_ids = [] - self._old_plugin_metadata = [] + self._old_plugin_metadata = [] # type: List[Dict[str, Any]] for plugin_id in old_plugin_ids: # Neither the installed packages nor the packages that are scheduled to remove are old plugins @@ -353,8 +349,8 @@ class Toolbox(QObject, Extension): return self._restart_required @pyqtSlot() - def restart(self): - CuraApplication.getInstance().windowClosed() + def restart(self) -> None: + self._application.windowClosed() def getRemotePackage(self, package_id: str) -> Optional[Dict]: # TODO: make the lookup in a dict, not a loop. canUpdate is called for every item. @@ -432,7 +428,8 @@ class Toolbox(QObject, Extension): Logger.log("i", "Toolbox: Requesting %s metadata from server.", type) request = QNetworkRequest(self._request_urls[type]) request.setRawHeader(*self._request_header) - self._network_manager.get(request) + if self._network_manager: + self._network_manager.get(request) @pyqtSlot(str) def startDownload(self, url: str) -> None: @@ -441,15 +438,15 @@ class Toolbox(QObject, Extension): self._download_request = QNetworkRequest(url) if hasattr(QNetworkRequest, "FollowRedirectsAttribute"): # Patch for Qt 5.6-5.8 - self._download_request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) + cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) if hasattr(QNetworkRequest, "RedirectPolicyAttribute"): # Patch for Qt 5.9+ - self._download_request.setAttribute(QNetworkRequest.RedirectPolicyAttribute, True) - self._download_request.setRawHeader(*self._request_header) - self._download_reply = self._network_manager.get(self._download_request) + cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.RedirectPolicyAttribute, True) + cast(QNetworkRequest, self._download_request).setRawHeader(*self._request_header) + self._download_reply = cast(QNetworkAccessManager, self._network_manager).get(self._download_request) self.setDownloadProgress(0) self.setIsDownloading(True) - self._download_reply.downloadProgress.connect(self._onDownloadProgress) + cast(QNetworkReply, self._download_reply).downloadProgress.connect(self._onDownloadProgress) @pyqtSlot() def cancelDownload(self) -> None: @@ -475,7 +472,6 @@ class Toolbox(QObject, Extension): self.resetDownload() def _onRequestFinished(self, reply: QNetworkReply) -> None: - if reply.error() == QNetworkReply.TimeoutError: Logger.log("w", "Got a timeout.") self.setViewPage("errored") @@ -551,12 +547,12 @@ class Toolbox(QObject, Extension): self.setDownloadProgress(new_progress) if bytes_sent == bytes_total: self.setIsDownloading(False) - self._download_reply.downloadProgress.disconnect(self._onDownloadProgress) + cast(QNetworkReply, self._download_reply).downloadProgress.disconnect(self._onDownloadProgress) # Must not delete the temporary file on Windows self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False) file_path = self._temp_plugin_file.name # Write first and close, otherwise on Windows, it cannot read the file - self._temp_plugin_file.write(self._download_reply.readAll()) + self._temp_plugin_file.write(cast(QNetworkReply, self._download_reply).readAll()) self._temp_plugin_file.close() self._onDownloadComplete(file_path) @@ -577,13 +573,13 @@ class Toolbox(QObject, Extension): # Getter & Setters for Properties: # -------------------------------------------------------------------------- - def setDownloadProgress(self, progress: Union[int, float]) -> None: + def setDownloadProgress(self, progress: float) -> None: if progress != self._download_progress: self._download_progress = progress self.onDownloadProgressChanged.emit() @pyqtProperty(int, fset = setDownloadProgress, notify = onDownloadProgressChanged) - def downloadProgress(self) -> int: + def downloadProgress(self) -> float: return self._download_progress def setIsDownloading(self, is_downloading: bool) -> None: diff --git a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py index c54ced6b13..495bbe1315 100644 --- a/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/ClusterUM3OutputDevice.py @@ -1,10 +1,12 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import Any, cast, Set, Tuple, Union + +from UM.FileHandler.FileHandler import FileHandler from UM.FileHandler.FileWriter import FileWriter #To choose based on the output file mode (text vs. binary). from UM.FileHandler.WriteFileJob import WriteFileJob #To call the file writer asynchronously. from UM.Logger import Logger -from UM.Application import Application from UM.Settings.ContainerRegistry import ContainerRegistry from UM.i18n import i18nCatalog from UM.Message import Message @@ -13,6 +15,7 @@ from UM.OutputDevice import OutputDeviceError #To show that something went wrong from UM.Scene.SceneNode import SceneNode #For typing. from UM.Version import Version #To check against firmware versions for support. +from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel @@ -25,7 +28,7 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtGui import QDesktopServices from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject -from time import time, sleep +from time import time from datetime import datetime from typing import Optional, Dict, List @@ -44,15 +47,15 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions. clusterPrintersChanged = pyqtSignal() - def __init__(self, device_id, address, properties, parent = None): + def __init__(self, device_id, address, properties, parent = None) -> None: super().__init__(device_id = device_id, address = address, properties=properties, parent = parent) self._api_prefix = "/cluster-api/v1/" self._number_of_extruders = 2 - self._dummy_lambdas = set() + self._dummy_lambdas = ("", {}, io.BytesIO()) #type: Tuple[str, Dict, Union[io.StringIO, io.BytesIO]] - self._print_jobs = [] + self._print_jobs = [] # type: List[PrintJobOutputModel] self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml") self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml") @@ -60,18 +63,18 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # See comments about this hack with the clusterPrintersChanged signal self.printersChanged.connect(self.clusterPrintersChanged) - self._accepts_commands = True + self._accepts_commands = True #type: bool # Cluster does not have authentication, so default to authenticated self._authentication_state = AuthState.Authenticated - self._error_message = None - self._write_job_progress_message = None - self._progress_message = None + self._error_message = None #type: Optional[Message] + self._write_job_progress_message = None #type: Optional[Message] + self._progress_message = None #type: Optional[Message] self._active_printer = None # type: Optional[PrinterOutputModel] - self._printer_selection_dialog = None + self._printer_selection_dialog = None #type: QObject self.setPriority(3) # Make sure the output device gets selected above local file output self.setName(self._id) @@ -80,25 +83,25 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network")) - self._printer_uuid_to_unique_name_mapping = {} + self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str] - self._finished_jobs = [] + self._finished_jobs = [] # type: List[PrintJobOutputModel] self._cluster_size = int(properties.get(b"cluster_size", 0)) - self._latest_reply_handler = None + self._latest_reply_handler = None #type: Optional[QNetworkReply] - def requestWrite(self, nodes: List[SceneNode], file_name=None, filter_by_machine=False, file_handler=None, **kwargs): + def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: self.writeStarted.emit(self) #Formats supported by this application (file types that we can actually write). if file_handler: file_formats = file_handler.getSupportedFileTypesWrite() else: - file_formats = Application.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() + file_formats = CuraApplication.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() #Create a list from the supported file formats string. - machine_file_formats = Application.getInstance().getGlobalContainerStack().getMetaDataEntry("file_formats").split(";") + machine_file_formats = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("file_formats").split(";") machine_file_formats = [file_type.strip() for file_type in machine_file_formats] #Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. if "application/x-ufp" not in machine_file_formats and self.printerType == "ultimaker3" and Version(self.firmwareVersion) >= Version("4.4"): @@ -115,9 +118,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): #Just take the first file format available. if file_handler is not None: - writer = file_handler.getWriterByMimeType(preferred_format["mime_type"]) + writer = file_handler.getWriterByMimeType(cast(str, preferred_format["mime_type"])) else: - writer = Application.getInstance().getMeshFileHandler().getWriterByMimeType(preferred_format["mime_type"]) + writer = CuraApplication.getInstance().getMeshFileHandler().getWriterByMimeType(cast(str, preferred_format["mime_type"])) #This function pauses with the yield, waiting on instructions on which printer it needs to print with. self._sending_job = self._sendPrintJob(writer, preferred_format, nodes) @@ -133,7 +136,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): def _spawnPrinterSelectionDialog(self): if self._printer_selection_dialog is None: path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml") - self._printer_selection_dialog = Application.getInstance().createQmlComponent(path, {"OutputDevice": self}) + self._printer_selection_dialog = CuraApplication.getInstance().createQmlComponent(path, {"OutputDevice": self}) if self._printer_selection_dialog is not None: self._printer_selection_dialog.show() @@ -179,10 +182,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): target_printer = yield #Potentially wait on the user to select a target printer. # Using buffering greatly reduces the write time for many lines of gcode + + stream = io.BytesIO() # type: Union[io.BytesIO, io.StringIO]# Binary mode. if preferred_format["mode"] == FileWriter.OutputMode.TextMode: stream = io.StringIO() - else: #Binary mode. - stream = io.BytesIO() job = WriteFileJob(writer, stream, nodes, preferred_format["mode"]) @@ -198,9 +201,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): yield True #Return that we had success! yield #To prevent having to catch the StopIteration exception. - from cura.Utils.Threading import call_on_qt_thread - - def _sendPrintJobWaitOnWriteJobFinished(self, job): + def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None: self._write_job_progress_message.hide() self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1, @@ -221,40 +222,41 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # Add user name to the print_job parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain")) - file_name = Application.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"] + file_name = CuraApplication.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"] output = stream.getvalue() #Either str or bytes depending on the output mode. if isinstance(stream, io.StringIO): - output = output.encode("utf-8") + output = cast(str, output).encode("utf-8") + output = cast(bytes, output) parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output)) - self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, onFinished=self._onPostPrintJobFinished, onProgress=self._onUploadPrintJobProgress) + self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, on_finished = self._onPostPrintJobFinished, on_progress = self._onUploadPrintJobProgress) - @pyqtProperty(QObject, notify=activePrinterChanged) + @pyqtProperty(QObject, notify = activePrinterChanged) def activePrinter(self) -> Optional[PrinterOutputModel]: return self._active_printer @pyqtSlot(QObject) - def setActivePrinter(self, printer: Optional[PrinterOutputModel]): + def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None: if self._active_printer != printer: if self._active_printer and self._active_printer.camera: self._active_printer.camera.stop() self._active_printer = printer self.activePrinterChanged.emit() - def _onPostPrintJobFinished(self, reply): + def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None: self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False - def _onUploadPrintJobProgress(self, bytes_sent:int, bytes_total:int): + def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None: if bytes_total > 0: new_progress = bytes_sent / bytes_total * 100 # Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get # timeout responses if this happens. self._last_response_time = time() - if new_progress > self._progress_message.getProgress(): + if self._progress_message and new_progress > self._progress_message.getProgress(): self._progress_message.show() # Ensure that the message is visible. self._progress_message.setProgress(bytes_sent / bytes_total * 100) @@ -271,16 +273,18 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._success_message.actionTriggered.connect(self._successMessageActionTriggered) self._success_message.show() else: - self._progress_message.setProgress(0) - self._progress_message.hide() + if self._progress_message is not None: + self._progress_message.setProgress(0) + self._progress_message.hide() - def _progressMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None: + def _progressMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: if action_id == "Abort": Logger.log("d", "User aborted sending print to remote.") - self._progress_message.hide() + if self._progress_message is not None: + self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False - Application.getInstance().getController().setActiveStage("PrepareStage") + CuraApplication.getInstance().getController().setActiveStage("PrepareStage") # After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request # the "reply" should be disconnected @@ -288,9 +292,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._latest_reply_handler.disconnect() self._latest_reply_handler = None - def _successMessageActionTriggered(self, message_id: Optional[str]=None, action_id: Optional[str]=None) -> None: + def _successMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None: if action_id == "View": - Application.getInstance().getController().setActiveStage("MonitorStage") + CuraApplication.getInstance().getController().setActiveStage("MonitorStage") @pyqtSlot() def openPrintJobControlPanel(self) -> None: @@ -302,21 +306,21 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): Logger.log("d", "Opening printer control panel...") QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers")) - @pyqtProperty("QVariantList", notify=printJobsChanged) - def printJobs(self)-> List[PrintJobOutputModel] : + @pyqtProperty("QVariantList", notify = printJobsChanged) + def printJobs(self)-> List[PrintJobOutputModel]: return self._print_jobs - @pyqtProperty("QVariantList", notify=printJobsChanged) + @pyqtProperty("QVariantList", notify = printJobsChanged) def queuedPrintJobs(self) -> List[PrintJobOutputModel]: return [print_job for print_job in self._print_jobs if print_job.state == "queued"] - @pyqtProperty("QVariantList", notify=printJobsChanged) + @pyqtProperty("QVariantList", notify = printJobsChanged) def activePrintJobs(self) -> List[PrintJobOutputModel]: return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"] - @pyqtProperty("QVariantList", notify=clusterPrintersChanged) - def connectedPrintersTypeCount(self) -> List[PrinterOutputModel]: - printer_count = {} + @pyqtProperty("QVariantList", notify = clusterPrintersChanged) + def connectedPrintersTypeCount(self) -> List[Dict[str, str]]: + printer_count = {} # type: Dict[str, int] for printer in self._printers: if printer.type in printer_count: printer_count[printer.type] += 1 @@ -324,20 +328,20 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printer_count[printer.type] = 1 result = [] for machine_type in printer_count: - result.append({"machine_type": machine_type, "count": printer_count[machine_type]}) + result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])}) return result - @pyqtSlot(int, result=str) + @pyqtSlot(int, result = str) def formatDuration(self, seconds: int) -> str: return Duration(seconds).getDisplayString(DurationFormat.Format.Short) - @pyqtSlot(int, result=str) + @pyqtSlot(int, result = str) def getTimeCompleted(self, time_remaining: int) -> str: current_time = time() datetime_completed = datetime.fromtimestamp(current_time + time_remaining) return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute) - @pyqtSlot(int, result=str) + @pyqtSlot(int, result = str) def getDateCompleted(self, time_remaining: int) -> str: current_time = time() datetime_completed = datetime.fromtimestamp(current_time + time_remaining) @@ -367,10 +371,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): self._finished_jobs = finished_jobs def _update(self) -> None: - if not super()._update(): - return - self.get("printers/", onFinished=self._onGetPrintersDataFinished) - self.get("print_jobs/", onFinished=self._onGetPrintJobsFinished) + super()._update() + self.get("printers/", on_finished = self._onGetPrintersDataFinished) + self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished) def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None: if not checkValidGetReply(reply): @@ -409,7 +412,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen] for removed_job in removed_jobs: - job_list_changed |= self._removeJob(removed_job) + job_list_changed = job_list_changed or self._removeJob(removed_job) if job_list_changed: self.printJobsChanged.emit() # Do a single emit for all print job changes. @@ -443,27 +446,27 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): if removed_printers or printer_list_changed: self.printersChanged.emit() - def _createPrinterModel(self, data: Dict) -> PrinterOutputModel: - printer = PrinterOutputModel(output_controller=ClusterUM3PrinterOutputController(self), - number_of_extruders=self._number_of_extruders) + def _createPrinterModel(self, data: Dict[str, Any]) -> PrinterOutputModel: + printer = PrinterOutputModel(output_controller = ClusterUM3PrinterOutputController(self), + number_of_extruders = self._number_of_extruders) printer.setCamera(NetworkCamera("http://" + data["ip_address"] + ":8080/?action=stream")) self._printers.append(printer) return printer - def _createPrintJobModel(self, data: Dict) -> PrintJobOutputModel: + def _createPrintJobModel(self, data: Dict[str, Any]) -> PrintJobOutputModel: print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self), key=data["uuid"], name= data["name"]) print_job.stateChanged.connect(self._printJobStateChanged) self._print_jobs.append(print_job) return print_job - def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict) -> None: + def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict[str, Any]) -> None: print_job.updateTimeTotal(data["time_total"]) print_job.updateTimeElapsed(data["time_elapsed"]) print_job.updateState(data["status"]) print_job.updateOwner(data["owner"]) - def _updatePrinter(self, printer: PrinterOutputModel, data: Dict) -> None: + def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None: # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer. # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping. self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"] @@ -479,7 +482,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): printer.updateKey(data["uuid"]) printer.updateType(data["machine_variant"]) - # Do not store the buildplate information that comes from connect if the current printer has not buildplate information + # Do not store the build plate information that comes from connect if the current printer has not build plate information if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False): printer.updateBuildplateName(data["build_plate"]["type"]) if not data["enabled"]: @@ -518,7 +521,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): brand=brand, color=color, name=name) extruder.updateActiveMaterial(material) - def _removeJob(self, job: PrintJobOutputModel): + def _removeJob(self, job: PrintJobOutputModel) -> bool: if job not in self._print_jobs: return False @@ -529,23 +532,23 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): return True - def _removePrinter(self, printer: PrinterOutputModel): + def _removePrinter(self, printer: PrinterOutputModel) -> None: self._printers.remove(printer) if self._active_printer == printer: self._active_printer = None self.activePrinterChanged.emit() -def loadJsonFromReply(reply): +def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]: try: result = json.loads(bytes(reply.readAll()).decode("utf-8")) except json.decoder.JSONDecodeError: Logger.logException("w", "Unable to decode JSON from reply.") - return + return None return result -def checkValidGetReply(reply): +def checkValidGetReply(reply: QNetworkReply) -> bool: status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code != 200: @@ -554,7 +557,8 @@ def checkValidGetReply(reply): return True -def findByKey(list, key): +def findByKey(list: List[Union[PrintJobOutputModel, PrinterOutputModel]], key: str) -> Optional[PrintJobOutputModel]: for item in list: if item.key == key: - return item \ No newline at end of file + return item + return None \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py index 9b25de7f42..c51092ed98 100644 --- a/plugins/UM3NetworkPrinting/DiscoverUM3Action.py +++ b/plugins/UM3NetworkPrinting/DiscoverUM3Action.py @@ -1,43 +1,47 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + import os.path import time +from typing import Optional from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject -from UM.Application import Application from UM.PluginRegistry import PluginRegistry from UM.Logger import Logger from UM.i18n import i18nCatalog +from cura.CuraApplication import CuraApplication from cura.MachineAction import MachineAction +from .UM3OutputDevicePlugin import UM3OutputDevicePlugin + catalog = i18nCatalog("cura") class DiscoverUM3Action(MachineAction): discoveredDevicesChanged = pyqtSignal() - def __init__(self): + def __init__(self) -> None: super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network")) self._qml_url = "DiscoverUM3Action.qml" - self._network_plugin = None + self._network_plugin = None #type: Optional[UM3OutputDevicePlugin] - self.__additional_components_context = None - self.__additional_component = None - self.__additional_components_view = None + self.__additional_components_view = None #type: Optional[QObject] - Application.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) + CuraApplication.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView) - self._last_zero_conf_event_time = time.time() + self._last_zero_conf_event_time = time.time() #type: float # Time to wait after a zero-conf service change before allowing a zeroconf reset - self._zero_conf_change_grace_period = 0.25 + self._zero_conf_change_grace_period = 0.25 #type: float @pyqtSlot() def startDiscovery(self): if not self._network_plugin: Logger.log("d", "Starting device discovery.") - self._network_plugin = Application.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting") + self._network_plugin = CuraApplication.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting") self._network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged) self.discoveredDevicesChanged.emit() @@ -93,16 +97,16 @@ class DiscoverUM3Action(MachineAction): return [] @pyqtSlot(str) - def setGroupName(self, group_name): + def setGroupName(self, group_name: str) -> None: Logger.log("d", "Attempting to set the group name of the active machine to %s", group_name) - global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() if global_container_stack: meta_data = global_container_stack.getMetaData() if "connect_group_name" in meta_data: previous_connect_group_name = meta_data["connect_group_name"] global_container_stack.setMetaDataEntry("connect_group_name", group_name) # Find all the places where there is the same group name and change it accordingly - Application.getInstance().getMachineManager().replaceContainersMetadata(key = "connect_group_name", value = previous_connect_group_name, new_value = group_name) + CuraApplication.getInstance().getMachineManager().replaceContainersMetadata(key = "connect_group_name", value = previous_connect_group_name, new_value = group_name) else: global_container_stack.addMetaDataEntry("connect_group_name", group_name) global_container_stack.addMetaDataEntry("hidden", False) @@ -112,9 +116,9 @@ class DiscoverUM3Action(MachineAction): self._network_plugin.reCheckConnections() @pyqtSlot(str) - def setKey(self, key): + def setKey(self, key: str) -> None: Logger.log("d", "Attempting to set the network key of the active machine to %s", key) - global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() if global_container_stack: meta_data = global_container_stack.getMetaData() if "um_network_key" in meta_data: @@ -124,7 +128,7 @@ class DiscoverUM3Action(MachineAction): Logger.log("d", "Removing old authentication id %s for device %s", global_container_stack.getMetaDataEntry("network_authentication_id", None), key) global_container_stack.removeMetaDataEntry("network_authentication_id") global_container_stack.removeMetaDataEntry("network_authentication_key") - Application.getInstance().getMachineManager().replaceContainersMetadata(key = "um_network_key", value = previous_network_key, new_value = key) + CuraApplication.getInstance().getMachineManager().replaceContainersMetadata(key = "um_network_key", value = previous_network_key, new_value = key) else: global_container_stack.addMetaDataEntry("um_network_key", key) @@ -134,7 +138,7 @@ class DiscoverUM3Action(MachineAction): @pyqtSlot(result = str) def getStoredKey(self) -> str: - global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() if global_container_stack: meta_data = global_container_stack.getMetaData() if "um_network_key" in meta_data: @@ -149,12 +153,12 @@ class DiscoverUM3Action(MachineAction): return "" @pyqtSlot(str, result = bool) - def existsKey(self, key) -> bool: - return Application.getInstance().getMachineManager().existNetworkInstances(network_key = key) + def existsKey(self, key: str) -> bool: + return CuraApplication.getInstance().getMachineManager().existNetworkInstances(network_key = key) @pyqtSlot() - def loadConfigurationFromPrinter(self): - machine_manager = Application.getInstance().getMachineManager() + def loadConfigurationFromPrinter(self) -> None: + machine_manager = CuraApplication.getInstance().getMachineManager() hotend_ids = machine_manager.printerOutputDevices[0].hotendIds for index in range(len(hotend_ids)): machine_manager.printerOutputDevices[0].hotendIdChanged.emit(index, hotend_ids[index]) @@ -162,16 +166,16 @@ class DiscoverUM3Action(MachineAction): for index in range(len(material_ids)): machine_manager.printerOutputDevices[0].materialIdChanged.emit(index, material_ids[index]) - def _createAdditionalComponentsView(self): + def _createAdditionalComponentsView(self) -> None: Logger.log("d", "Creating additional ui components for UM3.") # Create networking dialog path = os.path.join(PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"), "UM3InfoComponents.qml") - self.__additional_components_view = Application.getInstance().createQmlComponent(path, {"manager": self}) + self.__additional_components_view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) if not self.__additional_components_view: Logger.log("w", "Could not create ui components for UM3.") return # Create extra components - Application.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton")) - Application.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo")) + CuraApplication.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton")) + CuraApplication.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo")) diff --git a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py index 42f00beceb..9b90f8542d 100644 --- a/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/LegacyUM3OutputDevice.py @@ -1,3 +1,8 @@ +from typing import List, Optional + +from UM.FileHandler.FileHandler import FileHandler +from UM.Scene.SceneNode import SceneNode +from cura.CuraApplication import CuraApplication from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel @@ -9,12 +14,11 @@ from cura.Settings.ExtruderManager import ExtruderManager from UM.Logger import Logger from UM.Settings.ContainerRegistry import ContainerRegistry -from UM.Application import Application from UM.i18n import i18nCatalog from UM.Message import Message from PyQt5.QtNetwork import QNetworkRequest -from PyQt5.QtCore import QTimer, QCoreApplication +from PyQt5.QtCore import QTimer from PyQt5.QtWidgets import QMessageBox from .LegacyUM3PrinterOutputController import LegacyUM3PrinterOutputController @@ -39,7 +43,7 @@ i18n_catalog = i18nCatalog("cura") # 4. At this point the machine either has the state Authenticated or AuthenticationDenied. # 5. As a final step, we verify the authentication, as this forces the QT manager to setup the authenticator. class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): - def __init__(self, device_id, address: str, properties, parent = None): + def __init__(self, device_id, address: str, properties, parent = None) -> None: super().__init__(device_id = device_id, address = address, properties = properties, parent = parent) self._api_prefix = "/api/v1/" self._number_of_extruders = 2 @@ -125,7 +129,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): def connect(self): super().connect() self._setupMessages() - global_container = Application.getInstance().getGlobalContainerStack() + global_container = CuraApplication.getInstance().getGlobalContainerStack() if global_container: self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None) self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None) @@ -168,7 +172,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): # NotImplementedError. We can simply ignore these. pass - def requestWrite(self, nodes, file_name=None, filter_by_machine=False, file_handler=None, **kwargs): + def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: if not self.activePrinter: # No active printer. Unable to write return @@ -183,8 +187,8 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self.writeStarted.emit(self) - gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict", []) - active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate + gcode_dict = getattr(CuraApplication.getInstance().getController().getScene(), "gcode_dict", []) + active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate gcode_list = gcode_dict[active_build_plate_id] if not gcode_list: @@ -203,7 +207,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): for error in errors: detailed_text += error + "\n" - Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), + CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), text, informative_text, detailed_text, @@ -225,7 +229,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): for warning in warnings: detailed_text += warning + "\n" - Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), + CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"), text, informative_text, detailed_text, @@ -239,7 +243,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._startPrint() # Notify the UI that a switch to the print monitor should happen - Application.getInstance().getController().setActiveStage("MonitorStage") + CuraApplication.getInstance().getController().setActiveStage("MonitorStage") def _startPrint(self): Logger.log("i", "Sending print job to printer.") @@ -264,7 +268,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): # Abort was called. return - file_name = "%s.gcode.gz" % Application.getInstance().getPrintInformation().jobName + file_name = "%s.gcode.gz" % CuraApplication.getInstance().getPrintInformation().jobName self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode, onFinished=self._onPostPrintJobFinished) @@ -276,7 +280,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._progress_message.hide() self._compressing_gcode = False self._sending_gcode = False - Application.getInstance().getController().setActiveStage("PrepareStage") + CuraApplication.getInstance().getController().setActiveStage("PrepareStage") def _onPostPrintJobFinished(self, reply): self._progress_message.hide() @@ -301,7 +305,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): if button == QMessageBox.Yes: self._startPrint() else: - Application.getInstance().getController().setActiveStage("PrepareStage") + CuraApplication.getInstance().getController().setActiveStage("PrepareStage") # For some unknown reason Cura on OSX will hang if we do the call back code # immediately without first returning and leaving QML's event system. @@ -309,7 +313,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): def _checkForErrors(self): errors = [] - print_information = Application.getInstance().getPrintInformation() + print_information = CuraApplication.getInstance().getPrintInformation() if not print_information.materialLengths: Logger.log("w", "There is no material length information. Unable to check for errors.") return errors @@ -329,7 +333,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): def _checkForWarnings(self): warnings = [] - print_information = Application.getInstance().getPrintInformation() + print_information = CuraApplication.getInstance().getPrintInformation() if not print_information.materialLengths: Logger.log("w", "There is no material length information. Unable to check for warnings.") @@ -452,7 +456,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._authentication_failed_message.show() def _saveAuthentication(self): - global_container_stack = Application.getInstance().getGlobalContainerStack() + global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() if global_container_stack: if "network_authentication_key" in global_container_stack.getMetaData(): global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key) @@ -465,7 +469,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): global_container_stack.addMetaDataEntry("network_authentication_id", self._authentication_id) # Force save so we are sure the data is not lost. - Application.getInstance().saveStack(global_container_stack) + CuraApplication.getInstance().saveStack(global_container_stack) Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id, self._getSafeAuthKey()) else: @@ -496,7 +500,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): self._authentication_id = None self.post("auth/request", - json.dumps({"application": "Cura-" + Application.getInstance().getVersion(), + json.dumps({"application": "Cura-" + CuraApplication.getInstance().getVersion(), "user": self._getUserName()}).encode(), onFinished=self._onRequestAuthenticationFinished) @@ -542,7 +546,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): "Got status code {status_code} while trying to get printer data".format(status_code=status_code)) def materialHotendChangedMessage(self, callback): - Application.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"), + CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"), i18n_catalog.i18nc("@label", "Would you like to use your current printer configuration in Cura?"), i18n_catalog.i18nc("@label", diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 00eb2f0b25..69a0ecb40c 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -3,10 +3,10 @@ from UM.Logger import Logger from UM.i18n import i18nCatalog -from UM.Application import Application from UM.Qt.Duration import DurationFormat from UM.PluginRegistry import PluginRegistry +from cura.CuraApplication import CuraApplication from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel @@ -22,7 +22,7 @@ from threading import Thread, Event from time import time, sleep from queue import Queue from enum import IntEnum -from typing import Union, Optional, List +from typing import Union, Optional, List, cast import re import functools # Used for reduce @@ -35,7 +35,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): firmwareProgressChanged = pyqtSignal() firmwareUpdateStateChanged = pyqtSignal() - def __init__(self, serial_port: str, baud_rate: Optional[int] = None): + def __init__(self, serial_port: str, baud_rate: Optional[int] = None) -> None: super().__init__(serial_port) self.setName(catalog.i18nc("@item:inmenu", "USB printing")) self.setShortDescription(catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print via USB")) @@ -68,7 +68,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._is_printing = False # A print is being sent. ## Set when print is started in order to check running time. - self._print_start_time = None # type: Optional[int] + self._print_start_time = None # type: Optional[float] self._print_estimated_time = None # type: Optional[int] self._accepts_commands = True @@ -83,7 +83,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self.setConnectionText(catalog.i18nc("@info:status", "Connected via USB")) # Queue for commands that need to be sent. - self._command_queue = Queue() + self._command_queue = Queue() # type: Queue # Event to indicate that an "ok" was received from the printer after sending a command. self._command_received = Event() self._command_received.set() @@ -107,11 +107,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice): # cancel any ongoing preheat timer before starting a print self._printers[0].getController().stopPreheatTimers() - Application.getInstance().getController().setActiveStage("MonitorStage") + CuraApplication.getInstance().getController().setActiveStage("MonitorStage") # find the G-code for the active build plate to print - active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate - gcode_dict = getattr(Application.getInstance().getController().getScene(), "gcode_dict") + active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate + gcode_dict = getattr(CuraApplication.getInstance().getController().getScene(), "gcode_dict") gcode_list = gcode_dict[active_build_plate_id] self._printGCode(gcode_list) @@ -121,7 +121,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): def showFirmwareInterface(self): if self._firmware_view is None: path = os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml") - self._firmware_view = Application.getInstance().createQmlComponent(path, {"manager": self}) + self._firmware_view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) self._firmware_view.show() @@ -180,7 +180,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self.setFirmwareUpdateState(FirmwareUpdateState.completed) # Try to re-connect with the machine again, which must be done on the Qt thread, so we use call later. - Application.getInstance().callLater(self.connect) + CuraApplication.getInstance().callLater(self.connect) @pyqtProperty(float, notify = firmwareProgressChanged) def firmwareProgress(self): @@ -214,7 +214,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._gcode_position = 0 self._print_start_time = time() - self._print_estimated_time = int(Application.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds)) + self._print_estimated_time = int(CuraApplication.getInstance().getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.Seconds)) for i in range(0, 4): # Push first 4 entries before accepting other inputs self._sendNextGcodeLine() @@ -250,7 +250,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): except SerialException: Logger.log("w", "An exception occured while trying to create serial connection") return - container_stack = Application.getInstance().getGlobalContainerStack() + container_stack = CuraApplication.getInstance().getGlobalContainerStack() num_extruders = container_stack.getProperty("machine_extruder_count", "value") # Ensure that a printer is created. self._printers = [PrinterOutputModel(output_controller=GenericOutputController(self), number_of_extruders=num_extruders)] @@ -277,13 +277,12 @@ class USBPrinterOutputDevice(PrinterOutputDevice): if self._serial is None or self._connection_state != ConnectionState.connected: return - if type(command == str): - command = command.encode() - if not command.endswith(b"\n"): - command += b"\n" + new_command = cast(bytes, command) if type(command) is bytes else cast(str, command).encode() # type: bytes + if not new_command.endswith(b"\n"): + new_command += b"\n" try: self._command_received.clear() - self._serial.write(command) + self._serial.write(new_command) except SerialTimeoutException: Logger.log("w", "Timeout when sending command to printer via USB.") self._command_received.set() diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index abf3b9ece2..2ee85187ee 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -179,7 +179,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): return list(base_list) - __instance = None + __instance = None # type: USBPrinterOutputDeviceManager @classmethod def getInstance(cls, *args, **kwargs) -> "USBPrinterOutputDeviceManager": diff --git a/plugins/VersionUpgrade/VersionUpgrade21to22/Profile.py b/plugins/VersionUpgrade/VersionUpgrade21to22/Profile.py index becf29c242..161edcb67c 100644 --- a/plugins/VersionUpgrade/VersionUpgrade21to22/Profile.py +++ b/plugins/VersionUpgrade/VersionUpgrade21to22/Profile.py @@ -44,20 +44,18 @@ class Profile: # Parse the general section. self._name = parser.get("general", "name") - self._type = parser.get("general", "type", fallback = None) + self._type = parser.get("general", "type") + self._weight = None if "weight" in parser["general"]: self._weight = int(parser.get("general", "weight")) - else: - self._weight = None - self._machine_type_id = parser.get("general", "machine_type", fallback = None) - self._machine_variant_name = parser.get("general", "machine_variant", fallback = None) - self._machine_instance_name = parser.get("general", "machine_instance", fallback = None) + self._machine_type_id = parser.get("general", "machine_type") + self._machine_variant_name = parser.get("general", "machine_variant") + self._machine_instance_name = parser.get("general", "machine_instance") + self._material_name = None if "material" in parser["general"]: #Note: Material name is unused in this upgrade. self._material_name = parser.get("general", "material") elif self._type == "material": - self._material_name = parser.get("general", "name", fallback = None) - else: - self._material_name = None + self._material_name = parser.get("general", "name") # Parse the settings. self._settings = {} # type: Dict[str,str] diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 9d456c833d..eafb504deb 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -6,8 +6,10 @@ import io import json #To parse the product-to-id mapping file. import os.path #To find the product-to-id mapping. import sys -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, cast import xml.etree.ElementTree as ET +from typing import Dict +from typing import Iterator from UM.Resources import Resources from UM.Logger import Logger @@ -132,7 +134,7 @@ class XmlMaterialProfile(InstanceContainer): "version": self.CurrentFdmMaterialVersion}) ## Begin Metadata Block - builder.start("metadata") + builder.start("metadata") # type: ignore metadata = copy.deepcopy(self.getMetaData()) # setting_version is derived from the "version" tag in the schema, so don't serialize it into a file @@ -156,21 +158,21 @@ class XmlMaterialProfile(InstanceContainer): metadata.pop("name", "") ## Begin Name Block - builder.start("name") + builder.start("name") # type: ignore - builder.start("brand") + builder.start("brand") # type: ignore builder.data(metadata.pop("brand", "")) builder.end("brand") - builder.start("material") + builder.start("material") # type: ignore builder.data(metadata.pop("material", "")) builder.end("material") - builder.start("color") + builder.start("color") # type: ignore builder.data(metadata.pop("color_name", "")) builder.end("color") - builder.start("label") + builder.start("label") # type: ignore builder.data(self.getName()) builder.end("label") @@ -178,7 +180,7 @@ class XmlMaterialProfile(InstanceContainer): ## End Name Block for key, value in metadata.items(): - builder.start(key) + builder.start(key) # type: ignore if value is not None: #Nones get handled well by the builder. #Otherwise the builder always expects a string. #Deserialize expects the stringified version. @@ -190,10 +192,10 @@ class XmlMaterialProfile(InstanceContainer): ## End Metadata Block ## Begin Properties Block - builder.start("properties") + builder.start("properties") # type: ignore for key, value in properties.items(): - builder.start(key) + builder.start(key) # type: ignore builder.data(value) builder.end(key) @@ -201,14 +203,14 @@ class XmlMaterialProfile(InstanceContainer): ## End Properties Block ## Begin Settings Block - builder.start("settings") + builder.start("settings") # type: ignore if self.getMetaDataEntry("definition") == "fdmprinter": for instance in self.findInstances(): self._addSettingElement(builder, instance) - machine_container_map = {} - machine_variant_map = {} + machine_container_map = {} # type: Dict[str, InstanceContainer] + machine_variant_map = {} # type: Dict[str, Dict[str, Any]] variant_manager = CuraApplication.getInstance().getVariantManager() @@ -248,7 +250,7 @@ class XmlMaterialProfile(InstanceContainer): product = product_name break - builder.start("machine") + builder.start("machine") # type: ignore builder.start("machine_identifier", { "manufacturer": container.getMetaDataEntry("machine_manufacturer", definition_metadata.get("manufacturer", "Unknown")), @@ -264,7 +266,7 @@ class XmlMaterialProfile(InstanceContainer): self._addSettingElement(builder, instance) # Find all hotend sub-profiles corresponding to this material and machine and add them to this profile. - buildplate_dict = {} + buildplate_dict = {} # type: Dict[str, Any] for variant_name, variant_dict in machine_variant_map[definition_id].items(): variant_type = variant_dict["variant_node"].metadata["hardware_type"] from cura.Machines.VariantManager import VariantType @@ -812,11 +814,14 @@ class XmlMaterialProfile(InstanceContainer): if label is not None and label.text is not None: base_metadata["name"] = label.text else: - base_metadata["name"] = cls._profile_name(material.text, color.text) + if material is not None and color is not None: + base_metadata["name"] = cls._profile_name(material.text, color.text) + else: + base_metadata["name"] = "Unknown Material" - base_metadata["brand"] = brand.text if brand.text is not None else "Unknown Brand" - base_metadata["material"] = material.text if material.text is not None else "Unknown Type" - base_metadata["color_name"] = color.text if color.text is not None else "Unknown Color" + base_metadata["brand"] = brand.text if brand is not None and brand.text is not None else "Unknown Brand" + base_metadata["material"] = material.text if material is not None and material.text is not None else "Unknown Type" + base_metadata["color_name"] = color.text if color is not None and color.text is not None else "Unknown Color" continue #Setting_version is derived from the "version" tag in the schema earlier, so don't set it here. @@ -836,13 +841,13 @@ class XmlMaterialProfile(InstanceContainer): tag_name = _tag_without_namespace(entry) property_values[tag_name] = entry.text - base_metadata["approximate_diameter"] = str(round(float(property_values.get("diameter", 2.85)))) # In mm + base_metadata["approximate_diameter"] = str(round(float(cast(float, property_values.get("diameter", 2.85))))) # In mm base_metadata["properties"] = property_values base_metadata["definition"] = "fdmprinter" compatible_entries = data.iterfind("./um:settings/um:setting[@key='hardware compatible']", cls.__namespaces) try: - common_compatibility = cls._parseCompatibleValue(next(compatible_entries).text) + common_compatibility = cls._parseCompatibleValue(next(compatible_entries).text) # type: ignore except StopIteration: #No 'hardware compatible' setting. common_compatibility = True base_metadata["compatible"] = common_compatibility @@ -856,7 +861,8 @@ class XmlMaterialProfile(InstanceContainer): for entry in machine.iterfind("./um:setting", cls.__namespaces): key = entry.get("key") if key == "hardware compatible": - machine_compatibility = cls._parseCompatibleValue(entry.text) + if entry.text is not None: + machine_compatibility = cls._parseCompatibleValue(entry.text) for identifier in machine.iterfind("./um:machine_identifier", cls.__namespaces): machine_id_list = product_id_map.get(identifier.get("product"), []) @@ -864,11 +870,11 @@ class XmlMaterialProfile(InstanceContainer): machine_id_list = cls.getPossibleDefinitionIDsFromName(identifier.get("product")) for machine_id in machine_id_list: - definition_metadata = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id) - if not definition_metadata: + definition_metadatas = ContainerRegistry.getInstance().findDefinitionContainersMetadata(id = machine_id) + if not definition_metadatas: continue - definition_metadata = definition_metadata[0] + definition_metadata = definition_metadatas[0] machine_manufacturer = identifier.get("manufacturer", definition_metadata.get("manufacturer", "Unknown")) #If the XML material doesn't specify a manufacturer, use the one in the actual printer definition. @@ -891,7 +897,7 @@ class XmlMaterialProfile(InstanceContainer): result_metadata.append(new_material_metadata) buildplates = machine.iterfind("./um:buildplate", cls.__namespaces) - buildplate_map = {} + buildplate_map = {} # type: Dict[str, Dict[str, bool]] buildplate_map["buildplate_compatible"] = {} buildplate_map["buildplate_recommended"] = {} for buildplate in buildplates: @@ -912,10 +918,11 @@ class XmlMaterialProfile(InstanceContainer): buildplate_recommended = True for entry in settings: key = entry.get("key") - if key == "hardware compatible": - buildplate_compatibility = cls._parseCompatibleValue(entry.text) - elif key == "hardware recommended": - buildplate_recommended = cls._parseCompatibleValue(entry.text) + if entry.text is not None: + if key == "hardware compatible": + buildplate_compatibility = cls._parseCompatibleValue(entry.text) + elif key == "hardware recommended": + buildplate_recommended = cls._parseCompatibleValue(entry.text) buildplate_map["buildplate_compatible"][buildplate_id] = buildplate_compatibility buildplate_map["buildplate_recommended"][buildplate_id] = buildplate_recommended @@ -929,7 +936,8 @@ class XmlMaterialProfile(InstanceContainer): for entry in hotend.iterfind("./um:setting", cls.__namespaces): key = entry.get("key") if key == "hardware compatible": - hotend_compatibility = cls._parseCompatibleValue(entry.text) + if entry.text is not None: + hotend_compatibility = cls._parseCompatibleValue(entry.text) new_hotend_specific_material_id = container_id + "_" + machine_id + "_" + hotend_name.replace(" ", "_") diff --git a/run_mypy.py b/run_mypy.py index a72c555b8a..2a2c72dbbe 100644 --- a/run_mypy.py +++ b/run_mypy.py @@ -44,7 +44,7 @@ def main(): for mod in mods: print("------------- Checking module {mod}".format(**locals())) - result = subprocess.run([sys.executable, mypyModule, "-p", mod]) + result = subprocess.run([sys.executable, mypyModule, "-p", mod, "--ignore-missing-imports"]) if result.returncode != 0: print("\nModule {mod} failed checking. :(".format(**locals())) return 1 diff --git a/tests/Settings/TestExtruderStack.py b/tests/Settings/TestExtruderStack.py index ce829da4b3..b418ae4306 100644 --- a/tests/Settings/TestExtruderStack.py +++ b/tests/Settings/TestExtruderStack.py @@ -340,26 +340,4 @@ def test_setPropertyUser(key, property, value, extruder_stack): extruder_stack.setProperty(key, property, value) #The actual test. - extruder_stack.userChanges.setProperty.assert_called_once_with(key, property, value) #Make sure that the user container gets a setProperty call. - -## Tests setting properties on specific containers on the global stack. -@pytest.mark.parametrize("target_container, stack_variable", [ - ("user", "userChanges"), - ("quality_changes", "qualityChanges"), - ("quality", "quality"), - ("material", "material"), - ("variant", "variant") -]) -def test_setPropertyOtherContainers(target_container, stack_variable, extruder_stack): - #Other parameters that don't need to be varied. - key = "layer_height" - property = "value" - value = 0.1337 - #A mock container in the right spot. - container = unittest.mock.MagicMock() - container.getMetaDataEntry = unittest.mock.MagicMock(return_value = target_container) - setattr(extruder_stack, stack_variable, container) #For instance, set global_stack.qualityChanges = container. - - extruder_stack.setProperty(key, property, value, target_container = target_container) #The actual test. - - getattr(extruder_stack, stack_variable).setProperty.assert_called_once_with(key, property, value) #Make sure that the proper container gets a setProperty call. + extruder_stack.userChanges.setProperty.assert_called_once_with(key, property, value) #Make sure that the user container gets a setProperty call. \ No newline at end of file diff --git a/tests/Settings/TestGlobalStack.py b/tests/Settings/TestGlobalStack.py index 6bf10dd8c1..05c7cf1677 100755 --- a/tests/Settings/TestGlobalStack.py +++ b/tests/Settings/TestGlobalStack.py @@ -481,27 +481,4 @@ def test_setPropertyUser(key, property, value, global_stack): global_stack.setProperty(key, property, value) #The actual test. - global_stack.userChanges.setProperty.assert_called_once_with(key, property, value) #Make sure that the user container gets a setProperty call. - -## Tests setting properties on specific containers on the global stack. -@pytest.mark.parametrize("target_container, stack_variable", [ - ("user", "userChanges"), - ("quality_changes", "qualityChanges"), - ("quality", "quality"), - ("material", "material"), - ("variant", "variant"), - ("definition_changes", "definitionChanges") -]) -def test_setPropertyOtherContainers(target_container, stack_variable, global_stack): - #Other parameters that don't need to be varied. - key = "layer_height" - property = "value" - value = 0.1337 - #A mock container in the right spot. - container = unittest.mock.MagicMock() - container.getMetaDataEntry = unittest.mock.MagicMock(return_value = target_container) - setattr(global_stack, stack_variable, container) #For instance, set global_stack.qualityChanges = container. - - global_stack.setProperty(key, property, value, target_container = target_container) #The actual test. - - getattr(global_stack, stack_variable).setProperty.assert_called_once_with(key, property, value) #Make sure that the proper container gets a setProperty call. + global_stack.userChanges.setProperty.assert_called_once_with(key, property, value) #Make sure that the user container gets a setProperty call. \ No newline at end of file