From 6aedab78dc2e8c55fe6323ff3647203d8b994509 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Tue, 21 Apr 2020 16:58:45 +0200 Subject: [PATCH 01/27] Converted comments in dir Cura/cura to rst style Converted doxygen style comments to reStructuredText style in the files found in Cura/cura directory using the script dox_2_rst.py (provided in the Uranium repo). Comments were manually checked and changed if needed. --- cura/BuildVolume.py | 140 +++++++++++++------------ cura/CrashHandler.py | 6 +- cura/CuraActions.py | 25 +++-- cura/CuraApplication.py | 147 +++++++++++++++++---------- cura/CuraPackageManager.py | 12 ++- cura/LayerData.py | 7 +- cura/LayerDataBuilder.py | 13 ++- cura/LayerDataDecorator.py | 3 +- cura/LayerPolygon.py | 53 ++++++---- cura/MachineAction.py | 50 ++++++--- cura/OneAtATimeIterator.py | 51 +++++++--- cura/PickingPass.py | 28 +++-- cura/PreviewPass.py | 24 +++-- cura/PrintJobPreviewImageProvider.py | 8 +- cura/Snapshot.py | 12 ++- 15 files changed, 371 insertions(+), 208 deletions(-) diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 36edd427f7..76316f5e43 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -44,8 +44,9 @@ catalog = i18nCatalog("cura") PRIME_CLEARANCE = 6.5 -## Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas. class BuildVolume(SceneNode): + """Build volume is a special kind of node that is responsible for rendering the printable area & disallowed areas.""" + raftThicknessChanged = Signal() def __init__(self, application: "CuraApplication", parent: Optional[SceneNode] = None) -> None: @@ -113,7 +114,7 @@ class BuildVolume(SceneNode): self._has_errors = False self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged) - #Objects loaded at the moment. We are connected to the property changed events of these objects. + # Objects loaded at the moment. We are connected to the property changed events of these objects. self._scene_objects = set() # type: Set[SceneNode] self._scene_change_timer = QTimer() @@ -163,10 +164,12 @@ class BuildVolume(SceneNode): self._scene_objects = new_scene_objects self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered. - ## Updates the listeners that listen for changes in per-mesh stacks. - # - # \param node The node for which the decorators changed. def _updateNodeListeners(self, node: SceneNode): + """Updates the listeners that listen for changes in per-mesh stacks. + + :param node: The node for which the decorators changed. + """ + per_mesh_stack = node.callDecoration("getStack") if per_mesh_stack: per_mesh_stack.propertyChanged.connect(self._onSettingPropertyChanged) @@ -187,10 +190,14 @@ class BuildVolume(SceneNode): if shape: self._shape = shape - ## Get the length of the 3D diagonal through the build volume. - # - # This gives a sense of the scale of the build volume in general. def getDiagonalSize(self) -> float: + """Get the length of the 3D diagonal through the build volume. + + This gives a sense of the scale of the build volume in general. + + :return: length of the 3D diagonal through the build volume + """ + return math.sqrt(self._width * self._width + self._height * self._height + self._depth * self._depth) def getDisallowedAreas(self) -> List[Polygon]: @@ -226,9 +233,9 @@ class BuildVolume(SceneNode): return True - ## For every sliceable node, update node._outside_buildarea - # def updateNodeBoundaryCheck(self): + """For every sliceable node, update node._outside_buildarea""" + if not self._global_container_stack: return @@ -295,8 +302,13 @@ class BuildVolume(SceneNode): for child_node in children: child_node.setOutsideBuildArea(group_node.isOutsideBuildArea()) - ## Update the outsideBuildArea of a single node, given bounds or current build volume def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None) -> None: + """Update the outsideBuildArea of a single node, given bounds or current build volume + + :param node: single node + :param bounds: bounds or current build volume + """ + if not isinstance(node, CuraSceneNode) or self._global_container_stack is None: return @@ -484,8 +496,9 @@ class BuildVolume(SceneNode): self._disallowed_area_size = max(size, self._disallowed_area_size) return mb.build() - ## Recalculates the build volume & disallowed areas. def rebuild(self) -> None: + """Recalculates the build volume & disallowed areas.""" + if not self._width or not self._height or not self._depth: return @@ -586,8 +599,9 @@ class BuildVolume(SceneNode): def _onStackChanged(self): self._stack_change_timer.start() - ## Update the build volume visualization def _onStackChangeTimerFinished(self) -> None: + """Update the build volume visualization""" + if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) extruders = ExtruderManager.getInstance().getActiveExtruderStacks() @@ -712,15 +726,15 @@ class BuildVolume(SceneNode): self._depth = self._global_container_stack.getProperty("machine_depth", "value") self._shape = self._global_container_stack.getProperty("machine_shape", "value") - ## Calls _updateDisallowedAreas and makes sure the changes appear in the - # scene. - # - # This is required for a signal to trigger the update in one go. The - # ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``, - # since there may be other changes before it needs to be rebuilt, which - # would hit performance. - def _updateDisallowedAreasAndRebuild(self): + """Calls :py:meth:`cura.BuildVolume._updateDisallowedAreas` and makes sure the changes appear in the scene. + + This is required for a signal to trigger the update in one go. The + :py:meth:`cura.BuildVolume._updateDisallowedAreas` method itself shouldn't call + :py:meth:`cura.BuildVolume.rebuild`, since there may be other changes before it needs to be rebuilt, + which would hit performance. + """ + self._updateDisallowedAreas() self._updateRaftThickness() self._extra_z_clearance = self._calculateExtraZClearance(ExtruderManager.getInstance().getUsedExtruderStacks()) @@ -782,15 +796,14 @@ class BuildVolume(SceneNode): for extruder_id in result_areas_no_brim: self._disallowed_areas_no_brim.extend(result_areas_no_brim[extruder_id]) - ## Computes the disallowed areas for objects that are printed with print - # features. - # - # This means that the brim, travel avoidance and such will be applied to - # these features. - # - # \return A dictionary with for each used extruder ID the disallowed areas - # where that extruder may not print. def _computeDisallowedAreasPrinted(self, used_extruders): + """Computes the disallowed areas for objects that are printed with print features. + + This means that the brim, travel avoidance and such will be applied to these features. + + :return: A dictionary with for each used extruder ID the disallowed areas where that extruder may not print. + """ + result = {} adhesion_extruder = None #type: ExtruderStack for extruder in used_extruders: @@ -828,18 +841,18 @@ class BuildVolume(SceneNode): return result - ## Computes the disallowed areas for the prime blobs. - # - # These are special because they are not subject to things like brim or - # travel avoidance. They do get a dilute with the border size though - # because they may not intersect with brims and such of other objects. - # - # \param border_size The size with which to offset the disallowed areas - # due to skirt, brim, travel avoid distance, etc. - # \param used_extruders The extruder stacks to generate disallowed areas - # for. - # \return A dictionary with for each used extruder ID the prime areas. def _computeDisallowedAreasPrimeBlob(self, border_size: float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]: + """Computes the disallowed areas for the prime blobs. + + These are special because they are not subject to things like brim or travel avoidance. They do get a dilute + with the border size though because they may not intersect with brims and such of other objects. + + :param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance + , etc. + :param used_extruders: The extruder stacks to generate disallowed areas for. + :return: A dictionary with for each used extruder ID the prime areas. + """ + result = {} # type: Dict[str, List[Polygon]] if not self._global_container_stack: return result @@ -867,19 +880,18 @@ class BuildVolume(SceneNode): return result - ## Computes the disallowed areas that are statically placed in the machine. - # - # It computes different disallowed areas depending on the offset of the - # extruder. The resulting dictionary will therefore have an entry for each - # extruder that is used. - # - # \param border_size The size with which to offset the disallowed areas - # due to skirt, brim, travel avoid distance, etc. - # \param used_extruders The extruder stacks to generate disallowed areas - # for. - # \return A dictionary with for each used extruder ID the disallowed areas - # where that extruder may not print. def _computeDisallowedAreasStatic(self, border_size:float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]: + """Computes the disallowed areas that are statically placed in the machine. + + It computes different disallowed areas depending on the offset of the extruder. The resulting dictionary will + therefore have an entry for each extruder that is used. + + :param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance + , etc. + :param used_extruders: The extruder stacks to generate disallowed areas for. + :return: A dictionary with for each used extruder ID the disallowed areas where that extruder may not print. + """ + # Convert disallowed areas to polygons and dilate them. machine_disallowed_polygons = [] if self._global_container_stack is None: @@ -1010,13 +1022,14 @@ class BuildVolume(SceneNode): return result - ## Private convenience function to get a setting from every extruder. - # - # For single extrusion machines, this gets the setting from the global - # stack. - # - # \return A sequence of setting values, one for each extruder. def _getSettingFromAllExtruders(self, setting_key: str) -> List[Any]: + """Private convenience function to get a setting from every extruder. + + For single extrusion machines, this gets the setting from the global stack. + + :return: A sequence of setting values, one for each extruder. + """ + all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value") all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type") for i, (setting_value, setting_type) in enumerate(zip(all_values, all_types)): @@ -1101,12 +1114,13 @@ class BuildVolume(SceneNode): return move_from_wall_radius - ## Calculate the disallowed radius around the edge. - # - # This disallowed radius is to allow for space around the models that is - # not part of the collision radius, such as bed adhesion (skirt/brim/raft) - # and travel avoid distance. def getEdgeDisallowedSize(self): + """Calculate the disallowed radius around the edge. + + This disallowed radius is to allow for space around the models that is not part of the collision radius, such as + bed adhesion (skirt/brim/raft) and travel avoid distance. + """ + if not self._global_container_stack or not self._global_container_stack.extruderList: return 0 diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index ab7a7ebdd1..75fdd99798 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -150,8 +150,9 @@ class CrashHandler: self._sendCrashReport() os._exit(1) - ## Backup the current resource directories and create clean ones. def _backupAndStartClean(self): + """Backup the current resource directories and create clean ones.""" + Resources.factoryReset() self.early_crash_dialog.close() @@ -162,8 +163,9 @@ class CrashHandler: def _showDetailedReport(self): self.dialog.exec_() - ## Creates a modal dialog. def _createDialog(self): + """Creates a modal dialog.""" + self.dialog.setMinimumWidth(640) self.dialog.setMinimumHeight(640) self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report")) diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 20c44c7916..3b37c69adc 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -43,9 +43,10 @@ class CuraActions(QObject): event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues")], {}) cura.CuraApplication.CuraApplication.getInstance().functionEvent(event) - ## Reset camera position and direction to default @pyqtSlot() def homeCamera(self) -> None: + """Reset camera position and direction to default""" + scene = cura.CuraApplication.CuraApplication.getInstance().getController().getScene() camera = scene.getActiveCamera() if camera: @@ -54,9 +55,10 @@ class CuraActions(QObject): camera.setPerspective(True) camera.lookAt(Vector(0, 0, 0)) - ## Center all objects in the selection @pyqtSlot() def centerSelection(self) -> None: + """Center all objects in the selection""" + operation = GroupedOperation() for node in Selection.getAllSelectedObjects(): current_node = node @@ -73,18 +75,21 @@ class CuraActions(QObject): operation.addOperation(center_operation) operation.push() - ## Multiply all objects in the selection - # - # \param count The number of times to multiply the selection. @pyqtSlot(int) def multiplySelection(self, count: int) -> None: + """Multiply all objects in the selection + + :param count: The number of times to multiply the selection. + """ + 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: + """Delete all selected objects.""" + if not cura.CuraApplication.CuraApplication.getInstance().getController().getToolsEnabled(): return @@ -106,11 +111,13 @@ class CuraActions(QObject): op.push() - ## Set the extruder that should be used to print the selection. - # - # \param extruder_id The ID of the extruder stack to use for the selected objects. @pyqtSlot(str) def setExtruderForSelection(self, extruder_id: str) -> None: + """Set the extruder that should be used to print the selection. + + :param extruder_id: The ID of the extruder stack to use for the selected objects. + """ + operation = GroupedOperation() nodes_to_change = [] diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 993bb15ae2..a588be3d5c 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -258,9 +258,12 @@ class CuraApplication(QtApplication): def ultimakerCloudAccountRootUrl(self) -> str: return UltimakerCloudAuthentication.CuraCloudAccountAPIRoot - # Adds command line options to the command line parser. This should be called after the application is created and - # before the pre-start. def addCommandLineOptions(self): + """Adds command line options to the command line parser. + + This should be called after the application is created and before the pre-start. + """ + super().addCommandLineOptions() self._cli_parser.add_argument("--help", "-h", action = "store_true", @@ -322,8 +325,9 @@ class CuraApplication(QtApplication): Logger.log("i", "Single instance commands were sent, exiting") sys.exit(0) - # Adds expected directory names and search paths for Resources. def __addExpectedResourceDirsAndSearchPaths(self): + """Adds expected directory names and search paths for Resources.""" + # this list of dir names will be used by UM to detect an old cura directory for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]: Resources.addExpectedDirNameInData(dir_name) @@ -365,9 +369,12 @@ class CuraApplication(QtApplication): SettingDefinition.addSettingType("[int]", None, str, None) - # Adds custom property types, settings types, and extra operators (functions) that need to be registered in - # SettingDefinition and SettingFunction. def _initializeSettingFunctions(self): + """Adds custom property types, settings types, and extra operators (functions). + + Whom need to be registered in SettingDefinition and SettingFunction. + """ + self._cura_formula_functions = CuraFormulaFunctions(self) SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder) @@ -377,8 +384,9 @@ class CuraApplication(QtApplication): SettingFunction.registerOperator("valueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndex) SettingFunction.registerOperator("extruderValueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndexInExtruder) - # Adds all resources and container related resources. def __addAllResourcesAndContainerResources(self) -> None: + """Adds all resources and container related resources.""" + Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality") Resources.addStorageType(self.ResourceTypes.QualityChangesInstanceContainer, "quality_changes") Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants") @@ -403,8 +411,9 @@ class CuraApplication(QtApplication): Resources.addType(self.ResourceTypes.QmlFiles, "qml") Resources.addType(self.ResourceTypes.Firmware, "firmware") - # Adds all empty containers. def __addAllEmptyContainers(self) -> None: + """Adds all empty containers.""" + # Add empty variant, material and quality containers. # Since they are empty, they should never be serialized and instead just programmatically created. # We need them to simplify the switching between materials. @@ -429,9 +438,11 @@ class CuraApplication(QtApplication): self._container_registry.addContainer(cura.Settings.cura_empty_instance_containers.empty_quality_changes_container) self.empty_quality_changes_container = cura.Settings.cura_empty_instance_containers.empty_quality_changes_container - # Initializes the version upgrade manager with by providing the paths for each resource type and the latest - # versions. def __setLatestResouceVersionsForVersionUpgrade(self): + """Initializes the version upgrade manager with by providing the paths for each resource type and the latest + versions. + """ + self._version_upgrade_manager.setCurrentVersions( { ("quality", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"), @@ -446,8 +457,9 @@ class CuraApplication(QtApplication): } ) - # Runs preparations that needs to be done before the starting process. def startSplashWindowPhase(self) -> None: + """Runs preparations that needs to be done before the starting process.""" + super().startSplashWindowPhase() if not self.getIsHeadLess(): @@ -503,7 +515,7 @@ class CuraApplication(QtApplication): # Set the setting version for Preferences preferences = self.getPreferences() preferences.addPreference("metadata/setting_version", 0) - preferences.setValue("metadata/setting_version", self.SettingVersion) #Don't make it equal to the default so that the setting version always gets written to the file. + preferences.setValue("metadata/setting_version", self.SettingVersion) # Don't make it equal to the default so that the setting version always gets written to the file. preferences.addPreference("cura/active_mode", "simple") @@ -607,12 +619,13 @@ class CuraApplication(QtApplication): def callConfirmExitDialogCallback(self, yes_or_no: bool) -> None: self._confirm_exit_dialog_callback(yes_or_no) - ## Signal to connect preferences action in QML showPreferencesWindow = pyqtSignal() + """Signal to connect preferences action in QML""" - ## Show the preferences window @pyqtSlot() def showPreferences(self) -> None: + """Show the preferences window""" + self.showPreferencesWindow.emit() # This is called by drag-and-dropping curapackage files. @@ -630,10 +643,9 @@ class CuraApplication(QtApplication): self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Initializing Active Machine...")) super().setGlobalContainerStack(stack) - ## A reusable dialogbox - # showMessageBox = pyqtSignal(str,str, str, str, int, int, arguments = ["title", "text", "informativeText", "detailedText","buttons", "icon"]) + """ A reusable dialogbox""" def messageBox(self, title, text, informativeText = "", @@ -711,9 +723,12 @@ class CuraApplication(QtApplication): def setDefaultPath(self, key, default_path): self.getPreferences().setValue("local_file/%s" % key, QUrl(default_path).toLocalFile()) - ## Handle loading of all plugin types (and the backend explicitly) - # \sa PluginRegistry def _loadPlugins(self) -> None: + """Handle loading of all plugin types (and the backend explicitly) + + :py:class:`Uranium.UM.PluginRegistry` + """ + self._plugin_registry.setCheckIfTrusted(ApplicationMetadata.IsEnterpriseVersion) self._plugin_registry.addType("profile_reader", self._addProfileReader) @@ -737,9 +752,12 @@ class CuraApplication(QtApplication): self._plugins_loaded = True - ## Set a short, user-friendly hint about current loading status. - # The way this message is displayed depends on application state def _setLoadingHint(self, hint: str): + """Set a short, user-friendly hint about current loading status. + + The way this message is displayed depends on application state + """ + if self.started: Logger.info(hint) else: @@ -824,12 +842,14 @@ class CuraApplication(QtApplication): initializationFinished = pyqtSignal() - ## Run Cura without GUI elements and interaction (server mode). def runWithoutGUI(self): + """Run Cura without GUI elements and interaction (server mode).""" + self.closeSplash() - ## Run Cura with GUI (desktop mode). def runWithGUI(self): + """Run Cura with GUI (desktop mode).""" + self._setLoadingHint(self._i18n_catalog.i18nc("@info:progress", "Setting up scene...")) controller = self.getController() @@ -979,10 +999,13 @@ class CuraApplication(QtApplication): self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager() return self._setting_inheritance_manager - ## Get the machine action manager - # We ignore any *args given to this, as we also register the machine manager as qml singleton. - # It wants to give this function an engine and script engine, but we don't care about that. def getMachineActionManager(self, *args: Any) -> MachineActionManager.MachineActionManager: + """Get the machine action manager + + We ignore any *args given to this, as we also register the machine manager as qml singleton. + It wants to give this function an engine and script engine, but we don't care about that. + """ + return cast(MachineActionManager.MachineActionManager, self._machine_action_manager) @pyqtSlot(result = QObject) @@ -1002,8 +1025,9 @@ class CuraApplication(QtApplication): self._simple_mode_settings_manager = SimpleModeSettingsManager() return self._simple_mode_settings_manager - ## Handle Qt events def event(self, event): + """Handle Qt events""" + if event.type() == QEvent.FileOpen: if self._plugins_loaded: self._openFile(event.file()) @@ -1015,8 +1039,9 @@ class CuraApplication(QtApplication): def getAutoSave(self) -> Optional[AutoSave]: return self._auto_save - ## Get print information (duration / material used) def getPrintInformation(self): + """Get print information (duration / material used)""" + return self._print_information def getQualityProfilesDropDownMenuModel(self, *args, **kwargs): @@ -1032,10 +1057,12 @@ class CuraApplication(QtApplication): def getCuraAPI(self, *args, **kwargs) -> "CuraAPI": return self._cura_API - ## Registers objects for the QML engine to use. - # - # \param engine The QML engine. def registerObjects(self, engine): + """Registers objects for the QML engine to use. + + :param engine: The QML engine. + """ + super().registerObjects(engine) # global contexts @@ -1169,8 +1196,9 @@ class CuraApplication(QtApplication): if node is not None and (node.getMeshData() is not None or node.callDecoration("getLayerData")): self._update_platform_activity_timer.start() - ## Update scene bounding box for current build plate def updatePlatformActivity(self, node = None): + """Update scene bounding box for current build plate""" + count = 0 scene_bounding_box = None is_block_slicing_node = False @@ -1214,9 +1242,10 @@ class CuraApplication(QtApplication): self._platform_activity = True if count > 0 else False self.activityChanged.emit() - ## Select all nodes containing mesh data in the scene. @pyqtSlot() def selectAll(self): + """Select all nodes containing mesh data in the scene.""" + if not self.getController().getToolsEnabled(): return @@ -1235,9 +1264,10 @@ class CuraApplication(QtApplication): Selection.add(node) - ## Reset all translation on nodes with mesh data. @pyqtSlot() def resetAllTranslation(self): + """Reset all translation on nodes with mesh data.""" + Logger.log("i", "Resetting all scene translations") nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): @@ -1263,9 +1293,10 @@ class CuraApplication(QtApplication): op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0))) op.push() - ## Reset all transformations on nodes with mesh data. @pyqtSlot() def resetAll(self): + """Reset all transformations on nodes with mesh data.""" + Logger.log("i", "Resetting all scene transformations") nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): @@ -1291,9 +1322,10 @@ class CuraApplication(QtApplication): op.addOperation(SetTransformOperation(node, Vector(0, center_y, 0), Quaternion(), Vector(1, 1, 1))) op.push() - ## Arrange all objects. @pyqtSlot() def arrangeObjectsToAllBuildPlates(self) -> None: + """Arrange all objects.""" + nodes_to_arrange = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if not isinstance(node, SceneNode): @@ -1346,17 +1378,21 @@ class CuraApplication(QtApplication): nodes_to_arrange.append(node) self.arrange(nodes_to_arrange, fixed_nodes = []) - ## Arrange a set of nodes given a set of fixed nodes - # \param nodes nodes that we have to place - # \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None: + """Arrange a set of nodes given a set of fixed nodes + + :param nodes: nodes that we have to place + :param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes + """ + min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8)) job.start() - ## Reload all mesh data on the screen from file. @pyqtSlot() def reloadAll(self) -> None: + """Reload all mesh data on the screen from file.""" + Logger.log("i", "Reloading all loaded mesh data.") nodes = [] has_merged_nodes = False @@ -1466,8 +1502,9 @@ class CuraApplication(QtApplication): group_node.setName("MergedMesh") # add a specific name to distinguish this node - ## Updates origin position of all merged meshes def updateOriginOfMergedMeshes(self, _): + """Updates origin position of all merged meshes""" + group_nodes = [] for node in DepthFirstIterator(self.getController().getScene().getRoot()): if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh": @@ -1585,9 +1622,10 @@ class CuraApplication(QtApplication): scene from its source file. The function gets all the nodes that exist in the file through the job result, and then finds the scene node that it wants to refresh by its object id. Each job refreshes only one node. - :param job: The ReadMeshJob running in the background that reads all the meshes in a file - :return: None + :param job: The :py:class:`Uranium.UM.ReadMeshJob.ReadMeshJob` running in the background that reads all the + meshes in a file """ + job_result = job.getResult() # nodes that exist inside the file read by this job if len(job_result) == 0: Logger.log("e", "Reloading the mesh failed.") @@ -1633,12 +1671,15 @@ class CuraApplication(QtApplication): def additionalComponents(self): return self._additional_components - ## Add a component to a list of components to be reparented to another area in the GUI. - # The actual reparenting is done by the area itself. - # \param area_id \type{str} Identifying name of the area to which the component should be reparented - # \param component \type{QQuickComponent} The component that should be reparented @pyqtSlot(str, "QVariant") - def addAdditionalComponent(self, area_id, component): + def addAdditionalComponent(self, area_id: str, component): + """Add a component to a list of components to be reparented to another area in the GUI. + + The actual reparenting is done by the area itself. + :param area_id: dentifying name of the area to which the component should be reparented + :param (QQuickComponent) component: The component that should be reparented + """ + if area_id not in self._additional_components: self._additional_components[area_id] = [] self._additional_components[area_id].append(component) @@ -1653,10 +1694,13 @@ class CuraApplication(QtApplication): @pyqtSlot(QUrl, str) @pyqtSlot(QUrl) - ## Open a local file - # \param project_mode How to handle project files. Either None(default): Follow user preference, "open_as_model" or - # "open_as_project". This parameter is only considered if the file is a project file. def readLocalFile(self, file: QUrl, project_mode: Optional[str] = None): + """Open a local file + + :param project_mode: How to handle project files. Either None(default): Follow user preference, "open_as_model" + or "open_as_project". This parameter is only considered if the file is a project file. + """ + if not file.isValid(): return @@ -1829,9 +1873,8 @@ class CuraApplication(QtApplication): @pyqtSlot(str, result=bool) def checkIsValidProjectFile(self, file_url): - """ - Checks if the given file URL is a valid project file. - """ + """ Checks if the given file URL is a valid project file. """ + file_path = QUrl(file_url).toLocalFile() workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path) if workspace_reader is None: diff --git a/cura/CuraPackageManager.py b/cura/CuraPackageManager.py index a0d3a8d44a..fa862f3f9e 100644 --- a/cura/CuraPackageManager.py +++ b/cura/CuraPackageManager.py @@ -24,11 +24,15 @@ class CuraPackageManager(PackageManager): super().initialize() - ## Returns a list of where the package is used - # empty if it is never used. - # It loops through all the package contents and see if some of the ids are used. - # The list consists of 3-tuples: (global_stack, extruder_nr, container_id) def getMachinesUsingPackage(self, package_id: str) -> Tuple[List[Tuple[GlobalStack, str, str]], List[Tuple[GlobalStack, str, str]]]: + """Returns a list of where the package is used + + It loops through all the package contents and see if some of the ids are used. + + :param package_id: package id to search for + :return: empty if it is never used, otherwise a list consisting of 3-tuples + """ + ids = self.getPackageContainerIds(package_id) container_stacks = self._application.getContainerRegistry().findContainerStacks() global_stacks = [container_stack for container_stack in container_stacks if isinstance(container_stack, GlobalStack)] diff --git a/cura/LayerData.py b/cura/LayerData.py index 72824591ab..1b3f2a978e 100644 --- a/cura/LayerData.py +++ b/cura/LayerData.py @@ -3,9 +3,12 @@ from UM.Mesh.MeshData import MeshData -## Class to holds the layer mesh and information about the layers. -# Immutable, use LayerDataBuilder to create one of these. class LayerData(MeshData): + """Class to holds the layer mesh and information about the layers. + + Immutable, use :py:class:`cura.LayerDataBuilder.LayerDataBuilder` to create one of these. + """ + def __init__(self, vertices = None, normals = None, indices = None, colors = None, uvs = None, file_name = None, center_position = None, layers=None, element_counts=None, attributes=None): super().__init__(vertices=vertices, normals=normals, indices=indices, colors=colors, uvs=uvs, diff --git a/cura/LayerDataBuilder.py b/cura/LayerDataBuilder.py index e8d1b8c59f..4650248780 100755 --- a/cura/LayerDataBuilder.py +++ b/cura/LayerDataBuilder.py @@ -10,8 +10,9 @@ import numpy from typing import Dict, Optional -## Builder class for constructing a LayerData object class LayerDataBuilder(MeshBuilder): + """Builder class for constructing a :py:class:`cura.LayerData.LayerData` object""" + def __init__(self) -> None: super().__init__() self._layers = {} # type: Dict[int, Layer] @@ -42,11 +43,13 @@ class LayerDataBuilder(MeshBuilder): self._layers[layer].setThickness(thickness) - ## Return the layer data as LayerData. - # - # \param material_color_map: [r, g, b, a] for each extruder row. - # \param line_type_brightness: compatibility layer view uses line type brightness of 0.5 def build(self, material_color_map, line_type_brightness = 1.0): + """Return the layer data as :py:class:`cura.LayerData.LayerData`. + + :param material_color_map:: [r, g, b, a] for each extruder row. + :param line_type_brightness:: compatibility layer view uses line type brightness of 0.5 + """ + vertex_count = 0 index_count = 0 for layer, data in self._layers.items(): diff --git a/cura/LayerDataDecorator.py b/cura/LayerDataDecorator.py index 36466cac72..66924e8d2c 100644 --- a/cura/LayerDataDecorator.py +++ b/cura/LayerDataDecorator.py @@ -7,8 +7,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from cura.LayerData import LayerData -## Simple decorator to indicate a scene node holds layer data. class LayerDataDecorator(SceneNodeDecorator): + """Simple decorator to indicate a scene node holds layer data.""" + def __init__(self) -> None: super().__init__() self._layer_data = None # type: Optional[LayerData] diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py index 70d818f1ca..38d4f756a4 100644 --- a/cura/LayerPolygon.py +++ b/cura/LayerPolygon.py @@ -26,14 +26,17 @@ class LayerPolygon: __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType) - ## LayerPolygon, used in ProcessSlicedLayersJob - # \param extruder The position of the extruder - # \param line_types array with line_types - # \param data new_points - # \param line_widths array with line widths - # \param line_thicknesses: array with type as index and thickness as value - # \param line_feedrates array with line feedrates def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray, line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None: + """LayerPolygon, used in ProcessSlicedLayersJob + + :param extruder: The position of the extruder + :param line_types: array with line_types + :param data: new_points + :param line_widths: array with line widths + :param line_thicknesses: array with type as index and thickness as value + :param line_feedrates: array with line feedrates + """ + self._extruder = extruder self._types = line_types for i in range(len(self._types)): @@ -83,19 +86,22 @@ class LayerPolygon: self._vertex_begin = 0 self._vertex_end = numpy.sum( self._build_cache_needed_points ) - ## Set all the arrays provided by the function caller, representing the LayerPolygon - # The arrays are either by vertex or by indices. - # - # \param vertex_offset : determines where to start and end filling the arrays - # \param index_offset : determines where to start and end filling the arrays - # \param vertices : vertex numpy array to be filled - # \param colors : vertex numpy array to be filled - # \param line_dimensions : vertex numpy array to be filled - # \param feedrates : vertex numpy array to be filled - # \param extruders : vertex numpy array to be filled - # \param line_types : vertex numpy array to be filled - # \param indices : index numpy array to be filled def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray, colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray, extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None: + """Set all the arrays provided by the function caller, representing the LayerPolygon + + The arrays are either by vertex or by indices. + + :param vertex_offset: : determines where to start and end filling the arrays + :param index_offset: : determines where to start and end filling the arrays + :param vertices: : vertex numpy array to be filled + :param colors: : vertex numpy array to be filled + :param line_dimensions: : vertex numpy array to be filled + :param feedrates: : vertex numpy array to be filled + :param extruders: : vertex numpy array to be filled + :param line_types: : vertex numpy array to be filled + :param indices: : index numpy array to be filled + """ + if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None: self.buildCache() @@ -202,8 +208,12 @@ class LayerPolygon: def jumpCount(self): return self._jump_count - # Calculate normals for the entire polygon using numpy. def getNormals(self) -> numpy.ndarray: + """Calculate normals for the entire polygon using numpy. + + :return: normals for the entire polygon + """ + normals = numpy.copy(self._data) normals[:, 1] = 0.0 # We are only interested in 2D normals @@ -229,9 +239,10 @@ class LayerPolygon: __color_map = None # type: numpy.ndarray - ## Gets the instance of the VersionUpgradeManager, or creates one. @classmethod def getColorMap(cls) -> numpy.ndarray: + """Gets the instance of the VersionUpgradeManager, or creates one.""" + if cls.__color_map is None: theme = cast(Theme, QtApplication.getInstance().getTheme()) cls.__color_map = numpy.array([ diff --git a/cura/MachineAction.py b/cura/MachineAction.py index 0f05401c89..4c2b4e7aea 100644 --- a/cura/MachineAction.py +++ b/cura/MachineAction.py @@ -11,16 +11,22 @@ from UM.PluginObject import PluginObject from UM.PluginRegistry import PluginRegistry -## Machine actions are actions that are added to a specific machine type. Examples of such actions are -# updating the firmware, connecting with remote devices or doing bed leveling. A machine action can also have a -# qml, which should contain a "Cura.MachineAction" item. When activated, the item will be displayed in a dialog -# and this object will be added as "manager" (so all pyqtSlot() functions can be called by calling manager.func()) class MachineAction(QObject, PluginObject): + """Machine actions are actions that are added to a specific machine type. + + Examples of such actions are updating the firmware, connecting with remote devices or doing bed leveling. A + machine action can also have a qml, which should contain a :py:class:`cura.MachineAction.MachineAction` item. + When activated, the item will be displayed in a dialog and this object will be added as "manager" (so all + pyqtSlot() functions can be called by calling manager.func()) + """ - ## Create a new Machine action. - # \param key unique key of the machine action - # \param label Human readable label used to identify the machine action. def __init__(self, key: str, label: str = "") -> None: + """Create a new Machine action. + + :param key: unique key of the machine action + :param label: Human readable label used to identify the machine action. + """ + super().__init__() self._key = key self._label = label @@ -34,10 +40,14 @@ class MachineAction(QObject, PluginObject): def getKey(self) -> str: return self._key - ## Whether this action needs to ask the user anything. - # If not, we shouldn't present the user with certain screens which otherwise show up. - # Defaults to true to be in line with the old behaviour. def needsUserInteraction(self) -> bool: + """Whether this action needs to ask the user anything. + + If not, we shouldn't present the user with certain screens which otherwise show up. + + :return: Defaults to true to be in line with the old behaviour. + """ + return True @pyqtProperty(str, notify = labelChanged) @@ -49,17 +59,24 @@ class MachineAction(QObject, PluginObject): self._label = label self.labelChanged.emit() - ## Reset the action to it's default state. - # This should not be re-implemented by child classes, instead re-implement _reset. - # /sa _reset @pyqtSlot() def reset(self) -> None: + """Reset the action to it's default state. + + This should not be re-implemented by child classes, instead re-implement _reset. + + :py:meth:`cura.MachineAction.MachineAction._reset` + """ + self._finished = False self._reset() - ## Protected implementation of reset. - # /sa reset() def _reset(self) -> None: + """Protected implementation of reset. + + :py:meth:`cura.MachineAction.MachineAction.reset` + """ + pass @pyqtSlot() @@ -72,8 +89,9 @@ class MachineAction(QObject, PluginObject): def finished(self) -> bool: return self._finished - ## Protected helper to create a view object based on provided QML. def _createViewFromQML(self) -> Optional["QObject"]: + """Protected helper to create a view object based on provided QML.""" + plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) if plugin_path is None: Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId()) diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index 3373f2104f..4853cffcc1 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -7,18 +7,21 @@ from UM.Scene.Iterator import Iterator from UM.Scene.SceneNode import SceneNode from functools import cmp_to_key -## Iterator that returns a list of nodes in the order that they need to be printed -# If there is no solution an empty list is returned. -# Take note that the list of nodes can have children (that may or may not contain mesh data) class OneAtATimeIterator(Iterator.Iterator): + """Iterator that returns a list of nodes in the order that they need to be printed + + If there is no solution an empty list is returned. + Take note that the list of nodes can have children (that may or may not contain mesh data) + """ + def __init__(self, scene_node) -> None: super().__init__(scene_node) # Call super to make multiple inheritance work. self._hit_map = [[]] # type: List[List[bool]] # For each node, which other nodes this hits. A grid of booleans on which nodes hit which. self._original_node_list = [] # type: List[SceneNode] # The nodes that need to be checked for collisions. - ## Fills the ``_node_stack`` with a list of scene nodes that need to be - # printed in order. def _fillStack(self) -> None: + """Fills the ``_node_stack`` with a list of scene nodes that need to be printed in order. """ + node_list = [] for node in self._scene_node.getChildren(): if not issubclass(type(node), SceneNode): @@ -75,10 +78,14 @@ class OneAtATimeIterator(Iterator.Iterator): return True return False - ## Check for a node whether it hits any of the other nodes. - # \param node The node to check whether it collides with the other nodes. - # \param other_nodes The nodes to check for collisions. def _checkBlockMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool: + """Check for a node whether it hits any of the other nodes. + + :param node: The node to check whether it collides with the other nodes. + :param other_nodes: The nodes to check for collisions. + :return: returns collision between nodes + """ + node_index = self._original_node_list.index(node) for other_node in other_nodes: other_node_index = self._original_node_list.index(other_node) @@ -86,14 +93,26 @@ class OneAtATimeIterator(Iterator.Iterator): return True return False - ## Calculate score simply sums the number of other objects it 'blocks' def _calculateScore(self, a: SceneNode, b: SceneNode) -> int: + """Calculate score simply sums the number of other objects it 'blocks' + + :param a: node + :param b: node + :return: sum of the number of other objects + """ + score_a = sum(self._hit_map[self._original_node_list.index(a)]) score_b = sum(self._hit_map[self._original_node_list.index(b)]) return score_a - score_b - ## Checks if A can be printed before B def _checkHit(self, a: SceneNode, b: SceneNode) -> bool: + """Checks if a can be printed before b + + :param a: node + :param b: node + :return: true if a can be printed before b + """ + if a == b: return False @@ -116,12 +135,14 @@ class OneAtATimeIterator(Iterator.Iterator): return False -## Internal object used to keep track of a possible order in which to print objects. class _ObjectOrder: - ## Creates the _ObjectOrder instance. - # \param order List of indices in which to print objects, ordered by printing - # order. - # \param todo: List of indices which are not yet inserted into the order list. + """Internal object used to keep track of a possible order in which to print objects.""" + def __init__(self, order: List[SceneNode], todo: List[SceneNode]) -> None: + """Creates the _ObjectOrder instance. + + :param order: List of indices in which to print objects, ordered by printing order. + :param todo: List of indices which are not yet inserted into the order list. + """ self.order = order self.todo = todo diff --git a/cura/PickingPass.py b/cura/PickingPass.py index 75ee21ef41..f2dfbaaf1e 100644 --- a/cura/PickingPass.py +++ b/cura/PickingPass.py @@ -16,11 +16,15 @@ 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): + """A :py:class:`Uranium.UM.View.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 + """ + def __init__(self, width: int, height: int) -> None: super().__init__("picking", width, height) @@ -50,8 +54,14 @@ class PickingPass(RenderPass): batch.render(self._scene.getActiveCamera()) self.release() - ## Get the distance in mm from the camera to at a certain pixel coordinate. def getPickedDepth(self, x: int, y: int) -> float: + """Get the distance in mm from the camera to at a certain pixel coordinate. + + :param x: x component of coordinate vector in pixels + :param y: y component of coordinate vector in pixels + :return: distance in mm from the camera to pixel coordinate + """ + output = self.getOutput() window_size = self._renderer.getWindowSize() @@ -66,8 +76,14 @@ class PickingPass(RenderPass): distance = (distance & 0x00ffffff) / 1000. # drop the alpha channel and covert to mm return distance - ## Get the world coordinates of a picked point def getPickedPosition(self, x: int, y: int) -> Vector: + """Get the world coordinates of a picked point + + :param x: x component of coordinate vector in pixels + :param y: y component of coordinate vector in pixels + :return: vector of the world coordinate + """ + distance = self.getPickedDepth(x, y) camera = self._scene.getActiveCamera() if camera: diff --git a/cura/PreviewPass.py b/cura/PreviewPass.py index da60db2d99..c3d38cb6c6 100644 --- a/cura/PreviewPass.py +++ b/cura/PreviewPass.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, TYPE_CHECKING, cast +from typing import Optional, TYPE_CHECKING, cast, List from UM.Application import Application @@ -20,9 +20,14 @@ if TYPE_CHECKING: from UM.Scene.Camera import Camera -# Make color brighter by normalizing it (maximum factor 2.5 brighter) -# color_list is a list of 4 elements: [r, g, b, a], each element is a float 0..1 -def prettier_color(color_list): +def prettier_color(color_list: List[float]) -> List[float]: + """Make color brighter by normalizing + + maximum factor 2.5 brighter + + :param color_list: a list of 4 elements: [r, g, b, a], each element is a float 0..1 + :return: a normalized list of 4 elements: [r, g, b, a], each element is a float 0..1 + """ maximum = max(color_list[:3]) if maximum > 0: factor = min(1 / maximum, 2.5) @@ -31,11 +36,14 @@ def prettier_color(color_list): return [min(i * factor, 1.0) for i in color_list] -## A render pass subclass that renders slicable objects with default parameters. -# It uses the active camera by default, but it can be overridden to use a different camera. -# -# This is useful to get a preview image of a scene taken from a different location as the active camera. class PreviewPass(RenderPass): + """A :py:class:`Uranium.UM.View.RenderPass` subclass that renders slicable objects with default parameters. + + It uses the active camera by default, but it can be overridden to use a different camera. + + This is useful to get a preview image of a scene taken from a different location as the active camera. + """ + def __init__(self, width: int, height: int) -> None: super().__init__("preview", width, height, 0) diff --git a/cura/PrintJobPreviewImageProvider.py b/cura/PrintJobPreviewImageProvider.py index 8b46c6db37..321164adeb 100644 --- a/cura/PrintJobPreviewImageProvider.py +++ b/cura/PrintJobPreviewImageProvider.py @@ -10,8 +10,14 @@ class PrintJobPreviewImageProvider(QQuickImageProvider): def __init__(self): super().__init__(QQuickImageProvider.Image) - ## Request a new image. def requestImage(self, id: str, size: QSize) -> Tuple[QImage, QSize]: + """Request a new image. + + :param id: id of the requested image + :param size: is not used defaults to QSize(15, 15) + :return: an tuple containing the image and size + """ + # The id will have an uuid and an increment separated by a slash. As we don't care about the value of the # increment, we need to strip that first. uuid = id[id.find("/") + 1:] diff --git a/cura/Snapshot.py b/cura/Snapshot.py index 353b5ae17c..6f12aa88ba 100644 --- a/cura/Snapshot.py +++ b/cura/Snapshot.py @@ -30,11 +30,17 @@ class Snapshot: return min_x, max_x, min_y, max_y - ## Return a QImage of the scene - # Uses PreviewPass that leaves out some elements - # Aspect ratio assumes a square @staticmethod def snapshot(width = 300, height = 300): + """Return a QImage of the scene + + Uses PreviewPass that leaves out some elements Aspect ratio assumes a square + + :param width: width of the aspect ratio default 300 + :param height: height of the aspect ratio default 300 + :return: None when there is no model on the build plate otherwise it will return an image + """ + scene = Application.getInstance().getController().getScene() active_camera = scene.getActiveCamera() render_width, render_height = active_camera.getWindowSize() From 68318d20fd8de4d753fe0e73716a04a85ec4bb12 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 22 Apr 2020 20:57:41 +0200 Subject: [PATCH 02/27] Converted comments in dir Cura/cura/API to rst style Converted doxygen style comments to reStructuredText style in the files found in Cura/cura/API directory recursively using the script dox_2_rst.py (provided in the Uranium repo). Comments were manually checked and changed if needed. Comments from the hidden attributes in the class CuraAPI (_acount, _backups, _interface) were moved to the public property getters, so they docstrings are exposed to the user. --- cura/API/Account.py | 32 +++++++++++++--------- cura/API/Backups.py | 39 ++++++++++++++++----------- cura/API/Interface/Settings.py | 49 +++++++++++++++++++++------------- cura/API/Interface/__init__.py | 26 ++++++++++-------- cura/API/__init__.py | 22 ++++++++------- 5 files changed, 101 insertions(+), 67 deletions(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index 4391f730e5..a049b2924f 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -16,18 +16,23 @@ if TYPE_CHECKING: i18n_catalog = i18nCatalog("cura") -## The account API provides a version-proof bridge to use Ultimaker Accounts -# -# Usage: -# ``from cura.API import CuraAPI -# api = CuraAPI() -# api.account.login() -# api.account.logout() -# api.account.userProfile # Who is logged in`` -# class Account(QObject): - # Signal emitted when user logged in or out. + """The account API provides a version-proof bridge to use Ultimaker Accounts + + Usage: + + .. code-block:: python + + from cura.API import CuraAPI + api = CuraAPI() + api.account.login() + api.account.logout() + api.account.userProfile Who is logged in`` + """ + loginStateChanged = pyqtSignal(bool) + """Signal emitted when user logged in or out""" + accessTokenChanged = pyqtSignal() def __init__(self, application: "CuraApplication", parent = None) -> None: @@ -65,9 +70,10 @@ class Account(QObject): def _onAccessTokenChanged(self): self.accessTokenChanged.emit() - ## Returns a boolean indicating whether the given authentication is applied against staging or not. @property def is_staging(self) -> bool: + """Indication whether the given authentication is applied against staging or not.""" + return "staging" in self._oauth_root @pyqtProperty(bool, notify=loginStateChanged) @@ -113,10 +119,10 @@ class Account(QObject): def accessToken(self) -> Optional[str]: return self._authorization_service.getAccessToken() - # Get the profile of the logged in user - # @returns None if no user is logged in, a dict containing user_id, username and profile_image_url @pyqtProperty("QVariantMap", notify = loginStateChanged) def userProfile(self) -> Optional[Dict[str, Optional[str]]]: + """None if no user is logged in otherwise the logged in user as a dict containing containing user_id, username and profile_image_url """ + user_profile = self._authorization_service.getUserProfile() if not user_profile: return None diff --git a/cura/API/Backups.py b/cura/API/Backups.py index ef74e74be0..621dec01f2 100644 --- a/cura/API/Backups.py +++ b/cura/API/Backups.py @@ -8,28 +8,37 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -## The back-ups API provides a version-proof bridge between Cura's -# BackupManager and plug-ins that hook into it. -# -# Usage: -# ``from cura.API import CuraAPI -# api = CuraAPI() -# api.backups.createBackup() -# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})`` class Backups: + """The back-ups API provides a version-proof bridge between Cura's + + BackupManager and plug-ins that hook into it. + + Usage: + + .. code-block:: python + + from cura.API import CuraAPI + api = CuraAPI() + api.backups.createBackup() + api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})`` + """ def __init__(self, application: "CuraApplication") -> None: self.manager = BackupsManager(application) - ## 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) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]: + """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. + """ + return self.manager.createBackup() - ## Restore a back-up using the BackupsManager. - # \param zip_file A ZIP file containing the actual back-up data. - # \param meta_data Some metadata needed for restoring a back-up, like the - # Cura version number. def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None: + """Restore a back-up using the BackupsManager. + + :param zip_file: A ZIP file containing the actual back-up data. + :param meta_data: Some metadata needed for restoring a back-up, like the Cura version number. + """ + return self.manager.restoreBackup(zip_file, meta_data) diff --git a/cura/API/Interface/Settings.py b/cura/API/Interface/Settings.py index 371c40c14c..947b37fc88 100644 --- a/cura/API/Interface/Settings.py +++ b/cura/API/Interface/Settings.py @@ -7,32 +7,43 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -## The Interface.Settings API provides a version-proof bridge between Cura's -# (currently) sidebar UI and plug-ins that hook into it. -# -# Usage: -# ``from cura.API import CuraAPI -# api = CuraAPI() -# api.interface.settings.getContextMenuItems() -# data = { -# "name": "My Plugin Action", -# "iconName": "my-plugin-icon", -# "actions": my_menu_actions, -# "menu_item": MyPluginAction(self) -# } -# api.interface.settings.addContextMenuItem(data)`` - class Settings: + """The Interface.Settings API provides a version-proof bridge + between Cura's + + (currently) sidebar UI and plug-ins that hook into it. + + Usage: + + .. code-block:: python + + from cura.API import CuraAPI + api = CuraAPI() + api.interface.settings.getContextMenuItems() + data = { + "name": "My Plugin Action", + "iconName": "my-plugin-icon", + "actions": my_menu_actions, + "menu_item": MyPluginAction(self) + } + api.interface.settings.addContextMenuItem(data)`` + """ def __init__(self, application: "CuraApplication") -> None: self.application = application - ## Add items to the sidebar context menu. - # \param menu_item dict containing the menu item to add. def addContextMenuItem(self, menu_item: dict) -> None: + """Add items to the sidebar context menu. + + :param menu_item: dict containing the menu item to add. + """ + self.application.addSidebarCustomMenuItem(menu_item) - ## Get all custom items currently added to the sidebar context menu. - # \return List containing all custom context menu items. def getContextMenuItems(self) -> list: + """Get all custom items currently added to the sidebar context menu. + + :return: List containing all custom context menu items. + """ + return self.application.getSidebarCustomMenuItems() diff --git a/cura/API/Interface/__init__.py b/cura/API/Interface/__init__.py index cec174bf0a..13174070d1 100644 --- a/cura/API/Interface/__init__.py +++ b/cura/API/Interface/__init__.py @@ -9,18 +9,22 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -## The Interface class serves as a common root for the specific API -# methods for each interface element. -# -# Usage: -# ``from cura.API import CuraAPI -# api = CuraAPI() -# api.interface.settings.addContextMenuItem() -# api.interface.viewport.addOverlay() # Not implemented, just a hypothetical -# api.interface.toolbar.getToolButtonCount() # Not implemented, just a hypothetical -# # etc.`` - class Interface: + """The Interface class serves as a common root for the specific API + + methods for each interface element. + + Usage: + + .. code-block:: python + + from cura.API import CuraAPI + api = CuraAPI() + api.interface.settings.addContextMenuItem() + api.interface.viewport.addOverlay() # Not implemented, just a hypothetical + api.interface.toolbar.getToolButtonCount() # Not implemented, just a hypothetical + # etc + """ def __init__(self, application: "CuraApplication") -> None: # API methods specific to the settings portion of the UI diff --git a/cura/API/__init__.py b/cura/API/__init__.py index 26c9a4c829..97d0797430 100644 --- a/cura/API/__init__.py +++ b/cura/API/__init__.py @@ -12,13 +12,14 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -## The official Cura API that plug-ins can use to interact with Cura. -# -# Python does not technically prevent talking to other classes as well, but -# this API provides a version-safe interface with proper deprecation warnings -# etc. Usage of any other methods than the ones provided in this API can cause -# plug-ins to be unstable. class CuraAPI(QObject): + """The official Cura API that plug-ins can use to interact with Cura. + + Python does not technically prevent talking to other classes as well, but this API provides a version-safe + interface with proper deprecation warnings etc. Usage of any other methods than the ones provided in this API can + cause plug-ins to be unstable. + """ + # For now we use the same API version to be consistent. __instance = None # type: "CuraAPI" @@ -39,13 +40,10 @@ class CuraAPI(QObject): def __init__(self, application: Optional["CuraApplication"] = None) -> None: super().__init__(parent = CuraAPI._application) - # Accounts API self._account = Account(self._application) - # Backups API self._backups = Backups(self._application) - # Interface API self._interface = Interface(self._application) def initialize(self) -> None: @@ -53,12 +51,18 @@ class CuraAPI(QObject): @pyqtProperty(QObject, constant = True) def account(self) -> "Account": + """Accounts API""" + return self._account @property def backups(self) -> "Backups": + """Backups API""" + return self._backups @property def interface(self) -> "Interface": + """Interface API""" + return self._interface From 9b44ca37efb7b04fb821ee7abd740b75dd344b21 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Wed, 22 Apr 2020 21:42:09 +0200 Subject: [PATCH 03/27] Converted comments in dir Cura/cura/Arranging to rst style Converted doxygen style comments to reStructuredText style in the files found in Cura/cura/Arranging directory recursively using the script dox_2_rst.py (provided in the Uranium repo). Comments were manually checked and changed if needed. Various missing return types were added to complete Typing support --- cura/Arranging/Arrange.py | 86 ++++++++++--------- .../ArrangeObjectsAllBuildPlatesJob.py | 3 +- cura/Arranging/ShapeArray.py | 63 +++++++++----- 3 files changed, 88 insertions(+), 64 deletions(-) diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py index a70ccb9f0c..e83bf8e372 100644 --- a/cura/Arranging/Arrange.py +++ b/cura/Arranging/Arrange.py @@ -16,17 +16,20 @@ from collections import namedtuple import numpy import copy -## Return object for bestSpot LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"]) +"""Return object for bestSpot""" class Arrange: """ - The Arrange classed is used together with ShapeArray. Use it to find good locations for objects that you try to put + The Arrange classed is used together with :py:class:`cura.Arranging.ShapeArray.ShapeArray`. Use it to find good locations for objects that you try to put on a build place. Different priority schemes can be defined so it alters the behavior while using the same logic. - Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance. + .. note:: + + Make sure the scale is the same between :py:class:`cura.Arranging.ShapeArray.ShapeArray` objects and the :py:class:`cura.Arranging.Arrange.Arrange` instance. """ + build_volume = None # type: Optional[BuildVolume] def __init__(self, x, y, offset_x, offset_y, scale = 0.5): @@ -42,20 +45,20 @@ class Arrange: self._is_empty = True @classmethod - def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8): - """ - Helper to create an Arranger instance + def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8) -> "Arrange": + """Helper to create an :py:class:`cura.Arranging.Arrange.Arrange` instance Either fill in scene_root and create will find all sliceable nodes by itself, or use fixed_nodes to provide the nodes yourself. - :param scene_root: Root for finding all scene nodes - :param fixed_nodes: Scene nodes to be placed - :param scale: - :param x: - :param y: - :param min_offset: - :return: + + :param scene_root: Root for finding all scene nodes default = None + :param fixed_nodes: Scene nodes to be placed default = None + :param scale: default = 0.5 + :param x: default = 350 + :param y: default = 250 + :param min_offset: default = 8 """ + arranger = Arrange(x, y, x // 2, y // 2, scale = scale) arranger.centerFirst() @@ -90,19 +93,21 @@ class Arrange: arranger.place(0, 0, shape_arr, update_empty = False) return arranger - ## This resets the optimization for finding location based on size def resetLastPriority(self): + """This resets the optimization for finding location based on size""" + self._last_priority = 0 - def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1): - """ - Find placement for a node (using offset shape) and place it (using hull shape) - :param node: - :param offset_shape_arr: hapeArray with offset, for placing the shape - :param hull_shape_arr: ShapeArray without offset, used to find location - :param step: + def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool: + """ Find placement for a node (using offset shape) and place it (using hull shape) + + :param node: The node to be placed + :param offset_shape_arr: shape array with offset, for placing the shape + :param hull_shape_arr: shape array without offset, used to find location + :param step: default = 1 :return: the nodes that should be placed """ + best_spot = self.bestSpot( hull_shape_arr, start_prio = self._last_priority, step = step) x, y = best_spot.x, best_spot.y @@ -129,10 +134,8 @@ class Arrange: return found_spot def centerFirst(self): - """ - Fill priority, center is best. Lower value is better. - :return: - """ + """ Fill priority, center is best. Lower value is better. """ + # Square distance: creates a more round shape self._priority = numpy.fromfunction( lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32) @@ -140,23 +143,22 @@ class Arrange: self._priority_unique_values.sort() def backFirst(self): - """ - Fill priority, back is best. Lower value is better - :return: - """ + """ Fill priority, back is best. Lower value is better """ + self._priority = numpy.fromfunction( lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32) self._priority_unique_values = numpy.unique(self._priority) self._priority_unique_values.sort() - def checkShape(self, x, y, shape_arr): - """ - Return the amount of "penalty points" for polygon, which is the sum of priority + def checkShape(self, x, y, shape_arr) -> Optional[numpy.ndarray]: + """ Return the amount of "penalty points" for polygon, which is the sum of priority + :param x: x-coordinate to check shape - :param y: - :param shape_arr: the ShapeArray object to place + :param y: y-coordinate to check shape + :param shape_arr: the shape array object to place :return: None if occupied """ + x = int(self._scale * x) y = int(self._scale * y) offset_x = x + self._offset_x + shape_arr.offset_x @@ -180,14 +182,15 @@ class Arrange: offset_x:offset_x + shape_arr.arr.shape[1]] return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)]) - def bestSpot(self, shape_arr, start_prio = 0, step = 1): - """ - Find "best" spot for ShapeArray - :param shape_arr: + def bestSpot(self, shape_arr, start_prio = 0, step = 1) -> LocationSuggestion: + """ Find "best" spot for ShapeArray + + :param shape_arr: shape array :param start_prio: Start with this priority value (and skip the ones before) :param step: Slicing value, higher = more skips = faster but less accurate :return: namedtuple with properties x, y, penalty_points, priority. """ + start_idx_list = numpy.where(self._priority_unique_values == start_prio) if start_idx_list: try: @@ -210,15 +213,16 @@ class Arrange: return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-( def place(self, x, y, shape_arr, update_empty = True): - """ - Place the object. + """ Place the object. + Marks the locations in self._occupied and self._priority + :param x: :param y: :param shape_arr: :param update_empty: updates the _is_empty, used when adding disallowed areas - :return: """ + x = int(self._scale * x) y = int(self._scale * y) offset_x = x + self._offset_x + shape_arr.offset_x diff --git a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py index 7736efbeeb..0f337a229b 100644 --- a/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py +++ b/cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py @@ -18,8 +18,9 @@ from cura.Arranging.ShapeArray import ShapeArray from typing import List -## Do arrangements on multiple build plates (aka builtiplexer) class ArrangeArray: + """Do arrangements on multiple build plates (aka builtiplexer)""" + def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]) -> None: self._x = x self._y = y diff --git a/cura/Arranging/ShapeArray.py b/cura/Arranging/ShapeArray.py index 403db5e706..c704ae7ca2 100644 --- a/cura/Arranging/ShapeArray.py +++ b/cura/Arranging/ShapeArray.py @@ -11,19 +11,24 @@ if TYPE_CHECKING: from UM.Scene.SceneNode import SceneNode -## Polygon representation as an array for use with Arrange class ShapeArray: + """Polygon representation as an array for use with :py:class:`cura.Arranging.Arrange.Arrange`""" + def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None: self.arr = arr self.offset_x = offset_x self.offset_y = offset_y self.scale = scale - ## Instantiate from a bunch of vertices - # \param vertices - # \param scale scale the coordinates @classmethod def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray": + """Instantiate from a bunch of vertices + + :param vertices: + :param scale: scale the coordinates + :return: a shape array instantiated from a bunch of vertices + """ + # scale vertices = vertices * scale # flip y, x -> x, y @@ -44,12 +49,16 @@ class ShapeArray: arr[0][0] = 1 return cls(arr, offset_x, offset_y) - ## Instantiate an offset and hull ShapeArray from a scene node. - # \param node source node where the convex hull must be present - # \param min_offset offset for the offset ShapeArray - # \param scale scale the coordinates @classmethod def fromNode(cls, node: "SceneNode", min_offset: float, scale: float = 0.5, include_children: bool = False) -> Tuple[Optional["ShapeArray"], Optional["ShapeArray"]]: + """Instantiate an offset and hull ShapeArray from a scene node. + + :param node: source node where the convex hull must be present + :param min_offset: offset for the offset ShapeArray + :param scale: scale the coordinates + :return: A tuple containing an offset and hull shape array + """ + transform = node._transformation transform_x = transform._data[0][3] transform_y = transform._data[2][3] @@ -88,14 +97,19 @@ class ShapeArray: return offset_shape_arr, hull_shape_arr - ## Create np.array with dimensions defined by shape - # Fills polygon defined by vertices with ones, all other values zero - # Only works correctly for convex hull vertices - # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array - # \param shape numpy format shape, [x-size, y-size] - # \param vertices @classmethod def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array: + """Create :py:class:`numpy.ndarray` with dimensions defined by shape + + Fills polygon defined by vertices with ones, all other values zero + Only works correctly for convex hull vertices + Originally from: `Stackoverflow - generating a filled polygon inside a numpy array `_ + + :param shape: numpy format shape, [x-size, y-size] + :param vertices: + :return: numpy array with dimensions defined by shape + """ + base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill @@ -111,16 +125,21 @@ class ShapeArray: return base_array - ## Return indices that mark one side of the line, used by arrayFromPolygon - # Uses the line defined by p1 and p2 to check array of - # input indices against interpolated value - # Returns boolean array, with True inside and False outside of shape - # Originally from: http://stackoverflow.com/questions/37117878/generating-a-filled-polygon-inside-a-numpy-array - # \param p1 2-tuple with x, y for point 1 - # \param p2 2-tuple with x, y for point 2 - # \param base_array boolean array to project the line on @classmethod def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]: + """Return indices that mark one side of the line, used by arrayFromPolygon + + Uses the line defined by p1 and p2 to check array of + input indices against interpolated value + Returns boolean array, with True inside and False outside of shape + Originally from: `Stackoverflow - generating a filled polygon inside a numpy array `_ + + :param p1: 2-tuple with x, y for point 1 + :param p2: 2-tuple with x, y for point 2 + :param base_array: boolean array to project the line on + :return: A numpy array with indices that mark one side of the line + """ + if p1[0] == p2[0] and p1[1] == p2[1]: return None idxs = numpy.indices(base_array.shape) # Create 3D array of indices From 679739d09d1b0071e35cb89e15a7fd8d95c984a3 Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Mon, 27 Apr 2020 20:09:00 +0200 Subject: [PATCH 04/27] Applied requested changes proposed by @ninovanhooff Fixed a couple of missed double quotes, trailing and leading spaces, to long comment lines --- cura/API/Account.py | 2 +- cura/API/Backups.py | 2 +- cura/API/Interface/Settings.py | 2 +- cura/Arranging/Arrange.py | 12 ++++++------ cura/BuildVolume.py | 6 +++--- cura/CuraApplication.py | 7 +++---- cura/LayerDataBuilder.py | 4 ++-- cura/LayerPolygon.py | 18 +++++++++--------- cura/MachineAction.py | 2 +- 9 files changed, 27 insertions(+), 28 deletions(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index a049b2924f..fb38c04300 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -27,7 +27,7 @@ class Account(QObject): api = CuraAPI() api.account.login() api.account.logout() - api.account.userProfile Who is logged in`` + api.account.userProfile # Who is logged in """ loginStateChanged = pyqtSignal(bool) diff --git a/cura/API/Backups.py b/cura/API/Backups.py index 621dec01f2..cb5f2aa487 100644 --- a/cura/API/Backups.py +++ b/cura/API/Backups.py @@ -20,7 +20,7 @@ class Backups: from cura.API import CuraAPI api = CuraAPI() api.backups.createBackup() - api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})`` + api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"}) """ def __init__(self, application: "CuraApplication") -> None: diff --git a/cura/API/Interface/Settings.py b/cura/API/Interface/Settings.py index 947b37fc88..18653bc88e 100644 --- a/cura/API/Interface/Settings.py +++ b/cura/API/Interface/Settings.py @@ -26,7 +26,7 @@ class Settings: "actions": my_menu_actions, "menu_item": MyPluginAction(self) } - api.interface.settings.addContextMenuItem(data)`` + api.interface.settings.addContextMenuItem(data) """ def __init__(self, application: "CuraApplication") -> None: diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py index e83bf8e372..75f7a58c5e 100644 --- a/cura/Arranging/Arrange.py +++ b/cura/Arranging/Arrange.py @@ -99,7 +99,7 @@ class Arrange: self._last_priority = 0 def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool: - """ Find placement for a node (using offset shape) and place it (using hull shape) + """Find placement for a node (using offset shape) and place it (using hull shape) :param node: The node to be placed :param offset_shape_arr: shape array with offset, for placing the shape @@ -134,7 +134,7 @@ class Arrange: return found_spot def centerFirst(self): - """ Fill priority, center is best. Lower value is better. """ + """Fill priority, center is best. Lower value is better. """ # Square distance: creates a more round shape self._priority = numpy.fromfunction( @@ -143,7 +143,7 @@ class Arrange: self._priority_unique_values.sort() def backFirst(self): - """ Fill priority, back is best. Lower value is better """ + """Fill priority, back is best. Lower value is better """ self._priority = numpy.fromfunction( lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32) @@ -151,7 +151,7 @@ class Arrange: self._priority_unique_values.sort() def checkShape(self, x, y, shape_arr) -> Optional[numpy.ndarray]: - """ Return the amount of "penalty points" for polygon, which is the sum of priority + """Return the amount of "penalty points" for polygon, which is the sum of priority :param x: x-coordinate to check shape :param y: y-coordinate to check shape @@ -183,7 +183,7 @@ class Arrange: return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)]) def bestSpot(self, shape_arr, start_prio = 0, step = 1) -> LocationSuggestion: - """ Find "best" spot for ShapeArray + """Find "best" spot for ShapeArray :param shape_arr: shape array :param start_prio: Start with this priority value (and skip the ones before) @@ -213,7 +213,7 @@ class Arrange: return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority) # No suitable location found :-( def place(self, x, y, shape_arr, update_empty = True): - """ Place the object. + """Place the object. Marks the locations in self._occupied and self._priority diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 76316f5e43..cf0e18ad6a 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -885,7 +885,7 @@ class BuildVolume(SceneNode): It computes different disallowed areas depending on the offset of the extruder. The resulting dictionary will therefore have an entry for each extruder that is used. - + :param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance , etc. :param used_extruders: The extruder stacks to generate disallowed areas for. @@ -1117,8 +1117,8 @@ class BuildVolume(SceneNode): def getEdgeDisallowedSize(self): """Calculate the disallowed radius around the edge. - This disallowed radius is to allow for space around the models that is not part of the collision radius, such as - bed adhesion (skirt/brim/raft) and travel avoid distance. + This disallowed radius is to allow for space around the models that is not part of the collision radius, + such as bed adhesion (skirt/brim/raft) and travel avoid distance. """ if not self._global_container_stack or not self._global_container_stack.extruderList: diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index a588be3d5c..ee445cf0e3 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -440,8 +440,7 @@ class CuraApplication(QtApplication): def __setLatestResouceVersionsForVersionUpgrade(self): """Initializes the version upgrade manager with by providing the paths for each resource type and the latest - versions. - """ + versions. """ self._version_upgrade_manager.setCurrentVersions( { @@ -645,7 +644,7 @@ class CuraApplication(QtApplication): showMessageBox = pyqtSignal(str,str, str, str, int, int, arguments = ["title", "text", "informativeText", "detailedText","buttons", "icon"]) - """ A reusable dialogbox""" + """A reusable dialogbox""" def messageBox(self, title, text, informativeText = "", @@ -1873,7 +1872,7 @@ class CuraApplication(QtApplication): @pyqtSlot(str, result=bool) def checkIsValidProjectFile(self, file_url): - """ Checks if the given file URL is a valid project file. """ + """Checks if the given file URL is a valid project file. """ file_path = QUrl(file_url).toLocalFile() workspace_reader = self.getWorkspaceFileHandler().getReaderForFile(file_path) diff --git a/cura/LayerDataBuilder.py b/cura/LayerDataBuilder.py index 4650248780..e104273e37 100755 --- a/cura/LayerDataBuilder.py +++ b/cura/LayerDataBuilder.py @@ -46,8 +46,8 @@ class LayerDataBuilder(MeshBuilder): def build(self, material_color_map, line_type_brightness = 1.0): """Return the layer data as :py:class:`cura.LayerData.LayerData`. - :param material_color_map:: [r, g, b, a] for each extruder row. - :param line_type_brightness:: compatibility layer view uses line type brightness of 0.5 + :param material_color_map: [r, g, b, a] for each extruder row. + :param line_type_brightness: compatibility layer view uses line type brightness of 0.5 """ vertex_count = 0 diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py index 38d4f756a4..2cfff612b9 100644 --- a/cura/LayerPolygon.py +++ b/cura/LayerPolygon.py @@ -91,15 +91,15 @@ class LayerPolygon: The arrays are either by vertex or by indices. - :param vertex_offset: : determines where to start and end filling the arrays - :param index_offset: : determines where to start and end filling the arrays - :param vertices: : vertex numpy array to be filled - :param colors: : vertex numpy array to be filled - :param line_dimensions: : vertex numpy array to be filled - :param feedrates: : vertex numpy array to be filled - :param extruders: : vertex numpy array to be filled - :param line_types: : vertex numpy array to be filled - :param indices: : index numpy array to be filled + :param vertex_offset: determines where to start and end filling the arrays + :param index_offset: determines where to start and end filling the arrays + :param vertices: vertex numpy array to be filled + :param colors: vertex numpy array to be filled + :param line_dimensions: vertex numpy array to be filled + :param feedrates: vertex numpy array to be filled + :param extruders: vertex numpy array to be filled + :param line_types: vertex numpy array to be filled + :param indices: index numpy array to be filled """ if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None: diff --git a/cura/MachineAction.py b/cura/MachineAction.py index 4c2b4e7aea..e18c37ccb4 100644 --- a/cura/MachineAction.py +++ b/cura/MachineAction.py @@ -74,7 +74,7 @@ class MachineAction(QObject, PluginObject): def _reset(self) -> None: """Protected implementation of reset. - :py:meth:`cura.MachineAction.MachineAction.reset` + See also :py:meth:`cura.MachineAction.MachineAction.reset` """ pass From 8f3827d5aef4c5a35c55bed8ec4ffc6eb7fb6c57 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 8 May 2020 15:14:39 +0200 Subject: [PATCH 05/27] Convert doxygen to rst for 3MFReader/Writer and AMFReader --- plugins/3MFReader/ThreeMFReader.py | 43 +++++++----- plugins/3MFReader/ThreeMFWorkspaceReader.py | 73 ++++++++++++--------- plugins/3MFReader/WorkspaceDialog.py | 9 ++- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 8 ++- plugins/3MFWriter/ThreeMFWriter.py | 14 ++-- plugins/AMFReader/AMFReader.py | 12 ++-- 6 files changed, 95 insertions(+), 64 deletions(-) diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 1ef17458a6..32e12cb91a 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -32,8 +32,9 @@ except ImportError: import xml.etree.ElementTree as ET -## Base implementation for reading 3MF files. Has no support for textures. Only loads meshes! class ThreeMFReader(MeshReader): + """Base implementation for reading 3MF files. Has no support for textures. Only loads meshes!""" + def __init__(self) -> None: super().__init__() @@ -55,13 +56,17 @@ class ThreeMFReader(MeshReader): return Matrix() split_transformation = transformation.split() - ## Transformation is saved as: - ## M00 M01 M02 0.0 - ## M10 M11 M12 0.0 - ## M20 M21 M22 0.0 - ## M30 M31 M32 1.0 - ## We switch the row & cols as that is how everyone else uses matrices! temp_mat = Matrix() + """Transformation is saved as: + M00 M01 M02 0.0 + + M10 M11 M12 0.0 + + M20 M21 M22 0.0 + + M30 M31 M32 1.0 + We switch the row & cols as that is how everyone else uses matrices! + """ # Rotation & Scale temp_mat._data[0, 0] = split_transformation[0] temp_mat._data[1, 0] = split_transformation[1] @@ -80,9 +85,11 @@ class ThreeMFReader(MeshReader): return temp_mat - ## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node. - # \returns Scene node. def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]: + """Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node. + + :returns: Scene node. + """ try: node_name = savitar_node.getName() node_id = savitar_node.getId() @@ -243,15 +250,17 @@ class ThreeMFReader(MeshReader): return result - ## Create a scale vector based on a unit string. - # The core spec defines the following: - # * micron - # * millimeter (default) - # * centimeter - # * inch - # * foot - # * meter def _getScaleFromUnit(self, unit: Optional[str]) -> Vector: + """Create a scale vector based on a unit string. + + .. The core spec defines the following: + * micron + * millimeter (default) + * centimeter + * inch + * foot + * meter + """ conversion_to_mm = { "micron": 0.001, "millimeter": 1, diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 82b73c66d9..5230c68046 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -89,8 +89,9 @@ class ExtruderInfo: self.intent_info = None -## Base implementation for reading 3MF workspace files. class ThreeMFWorkspaceReader(WorkspaceReader): + """Base implementation for reading 3MF workspace files.""" + def __init__(self) -> None: super().__init__() @@ -130,18 +131,21 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._old_new_materials = {} 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: str): + """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. + """ 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] - ## Separates the given file list into a list of GlobalStack files and a list of ExtruderStack files. - # - # In old versions, extruder stack files have the same suffix as container stack files ".stack.cfg". - # def _determineGlobalAndExtruderStackFiles(self, project_file_name: str, file_list: List[str]) -> Tuple[str, List[str]]: + """Separates the given file list into a list of GlobalStack files and a list of ExtruderStack files. + + In old versions, extruder stack files have the same suffix as container stack files ".stack.cfg". + """ + archive = zipfile.ZipFile(project_file_name, "r") global_stack_file_list = [name for name in file_list if name.endswith(self._global_stack_suffix)] @@ -181,10 +185,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return global_stack_file_list[0], extruder_stack_file_list - ## read some info so we can make decisions - # \param file_name - # \param show_dialog In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog. def preRead(self, file_name, show_dialog=True, *args, **kwargs): + """Read some info so we can make decisions + + :param file_name: + :param show_dialog: In case we use preRead() to check if a file is a valid project file, + we don't want to show a dialog. + """ self._clearState() self._3mf_mesh_reader = Application.getInstance().getMeshFileHandler().getReaderForFile(file_name) @@ -578,15 +585,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return WorkspaceReader.PreReadResult.accepted - ## Read the project file - # Add all the definitions / materials / quality changes that do not exist yet. Then it loads - # all the stacks into the container registry. In some cases it will reuse the container for the global stack. - # It handles old style project files containing .stack.cfg as well as new style project files - # containing global.cfg / extruder.cfg - # - # \param file_name @call_on_qt_thread def read(self, file_name): + """Read the project file + + Add all the definitions / materials / quality changes that do not exist yet. Then it loads + all the stacks into the container registry. In some cases it will reuse the container for the global stack. + It handles old style project files containing .stack.cfg as well as new style project files + containing global.cfg / extruder.cfg + + :param file_name: + """ application = CuraApplication.getInstance() archive = zipfile.ZipFile(file_name, "r") @@ -856,19 +865,22 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._machine_info.quality_changes_info.name = quality_changes_name - ## Helper class to create a new quality changes profile. - # - # This will then later be filled with the appropriate data. - # \param quality_type The quality type of the new profile. - # \param intent_category The intent category of the new profile. - # \param name The name for the profile. This will later be made unique so - # it doesn't need to be unique yet. - # \param global_stack The global stack showing the configuration that the - # profile should be created for. - # \param extruder_stack The extruder stack showing the configuration that - # the profile should be created for. If this is None, it will be created - # for the global stack. def _createNewQualityChanges(self, quality_type: str, intent_category: Optional[str], name: str, global_stack: GlobalStack, extruder_stack: Optional[ExtruderStack]) -> InstanceContainer: + """Helper class to create a new quality changes profile. + + This will then later be filled with the appropriate data. + + :param quality_type: The quality type of the new profile. + :param intent_category: The intent category of the new profile. + :param name: The name for the profile. This will later be made unique so + it doesn't need to be unique yet. + :param global_stack: The global stack showing the configuration that the + profile should be created for. + :param extruder_stack: The extruder stack showing the configuration that + the profile should be created for. If this is None, it will be created + for the global stack. + """ + container_registry = CuraApplication.getInstance().getContainerRegistry() base_id = global_stack.definition.getId() if extruder_stack is None else extruder_stack.getId() new_id = base_id + "_" + name @@ -1077,9 +1089,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def _getXmlProfileClass(self): return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile")) - ## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data. @staticmethod def _getContainerIdListFromSerialized(serialized): + """Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.""" + parser = ConfigParser(interpolation = None, empty_lines_in_values = False) parser.read_string(serialized) diff --git a/plugins/3MFReader/WorkspaceDialog.py b/plugins/3MFReader/WorkspaceDialog.py index 3df7f1f570..3c97146583 100644 --- a/plugins/3MFReader/WorkspaceDialog.py +++ b/plugins/3MFReader/WorkspaceDialog.py @@ -229,9 +229,10 @@ class WorkspaceDialog(QObject): if key in self._result: self._result[key] = strategy - ## Close the backend: otherwise one could end up with "Slicing..." @pyqtSlot() def closeBackend(self): + """Close the backend: otherwise one could end up with "Slicing...""" + Application.getInstance().getBackend().close() def setMaterialConflict(self, material_conflict): @@ -283,8 +284,9 @@ class WorkspaceDialog(QObject): self.showDialogSignal.emit() @pyqtSlot() - ## Used to notify the dialog so the lock can be released. def notifyClosed(self): + """Used to notify the dialog so the lock can be released.""" + self._result = {} # The result should be cleared before hide, because after it is released the main thread lock self._visible = False try: @@ -319,8 +321,9 @@ class WorkspaceDialog(QObject): self._view.hide() self.hide() - ## Block thread until the dialog is closed. def waitForClose(self): + """Block thread until the dialog is closed.""" + if self._visible: if threading.current_thread() != threading.main_thread(): self._lock.acquire() diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 4b6b978342..a8d42ab2ee 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -107,11 +107,13 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): import json archive.writestr(file_in_archive, json.dumps(metadata, separators = (", ", ": "), indent = 4, skipkeys = True)) - ## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive. - # \param container That follows the \type{ContainerInterface} to archive. - # \param archive The archive to write to. @staticmethod def _writeContainerToArchive(container, archive): + """Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive. + + :param container: That follows the :type{ContainerInterface} to archive. + :param archive: The archive to write to. + """ if isinstance(container, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())): return # Empty file, do nothing. diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 05dc26f9ad..343b94a08b 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -60,15 +60,19 @@ class ThreeMFWriter(MeshWriter): result += str(matrix._data[2, 3]) return result - ## Should we store the archive - # Note that if this is true, the archive will not be closed. - # The object that set this parameter is then responsible for closing it correctly! def setStoreArchive(self, store_archive): + """Should we store the archive + + Note that if this is true, the archive will not be closed. + The object that set this parameter is then responsible for closing it correctly! + """ self._store_archive = store_archive - ## Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode - # \returns Uranium Scene node. def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()): + """Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode + + :returns: Uranium Scene node. + """ if not isinstance(um_node, SceneNode): return None diff --git a/plugins/AMFReader/AMFReader.py b/plugins/AMFReader/AMFReader.py index 794f2798ec..5196119d6c 100644 --- a/plugins/AMFReader/AMFReader.py +++ b/plugins/AMFReader/AMFReader.py @@ -147,13 +147,13 @@ class AMFReader(MeshReader): return group_node - ## Converts a Trimesh to Uranium's MeshData. - # \param tri_node A Trimesh containing the contents of a file that was - # just read. - # \param file_name The full original filename used to watch for changes - # \return Mesh data from the Trimesh in a way that Uranium can understand - # it. def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData: + """Converts a Trimesh to Uranium's MeshData. + + :param tri_node: A Trimesh containing the contents of a file that was just read. + :param file_name: The full original filename used to watch for changes + :return: Mesh data from the Trimesh in a way that Uranium can understand it. + """ tri_faces = tri_node.faces tri_vertices = tri_node.vertices From 797d6ed4381f70fa91b623dbd7b9e1b61053e04d Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 8 May 2020 15:23:51 +0200 Subject: [PATCH 06/27] Convert doxygen to rst for CuraEngineBackend --- .../CuraEngineBackend/CuraEngineBackend.py | 256 +++++++++++------- .../ProcessSlicedLayersJob.py | 22 +- plugins/CuraEngineBackend/StartSliceJob.py | 85 +++--- 3 files changed, 220 insertions(+), 143 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 3dd0589865..4de10c5ed0 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -42,12 +42,14 @@ catalog = i18nCatalog("cura") class CuraEngineBackend(QObject, Backend): backendError = Signal() - ## Starts the back-end plug-in. - # - # 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) -> None: + """Starts the back-end plug-in. + + This registers all the signal listeners and prepares for communication + with the back-end in general. + CuraEngineBackend is exposed to qml as well. + """ + 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. @@ -177,18 +179,22 @@ class CuraEngineBackend(QObject, Backend): self._machine_error_checker = self._application.getMachineErrorChecker() self._machine_error_checker.errorCheckFinished.connect(self._onStackErrorCheckFinished) - ## Terminate the engine process. - # - # This function should terminate the engine process. - # Called when closing the application. def close(self) -> None: + """Terminate the engine process. + + This function should terminate the engine process. + Called when closing the application. + """ + # 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) -> List[str]: + """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. + """ command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""] parser = argparse.ArgumentParser(prog = "cura", add_help = False) @@ -199,17 +205,18 @@ class CuraEngineBackend(QObject, Backend): return command - ## Emitted when we get a message containing print duration and material amount. - # This also implies the slicing has finished. - # \param time The amount of time the print will take. - # \param material_amount The amount of material the print will use. printDurationMessage = Signal() - - ## Emitted when the slicing process starts. + """Emitted when we get a message containing print duration and material amount. + + This also implies the slicing has finished. + :param time: The amount of time the print will take. + :param material_amount: The amount of material the print will use. + """ slicingStarted = Signal() + """Emitted when the slicing process starts.""" - ## Emitted when the slicing process is aborted forcefully. slicingCancelled = Signal() + """Emitted when the slicing process is aborted forcefully.""" @pyqtSlot() def stopSlicing(self) -> None: @@ -226,14 +233,16 @@ class CuraEngineBackend(QObject, Backend): if self._error_message: self._error_message.hide() - ## Manually triggers a reslice @pyqtSlot() def forceSlice(self) -> None: + """Manually triggers a reslice""" + self.markSliceAll() self.slice() - ## Perform a slice of the scene. def slice(self) -> None: + """Perform a slice of the scene.""" + Logger.log("i", "Starting to slice...") self._slice_start_time = time() if not self._build_plates_to_be_sliced: @@ -289,9 +298,11 @@ class CuraEngineBackend(QObject, Backend): self._start_slice_job.start() self._start_slice_job.finished.connect(self._onStartSliceCompleted) - ## Terminate the engine process. - # Start the engine process by calling _createSocket() def _terminate(self) -> None: + """Terminate the engine process. + + Start the engine process by calling _createSocket() + """ self._slicing = False self._stored_layer_data = [] if self._start_slice_job_build_plate in self._stored_optimized_layer_data: @@ -316,15 +327,17 @@ class CuraEngineBackend(QObject, Backend): except Exception as e: # terminating a process that is already terminating causes an exception, silently ignore this. Logger.log("d", "Exception occurred while trying to kill the engine %s", str(e)) - ## Event handler to call when the job to initiate the slicing process is - # completed. - # - # When the start slice job is successfully completed, it will be happily - # slicing. This function handles any errors that may occur during the - # bootstrapping of a slice job. - # - # \param job The start slice job that was just finished. def _onStartSliceCompleted(self, job: StartSliceJob) -> None: + """Event handler to call when the job to initiate the slicing process is + + completed. + + When the start slice job is successfully completed, it will be happily + slicing. This function handles any errors that may occur during the + bootstrapping of a slice job. + + :param job: The start slice job that was just finished. + """ if self._error_message: self._error_message.hide() @@ -443,11 +456,13 @@ class CuraEngineBackend(QObject, Backend): if self._slice_start_time: Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time ) - ## Determine enable or disable auto slicing. Return True for enable timer and False otherwise. - # It disables when - # - preference auto slice is off - # - decorator isBlockSlicing is found (used in g-code reader) def determineAutoSlicing(self) -> bool: + """Determine enable or disable auto slicing. Return True for enable timer and False otherwise. + + It disables when: + - preference auto slice is off + - decorator isBlockSlicing is found (used in g-code reader) + """ enable_timer = True self._is_disabled = False @@ -472,8 +487,9 @@ class CuraEngineBackend(QObject, Backend): self.disableTimer() return False - ## Return a dict with number of objects per build plate def _numObjectsPerBuildPlate(self) -> Dict[int, int]: + """Return a dict with number of objects per build plate""" + num_objects = defaultdict(int) #type: Dict[int, int] for node in DepthFirstIterator(self._scene.getRoot()): # Only count sliceable objects @@ -483,12 +499,14 @@ class CuraEngineBackend(QObject, Backend): num_objects[build_plate_number] += 1 return num_objects - ## Listener for when the scene has changed. - # - # 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: SceneNode) -> None: + """Listener for when the scene has changed. + + This should start a slice if the scene is now ready to slice. + + :param source: The scene node that was changed. + """ + if not source.callDecoration("isSliceable"): return @@ -536,10 +554,12 @@ class CuraEngineBackend(QObject, Backend): self._invokeSlice() - ## Called when an error occurs in the socket connection towards the engine. - # - # \param error The exception that occurred. def _onSocketError(self, error: Arcus.Error) -> None: + """Called when an error occurs in the socket connection towards the engine. + + :param error: The exception that occurred. + """ + if self._application.isShuttingDown(): return @@ -567,8 +587,9 @@ class CuraEngineBackend(QObject, Backend): break return has_slicable - ## Remove old layer data (if any) def _clearLayerData(self, build_plate_numbers: Set = None) -> None: + """Remove old layer data (if any)""" + # Clear out any old gcode self._scene.gcode_dict = {} # type: ignore @@ -583,8 +604,9 @@ class CuraEngineBackend(QObject, Backend): 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) -> None: + """Convenient function: mark everything to slice, emit state and clear layer data""" + # CURA-6604: If there's no slicable object, do not (try to) trigger slice, which will clear all the current # gcode. This can break Gcode file loading if it tries to remove it afterwards. if not self.hasSlicableObject(): @@ -597,10 +619,12 @@ class CuraEngineBackend(QObject, Backend): # With manually having to slice, we want to clear the old invalid layer data. self._clearLayerData() - ## 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: SettingInstance, property: str) -> None: + """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. + """ if property == "value": # Only reslice if the value has changed. self.needsSlicing() self._onChanged() @@ -618,25 +642,31 @@ class CuraEngineBackend(QObject, Backend): self.needsSlicing() self._onChanged() - ## 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: Arcus.PythonMessage) -> None: + """Called when a sliced layer data message is received from the engine. + + :param message: The protobuf message containing sliced layer data. + """ + 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: Arcus.PythonMessage) -> None: + """Called when an optimized sliced layer data message is received from the engine. + + :param message: The protobuf message containing sliced layer data. + """ + if self._start_slice_job_build_plate is not 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) - ## Called when a progress message is received from the engine. - # - # \param message The protobuf message containing the slicing progress. def _onProgressMessage(self, message: Arcus.PythonMessage) -> None: + """Called when a progress message is received from the engine. + + :param message: The protobuf message containing the slicing progress. + """ + self.processingProgress.emit(message.amount) self.setState(BackendState.Processing) @@ -653,10 +683,12 @@ class CuraEngineBackend(QObject, Backend): else: self._change_timer.start() - ## 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: Arcus.PythonMessage) -> None: + """Called when the engine sends a message that slicing is finished. + + :param message: The protobuf message signalling that slicing is finished. + """ + self.setState(BackendState.Done) self.processingProgress.emit(1.0) @@ -698,27 +730,32 @@ class CuraEngineBackend(QObject, Backend): self.enableTimer() # manually enable timer to be able to invoke slice, also when in manual slice mode self._invokeSlice() - ## 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: Arcus.PythonMessage) -> None: + """Called when a g-code message is received from the engine. + + :param message: The protobuf message containing g-code, encoded as UTF-8. + """ + try: 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. except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. pass # Throw the message away. - ## 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: Arcus.PythonMessage) -> None: + """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. + """ + try: 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. except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. pass # Throw the message away. - ## Creates a new socket connection. def _createSocket(self, protocol_file: str = None) -> None: + """Creates a new socket connection.""" + if not protocol_file: plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId()) if not plugin_path: @@ -728,10 +765,12 @@ class CuraEngineBackend(QObject, Backend): 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: Any, **kwargs: Any) -> None: + """Called when anything has changed to the stuff that needs to be sliced. + + This indicates that we should probably re-slice soon. + """ + self.needsSlicing() if self._use_timer: # if the error check is scheduled, wait for the error check finish signal to trigger auto-slice, @@ -745,11 +784,13 @@ class CuraEngineBackend(QObject, Backend): else: self._change_timer.start() - ## Called when a print time message is received from the engine. - # - # \param message The protobuf message containing the print time per feature and - # material amount per extruder def _onPrintTimeMaterialEstimates(self, message: Arcus.PythonMessage) -> None: + """Called when a print time message is received from the engine. + + :param message: The protobuf message containing the print time per feature and + material amount per extruder + """ + material_amounts = [] for index in range(message.repeatedMessageCount("materialEstimates")): material_amounts.append(message.getRepeatedMessage("materialEstimates", index).material_amount) @@ -757,10 +798,12 @@ class CuraEngineBackend(QObject, Backend): times = self._parseMessagePrintTimes(message) self.printDurationMessage.emit(self._start_slice_job_build_plate, times, material_amounts) - ## 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: Arcus.PythonMessage) -> Dict[str, float]: + """Called for parsing message to retrieve estimated time per feature + + :param message: The protobuf message containing the print time per feature + """ + result = { "inset_0": message.time_inset_0, "inset_x": message.time_inset_x, @@ -777,19 +820,22 @@ class CuraEngineBackend(QObject, Backend): } return result - ## Called when the back-end connects to the front-end. def _onBackendConnected(self) -> None: + """Called when the back-end connects to the front-end.""" + if self._restart: self._restart = False self._onChanged() - ## Called when the user starts using some tool. - # - # When the user starts using a tool, we should pause slicing to prevent - # continuously slicing while the user is dragging some tool handle. - # - # \param tool The tool that the user is using. def _onToolOperationStarted(self, tool: Tool) -> None: + """Called when the user starts using some tool. + + When the user starts using a tool, we should pause slicing to prevent + continuously slicing while the user is dragging some tool handle. + + :param tool: The tool that the user is using. + """ + 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 @@ -797,12 +843,14 @@ class CuraEngineBackend(QObject, Backend): self._terminate() self._createSocket() - ## Called when the user stops using some tool. - # - # This indicates that we can safely start slicing again. - # - # \param tool The tool that the user was using. def _onToolOperationStopped(self, tool: Tool) -> None: + """Called when the user stops using some tool. + + This indicates that we can safely start slicing again. + + :param tool: The tool that the user was using. + """ + self._tool_active = False # React on scene change again self.determineAutoSlicing() # Switch timer on if appropriate # Process all the postponed scene changes @@ -816,8 +864,9 @@ class CuraEngineBackend(QObject, Backend): 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) -> None: + """Called when the user changes the active view mode.""" + view = self._application.getController().getActiveView() if view: active_build_plate = self._application.getMultiBuildPlateModel().activeBuildPlate @@ -835,17 +884,20 @@ class CuraEngineBackend(QObject, Backend): else: self._layer_view_active = False - ## Called when the back-end self-terminates. - # - # We should reset our state and start listening for new connections. def _onBackendQuit(self) -> None: + """Called when the back-end self-terminates. + + We should reset our state and start listening for new connections. + """ + if not self._restart: if self._process: # type: ignore Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) # type: ignore self._process = None # type: ignore - ## Called when the global container stack changes def _onGlobalStackChanged(self) -> None: + """Called when the global container stack changes""" + if self._global_container_stack: self._global_container_stack.propertyChanged.disconnect(self._onSettingChanged) self._global_container_stack.containersChanged.disconnect(self._onChanged) @@ -874,15 +926,18 @@ class CuraEngineBackend(QObject, Backend): Logger.log("d", "See if there is more to slice(2)...") self._invokeSlice() - ## Connect slice function to timer. def enableTimer(self) -> None: + """Connect slice function to timer.""" + 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) -> None: + """Disconnect slice function from timer. + + This means that slicing will not be triggered automatically + """ if self._use_timer: self._use_timer = False self._change_timer.timeout.disconnect(self.slice) @@ -894,8 +949,9 @@ class CuraEngineBackend(QObject, Backend): if auto_slice: self._change_timer.start() - ## Tickle the backend so in case of auto slicing, it starts the timer. def tickle(self) -> None: + """Tickle the backend so in case of auto slicing, it starts the timer.""" + if self._use_timer: self._change_timer.start() diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 32d60eb68b..9cd48cdc94 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -28,10 +28,12 @@ from cura.Machines.Models.ExtrudersModel import ExtrudersModel catalog = i18nCatalog("cura") -## Return a 4-tuple with floats 0-1 representing the html color code -# -# \param color_code html color code, i.e. "#FF0000" -> red def colorCodeToRGBA(color_code): + """Return a 4-tuple with floats 0-1 representing the html color code + + :param color_code: html color code, i.e. "#FF0000" -> red + """ + if color_code is None: Logger.log("w", "Unable to convert color code, returning default") return [0, 0, 0, 1] @@ -51,13 +53,15 @@ class ProcessSlicedLayersJob(Job): self._abort_requested = False self._build_plate_number = None - ## Aborts the processing of layers. - # - # This abort is made on a best-effort basis, meaning that the actual - # job thread will check once in a while to see whether an abort is - # requested and then stop processing by itself. There is no guarantee - # that the abort will stop the job any time soon or even at all. def abort(self): + """Aborts the processing of layers. + + This abort is made on a best-effort basis, meaning that the actual + job thread will check once in a while to see whether an abort is + requested and then stop processing by itself. There is no guarantee + that the abort will stop the job any time soon or even at all. + """ + self._abort_requested = True def setBuildPlate(self, new_value): diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index a99c559bac..ef9aeef122 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -40,8 +40,9 @@ class StartJobResult(IntEnum): ObjectsWithDisabledExtruder = 8 -## Formatter class that handles token expansion in start/end gcode class GcodeStartEndFormatter(Formatter): + """Formatter class that handles token expansion in start/end gcode""" + def __init__(self, default_extruder_nr: int = -1) -> None: super().__init__() self._default_extruder_nr = default_extruder_nr @@ -82,8 +83,9 @@ class GcodeStartEndFormatter(Formatter): return value -## Job class that builds up the message of scene data to send to CuraEngine. class StartSliceJob(Job): + """Job class that builds up the message of scene data to send to CuraEngine.""" + def __init__(self, slice_message: Arcus.PythonMessage) -> None: super().__init__() @@ -100,9 +102,11 @@ class StartSliceJob(Job): 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: ContainerStack) -> bool: + """Check if a stack has any errors.""" + + """returns true if it has errors, false otherwise.""" + if stack is None: return False @@ -119,8 +123,9 @@ class StartSliceJob(Job): Job.yieldThread() return False - ## Runs the job that initiates the slicing. def run(self) -> None: + """Runs the job that initiates the slicing.""" + if self._build_plate_number is None: self.setResult(StartJobResult.Error) return @@ -323,14 +328,14 @@ class StartSliceJob(Job): def setIsCancelled(self, value: bool): self._is_cancelled = value - ## Creates a dictionary of tokens to replace in g-code pieces. - # - # This indicates what should be replaced in the start and end g-codes. - # \param stack The stack to get the settings from to replace the tokens - # with. - # \return A dictionary of replacement tokens to the values they should be - # replaced with. def _buildReplacementTokens(self, stack: ContainerStack) -> Dict[str, Any]: + """Creates a dictionary of tokens to replace in g-code pieces. + + This indicates what should be replaced in the start and end g-codes. + :param stack: The stack to get the settings from to replace the tokens with. + :return: A dictionary of replacement tokens to the values they should be replaced with. + """ + result = {} for key in stack.getAllKeys(): value = stack.getProperty(key, "value") @@ -358,10 +363,12 @@ class StartSliceJob(Job): extruder_nr = extruder_stack.getProperty("extruder_nr", "value") self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack) - ## 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) -> str: + """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 + """ if not self._all_extruders_settings: self._cacheAllExtruderSettings() @@ -377,8 +384,9 @@ class StartSliceJob(Job): Logger.logException("w", "Unable to do token replacement on start/end g-code") return str(value) - ## Create extruder message from stack def _buildExtruderMessage(self, stack: ContainerStack) -> None: + """Create extruder message from stack""" + message = self._slice_message.addRepeatedMessage("extruders") message.id = int(stack.getMetaDataEntry("position")) if not self._all_extruders_settings: @@ -407,11 +415,13 @@ class StartSliceJob(Job): setting.value = str(value).encode("utf-8") Job.yieldThread() - ## Sends all global settings to the engine. - # - # The settings are taken from the global stack. This does not include any - # per-extruder settings or per-object settings. def _buildGlobalSettingsMessage(self, stack: ContainerStack) -> None: + """Sends all global settings to the engine. + + The settings are taken from the global stack. This does not include any + per-extruder settings or per-object settings. + """ + if not self._all_extruders_settings: self._cacheAllExtruderSettings() @@ -445,15 +455,16 @@ class StartSliceJob(Job): setting_message.value = str(value).encode("utf-8") Job.yieldThread() - ## Sends for some settings which extruder they should fallback to if not - # set. - # - # This is only set for settings that have the limit_to_extruder - # property. - # - # \param stack The global stack with all settings, from which to read the - # limit_to_extruder property. def _buildGlobalInheritsStackMessage(self, stack: ContainerStack) -> None: + """Sends for some settings which extruder they should fallback to if not set. + + This is only set for settings that have the limit_to_extruder + property. + + :param stack: The global stack with all settings, from which to read the + limit_to_extruder property. + """ + 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. @@ -462,10 +473,13 @@ class StartSliceJob(Job): setting_extruder.extruder = extruder_position Job.yieldThread() - ## Check if a node has per object settings and ensure that they are set correctly in the message - # \param node Node to check. - # \param message object_lists message to put the per object settings in def _handlePerObjectSettings(self, node: CuraSceneNode, message: Arcus.PythonMessage): + """Check if a node has per object settings and ensure that they are set correctly in the message + + :param node: Node to check. + :param message: object_lists message to put the per object settings in + """ + stack = node.callDecoration("getStack") # Check if the node has a stack attached to it and the stack has any settings in the top container. @@ -501,10 +515,13 @@ class StartSliceJob(Job): Job.yieldThread() - ## Recursive function to put all settings that require each other for value changes in a list - # \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: Set[str], relations: List[SettingRelation]): + """Recursive function to put all settings that require each other for value changes in a list + + :param relations_set: Set of keys of settings that are influenced + :param relations: list of relation objects that need to be checked. + """ + for relation in filter(lambda r: r.role == "value" or r.role == "limit_to_extruder", relations): if relation.type == RelationType.RequiresTarget: continue From d69bf844243eb10a398fcda9875c7f04433c302a Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 8 May 2020 15:39:16 +0200 Subject: [PATCH 07/27] Updated comments in Backup Converted doxygen style comments to reStructuredText style in the files found in Cura/cura/Backup directory recursively using the script dox_2_rst.py (provided in the Uranium repo). Comments were manually checked and changed if needed. --- cura/Backups/Backup.py | 47 +++++++++++++++++++++------------- cura/Backups/BackupsManager.py | 2 ++ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 4d24a46384..6d1906e1dc 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -18,24 +18,26 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -## The back-up class holds all data about a back-up. -# -# It is also responsible for reading and writing the zip file to the user data -# folder. class Backup: - # These files should be ignored when making a backup. - IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"] + """The back-up class holds all data about a back-up. + + It is also responsible for reading and writing the zip file to the user data folder. + """ + + IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"] + """These files should be ignored when making a backup.""" - # Re-use translation catalog. catalog = i18nCatalog("cura") + """Re-use translation catalog""" def __init__(self, application: "CuraApplication", zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None: self._application = application self.zip_file = zip_file # type: Optional[bytes] self.meta_data = meta_data # type: Optional[Dict[str, str]] - ## Create a back-up from the current user config folder. def makeFromCurrent(self) -> None: + """Create a back-up from the current user config folder.""" + cura_release = self._application.getVersion() version_data_dir = Resources.getDataStoragePath() @@ -77,10 +79,13 @@ class Backup: "plugin_count": str(plugin_count) } - ## Make a full archive from the given root path with the given name. - # \param root_path The root directory to archive recursively. - # \return The archive as bytes. def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]: + """Make a full archive from the given root path with the given name. + + :param root_path: The root directory to archive recursively. + :return: The archive as bytes. + """ + ignore_string = re.compile("|".join(self.IGNORED_FILES)) try: archive = ZipFile(buffer, "w", ZIP_DEFLATED) @@ -99,13 +104,17 @@ class Backup: "Could not create archive from user data directory: {}".format(error))) return None - ## Show a UI message. def _showMessage(self, message: str) -> None: + """Show a UI message.""" + Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show() - ## Restore this back-up. - # \return Whether we had success or not. def restore(self) -> bool: + """Restore this back-up. + + :return: Whether we had success or not. + """ + if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None): # We can restore without the minimum required information. Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.") @@ -139,12 +148,14 @@ class Backup: return extracted - ## Extract the whole archive to the given target path. - # \param archive The archive as ZipFile. - # \param target_path The target path. - # \return Whether we had success or not. @staticmethod def _extractArchive(archive: "ZipFile", target_path: str) -> bool: + """Extract the whole archive to the given target path. + + :param archive: The archive as ZipFile. + :param target_path: The target path. + :return: Whether we had success or not. + """ # Implement security recommendations: Sanity check on zip files will make it harder to spoof. from cura.CuraApplication import CuraApplication diff --git a/cura/Backups/BackupsManager.py b/cura/Backups/BackupsManager.py index f335a3fb04..fb758455c1 100644 --- a/cura/Backups/BackupsManager.py +++ b/cura/Backups/BackupsManager.py @@ -24,6 +24,7 @@ class BackupsManager: def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]: """ 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). """ @@ -37,6 +38,7 @@ class BackupsManager: def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None: """ Restore a back-up from a given ZipFile. + :param zip_file: A bytes object containing the actual back-up. :param meta_data: A dict containing some metadata that is needed to restore the back-up correctly. """ From 54d2fe95d15e43c53261cdccf11f0f6fe81c1a2d Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 8 May 2020 15:43:12 +0200 Subject: [PATCH 08/27] Convert doxygen to rst for CuraProfileReader/Writer --- .../CuraProfileReader/CuraProfileReader.py | 70 +++++++++++-------- .../CuraProfileWriter/CuraProfileWriter.py | 17 +++-- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/plugins/CuraProfileReader/CuraProfileReader.py b/plugins/CuraProfileReader/CuraProfileReader.py index d4e5d393b2..fa65033691 100644 --- a/plugins/CuraProfileReader/CuraProfileReader.py +++ b/plugins/CuraProfileReader/CuraProfileReader.py @@ -13,23 +13,30 @@ from cura.ReaderWriters.ProfileReader import ProfileReader import zipfile -## A plugin that reads profile data from Cura profile files. -# -# It reads a profile from a .curaprofile file, and returns it as a profile -# instance. + class CuraProfileReader(ProfileReader): - ## Initialises the cura profile reader. - # This does nothing since the only other function is basically stateless. + """A plugin that reads profile data from Cura profile files. + + It reads a profile from a .curaprofile file, and returns it as a profile + instance. + """ + def __init__(self) -> None: + """Initialises the cura profile reader. + + This does nothing since the only other function is basically stateless. + """ super().__init__() - ## Reads a cura profile from a file and returns it. - # - # \param file_name The file to read the cura profile from. - # \return The cura profiles that were in the file, if any. If the file - # could not be read or didn't contain a valid profile, ``None`` is - # returned. def read(self, file_name: str) -> List[Optional[InstanceContainer]]: + """Reads a cura profile from a file and returns it. + + :param file_name: The file to read the cura profile from. + :return: The cura profiles that were in the file, if any. If the file + could not be read or didn't contain a valid profile, ``None`` is + returned. + """ + try: with zipfile.ZipFile(file_name, "r") as archive: results = [] # type: List[Optional[InstanceContainer]] @@ -50,13 +57,14 @@ class CuraProfileReader(ProfileReader): serialized_bytes = fhandle.read() return [self._loadProfile(serialized, profile_id) for serialized, profile_id in self._upgradeProfile(serialized_bytes, file_name)] - ## Convert a profile from an old Cura to this Cura if needed. - # - # \param serialized The profile data to convert in the serialized on-disk - # format. - # \param profile_id The name of the profile. - # \return List of serialized profile strings and matching profile names. def _upgradeProfile(self, serialized: str, profile_id: str) -> List[Tuple[str, str]]: + """Convert a profile from an old Cura to this Cura if needed. + + :param serialized: The profile data to convert in the serialized on-disk format. + :param profile_id: The name of the profile. + :return: List of serialized profile strings and matching profile names. + """ + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -75,12 +83,14 @@ class CuraProfileReader(ProfileReader): else: return [(serialized, profile_id)] - ## Load a profile from a serialized string. - # - # \param serialized The profile data to read. - # \param profile_id The name of the profile. - # \return The profile that was stored in the string. def _loadProfile(self, serialized: str, profile_id: str) -> Optional[InstanceContainer]: + """Load a profile from a serialized string. + + :param serialized: The profile data to read. + :param profile_id: The name of the profile. + :return: The profile that was stored in the string. + """ + # Create an empty profile. profile = InstanceContainer(profile_id) profile.setMetaDataEntry("type", "quality_changes") @@ -102,13 +112,15 @@ class CuraProfileReader(ProfileReader): profile.setMetaDataEntry("definition", active_quality_definition) return profile - ## Upgrade a serialized profile to the current profile format. - # - # \param serialized The profile data to convert. - # \param profile_id The name of the profile. - # \param source_version The profile version of 'serialized'. - # \return List of serialized profile strings and matching profile names. def _upgradeProfileVersion(self, serialized: str, profile_id: str, main_version: int, setting_version: int) -> List[Tuple[str, str]]: + """Upgrade a serialized profile to the current profile format. + + :param serialized: The profile data to convert. + :param profile_id: The name of the profile. + :param source_version: The profile version of 'serialized'. + :return: List of serialized profile strings and matching profile names. + """ + source_version = main_version * 1000000 + setting_version from UM.VersionUpgradeManager import VersionUpgradeManager diff --git a/plugins/CuraProfileWriter/CuraProfileWriter.py b/plugins/CuraProfileWriter/CuraProfileWriter.py index 78f0b078d9..6643bd8d1f 100644 --- a/plugins/CuraProfileWriter/CuraProfileWriter.py +++ b/plugins/CuraProfileWriter/CuraProfileWriter.py @@ -6,15 +6,18 @@ from UM.Logger import Logger from cura.ReaderWriters.ProfileWriter import ProfileWriter import zipfile -## Writes profiles to Cura's own profile format with config files. class CuraProfileWriter(ProfileWriter): - ## Writes a profile to the specified file path. - # - # \param path \type{string} The file to output to. - # \param profiles \type{Profile} \type{List} The profile(s) to write to that file. - # \return \code True \endcode if the writing was successful, or \code - # False \endcode if it wasn't. + """Writes profiles to Cura's own profile format with config files.""" + def write(self, path, profiles): + """Writes a profile to the specified file path. + + :param path: :type{string} The file to output to. + :param profiles: :type{Profile} :type{List} The profile(s) to write to that file. + :return: True if the writing was successful, or + False if it wasn't. + """ + if type(profiles) != list: profiles = [profiles] From 6ca9b4678e7a2bb6bb6f0122b71e32e81f6884a3 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 8 May 2020 15:46:02 +0200 Subject: [PATCH 09/27] Convert doxygen to rst for Firmware update plugins --- .../FirmwareUpdateChecker.py | 25 +++++++++++-------- .../FirmwareUpdateCheckerJob.py | 3 ++- .../FirmwareUpdaterMachineAction.py | 4 ++- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py index 9c4d498d7e..92678cb546 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py @@ -18,10 +18,12 @@ from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage i18n_catalog = i18nCatalog("cura") -## This Extension checks for new versions of the firmware based on the latest checked version number. -# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy -# to change it to work for other applications. class FirmwareUpdateChecker(Extension): + """This Extension checks for new versions of the firmware based on the latest checked version number. + + The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy + to change it to work for other applications. + """ def __init__(self) -> None: super().__init__() @@ -35,8 +37,9 @@ class FirmwareUpdateChecker(Extension): self._check_job = None self._checked_printer_names = set() # type: Set[str] - ## Callback for the message that is spawned when there is a new version. def _onActionTriggered(self, message, action): + """Callback for the message that is spawned when there is a new version.""" + if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD: machine_id = message.getMachineId() download_url = message.getDownloadUrl() @@ -57,13 +60,15 @@ class FirmwareUpdateChecker(Extension): def _onJobFinished(self, *args, **kwargs): self._check_job = None - ## Connect with software.ultimaker.com, load latest.version and check version info. - # If the version info is different from the current version, spawn a message to - # allow the user to download it. - # - # \param silent type(boolean) Suppresses messages other than "new version found" messages. - # This is used when checking for a new firmware version at startup. def checkFirmwareVersion(self, container = None, silent = False): + """Connect with software.ultimaker.com, load latest.version and check version info. + + If the version info is different from the current version, spawn a message to + allow the user to download it. + + :param silent: type(boolean) Suppresses messages other than "new version found" messages. + This is used when checking for a new firmware version at startup. + """ container_name = container.definition.getName() if container_name in self._checked_printer_names: return diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py index 279b397777..f049542db1 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py @@ -21,8 +21,9 @@ from UM.i18n import i18nCatalog i18n_catalog = i18nCatalog("cura") -## This job checks if there is an update available on the provided URL. class FirmwareUpdateCheckerJob(Job): + """This job checks if there is an update available on the provided URL.""" + STRING_ZERO_VERSION = "0.0.0" STRING_EPSILON_VERSION = "0.0.1" ZERO_VERSION = Version(STRING_ZERO_VERSION) diff --git a/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py b/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py index e2b0041674..35f338fb04 100644 --- a/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py +++ b/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py @@ -19,8 +19,10 @@ if MYPY: catalog = i18nCatalog("cura") -## Upgrade the firmware of a machine by USB with this action. + class FirmwareUpdaterMachineAction(MachineAction): + """Upgrade the firmware of a machine by USB with this action.""" + def __init__(self) -> None: super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Update Firmware")) self._qml_url = "FirmwareUpdaterMachineAction.qml" From 40327c42593e943ce91a2459eb331933cd376dbd Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 8 May 2020 15:58:51 +0200 Subject: [PATCH 10/27] Convert doxygen to rst for GcodeReader, GcodeGzReader/Writer, GCodeProfileReader --- plugins/GCodeGzReader/GCodeGzReader.py | 9 ++- plugins/GCodeGzWriter/GCodeGzWriter.py | 33 ++++---- .../GCodeProfileReader/GCodeProfileReader.py | 78 +++++++++++-------- plugins/GCodeReader/FlavorParser.py | 22 ++++-- plugins/GCodeReader/RepRapFlavorParser.py | 20 +++-- 5 files changed, 98 insertions(+), 64 deletions(-) diff --git a/plugins/GCodeGzReader/GCodeGzReader.py b/plugins/GCodeGzReader/GCodeGzReader.py index a528b494e9..b4d9e85b3e 100644 --- a/plugins/GCodeGzReader/GCodeGzReader.py +++ b/plugins/GCodeGzReader/GCodeGzReader.py @@ -7,10 +7,13 @@ from UM.Mesh.MeshReader import MeshReader #The class we're extending/implementin from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType #To add the .gcode.gz files to the MIME type database. from UM.PluginRegistry import PluginRegistry -## A file reader that reads gzipped g-code. -# -# If you're zipping g-code, you might as well use gzip! + class GCodeGzReader(MeshReader): + """A file reader that reads gzipped g-code. + + If you're zipping g-code, you might as well use gzip! + """ + def __init__(self) -> None: super().__init__() MimeTypeDatabase.addMimeType( diff --git a/plugins/GCodeGzWriter/GCodeGzWriter.py b/plugins/GCodeGzWriter/GCodeGzWriter.py index cbbfb8f986..e9dcbad76b 100644 --- a/plugins/GCodeGzWriter/GCodeGzWriter.py +++ b/plugins/GCodeGzWriter/GCodeGzWriter.py @@ -13,26 +13,31 @@ from UM.Scene.SceneNode import SceneNode #For typing. from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") -## A file writer that writes gzipped g-code. -# -# If you're zipping g-code, you might as well use gzip! + class GCodeGzWriter(MeshWriter): + """A file writer that writes gzipped g-code. + + If you're zipping g-code, you might as well use gzip! + """ + def __init__(self) -> None: super().__init__(add_to_recent_files = False) - ## Writes the gzipped g-code to a stream. - # - # Note that even though the function accepts a collection of nodes, the - # entire scene is always written to the file since it is not possible to - # separate the g-code for just specific nodes. - # - # \param stream The stream to write the gzipped g-code to. - # \param nodes This is ignored. - # \param mode Additional information on what type of stream to use. This - # must always be binary mode. - # \return Whether the write was successful. def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool: + """Writes the gzipped g-code to a stream. + + Note that even though the function accepts a collection of nodes, the + entire scene is always written to the file since it is not possible to + separate the g-code for just specific nodes. + + :param stream: The stream to write the gzipped g-code to. + :param nodes: This is ignored. + :param mode: Additional information on what type of stream to use. This + must always be binary mode. + :return: Whether the write was successful. + """ + if mode != MeshWriter.OutputMode.BinaryMode: Logger.log("e", "GCodeGzWriter does not support text mode.") self.setInformation(catalog.i18nc("@error:not supported", "GCodeGzWriter does not support text mode.")) diff --git a/plugins/GCodeProfileReader/GCodeProfileReader.py b/plugins/GCodeProfileReader/GCodeProfileReader.py index 9fbae7b473..2b80e80085 100644 --- a/plugins/GCodeProfileReader/GCodeProfileReader.py +++ b/plugins/GCodeProfileReader/GCodeProfileReader.py @@ -12,40 +12,48 @@ catalog = i18nCatalog("cura") from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException -## A class that reads profile data from g-code files. -# -# It reads the profile data from g-code files and stores it in a new profile. -# This class currently does not process the rest of the g-code in any way. class GCodeProfileReader(ProfileReader): - ## The file format version of the serialized g-code. - # - # It can only read settings with the same version as the version it was - # written with. If the file format is changed in a way that breaks reverse - # compatibility, increment this version number! - version = 3 + """A class that reads profile data from g-code files. + + It reads the profile data from g-code files and stores it in a new profile. + This class currently does not process the rest of the g-code in any way. + """ + + version = 3 + """The file format version of the serialized g-code. + + It can only read settings with the same version as the version it was + written with. If the file format is changed in a way that breaks reverse + compatibility, increment this version number! + """ - ## Dictionary that defines how characters are escaped when embedded in - # g-code. - # - # Note that the keys of this dictionary are regex strings. The values are - # not. escape_characters = { re.escape("\\\\"): "\\", #The escape character. re.escape("\\n"): "\n", #Newlines. They break off the comment. re.escape("\\r"): "\r" #Carriage return. Windows users may need this for visualisation in their editors. } + """Dictionary that defines how characters are escaped when embedded in + + g-code. + + Note that the keys of this dictionary are regex strings. The values are + not. + """ - ## Initialises the g-code reader as a profile reader. def __init__(self): + """Initialises the g-code reader as a profile reader.""" + super().__init__() - ## Reads a g-code file, loading the profile from it. - # - # \param file_name The name of the file to read the profile from. - # \return The profile that was in the specified file, if any. If the - # specified file was no g-code or contained no parsable profile, \code - # None \endcode is returned. def read(self, file_name): + """Reads a g-code file, loading the profile from it. + + :param file_name: The name of the file to read the profile from. + :return: The profile that was in the specified file, if any. If the + specified file was no g-code or contained no parsable profile, + None is returned. + """ + if file_name.split(".")[-1] != "gcode": return None @@ -94,22 +102,28 @@ class GCodeProfileReader(ProfileReader): profiles.append(readQualityProfileFromString(profile_string)) return profiles -## Unescape a string which has been escaped for use in a gcode comment. -# -# \param string The string to unescape. -# \return \type{str} The unscaped string. -def unescapeGcodeComment(string): + +def unescapeGcodeComment(string: str) -> str: + """Unescape a string which has been escaped for use in a gcode comment. + + :param string: The string to unescape. + :return: The unescaped string. + """ + # Un-escape the serialized profile. pattern = re.compile("|".join(GCodeProfileReader.escape_characters.keys())) # Perform the replacement with a regular expression. return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string) -## Read in a profile from a serialized string. -# -# \param profile_string The profile data in serialized form. -# \return \type{Profile} the resulting Profile object or None if it could not be read. -def readQualityProfileFromString(profile_string): + +def readQualityProfileFromString(profile_string) -> InstanceContainer: + """Read in a profile from a serialized string. + + :param profile_string: The profile data in serialized form. + :return: The resulting Profile object or None if it could not be read. + """ + # Create an empty profile - the id and name will be changed by the ContainerRegistry profile = InstanceContainer("") try: diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index 7b19fdb160..bcb7efc816 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -28,9 +28,8 @@ PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optiona 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: + """This parser is intended to interpret the common firmware codes among all the different flavors""" def __init__(self) -> None: CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage) @@ -212,8 +211,9 @@ class FlavorParser: # G0 and G1 should be handled exactly the same. _gCode1 = _gCode0 - ## Home the head. def _gCode28(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: + """Home the head.""" + 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, @@ -221,21 +221,26 @@ class FlavorParser: position.f, position.e) - ## Set the absolute positioning def _gCode90(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: + """Set the absolute positioning""" + self._is_absolute_positioning = True self._is_absolute_extrusion = True return position - ## Set the relative positioning def _gCode91(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: + """Set the relative positioning""" + 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: PositionOptional, path: List[List[Union[float, int]]]) -> Position: + """Reset the current position to the values specified. + + For example: G92 X10 will set the X to 10 without any physical motion. + """ + 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 @@ -291,8 +296,9 @@ class FlavorParser: _type_keyword = ";TYPE:" _layer_keyword = ";LAYER:" - ## For showing correct x, y offsets for each extruder def _extruderOffsets(self) -> Dict[int, List[float]]: + """For showing correct x, y offsets for each extruder""" + result = {} for extruder in ExtruderManager.getInstance().getActiveExtruderStacks(): result[int(extruder.getMetaData().get("position", "0"))] = [ diff --git a/plugins/GCodeReader/RepRapFlavorParser.py b/plugins/GCodeReader/RepRapFlavorParser.py index 2a24d16add..05f86beab0 100644 --- a/plugins/GCodeReader/RepRapFlavorParser.py +++ b/plugins/GCodeReader/RepRapFlavorParser.py @@ -3,8 +3,10 @@ from . import FlavorParser -## This parser is intended to interpret the RepRap Firmware g-code flavor. + class RepRapFlavorParser(FlavorParser.FlavorParser): + """This parser is intended to interpret the RepRap Firmware g-code flavor.""" + def __init__(self): super().__init__() @@ -17,16 +19,20 @@ class RepRapFlavorParser(FlavorParser.FlavorParser): # Set relative extrusion mode self._is_absolute_extrusion = False - ## Set the absolute positioning - # RepRapFlavor code G90 sets position of X, Y, Z to absolute - # For absolute E, M82 is used def _gCode90(self, position, params, path): + """Set the absolute positioning + + RepRapFlavor code G90 sets position of X, Y, Z to absolute + For absolute E, M82 is used + """ self._is_absolute_positioning = True return position - ## Set the relative positioning - # RepRapFlavor code G91 sets position of X, Y, Z to relative - # For relative E, M83 is used def _gCode91(self, position, params, path): + """Set the relative positioning + + RepRapFlavor code G91 sets position of X, Y, Z to relative + For relative E, M83 is used + """ self._is_absolute_positioning = False return position \ No newline at end of file From 553b09b6cf35568e1ee791cc4c2bab8026872ae7 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 8 May 2020 16:20:55 +0200 Subject: [PATCH 11/27] Convert doxygen to rst for GcodeWriter, LegacyProfileReader, MachineSettingsAction, ModelChecker --- plugins/GCodeWriter/GCodeWriter.py | 91 ++++++++++--------- .../LegacyProfileReader.py | 77 +++++++++------- .../tests/TestLegacyProfileReader.py | 6 +- .../MachineSettingsAction.py | 12 ++- plugins/ModelChecker/ModelChecker.py | 18 ++-- 5 files changed, 117 insertions(+), 87 deletions(-) diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index 792b2aff10..93ff0f51ff 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -14,34 +14,40 @@ from cura.Machines.ContainerTree import ContainerTree from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") -## Writes g-code to a file. -# -# While this poses as a mesh writer, what this really does is take the g-code -# in the entire scene and write it to an output device. Since the g-code of a -# single mesh isn't separable from the rest what with rafts and travel moves -# and all, it doesn't make sense to write just a single mesh. -# -# So this plug-in takes the g-code that is stored in the root of the scene -# node tree, adds a bit of extra information about the profiles and writes -# that to the output device. -class GCodeWriter(MeshWriter): - ## The file format version of the serialised g-code. - # - # It can only read settings with the same version as the version it was - # written with. If the file format is changed in a way that breaks reverse - # compatibility, increment this version number! - version = 3 - ## Dictionary that defines how characters are escaped when embedded in - # g-code. - # - # Note that the keys of this dictionary are regex strings. The values are - # not. +class GCodeWriter(MeshWriter): + """Writes g-code to a file. + + While this poses as a mesh writer, what this really does is take the g-code + in the entire scene and write it to an output device. Since the g-code of a + single mesh isn't separable from the rest what with rafts and travel moves + and all, it doesn't make sense to write just a single mesh. + + So this plug-in takes the g-code that is stored in the root of the scene + node tree, adds a bit of extra information about the profiles and writes + that to the output device. + """ + + version = 3 + """The file format version of the serialised g-code. + + It can only read settings with the same version as the version it was + written with. If the file format is changed in a way that breaks reverse + compatibility, increment this version number! + """ + escape_characters = { re.escape("\\"): "\\\\", # The escape character. re.escape("\n"): "\\n", # Newlines. They break off the comment. re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors. } + """Dictionary that defines how characters are escaped when embedded in + + g-code. + + Note that the keys of this dictionary are regex strings. The values are + not. + """ _setting_keyword = ";SETTING_" @@ -50,17 +56,19 @@ class GCodeWriter(MeshWriter): self._application = Application.getInstance() - ## Writes the g-code for the entire scene to a stream. - # - # Note that even though the function accepts a collection of nodes, the - # entire scene is always written to the file since it is not possible to - # separate the g-code for just specific nodes. - # - # \param stream The stream to write the g-code to. - # \param nodes This is ignored. - # \param mode Additional information on how to format the g-code in the - # file. This must always be text mode. def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode): + """Writes the g-code for the entire scene to a stream. + + Note that even though the function accepts a collection of nodes, the + entire scene is always written to the file since it is not possible to + separate the g-code for just specific nodes. + + :param stream: The stream to write the g-code to. + :param nodes: This is ignored. + :param mode: Additional information on how to format the g-code in the + file. This must always be text mode. + """ + if mode != MeshWriter.OutputMode.TextMode: Logger.log("e", "GCodeWriter does not support non-text mode.") self.setInformation(catalog.i18nc("@error:not supported", "GCodeWriter does not support non-text mode.")) @@ -88,8 +96,9 @@ class GCodeWriter(MeshWriter): self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting.")) return False - ## Create a new container with container 2 as base and container 1 written over it. def _createFlattenedContainerInstance(self, instance_container1, instance_container2): + """Create a new container with container 2 as base and container 1 written over it.""" + flat_container = InstanceContainer(instance_container2.getName()) # The metadata includes id, name and definition @@ -106,15 +115,15 @@ class GCodeWriter(MeshWriter): return flat_container - ## Serialises a container stack to prepare it for writing at the end of the - # g-code. - # - # The settings are serialised, and special characters (including newline) - # are escaped. - # - # \param settings A container stack to serialise. - # \return A serialised string of the settings. def _serialiseSettings(self, stack): + """Serialises a container stack to prepare it for writing at the end of the g-code. + + The settings are serialised, and special characters (including newline) + are escaped. + + :param stack: A container stack to serialise. + :return: A serialised string of the settings. + """ container_registry = self._application.getContainerRegistry() prefix = self._setting_keyword + str(GCodeWriter.version) + " " # The prefix to put before each line. diff --git a/plugins/LegacyProfileReader/LegacyProfileReader.py b/plugins/LegacyProfileReader/LegacyProfileReader.py index 87b26eb4ec..c67136ce9b 100644 --- a/plugins/LegacyProfileReader/LegacyProfileReader.py +++ b/plugins/LegacyProfileReader/LegacyProfileReader.py @@ -16,58 +16,67 @@ from UM.Settings.InstanceContainer import InstanceContainer # The new profile t from cura.ReaderWriters.ProfileReader import ProfileReader # The plug-in type to implement. -## A plugin that reads profile data from legacy Cura versions. -# -# It reads a profile from an .ini file, and performs some translations on it. -# Not all translations are correct, mind you, but it is a best effort. class LegacyProfileReader(ProfileReader): - ## Initialises the legacy profile reader. - # - # This does nothing since the only other function is basically stateless. + """A plugin that reads profile data from legacy Cura versions. + + It reads a profile from an .ini file, and performs some translations on it. + Not all translations are correct, mind you, but it is a best effort. + """ + def __init__(self): + """Initialises the legacy profile reader. + + This does nothing since the only other function is basically stateless. + """ + super().__init__() - ## Prepares the default values of all legacy settings. - # - # These are loaded from the Dictionary of Doom. - # - # \param json The JSON file to load the default setting values from. This - # should not be a URL but a pre-loaded JSON handle. - # \return A dictionary of the default values of the legacy Cura version. def prepareDefaults(self, json: Dict[str, Dict[str, str]]) -> Dict[str, str]: + """Prepares the default values of all legacy settings. + + These are loaded from the Dictionary of Doom. + + :param json: The JSON file to load the default setting values from. This + should not be a URL but a pre-loaded JSON handle. + :return: A dictionary of the default values of the legacy Cura version. + """ + defaults = {} if "defaults" in json: for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict. defaults[key] = json["defaults"][key] return defaults - ## Prepares the local variables that can be used in evaluation of computing - # new setting values from the old ones. - # - # This fills a dictionary with all settings from the legacy Cura version - # and their values, so that they can be used in evaluating the new setting - # values as Python code. - # - # \param config_parser The ConfigParser that finds the settings in the - # legacy profile. - # \param config_section The section in the profile where the settings - # should be found. - # \param defaults The default values for all settings in the legacy Cura. - # \return A set of local variables, one for each setting in the legacy - # profile. def prepareLocals(self, config_parser, config_section, defaults): + """Prepares the local variables that can be used in evaluation of computing + + new setting values from the old ones. + + This fills a dictionary with all settings from the legacy Cura version + and their values, so that they can be used in evaluating the new setting + values as Python code. + + :param config_parser: The ConfigParser that finds the settings in the + legacy profile. + :param config_section: The section in the profile where the settings + should be found. + :param defaults: The default values for all settings in the legacy Cura. + :return: A set of local variables, one for each setting in the legacy + profile. + """ copied_locals = defaults.copy() # Don't edit the original! for option in config_parser.options(config_section): copied_locals[option] = config_parser.get(config_section, option) return copied_locals - ## Reads a legacy Cura profile from a file and returns it. - # - # \param file_name The file to read the legacy Cura profile from. - # \return The legacy Cura profile that was in the file, if any. If the - # file could not be read or didn't contain a valid profile, \code None - # \endcode is returned. def read(self, file_name): + """Reads a legacy Cura profile from a file and returns it. + + :param file_name: The file to read the legacy Cura profile from. + :return: The legacy Cura profile that was in the file, if any. If the + file could not be read or didn't contain a valid profile, None is returned. + """ + if file_name.split(".")[-1] != "ini": return None global_container_stack = Application.getInstance().getGlobalContainerStack() diff --git a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py index cd0f681828..64f3e1d404 100644 --- a/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py +++ b/plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py @@ -13,7 +13,7 @@ import UM.PluginRegistry # To mock the plug-in registry out. import UM.Settings.ContainerRegistry # To mock the container registry out. import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function. -import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module. +import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module. @pytest.fixture @@ -126,9 +126,11 @@ test_prepareLocalsNoSectionErrorData = [ ) ] -## Test cases where a key error is expected. + @pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsNoSectionErrorData) def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, defaults): + """Test cases where a key error is expected.""" + parser = configparser.ConfigParser() parser.read_dict(parser_data) diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index 28535024a7..c82ef8f351 100755 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -23,9 +23,11 @@ if TYPE_CHECKING: catalog = UM.i18n.i18nCatalog("cura") -## This action allows for certain settings that are "machine only") to be modified. -# It automatically detects machine definitions that it knows how to change and attaches itself to those. class MachineSettingsAction(MachineAction): + """This action allows for certain settings that are "machine only") to be modified. + + It automatically detects machine definitions that it knows how to change and attaches itself to those. + """ def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings")) self._qml_url = "MachineSettingsAction.qml" @@ -56,9 +58,11 @@ class MachineSettingsAction(MachineAction): if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine": self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey()) - ## Triggered when the global container stack changes or when the g-code - # flavour setting is changed. def _updateHasMaterialsInContainerTree(self) -> None: + """Triggered when the global container stack changes or when the g-code + + flavour setting is changed. + """ global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return diff --git a/plugins/ModelChecker/ModelChecker.py b/plugins/ModelChecker/ModelChecker.py index 057ee14945..138d84cc86 100644 --- a/plugins/ModelChecker/ModelChecker.py +++ b/plugins/ModelChecker/ModelChecker.py @@ -18,8 +18,8 @@ catalog = i18nCatalog("cura") class ModelChecker(QObject, Extension): - ## Signal that gets emitted when anything changed that we need to check. onChanged = pyqtSignal() + """Signal that gets emitted when anything changed that we need to check.""" def __init__(self): super().__init__() @@ -47,11 +47,13 @@ class ModelChecker(QObject, Extension): if not isinstance(args[0], Camera): self._change_timer.start() - ## Called when plug-ins are initialized. - # - # This makes sure that we listen to changes of the material and that the - # button is created that indicates warnings with the current set-up. def _pluginsInitialized(self): + """Called when plug-ins are initialized. + + This makes sure that we listen to changes of the material and that the + button is created that indicates warnings with the current set-up. + """ + Application.getInstance().getMachineManager().rootMaterialChanged.connect(self.onChanged) self._createView() @@ -106,8 +108,12 @@ class ModelChecker(QObject, Extension): if node.callDecoration("isSliceable"): yield node - ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. def _createView(self): + """Creates the view used by show popup. + + The view is saved because of the fairly aggressive garbage collection. + """ + Logger.log("d", "Creating model checker view.") # Create the plugin dialog component From a4fe3d7685b677dd121262b2a1b137c33ba7ecd3 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 8 May 2020 16:28:07 +0200 Subject: [PATCH 12/27] Convert doxygen to rst for POS, MonitorStage, PostProcessing --- plugins/MonitorStage/MonitorStage.py | 144 +++++++++--------- .../PerObjectSettingVisibilityHandler.py | 6 +- .../PerObjectSettingsTool.py | 27 ++-- .../PostProcessingPlugin.py | 49 +++--- plugins/PostProcessingPlugin/Script.py | 61 +++++--- .../scripts/FilamentChange.py | 8 +- .../scripts/PauseAtHeight.py | 11 +- .../scripts/RetractContinue.py | 4 +- .../scripts/SearchAndReplace.py | 11 +- 9 files changed, 180 insertions(+), 141 deletions(-) diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py index 3d2a1c3f37..4e4c442b4c 100644 --- a/plugins/MonitorStage/MonitorStage.py +++ b/plugins/MonitorStage/MonitorStage.py @@ -1,72 +1,72 @@ -# Copyright (c) 2017 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. -import os.path -from UM.Application import Application -from cura.Stages.CuraStage import CuraStage - - -## Stage for monitoring a 3D printing while it's printing. -class MonitorStage(CuraStage): - - def __init__(self, parent = None): - super().__init__(parent) - - # Wait until QML engine is created, otherwise creating the new QML components will fail - Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) - self._printer_output_device = None - - self._active_print_job = None - self._active_printer = None - - def _setActivePrintJob(self, print_job): - if self._active_print_job != print_job: - self._active_print_job = print_job - - def _setActivePrinter(self, printer): - if self._active_printer != printer: - if self._active_printer: - self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged) - self._active_printer = printer - if self._active_printer: - self._setActivePrintJob(self._active_printer.activePrintJob) - # Jobs might change, so we need to listen to it's changes. - self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged) - else: - self._setActivePrintJob(None) - - def _onActivePrintJobChanged(self): - self._setActivePrintJob(self._active_printer.activePrintJob) - - def _onActivePrinterChanged(self): - self._setActivePrinter(self._printer_output_device.activePrinter) - - def _onOutputDevicesChanged(self): - try: - # We assume that you are monitoring the device with the highest priority. - new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0] - if new_output_device != self._printer_output_device: - if self._printer_output_device: - try: - self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged) - except TypeError: - # Ignore stupid "Not connected" errors. - pass - - self._printer_output_device = new_output_device - - self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged) - self._setActivePrinter(self._printer_output_device.activePrinter) - except IndexError: - pass - - def _onEngineCreated(self): - # We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early) - Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) - self._onOutputDevicesChanged() - - plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId()) - if plugin_path is not None: - menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml") - main_component_path = os.path.join(plugin_path, "MonitorMain.qml") - self.addDisplayComponent("menu", menu_component_path) - self.addDisplayComponent("main", main_component_path) +# Copyright (c) 2017 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +import os.path +from UM.Application import Application +from cura.Stages.CuraStage import CuraStage + + +class MonitorStage(CuraStage): + """Stage for monitoring a 3D printing while it's printing.""" + + def __init__(self, parent = None): + super().__init__(parent) + + # Wait until QML engine is created, otherwise creating the new QML components will fail + Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated) + self._printer_output_device = None + + self._active_print_job = None + self._active_printer = None + + def _setActivePrintJob(self, print_job): + if self._active_print_job != print_job: + self._active_print_job = print_job + + def _setActivePrinter(self, printer): + if self._active_printer != printer: + if self._active_printer: + self._active_printer.activePrintJobChanged.disconnect(self._onActivePrintJobChanged) + self._active_printer = printer + if self._active_printer: + self._setActivePrintJob(self._active_printer.activePrintJob) + # Jobs might change, so we need to listen to it's changes. + self._active_printer.activePrintJobChanged.connect(self._onActivePrintJobChanged) + else: + self._setActivePrintJob(None) + + def _onActivePrintJobChanged(self): + self._setActivePrintJob(self._active_printer.activePrintJob) + + def _onActivePrinterChanged(self): + self._setActivePrinter(self._printer_output_device.activePrinter) + + def _onOutputDevicesChanged(self): + try: + # We assume that you are monitoring the device with the highest priority. + new_output_device = Application.getInstance().getMachineManager().printerOutputDevices[0] + if new_output_device != self._printer_output_device: + if self._printer_output_device: + try: + self._printer_output_device.printersChanged.disconnect(self._onActivePrinterChanged) + except TypeError: + # Ignore stupid "Not connected" errors. + pass + + self._printer_output_device = new_output_device + + self._printer_output_device.printersChanged.connect(self._onActivePrinterChanged) + self._setActivePrinter(self._printer_output_device.activePrinter) + except IndexError: + pass + + def _onEngineCreated(self): + # We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early) + Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged) + self._onOutputDevicesChanged() + + plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId()) + if plugin_path is not None: + menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml") + main_component_path = os.path.join(plugin_path, "MonitorMain.qml") + self.addDisplayComponent("menu", menu_component_path) + self.addDisplayComponent("main", main_component_path) diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py index 78da0512f7..c44e4a4df3 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py @@ -15,9 +15,11 @@ from cura.Settings.ExtruderManager import ExtruderManager #To get global-inherit from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -## The per object setting visibility handler ensures that only setting -# definitions that have a matching instance Container are returned as visible. class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler): + """The per object setting visibility handler ensures that only setting + + definitions that have a matching instance Container are returned as visible. + """ def __init__(self, parent = None, *args, **kwargs): super().__init__(parent = parent, *args, **kwargs) diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py index 4f0d90a8e3..d23096c0a3 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py @@ -12,9 +12,11 @@ from UM.Settings.SettingInstance import SettingInstance from UM.Event import Event -## This tool allows the user to add & change settings per node in the scene. -# The settings per object are kept in a ContainerStack, which is linked to a node by decorator. class PerObjectSettingsTool(Tool): + """This tool allows the user to add & change settings per node in the scene. + + The settings per object are kept in a ContainerStack, which is linked to a node by decorator. + """ def __init__(self): super().__init__() self._model = None @@ -48,26 +50,31 @@ class PerObjectSettingsTool(Tool): except AttributeError: return "" - ## Gets the active extruder of the currently selected object. - # - # \return The active extruder of the currently selected object. def getSelectedActiveExtruder(self): + """Gets the active extruder of the currently selected object. + + :return: The active extruder of the currently selected object. + """ + selected_object = Selection.getSelectedObject(0) return selected_object.callDecoration("getActiveExtruder") - ## Changes the active extruder of the currently selected object. - # - # \param extruder_stack_id The ID of the extruder to print the currently - # selected object with. def setSelectedActiveExtruder(self, extruder_stack_id): + """Changes the active extruder of the currently selected object. + + :param extruder_stack_id: The ID of the extruder to print the currently + selected object with. + """ + selected_object = Selection.getSelectedObject(0) stack = selected_object.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway. if not stack: selected_object.addDecorator(SettingOverrideDecorator()) selected_object.callDecoration("setActiveExtruder", extruder_stack_id) - ## Returns True when the mesh_type was changed, False when current mesh_type == mesh_type def setMeshType(self, mesh_type: str) -> bool: + """Returns True when the mesh_type was changed, False when current mesh_type == mesh_type""" + old_mesh_type = self.getMeshType() if old_mesh_type == mesh_type: return False diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 9bf8062ffd..c1135640dd 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -27,9 +27,8 @@ if TYPE_CHECKING: from .Script import Script -## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated -# g-code files. class PostProcessingPlugin(QObject, Extension): + """Extension type plugin that enables pre-written scripts to post process g-code files.""" def __init__(self, parent = None) -> None: QObject.__init__(self, parent) Extension.__init__(self) @@ -69,8 +68,9 @@ class PostProcessingPlugin(QObject, Extension): except IndexError: return "" - ## Execute all post-processing scripts on the gcode. def execute(self, output_device) -> None: + """Execute all post-processing scripts on the gcode.""" + scene = Application.getInstance().getController().getScene() # If the scene does not have a gcode, do nothing if not hasattr(scene, "gcode_dict"): @@ -119,9 +119,10 @@ class PostProcessingPlugin(QObject, Extension): self.selectedIndexChanged.emit() #Ensure that settings are updated self._propertyChanged() - ## Remove a script from the active script list by index. @pyqtSlot(int) def removeScriptByIndex(self, index: int) -> None: + """Remove a script from the active script list by index.""" + self._script_list.pop(index) if len(self._script_list) - 1 < self._selected_script_index: self._selected_script_index = len(self._script_list) - 1 @@ -129,10 +130,12 @@ class PostProcessingPlugin(QObject, Extension): self.selectedIndexChanged.emit() # Ensure that settings are updated self._propertyChanged() - ## Load all scripts from all paths where scripts can be found. - # - # This should probably only be done on init. def loadAllScripts(self) -> None: + """Load all scripts from all paths where scripts can be found. + + This should probably only be done on init. + """ + if self._loaded_scripts: # Already loaded. return @@ -152,10 +155,12 @@ class PostProcessingPlugin(QObject, Extension): self.loadScripts(path) - ## Load all scripts from provided path. - # This should probably only be done on init. - # \param path Path to check for scripts. def loadScripts(self, path: str) -> None: + """Load all scripts from provided path. + + This should probably only be done on init. + :param path: Path to check for scripts. + """ if ApplicationMetadata.IsEnterpriseVersion: # Delete all __pycache__ not in installation folder, as it may present a security risk. @@ -173,8 +178,8 @@ class PostProcessingPlugin(QObject, Extension): if not is_in_installation_path: TrustBasics.removeCached(path) - ## Load all scripts in the scripts folders scripts = pkgutil.iter_modules(path = [path]) + """Load all scripts in the scripts folders""" for loader, script_name, ispkg in scripts: # Iterate over all scripts. if script_name not in sys.modules: @@ -278,9 +283,8 @@ class PostProcessingPlugin(QObject, Extension): self.scriptListChanged.emit() self._propertyChanged() - ## When the global container stack is changed, swap out the list of active - # scripts. def _onGlobalContainerStackChanged(self) -> None: + """When the global container stack is changed, swap out the list of active scripts.""" if self._global_container_stack: self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata) @@ -323,8 +327,12 @@ class PostProcessingPlugin(QObject, Extension): # We do want to listen to other events. self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata) - ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. def _createView(self) -> None: + """Creates the view used by show popup. + + The view is saved because of the fairly aggressive garbage collection. + """ + Logger.log("d", "Creating post processing plugin view.") self.loadAllScripts() @@ -340,8 +348,9 @@ class PostProcessingPlugin(QObject, Extension): # Create the save button component CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton")) - ## Show the (GUI) popup of the post processing plugin. def showPopup(self) -> None: + """Show the (GUI) popup of the post processing plugin.""" + if self._view is None: self._createView() if self._view is None: @@ -349,11 +358,13 @@ class PostProcessingPlugin(QObject, Extension): return self._view.show() - ## Property changed: trigger re-slice - # To do this we use the global container stack propertyChanged. - # Re-slicing is necessary for setting changes in this plugin, because the changes - # are applied only once per "fresh" gcode def _propertyChanged(self) -> None: + """Property changed: trigger re-slice + + To do this we use the global container stack propertyChanged. + Re-slicing is necessary for setting changes in this plugin, because the changes + are applied only once per "fresh" gcode + """ global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack is not None: global_container_stack.propertyChanged.emit("post_processing_plugin", "value") diff --git a/plugins/PostProcessingPlugin/Script.py b/plugins/PostProcessingPlugin/Script.py index e502f107f9..d15b209478 100644 --- a/plugins/PostProcessingPlugin/Script.py +++ b/plugins/PostProcessingPlugin/Script.py @@ -23,9 +23,10 @@ if TYPE_CHECKING: from UM.Settings.Interfaces import DefinitionContainerInterface -## Base class for scripts. All scripts should inherit the script class. @signalemitter class Script: + """Base class for scripts. All scripts should inherit the script class.""" + def __init__(self) -> None: super().__init__() self._stack = None # type: Optional[ContainerStack] @@ -78,13 +79,15 @@ class Script: if global_container_stack is not None: global_container_stack.propertyChanged.emit(key, property_name) - ## Needs to return a dict that can be used to construct a settingcategory file. - # See the example script for an example. - # It follows the same style / guides as the Uranium settings. - # Scripts can either override getSettingData directly, or use getSettingDataString - # to return a string that will be parsed as json. The latter has the benefit over - # returning a dict in that the order of settings is maintained. def getSettingData(self) -> Dict[str, Any]: + """Needs to return a dict that can be used to construct a settingcategory file. + + See the example script for an example. + It follows the same style / guides as the Uranium settings. + Scripts can either override getSettingData directly, or use getSettingDataString + to return a string that will be parsed as json. The latter has the benefit over + returning a dict in that the order of settings is maintained. + """ setting_data_as_string = self.getSettingDataString() setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict) return setting_data @@ -104,15 +107,18 @@ class Script: return self._stack.getId() return None - ## Convenience function that retrieves value of a setting from the stack. def getSettingValueByKey(self, key: str) -> Any: + """Convenience function that retrieves value of a setting from the stack.""" + if self._stack is not None: return self._stack.getProperty(key, "value") return None - ## Convenience function that finds the value in a line of g-code. - # When requesting key = x from line "G1 X100" the value 100 is returned. def getValue(self, line: str, key: str, default = None) -> Any: + """Convenience function that finds the value in a line of g-code. + + When requesting key = x from line "G1 X100" the value 100 is returned. + """ if not key in line or (';' in line and line.find(key) > line.find(';')): return default sub_part = line[line.find(key) + 1:] @@ -127,20 +133,23 @@ class Script: except ValueError: #Not a number at all. return default - ## Convenience function to produce a line of g-code. - # - # You can put in an original g-code line and it'll re-use all the values - # in that line. - # All other keyword parameters are put in the result in g-code's format. - # For instance, if you put ``G=1`` in the parameters, it will output - # ``G1``. If you put ``G=1, X=100`` in the parameters, it will output - # ``G1 X100``. The parameters G and M will always be put first. The - # parameters T and S will be put second (or first if there is no G or M). - # The rest of the parameters will be put in arbitrary order. - # \param line The original g-code line that must be modified. If not - # provided, an entirely new g-code line will be produced. - # \return A line of g-code with the desired parameters filled in. def putValue(self, line: str = "", **kwargs) -> str: + """Convenience function to produce a line of g-code. + + You can put in an original g-code line and it'll re-use all the values + in that line. + All other keyword parameters are put in the result in g-code's format. + For instance, if you put ``G=1`` in the parameters, it will output + ``G1``. If you put ``G=1, X=100`` in the parameters, it will output + ``G1 X100``. The parameters G and M will always be put first. The + parameters T and S will be put second (or first if there is no G or M). + The rest of the parameters will be put in arbitrary order. + + :param line: The original g-code line that must be modified. If not + provided, an entirely new g-code line will be produced. + :return: A line of g-code with the desired parameters filled in. + """ + #Strip the comment. comment = "" if ";" in line: @@ -179,7 +188,9 @@ class Script: return result - ## This is called when the script is executed. - # It gets a list of g-code strings and needs to return a (modified) list. def execute(self, data: List[str]) -> List[str]: + """This is called when the script is executed. + + It gets a list of g-code strings and needs to return a (modified) list. + """ raise NotImplementedError() diff --git a/plugins/PostProcessingPlugin/scripts/FilamentChange.py b/plugins/PostProcessingPlugin/scripts/FilamentChange.py index 943ca30f2e..351b985d49 100644 --- a/plugins/PostProcessingPlugin/scripts/FilamentChange.py +++ b/plugins/PostProcessingPlugin/scripts/FilamentChange.py @@ -63,10 +63,12 @@ class FilamentChange(Script): } }""" - ## Inserts the filament change g-code at specific layer numbers. - # \param data A list of layers of g-code. - # \return A similar list, with filament change commands inserted. def execute(self, data: List[str]): + """Inserts the filament change g-code at specific layer numbers. + + :param data: A list of layers of g-code. + :return: A similar list, with filament change commands inserted. + """ layer_nums = self.getSettingValueByKey("layer_number") initial_retract = self.getSettingValueByKey("initial_retract") later_retract = self.getSettingValueByKey("later_retract") diff --git a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py index 1ba8b8213b..766677af2a 100644 --- a/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py +++ b/plugins/PostProcessingPlugin/scripts/PauseAtHeight.py @@ -134,9 +134,8 @@ class PauseAtHeight(Script): } }""" - ## Get the X and Y values for a layer (will be used to get X and Y of the - # layer after the pause). def getNextXY(self, layer: str) -> Tuple[float, float]: + """Get the X and Y values for a layer (will be used to get X and Y of the layer after the pause).""" lines = layer.split("\n") for line in lines: if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None: @@ -145,10 +144,12 @@ class PauseAtHeight(Script): return x, y return 0, 0 - ## Inserts the pause commands. - # \param data: List of layers. - # \return New list of layers. def execute(self, data: List[str]) -> List[str]: + """Inserts the pause commands. + + :param data: List of layers. + :return: New list of layers. + """ pause_at = self.getSettingValueByKey("pause_at") pause_height = self.getSettingValueByKey("pause_height") pause_layer = self.getSettingValueByKey("pause_layer") diff --git a/plugins/PostProcessingPlugin/scripts/RetractContinue.py b/plugins/PostProcessingPlugin/scripts/RetractContinue.py index e437439287..3e095bd395 100644 --- a/plugins/PostProcessingPlugin/scripts/RetractContinue.py +++ b/plugins/PostProcessingPlugin/scripts/RetractContinue.py @@ -5,8 +5,10 @@ import math from ..Script import Script -## Continues retracting during all travel moves. + class RetractContinue(Script): + """Continues retracting during all travel moves.""" + def getSettingDataString(self): return """{ "name": "Retract Continue", diff --git a/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py index 68d697e470..b601a037da 100644 --- a/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py +++ b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py @@ -5,11 +5,14 @@ import re #To perform the search and replace. from ..Script import Script -## Performs a search-and-replace on all g-code. -# -# Due to technical limitations, the search can't cross the border between -# layers. + class SearchAndReplace(Script): + """Performs a search-and-replace on all g-code. + + Due to technical limitations, the search can't cross the border between + layers. + """ + def getSettingDataString(self): return """{ "name": "Search and Replace", From a09cd0e63e6b5cffba068c9f704b01280b2313e7 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 8 May 2020 17:37:49 +0200 Subject: [PATCH 13/27] Convert doxygen to rst for Prepare, Preview, RemovableOutputDevice --- plugins/PrepareStage/PrepareStage.py | 38 ++++++++++--------- plugins/PreviewStage/PreviewStage.py | 38 +++++++++++-------- .../LinuxRemovableDrivePlugin.py | 12 +++--- .../OSXRemovableDrivePlugin.py | 4 +- .../RemovableDriveOutputDevice.py | 36 +++++++++--------- .../WindowsRemovableDrivePlugin.py | 3 +- 6 files changed, 74 insertions(+), 57 deletions(-) diff --git a/plugins/PrepareStage/PrepareStage.py b/plugins/PrepareStage/PrepareStage.py index c2dee9693b..2d7ee9ee4f 100644 --- a/plugins/PrepareStage/PrepareStage.py +++ b/plugins/PrepareStage/PrepareStage.py @@ -1,19 +1,21 @@ -# Copyright (c) 2019 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import os.path -from UM.Application import Application -from UM.PluginRegistry import PluginRegistry -from cura.Stages.CuraStage import CuraStage - -## Stage for preparing model (slicing). -class PrepareStage(CuraStage): - def __init__(self, parent = None): - super().__init__(parent) - Application.getInstance().engineCreatedSignal.connect(self._engineCreated) - - def _engineCreated(self): - menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml") - main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMain.qml") - self.addDisplayComponent("menu", menu_component_path) +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import os.path +from UM.Application import Application +from UM.PluginRegistry import PluginRegistry +from cura.Stages.CuraStage import CuraStage + + +class PrepareStage(CuraStage): + """Stage for preparing model (slicing).""" + + def __init__(self, parent = None): + super().__init__(parent) + Application.getInstance().engineCreatedSignal.connect(self._engineCreated) + + def _engineCreated(self): + menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml") + main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMain.qml") + self.addDisplayComponent("menu", menu_component_path) self.addDisplayComponent("main", main_component_path) \ No newline at end of file diff --git a/plugins/PreviewStage/PreviewStage.py b/plugins/PreviewStage/PreviewStage.py index 1c487c8340..deec8b4197 100644 --- a/plugins/PreviewStage/PreviewStage.py +++ b/plugins/PreviewStage/PreviewStage.py @@ -12,37 +12,45 @@ if TYPE_CHECKING: from UM.View.View import View -## Displays a preview of what you're about to print. -# -# The Python component of this stage just loads PreviewMain.qml for display -# when the stage is selected, and makes sure that it reverts to the previous -# view when the previous stage is activated. class PreviewStage(CuraStage): + """Displays a preview of what you're about to print. + + The Python component of this stage just loads PreviewMain.qml for display + when the stage is selected, and makes sure that it reverts to the previous + view when the previous stage is activated. + """ + def __init__(self, application: QtApplication, parent = None) -> None: super().__init__(parent) self._application = application self._application.engineCreatedSignal.connect(self._engineCreated) self._previously_active_view = None # type: Optional[View] - ## When selecting the stage, remember which was the previous view so that - # we can revert to that view when we go out of the stage later. def onStageSelected(self) -> None: + """When selecting the stage, remember which was the previous view so that + + we can revert to that view when we go out of the stage later. + """ self._previously_active_view = self._application.getController().getActiveView() - ## Called when going to a different stage (away from the Preview Stage). - # - # When going to a different stage, the view should be reverted to what it - # was before. Normally, that just reverts it to solid view. def onStageDeselected(self) -> None: + """Called when going to a different stage (away from the Preview Stage). + + When going to a different stage, the view should be reverted to what it + was before. Normally, that just reverts it to solid view. + """ + if self._previously_active_view is not None: self._application.getController().setActiveView(self._previously_active_view.getPluginId()) self._previously_active_view = None - ## Delayed load of the QML files. - # - # We need to make sure that the QML engine is running before we can load - # these. def _engineCreated(self) -> None: + """Delayed load of the QML files. + + We need to make sure that the QML engine is running before we can load + these. + """ + plugin_path = self._application.getPluginRegistry().getPluginPath(self.getPluginId()) if plugin_path is not None: menu_component_path = os.path.join(plugin_path, "PreviewMenu.qml") diff --git a/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py index cf889ebb12..3661bfa63c 100644 --- a/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py +++ b/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py @@ -10,12 +10,14 @@ import glob import os import subprocess -## Support for removable devices on Linux. -# -# TODO: This code uses the most basic interfaces for handling this. -# We should instead use UDisks2 to handle mount/unmount and hotplugging events. -# + class LinuxRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): + """Support for removable devices on Linux. + + TODO: This code uses the most basic interfaces for handling this. + We should instead use UDisks2 to handle mount/unmount and hotplugging events. + """ + def checkRemovableDrives(self): drives = {} for volume in glob.glob("/media/*"): diff --git a/plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py index 0d2c474e42..70e2107898 100644 --- a/plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py +++ b/plugins/RemovableDriveOutputDevice/OSXRemovableDrivePlugin.py @@ -9,8 +9,10 @@ import os import plistlib -## Support for removable devices on Mac OSX + class OSXRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): + """Support for removable devices on Mac OSX""" + def checkRemovableDrives(self): drives = {} p = subprocess.Popen(["system_profiler", "SPUSBDataType", "-xml"], stdout = subprocess.PIPE, stderr = subprocess.PIPE) diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py index c81e4a76bc..a4fe7309f7 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -28,17 +28,19 @@ class RemovableDriveOutputDevice(OutputDevice): self._writing = False self._stream = None - ## Request the specified nodes to be written to the removable drive. - # - # \param nodes A collection of scene nodes that should be written to the - # removable drive. - # \param file_name \type{string} A suggestion for the file name to write - # to. If none is provided, a file name will be made from the names of the - # meshes. - # \param limit_mimetypes Should we limit the available MIME types to the - # MIME types available to the currently active machine? - # def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): + """Request the specified nodes to be written to the removable drive. + + :param nodes: A collection of scene nodes that should be written to the + removable drive. + :param file_name: :type{string} A suggestion for the file name to write to. + If none is provided, a file name will be made from the names of the + meshes. + :param limit_mimetypes: Should we limit the available MIME types to the + MIME types available to the currently active machine? + + """ + filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do) if self._writing: raise OutputDeviceError.DeviceBusyError() @@ -106,14 +108,14 @@ class RemovableDriveOutputDevice(OutputDevice): Logger.log("e", "Operating system would not let us write to %s: %s", file_name, str(e)) raise OutputDeviceError.WriteRequestFailedError(catalog.i18nc("@info:status Don't translate the XML tags or !", "Could not save to {0}: {1}").format(file_name, str(e))) from e - ## Generate a file name automatically for the specified nodes to be saved - # in. - # - # The name generated will be the name of one of the nodes. Which node that - # is can not be guaranteed. - # - # \param nodes A collection of nodes for which to generate a file name. def _automaticFileName(self, nodes): + """Generate a file name automatically for the specified nodes to be saved in. + + The name generated will be the name of one of the nodes. Which node that + is can not be guaranteed. + + :param nodes: A collection of nodes for which to generate a file name. + """ for root in nodes: for child in BreadthFirstIterator(root): if child.getMeshData(): diff --git a/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py index 8a183c25f4..ddcabd7311 100644 --- a/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py +++ b/plugins/RemovableDriveOutputDevice/WindowsRemovableDrivePlugin.py @@ -42,8 +42,9 @@ ctypes.windll.kernel32.DeviceIoControl.argtypes = [ #type: ignore ctypes.windll.kernel32.DeviceIoControl.restype = wintypes.BOOL #type: ignore -## Removable drive support for windows class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): + """Removable drive support for windows""" + def checkRemovableDrives(self): drives = {} From 120541a8dba735029187e56ae99cf8578d640fad Mon Sep 17 00:00:00 2001 From: Jelle Spijker Date: Fri, 8 May 2020 18:59:38 +0200 Subject: [PATCH 14/27] Updated comments in Models Converted doxygen style comments to reStructuredText style in the files found in Cura/cura/Model directory recursively using the script dox_2_rst.py (provided in the Uranium repo). Comments were manually checked and changed if needed. Note: dox_2rst.py struggles with decorated functions. --- cura/Machines/ContainerNode.py | 60 ++++--- cura/Machines/ContainerTree.py | 148 ++++++++++-------- cura/Machines/IntentNode.py | 8 +- cura/Machines/MachineErrorChecker.py | 43 +++-- cura/Machines/MachineNode.py | 90 +++++------ cura/Machines/MaterialGroup.py | 25 +-- cura/Machines/MaterialNode.py | 49 +++--- cura/Machines/Models/BaseMaterialsModel.py | 37 +++-- .../CustomQualityProfilesDropDownMenuModel.py | 3 +- .../Models/DiscoveredCloudPrintersModel.py | 26 +-- .../Models/DiscoveredPrintersModel.py | 19 ++- cura/Machines/Models/ExtrudersModel.py | 61 ++++---- .../Machines/Models/FavoriteMaterialsModel.py | 7 +- .../Models/FirstStartMachineActionsModel.py | 15 +- cura/Machines/Models/GlobalStacksModel.py | 3 +- cura/Machines/Models/IntentCategoryModel.py | 27 ++-- cura/Machines/Models/IntentModel.py | 3 +- .../Models/MaterialManagementModel.py | 140 +++++++++-------- cura/Machines/Models/MultiBuildPlateModel.py | 11 +- .../Machines/Models/QualityManagementModel.py | 118 ++++++++------ .../QualityProfilesDropDownMenuModel.py | 5 +- cura/Machines/Models/QualitySettingsModel.py | 5 +- cura/Machines/QualityChangesGroup.py | 10 +- cura/Machines/QualityGroup.py | 46 +++--- cura/Machines/QualityNode.py | 12 +- cura/Machines/VariantNode.py | 47 +++--- 26 files changed, 577 insertions(+), 441 deletions(-) diff --git a/cura/Machines/ContainerNode.py b/cura/Machines/ContainerNode.py index 8a9ddcc39b..1fb02b099f 100644 --- a/cura/Machines/ContainerNode.py +++ b/cura/Machines/ContainerNode.py @@ -9,47 +9,59 @@ from UM.Logger import Logger from UM.Settings.InstanceContainer import InstanceContainer -## A node in the container tree. It represents one container. -# -# The container it represents is referenced by its container_id. During normal -# use of the tree, this container is not constructed. Only when parts of the -# tree need to get loaded in the container stack should it get constructed. class ContainerNode: - ## Creates a new node for the container tree. - # \param container_id The ID of the container that this node should - # represent. + """A node in the container tree. It represents one container. + + The container it represents is referenced by its container_id. During normal use of the tree, this container is + not constructed. Only when parts of the tree need to get loaded in the container stack should it get constructed. + """ + def __init__(self, container_id: str) -> None: + """Creates a new node for the container tree. + + :param container_id: The ID of the container that this node should represent. + """ + self.container_id = container_id self._container = None # type: Optional[InstanceContainer] self.children_map = {} # type: Dict[str, ContainerNode] # Mapping from container ID to container node. - ## Gets the metadata of the container that this node represents. - # Getting the metadata from the container directly is about 10x as fast. - # \return The metadata of the container in this node. def getMetadata(self) -> Dict[str, Any]: + """Gets the metadata of the container that this node represents. + + Getting the metadata from the container directly is about 10x as fast. + + :return: The metadata of the container in this node. + """ + return ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)[0] - ## Get an entry from the metadata of the container that this node contains. - # - # This is just a convenience function. - # \param entry The metadata entry key to return. - # \param default If the metadata is not present or the container is not - # found, the value of this default is returned. - # \return The value of the metadata entry, or the default if it was not - # present. def getMetaDataEntry(self, entry: str, default: Any = None) -> Any: + """Get an entry from the metadata of the container that this node contains. + + This is just a convenience function. + + :param entry: The metadata entry key to return. + :param default: If the metadata is not present or the container is not found, the value of this default is + returned. + + :return: The value of the metadata entry, or the default if it was not present. + """ + container_metadata = ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id) if len(container_metadata) == 0: return default return container_metadata[0].get(entry, default) - ## The container that this node's container ID refers to. - # - # This can be used to finally instantiate the container in order to put it - # in the container stack. - # \return A container. @property def container(self) -> Optional[InstanceContainer]: + """The container that this node's container ID refers to. + + This can be used to finally instantiate the container in order to put it in the container stack. + + :return: A container. + """ + if not self._container: container_list = ContainerRegistry.getInstance().findInstanceContainers(id = self.container_id) if len(container_list) == 0: diff --git a/cura/Machines/ContainerTree.py b/cura/Machines/ContainerTree.py index a7bb0610bd..7902f8163c 100644 --- a/cura/Machines/ContainerTree.py +++ b/cura/Machines/ContainerTree.py @@ -19,17 +19,16 @@ if TYPE_CHECKING: from UM.Settings.ContainerStack import ContainerStack -## This class contains a look-up tree for which containers are available at -# which stages of configuration. -# -# The tree starts at the machine definitions. For every distinct definition -# there will be one machine node here. -# -# All of the fallbacks for material choices, quality choices, etc. should be -# encoded in this tree. There must always be at least one child node (for -# nodes that have children) but that child node may be a node representing the -# empty instance container. class ContainerTree: + """This class contains a look-up tree for which containers are available at which stages of configuration. + + The tree starts at the machine definitions. For every distinct definition there will be one machine node here. + + All of the fallbacks for material choices, quality choices, etc. should be encoded in this tree. There must + always be at least one child node (for nodes that have children) but that child node may be a node representing + the empty instance container. + """ + __instance = None # type: Optional["ContainerTree"] @classmethod @@ -43,13 +42,15 @@ class ContainerTree: self.materialsChanged = Signal() # Emitted when any of the material nodes in the tree got changed. cura.CuraApplication.CuraApplication.getInstance().initializationFinished.connect(self._onStartupFinished) # Start the background task to load more machine nodes after start-up is completed. - ## Get the quality groups available for the currently activated printer. - # - # This contains all quality groups, enabled or disabled. To check whether - # the quality group can be activated, test for the - # ``QualityGroup.is_available`` property. - # \return For every quality type, one quality group. def getCurrentQualityGroups(self) -> Dict[str, "QualityGroup"]: + """Get the quality groups available for the currently activated printer. + + This contains all quality groups, enabled or disabled. To check whether the quality group can be activated, + test for the ``QualityGroup.is_available`` property. + + :return: For every quality type, one quality group. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return {} @@ -58,14 +59,15 @@ class ContainerTree: extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList] return self.machines[global_stack.definition.getId()].getQualityGroups(variant_names, material_bases, extruder_enabled) - ## Get the quality changes groups available for the currently activated - # printer. - # - # This contains all quality changes groups, enabled or disabled. To check - # whether the quality changes group can be activated, test for the - # ``QualityChangesGroup.is_available`` property. - # \return A list of all quality changes groups. def getCurrentQualityChangesGroups(self) -> List["QualityChangesGroup"]: + """Get the quality changes groups available for the currently activated printer. + + This contains all quality changes groups, enabled or disabled. To check whether the quality changes group can + be activated, test for the ``QualityChangesGroup.is_available`` property. + + :return: A list of all quality changes groups. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return [] @@ -74,31 +76,43 @@ class ContainerTree: extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList] return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled) - ## Ran after completely starting up the application. def _onStartupFinished(self) -> None: + """Ran after completely starting up the application.""" + currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks. JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added)) - ## Dictionary-like object that contains the machines. - # - # This handles the lazy loading of MachineNodes. class _MachineNodeMap: + """Dictionary-like object that contains the machines. + + This handles the lazy loading of MachineNodes. + """ + def __init__(self) -> None: self._machines = {} # type: Dict[str, MachineNode] - ## Returns whether a printer with a certain definition ID exists. This - # is regardless of whether or not the printer is loaded yet. - # \param definition_id The definition to look for. - # \return Whether or not a printer definition exists with that name. def __contains__(self, definition_id: str) -> bool: + """Returns whether a printer with a certain definition ID exists. + + This is regardless of whether or not the printer is loaded yet. + + :param definition_id: The definition to look for. + + :return: Whether or not a printer definition exists with that name. + """ + return len(ContainerRegistry.getInstance().findContainersMetadata(id = definition_id)) > 0 - ## Returns a machine node for the specified definition ID. - # - # If the machine node wasn't loaded yet, this will load it lazily. - # \param definition_id The definition to look for. - # \return A machine node for that definition. def __getitem__(self, definition_id: str) -> MachineNode: + """Returns a machine node for the specified definition ID. + + If the machine node wasn't loaded yet, this will load it lazily. + + :param definition_id: The definition to look for. + + :return: A machine node for that definition. + """ + if definition_id not in self._machines: start_time = time.time() self._machines[definition_id] = MachineNode(definition_id) @@ -106,46 +120,58 @@ class ContainerTree: Logger.log("d", "Adding container tree for {definition_id} took {duration} seconds.".format(definition_id = definition_id, duration = time.time() - start_time)) return self._machines[definition_id] - ## Gets a machine node for the specified definition ID, with default. - # - # The default is returned if there is no definition with the specified - # ID. If the machine node wasn't loaded yet, this will load it lazily. - # \param definition_id The definition to look for. - # \param default The machine node to return if there is no machine - # with that definition (can be ``None`` optionally or if not - # provided). - # \return A machine node for that definition, or the default if there - # is no definition with the provided definition_id. def get(self, definition_id: str, default: Optional[MachineNode] = None) -> Optional[MachineNode]: + """Gets a machine node for the specified definition ID, with default. + + The default is returned if there is no definition with the specified ID. If the machine node wasn't + loaded yet, this will load it lazily. + + :param definition_id: The definition to look for. + :param default: The machine node to return if there is no machine with that definition (can be ``None`` + optionally or if not provided). + + :return: A machine node for that definition, or the default if there is no definition with the provided + definition_id. + """ + if definition_id not in self: return default return self[definition_id] - ## Returns whether we've already cached this definition's node. - # \param definition_id The definition that we may have cached. - # \return ``True`` if it's cached. def is_loaded(self, definition_id: str) -> bool: + """Returns whether we've already cached this definition's node. + + :param definition_id: The definition that we may have cached. + + :return: ``True`` if it's cached. + """ + return definition_id in self._machines - ## Pre-loads all currently added printers as a background task so that - # switching printers in the interface is faster. class _MachineNodeLoadJob(Job): - ## Creates a new background task. - # \param tree_root The container tree instance. This cannot be - # obtained through the singleton static function since the instance - # may not yet be constructed completely. - # \param container_stacks All of the stacks to pre-load the container - # trees for. This needs to be provided from here because the stacks - # need to be constructed on the main thread because they are QObject. + """Pre-loads all currently added printers as a background task so that switching printers in the interface is + faster. + """ + def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]) -> None: + """Creates a new background task. + + :param tree_root: The container tree instance. This cannot be obtained through the singleton static + function since the instance may not yet be constructed completely. + :param container_stacks: All of the stacks to pre-load the container trees for. This needs to be provided + from here because the stacks need to be constructed on the main thread because they are QObject. + """ + self.tree_root = tree_root self.container_stacks = container_stacks super().__init__() - ## Starts the background task. - # - # The ``JobQueue`` will schedule this on a different thread. def run(self) -> None: + """Starts the background task. + + The ``JobQueue`` will schedule this on a different thread. + """ + for stack in self.container_stacks: # Load all currently-added containers. if not isinstance(stack, GlobalStack): continue diff --git a/cura/Machines/IntentNode.py b/cura/Machines/IntentNode.py index 2b3a596f81..f25a171d55 100644 --- a/cura/Machines/IntentNode.py +++ b/cura/Machines/IntentNode.py @@ -11,10 +11,12 @@ if TYPE_CHECKING: from cura.Machines.QualityNode import QualityNode -## This class represents an intent profile in the container tree. -# -# This class has no more subnodes. class IntentNode(ContainerNode): + """This class represents an intent profile in the container tree. + + This class has no more subnodes. + """ + def __init__(self, container_id: str, quality: "QualityNode") -> None: super().__init__(container_id) self.quality = quality diff --git a/cura/Machines/MachineErrorChecker.py b/cura/Machines/MachineErrorChecker.py index 7a5291dac5..bc9ef723d4 100644 --- a/cura/Machines/MachineErrorChecker.py +++ b/cura/Machines/MachineErrorChecker.py @@ -13,16 +13,16 @@ from UM.Settings.SettingDefinition import SettingDefinition from UM.Settings.Validator import ValidatorState import cura.CuraApplication -# -# This class performs setting error checks for the currently active machine. -# -# The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag. -# The idea here is to split the whole error check into small tasks, each of which only checks a single setting key -# in a stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should -# be good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait -# for it to finish the complete work. -# + class MachineErrorChecker(QObject): + """This class performs setting error checks for the currently active machine. + + The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag. The idea + here is to split the whole error check into small tasks, each of which only checks a single setting key in a + stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should be + good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait + for it to finish the complete work. + """ def __init__(self, parent: Optional[QObject] = None) -> None: super().__init__(parent) @@ -92,24 +92,37 @@ class MachineErrorChecker(QObject): def needToWaitForResult(self) -> bool: return self._need_to_check or self._check_in_progress - # Start the error check for property changed - # this is seperate from the startErrorCheck because it ignores a number property types def startErrorCheckPropertyChanged(self, key: str, property_name: str) -> None: + """Start the error check for property changed + + this is seperate from the startErrorCheck because it ignores a number property types + + :param key: + :param property_name: + """ + if property_name != "value": return self.startErrorCheck() - # Starts the error check timer to schedule a new error check. def startErrorCheck(self, *args: Any) -> None: + """Starts the error check timer to schedule a new error check. + + :param args: + """ + if not self._check_in_progress: self._need_to_check = True self.needToWaitForResultChanged.emit() self._error_check_timer.start() - # This function is called by the timer to reschedule a new error check. - # If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag - # to notify the current check to stop and start a new one. def _rescheduleCheck(self) -> None: + """This function is called by the timer to reschedule a new error check. + + If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag + to notify the current check to stop and start a new one. + """ + if self._check_in_progress and not self._need_to_check: self._need_to_check = True self.needToWaitForResultChanged.emit() diff --git a/cura/Machines/MachineNode.py b/cura/Machines/MachineNode.py index 6a415b01c4..c546f4d68d 100644 --- a/cura/Machines/MachineNode.py +++ b/cura/Machines/MachineNode.py @@ -17,10 +17,12 @@ from cura.Machines.VariantNode import VariantNode import UM.FlameProfiler -## This class represents a machine in the container tree. -# -# The subnodes of these nodes are variants. class MachineNode(ContainerNode): + """This class represents a machine in the container tree. + + The subnodes of these nodes are variants. + """ + def __init__(self, container_id: str) -> None: super().__init__(container_id) self.variants = {} # type: Dict[str, VariantNode] # Mapping variant names to their nodes. @@ -47,20 +49,21 @@ class MachineNode(ContainerNode): self._loadAll() - ## Get the available quality groups for this machine. - # - # This returns all quality groups, regardless of whether they are - # available to the combination of extruders or not. On the resulting - # quality groups, the is_available property is set to indicate whether the - # quality group can be selected according to the combination of extruders - # in the parameters. - # \param variant_names The names of the variants loaded in each extruder. - # \param material_bases The base file names of the materials loaded in - # each extruder. - # \param extruder_enabled Whether or not the extruders are enabled. This - # allows the function to set the is_available properly. - # \return For each available quality type, a QualityGroup instance. def getQualityGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> Dict[str, QualityGroup]: + """Get the available quality groups for this machine. + + This returns all quality groups, regardless of whether they are available to the combination of extruders or + not. On the resulting quality groups, the is_available property is set to indicate whether the quality group + can be selected according to the combination of extruders in the parameters. + + :param variant_names: The names of the variants loaded in each extruder. + :param material_bases: The base file names of the materials loaded in each extruder. + :param extruder_enabled: Whether or not the extruders are enabled. This allows the function to set the + is_available properly. + + :return: For each available quality type, a QualityGroup instance. + """ + if len(variant_names) != len(material_bases) or len(variant_names) != len(extruder_enabled): Logger.log("e", "The number of extruders in the list of variants (" + str(len(variant_names)) + ") is not equal to the number of extruders in the list of materials (" + str(len(material_bases)) + ") or the list of enabled extruders (" + str(len(extruder_enabled)) + ").") return {} @@ -98,28 +101,26 @@ class MachineNode(ContainerNode): quality_groups[quality_type].is_available = True return quality_groups - ## Returns all of the quality changes groups available to this printer. - # - # The quality changes groups store which quality type and intent category - # they were made for, but not which material and nozzle. Instead for the - # quality type and intent category, the quality changes will always be - # available but change the quality type and intent category when - # activated. - # - # The quality changes group does depend on the printer: Which quality - # definition is used. - # - # The quality changes groups that are available do depend on the quality - # types that are available, so it must still be known which extruders are - # enabled and which materials and variants are loaded in them. This allows - # setting the correct is_available flag. - # \param variant_names The names of the variants loaded in each extruder. - # \param material_bases The base file names of the materials loaded in - # each extruder. - # \param extruder_enabled For each extruder whether or not they are - # enabled. - # \return List of all quality changes groups for the printer. def getQualityChangesGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> List[QualityChangesGroup]: + """Returns all of the quality changes groups available to this printer. + + The quality changes groups store which quality type and intent category they were made for, but not which + material and nozzle. Instead for the quality type and intent category, the quality changes will always be + available but change the quality type and intent category when activated. + + The quality changes group does depend on the printer: Which quality definition is used. + + The quality changes groups that are available do depend on the quality types that are available, so it must + still be known which extruders are enabled and which materials and variants are loaded in them. This allows + setting the correct is_available flag. + + :param variant_names: The names of the variants loaded in each extruder. + :param material_bases: The base file names of the materials loaded in each extruder. + :param extruder_enabled: For each extruder whether or not they are enabled. + + :return: List of all quality changes groups for the printer. + """ + machine_quality_changes = ContainerRegistry.getInstance().findContainersMetadata(type = "quality_changes", definition = self.quality_definition) # All quality changes for each extruder. groups_by_name = {} #type: Dict[str, QualityChangesGroup] # Group quality changes profiles by their display name. The display name must be unique for quality changes. This finds profiles that belong together in a group. @@ -147,18 +148,19 @@ class MachineNode(ContainerNode): return list(groups_by_name.values()) - ## Gets the preferred global quality node, going by the preferred quality - # type. - # - # If the preferred global quality is not in there, an arbitrary global - # quality is taken. - # If there are no global qualities, an empty quality is returned. def preferredGlobalQuality(self) -> "QualityNode": + """Gets the preferred global quality node, going by the preferred quality type. + + If the preferred global quality is not in there, an arbitrary global quality is taken. If there are no global + qualities, an empty quality is returned. + """ + return self.global_qualities.get(self.preferred_quality_type, next(iter(self.global_qualities.values()))) - ## (Re)loads all variants under this printer. @UM.FlameProfiler.profile def _loadAll(self) -> None: + """(Re)loads all variants under this printer.""" + container_registry = ContainerRegistry.getInstance() if not self.has_variants: self.variants["empty"] = VariantNode("empty_variant", machine = self) diff --git a/cura/Machines/MaterialGroup.py b/cura/Machines/MaterialGroup.py index e05647e674..bce7b410c3 100644 --- a/cura/Machines/MaterialGroup.py +++ b/cura/Machines/MaterialGroup.py @@ -7,18 +7,21 @@ if TYPE_CHECKING: from cura.Machines.MaterialNode import MaterialNode -## A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile. -# The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For -# example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4", -# and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs". -# -# Using "generic_abs" as an example, the MaterialGroup for "generic_abs" will contain the following information: -# - name: "generic_abs", root_material_id -# - root_material_node: MaterialNode of "generic_abs" -# - derived_material_node_list: A list of MaterialNodes that are derived from "generic_abs", -# so "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc. -# class MaterialGroup: + """A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile. + + The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For + example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4", + and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs". + + Using "generic_abs" as an example, the MaterialGroup for "generic_abs" will contain the following information: + - name: "generic_abs", root_material_id + - root_material_node: MaterialNode of "generic_abs" + - derived_material_node_list: A list of MaterialNodes that are derived from "generic_abs", so + "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc. + + """ + __slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list") def __init__(self, name: str, root_material_node: "MaterialNode") -> None: diff --git a/cura/Machines/MaterialNode.py b/cura/Machines/MaterialNode.py index dcd4adcfdb..333452cc01 100644 --- a/cura/Machines/MaterialNode.py +++ b/cura/Machines/MaterialNode.py @@ -15,10 +15,12 @@ if TYPE_CHECKING: from cura.Machines.VariantNode import VariantNode -## Represents a material in the container tree. -# -# Its subcontainers are quality profiles. class MaterialNode(ContainerNode): + """Represents a material in the container tree. + + Its subcontainers are quality profiles. + """ + def __init__(self, container_id: str, variant: "VariantNode") -> None: super().__init__(container_id) self.variant = variant @@ -34,16 +36,16 @@ class MaterialNode(ContainerNode): container_registry.containerRemoved.connect(self._onRemoved) container_registry.containerMetaDataChanged.connect(self._onMetadataChanged) - ## Finds the preferred quality for this printer with this material and this - # variant loaded. - # - # If the preferred quality is not available, an arbitrary quality is - # returned. If there is a configuration mistake (like a typo in the - # preferred quality) this returns a random available quality. If there are - # no available qualities, this will return the empty quality node. - # \return The node for the preferred quality, or any arbitrary quality if - # there is no match. def preferredQuality(self) -> QualityNode: + """Finds the preferred quality for this printer with this material and this variant loaded. + + If the preferred quality is not available, an arbitrary quality is returned. If there is a configuration + mistake (like a typo in the preferred quality) this returns a random available quality. If there are no + available qualities, this will return the empty quality node. + + :return: The node for the preferred quality, or any arbitrary quality if there is no match. + """ + for quality_id, quality_node in self.qualities.items(): if self.variant.machine.preferred_quality_type == quality_node.quality_type: return quality_node @@ -107,10 +109,13 @@ class MaterialNode(ContainerNode): if not self.qualities: self.qualities["empty_quality"] = QualityNode("empty_quality", parent = self) - ## Triggered when any container is removed, but only handles it when the - # container is removed that this node represents. - # \param container The container that was allegedly removed. def _onRemoved(self, container: ContainerInterface) -> None: + """Triggered when any container is removed, but only handles it when the container is removed that this node + represents. + + :param container: The container that was allegedly removed. + """ + if container.getId() == self.container_id: # Remove myself from my parent. if self.base_file in self.variant.materials: @@ -119,13 +124,15 @@ class MaterialNode(ContainerNode): self.variant.materials["empty_material"] = MaterialNode("empty_material", variant = self.variant) self.materialChanged.emit(self) - ## Triggered when any metadata changed in any container, but only handles - # it when the metadata of this node is changed. - # \param container The container whose metadata changed. - # \param kwargs Key-word arguments provided when changing the metadata. - # These are ignored. As far as I know they are never provided to this - # call. def _onMetadataChanged(self, container: ContainerInterface, **kwargs: Any) -> None: + """Triggered when any metadata changed in any container, but only handles it when the metadata of this node is + changed. + + :param container: The container whose metadata changed. + :param kwargs: Key-word arguments provided when changing the metadata. These are ignored. As far as I know they + are never provided to this call. + """ + if container.getId() != self.container_id: return diff --git a/cura/Machines/Models/BaseMaterialsModel.py b/cura/Machines/Models/BaseMaterialsModel.py index 5e672faa12..3bc7803e9a 100644 --- a/cura/Machines/Models/BaseMaterialsModel.py +++ b/cura/Machines/Models/BaseMaterialsModel.py @@ -13,11 +13,13 @@ from cura.Machines.ContainerTree import ContainerTree from cura.Machines.MaterialNode import MaterialNode from cura.Settings.CuraContainerRegistry import CuraContainerRegistry -## This is the base model class for GenericMaterialsModel and MaterialBrandsModel. -# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately. -# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top -# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu class BaseMaterialsModel(ListModel): + """This is the base model class for GenericMaterialsModel and MaterialBrandsModel. + + Those 2 models are used by the material drop down menu to show generic materials and branded materials + separately. The extruder position defined here is being used to bound a menu to the correct extruder. This is + used in the top bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu + """ extruderPositionChanged = pyqtSignal() enabledChanged = pyqtSignal() @@ -121,10 +123,13 @@ class BaseMaterialsModel(ListModel): def enabled(self): return self._enabled - ## Triggered when a list of materials changed somewhere in the container - # tree. This change may trigger an _update() call when the materials - # changed for the configuration that this model is looking for. def _materialsListChanged(self, material: MaterialNode) -> None: + """Triggered when a list of materials changed somewhere in the container + + tree. This change may trigger an _update() call when the materials changed for the configuration that this + model is looking for. + """ + if self._extruder_stack is None: return if material.variant.container_id != self._extruder_stack.variant.getId(): @@ -136,14 +141,15 @@ class BaseMaterialsModel(ListModel): return self._onChanged() - ## Triggered when the list of favorite materials is changed. def _favoritesChanged(self, material_base_file: str) -> None: + """Triggered when the list of favorite materials is changed.""" + if material_base_file in self._available_materials: self._onChanged() - ## This is an abstract method that needs to be implemented by the specific - # models themselves. def _update(self): + """This is an abstract method that needs to be implemented by the specific models themselves. """ + self._favorite_ids = set(cura.CuraApplication.CuraApplication.getInstance().getPreferences().getValue("cura/favorite_materials").split(";")) # Update the available materials (ContainerNode) for the current active machine and extruder setup. @@ -163,10 +169,10 @@ class BaseMaterialsModel(ListModel): approximate_material_diameter = extruder_stack.getApproximateMaterialDiameter() self._available_materials = {key: material for key, material in materials.items() if float(material.getMetaDataEntry("approximate_diameter", -1)) == approximate_material_diameter} - ## This method is used by all material models in the beginning of the - # _update() method in order to prevent errors. It's the same in all models - # so it's placed here for easy access. def _canUpdate(self): + """This method is used by all material models in the beginning of the _update() method in order to prevent + errors. It's the same in all models so it's placed here for easy access. """ + global_stack = self._machine_manager.activeMachine if global_stack is None or not self._enabled: return False @@ -177,9 +183,10 @@ class BaseMaterialsModel(ListModel): return True - ## This is another convenience function which is shared by all material - # models so it's put here to avoid having so much duplicated code. def _createMaterialItem(self, root_material_id, container_node): + """This is another convenience function which is shared by all material models so it's put here to avoid having + so much duplicated code. """ + metadata_list = CuraContainerRegistry.getInstance().findContainersMetadata(id = container_node.container_id) if not metadata_list: return None diff --git a/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py b/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py index 1ab7e21700..ce4b87da2b 100644 --- a/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py +++ b/cura/Machines/Models/CustomQualityProfilesDropDownMenuModel.py @@ -14,9 +14,8 @@ if TYPE_CHECKING: from UM.Settings.Interfaces import ContainerInterface -## This model is used for the custom profile items in the profile drop down -# menu. class CustomQualityProfilesDropDownMenuModel(QualityProfilesDropDownMenuModel): + """This model is used for the custom profile items in the profile drop down menu.""" def __init__(self, parent: Optional["QObject"] = None) -> None: super().__init__(parent) diff --git a/cura/Machines/Models/DiscoveredCloudPrintersModel.py b/cura/Machines/Models/DiscoveredCloudPrintersModel.py index 23dcba6de7..692ed49593 100644 --- a/cura/Machines/Models/DiscoveredCloudPrintersModel.py +++ b/cura/Machines/Models/DiscoveredCloudPrintersModel.py @@ -9,9 +9,9 @@ if TYPE_CHECKING: class DiscoveredCloudPrintersModel(ListModel): - """ - Model used to inform the application about newly added cloud printers, which are discovered from the user's account - """ + """Model used to inform the application about newly added cloud printers, which are discovered from the user's + account """ + DeviceKeyRole = Qt.UserRole + 1 DeviceNameRole = Qt.UserRole + 2 DeviceTypeRole = Qt.UserRole + 3 @@ -31,18 +31,24 @@ class DiscoveredCloudPrintersModel(ListModel): self._application = application # type: CuraApplication def addDiscoveredCloudPrinters(self, new_devices: List[Dict[str, str]]) -> None: - """ - Adds all the newly discovered cloud printers into the DiscoveredCloudPrintersModel. + """Adds all the newly discovered cloud printers into the DiscoveredCloudPrintersModel. + + Example new_devices entry: + + .. code-block:: python - :param new_devices: List of dictionaries which contain information about added cloud printers. Example: { "key": "YjW8pwGYcaUvaa0YgVyWeFkX3z", "name": "NG 001", "machine_type": "Ultimaker S5", "firmware_version": "5.5.12.202001" } + + :param new_devices: List of dictionaries which contain information about added cloud printers. + :return: None """ + self._discovered_cloud_printers_list.extend(new_devices) self._update() @@ -51,21 +57,21 @@ class DiscoveredCloudPrintersModel(ListModel): @pyqtSlot() def clear(self) -> None: - """ - Clears the contents of the DiscoveredCloudPrintersModel. + """Clears the contents of the DiscoveredCloudPrintersModel. :return: None """ + self._discovered_cloud_printers_list = [] self._update() self.cloudPrintersDetectedChanged.emit(False) def _update(self) -> None: - """ - Sorts the newly discovered cloud printers by name and then updates the ListModel. + """Sorts the newly discovered cloud printers by name and then updates the ListModel. :return: None """ + items = self._discovered_cloud_printers_list[:] items.sort(key = lambda k: k["name"]) self.setItems(items) diff --git a/cura/Machines/Models/DiscoveredPrintersModel.py b/cura/Machines/Models/DiscoveredPrintersModel.py index 6d1bbdb698..176baf47e4 100644 --- a/cura/Machines/Models/DiscoveredPrintersModel.py +++ b/cura/Machines/Models/DiscoveredPrintersModel.py @@ -115,12 +115,11 @@ class DiscoveredPrinter(QObject): return catalog.i18nc("@label", "Available networked printers") -# -# Discovered printers are all the printers that were found on the network, which provide a more convenient way -# to add networked printers (Plugin finds a bunch of printers, user can select one from the list, plugin can then -# add that printer to Cura as the active one). -# class DiscoveredPrintersModel(QObject): + """Discovered printers are all the printers that were found on the network, which provide a more convenient way to + add networked printers (Plugin finds a bunch of printers, user can select one from the list, plugin can then add + that printer to Cura as the active one). + """ def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None: super().__init__(parent) @@ -254,8 +253,14 @@ class DiscoveredPrintersModel(QObject): del self._discovered_printer_by_ip_dict[ip_address] self.discoveredPrintersChanged.emit() - # A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer. - # This function invokes the given discovered printer's "create_callback" to do this. + @pyqtSlot("QVariant") def createMachineFromDiscoveredPrinter(self, discovered_printer: "DiscoveredPrinter") -> None: + """A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer. + + This function invokes the given discovered printer's "create_callback" to do this + + :param discovered_printer: + """ + discovered_printer.create_callback(discovered_printer.getKey()) diff --git a/cura/Machines/Models/ExtrudersModel.py b/cura/Machines/Models/ExtrudersModel.py index 9eee7f5f9e..942799cf43 100644 --- a/cura/Machines/Models/ExtrudersModel.py +++ b/cura/Machines/Models/ExtrudersModel.py @@ -15,27 +15,27 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## Model that holds extruders. -# -# This model is designed for use by any list of extruders, but specifically -# intended for drop-down lists of the current machine's extruders in place of -# settings. class ExtrudersModel(ListModel): + """Model that holds extruders. + + This model is designed for use by any list of extruders, but specifically intended for drop-down lists of the + current machine's extruders in place of settings. + """ + # The ID of the container stack for the extruder. IdRole = Qt.UserRole + 1 - ## Human-readable name of the extruder. NameRole = Qt.UserRole + 2 + """Human-readable name of the extruder.""" - ## Colour of the material loaded in the extruder. ColorRole = Qt.UserRole + 3 + """Colour of the material loaded in the extruder.""" - ## Index of the extruder, which is also the value of the setting itself. - # - # An index of 0 indicates the first extruder, an index of 1 the second - # one, and so on. This is the value that will be saved in instance - # containers. IndexRole = Qt.UserRole + 4 + """Index of the extruder, which is also the value of the setting itself. + + An index of 0 indicates the first extruder, an index of 1 the second one, and so on. This is the value that will + be saved in instance containers. """ # The ID of the definition of the extruder. DefinitionRole = Qt.UserRole + 5 @@ -50,18 +50,18 @@ class ExtrudersModel(ListModel): MaterialBrandRole = Qt.UserRole + 9 ColorNameRole = Qt.UserRole + 10 - ## Is the extruder enabled? EnabledRole = Qt.UserRole + 11 + """Is the extruder enabled?""" - ## List of colours to display if there is no material or the material has no known - # colour. defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"] + """List of colours to display if there is no material or the material has no known colour. """ - ## Initialises the extruders model, defining the roles and listening for - # changes in the data. - # - # \param parent Parent QtObject of this list. def __init__(self, parent = None): + """Initialises the extruders model, defining the roles and listening for changes in the data. + + :param parent: Parent QtObject of this list. + """ + super().__init__(parent) self.addRoleName(self.IdRole, "id") @@ -101,14 +101,15 @@ class ExtrudersModel(ListModel): def addOptionalExtruder(self): return self._add_optional_extruder - ## Links to the stack-changed signal of the new extruders when an extruder - # is swapped out or added in the current machine. - # - # \param machine_id The machine for which the extruders changed. This is - # filled by the ExtruderManager.extrudersChanged signal when coming from - # that signal. Application.globalContainerStackChanged doesn't fill this - # signal; it's assumed to be the current printer in that case. def _extrudersChanged(self, machine_id = None): + """Links to the stack-changed signal of the new extruders when an extruder is swapped out or added in the + current machine. + + :param machine_id: The machine for which the extruders changed. This is filled by the + ExtruderManager.extrudersChanged signal when coming from that signal. Application.globalContainerStackChanged + doesn't fill this signal; it's assumed to be the current printer in that case. + """ + machine_manager = Application.getInstance().getMachineManager() if machine_id is not None: if machine_manager.activeMachine is None: @@ -146,11 +147,13 @@ class ExtrudersModel(ListModel): def _updateExtruders(self): self._update_extruder_timer.start() - ## Update the list of extruders. - # - # This should be called whenever the list of extruders changes. @UM.FlameProfiler.profile def __updateExtruders(self): + """Update the list of extruders. + + This should be called whenever the list of extruders changes. + """ + extruders_changed = False if self.count != 0: diff --git a/cura/Machines/Models/FavoriteMaterialsModel.py b/cura/Machines/Models/FavoriteMaterialsModel.py index 6b8f0e8e56..203888d6fb 100644 --- a/cura/Machines/Models/FavoriteMaterialsModel.py +++ b/cura/Machines/Models/FavoriteMaterialsModel.py @@ -4,16 +4,17 @@ from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel import cura.CuraApplication # To listen to changes to the preferences. -## Model that shows the list of favorite materials. class FavoriteMaterialsModel(BaseMaterialsModel): + """Model that shows the list of favorite materials.""" + def __init__(self, parent = None): super().__init__(parent) cura.CuraApplication.CuraApplication.getInstance().getPreferences().preferenceChanged.connect(self._onFavoritesChanged) self._onChanged() - ## Triggered when any preference changes, but only handles it when the list - # of favourites is changed. def _onFavoritesChanged(self, preference_key: str) -> None: + """Triggered when any preference changes, but only handles it when the list of favourites is changed. """ + if preference_key != "cura/favorite_materials": return self._onChanged() diff --git a/cura/Machines/Models/FirstStartMachineActionsModel.py b/cura/Machines/Models/FirstStartMachineActionsModel.py index 92caed7b12..7d83f0bff2 100644 --- a/cura/Machines/Models/FirstStartMachineActionsModel.py +++ b/cura/Machines/Models/FirstStartMachineActionsModel.py @@ -11,13 +11,13 @@ if TYPE_CHECKING: from cura.CuraApplication import CuraApplication -# -# This model holds all first-start machine actions for the currently active machine. It has 2 roles: -# - title : the title/name of the action -# - content : the QObject of the QML content of the action -# - action : the MachineAction object itself -# class FirstStartMachineActionsModel(ListModel): + """This model holds all first-start machine actions for the currently active machine. It has 2 roles: + + - title : the title/name of the action + - content : the QObject of the QML content of the action + - action : the MachineAction object itself + """ TitleRole = Qt.UserRole + 1 ContentRole = Qt.UserRole + 2 @@ -73,9 +73,10 @@ class FirstStartMachineActionsModel(ListModel): self._current_action_index += 1 self.currentActionIndexChanged.emit() - # Resets the current action index to 0 so the wizard panel can show actions from the beginning. @pyqtSlot() def reset(self) -> None: + """Resets the current action index to 0 so the wizard panel can show actions from the beginning.""" + self._current_action_index = 0 self.currentActionIndexChanged.emit() diff --git a/cura/Machines/Models/GlobalStacksModel.py b/cura/Machines/Models/GlobalStacksModel.py index 9db4ffe6db..73d58f22c0 100644 --- a/cura/Machines/Models/GlobalStacksModel.py +++ b/cura/Machines/Models/GlobalStacksModel.py @@ -42,8 +42,9 @@ class GlobalStacksModel(ListModel): CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged) self._updateDelayed() - ## Handler for container added/removed events from registry def _onContainerChanged(self, container) -> None: + """Handler for container added/removed events from registry""" + # We only need to update when the added / removed container GlobalStack if isinstance(container, GlobalStack): self._updateDelayed() diff --git a/cura/Machines/Models/IntentCategoryModel.py b/cura/Machines/Models/IntentCategoryModel.py index 427e60ec0c..d8502a7574 100644 --- a/cura/Machines/Models/IntentCategoryModel.py +++ b/cura/Machines/Models/IntentCategoryModel.py @@ -18,9 +18,9 @@ from UM.i18n import i18nCatalog catalog = i18nCatalog("cura") -## Lists the intent categories that are available for the current printer -# configuration. class IntentCategoryModel(ListModel): + """Lists the intent categories that are available for the current printer configuration. """ + NameRole = Qt.UserRole + 1 IntentCategoryRole = Qt.UserRole + 2 WeightRole = Qt.UserRole + 3 @@ -31,10 +31,12 @@ class IntentCategoryModel(ListModel): _translations = collections.OrderedDict() # type: "collections.OrderedDict[str,Dict[str,Optional[str]]]" - # Translations to user-visible string. Ordered by weight. - # TODO: Create a solution for this name and weight to be used dynamically. @classmethod def _get_translations(cls): + """Translations to user-visible string. Ordered by weight. + + TODO: Create a solution for this name and weight to be used dynamically. + """ if len(cls._translations) == 0: cls._translations["default"] = { "name": catalog.i18nc("@label", "Default") @@ -53,9 +55,12 @@ class IntentCategoryModel(ListModel): } return cls._translations - ## Creates a new model for a certain intent category. - # \param The category to list the intent profiles for. def __init__(self, intent_category: str) -> None: + """Creates a new model for a certain intent category. + + :param intent_category: category to list the intent profiles for. + """ + super().__init__() self._intent_category = intent_category @@ -84,16 +89,18 @@ class IntentCategoryModel(ListModel): self.update() - ## Updates the list of intents if an intent profile was added or removed. def _onContainerChange(self, container: "ContainerInterface") -> None: + """Updates the list of intents if an intent profile was added or removed.""" + if container.getMetaDataEntry("type") == "intent": self.update() def update(self): self._update_timer.start() - ## Updates the list of intents. def _update(self) -> None: + """Updates the list of intents.""" + available_categories = IntentManager.getInstance().currentAvailableIntentCategories() result = [] for category in available_categories: @@ -109,9 +116,9 @@ class IntentCategoryModel(ListModel): result.sort(key = lambda k: k["weight"]) self.setItems(result) - ## Get a display value for a category. - ## for categories and keys @staticmethod def translation(category: str, key: str, default: Optional[str] = None): + """Get a display value for a category.for categories and keys""" + display_strings = IntentCategoryModel._get_translations().get(category, {}) return display_strings.get(key, default) diff --git a/cura/Machines/Models/IntentModel.py b/cura/Machines/Models/IntentModel.py index 951be7ab2d..0ec7e268f0 100644 --- a/cura/Machines/Models/IntentModel.py +++ b/cura/Machines/Models/IntentModel.py @@ -98,8 +98,9 @@ class IntentModel(ListModel): new_items = sorted(new_items, key = lambda x: x["layer_height"]) self.setItems(new_items) - ## Get the active materials for all extruders. No duplicates will be returned def _getActiveMaterials(self) -> Set["MaterialNode"]: + """Get the active materials for all extruders. No duplicates will be returned""" + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return set() diff --git a/cura/Machines/Models/MaterialManagementModel.py b/cura/Machines/Models/MaterialManagementModel.py index f00b81e987..d18b040936 100644 --- a/cura/Machines/Models/MaterialManagementModel.py +++ b/cura/Machines/Models/MaterialManagementModel.py @@ -19,28 +19,31 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## Proxy class to the materials page in the preferences. -# -# This class handles the actions in that page, such as creating new materials, -# renaming them, etc. class MaterialManagementModel(QObject): - ## Triggered when a favorite is added or removed. - # \param The base file of the material is provided as parameter when this - # emits. - favoritesChanged = pyqtSignal(str) + """Proxy class to the materials page in the preferences. + + This class handles the actions in that page, such as creating new materials, renaming them, etc. + """ + + favoritesChanged = pyqtSignal(str) + """Triggered when a favorite is added or removed. + + :param The base file of the material is provided as parameter when this emits + """ - ## Can a certain material be deleted, or is it still in use in one of the - # container stacks anywhere? - # - # We forbid the user from deleting a material if it's in use in any stack. - # Deleting it while it's in use can lead to corrupted stacks. In the - # future we might enable this functionality again (deleting the material - # from those stacks) but for now it is easier to prevent the user from - # doing this. - # \param material_node The ContainerTree node of the material to check. - # \return Whether or not the material can be removed. @pyqtSlot("QVariant", result = bool) def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool: + """Can a certain material be deleted, or is it still in use in one of the container stacks anywhere? + + We forbid the user from deleting a material if it's in use in any stack. Deleting it while it's in use can + lead to corrupted stacks. In the future we might enable this functionality again (deleting the material from + those stacks) but for now it is easier to prevent the user from doing this. + + :param material_node: The ContainerTree node of the material to check. + + :return: Whether or not the material can be removed. + """ + container_registry = CuraContainerRegistry.getInstance() ids_to_remove = {metadata.get("id", "") for metadata in container_registry.findInstanceContainersMetadata(base_file = material_node.base_file)} for extruder_stack in container_registry.findContainerStacks(type = "extruder_train"): @@ -48,11 +51,14 @@ class MaterialManagementModel(QObject): return False return True - ## Change the user-visible name of a material. - # \param material_node The ContainerTree node of the material to rename. - # \param name The new name for the material. @pyqtSlot("QVariant", str) def setMaterialName(self, material_node: "MaterialNode", name: str) -> None: + """Change the user-visible name of a material. + + :param material_node: The ContainerTree node of the material to rename. + :param name: The new name for the material. + """ + container_registry = CuraContainerRegistry.getInstance() root_material_id = material_node.base_file if container_registry.isReadOnly(root_material_id): @@ -60,18 +66,20 @@ class MaterialManagementModel(QObject): return return container_registry.findContainers(id = root_material_id)[0].setName(name) - ## Deletes a material from Cura. - # - # This function does not do any safety checking any more. Please call this - # function only if: - # - The material is not read-only. - # - The material is not used in any stacks. - # If the material was not lazy-loaded yet, this will fully load the - # container. When removing this material node, all other materials with - # the same base fill will also be removed. - # \param material_node The material to remove. @pyqtSlot("QVariant") def removeMaterial(self, material_node: "MaterialNode") -> None: + """Deletes a material from Cura. + + This function does not do any safety checking any more. Please call this function only if: + - The material is not read-only. + - The material is not used in any stacks. + + If the material was not lazy-loaded yet, this will fully load the container. When removing this material + node, all other materials with the same base fill will also be removed. + + :param material_node: The material to remove. + """ + container_registry = CuraContainerRegistry.getInstance() materials_this_base_file = container_registry.findContainersMetadata(base_file = material_node.base_file) @@ -89,17 +97,19 @@ class MaterialManagementModel(QObject): for material_metadata in materials_this_base_file: container_registry.removeContainer(material_metadata["id"]) - ## Creates a duplicate of a material with the same GUID and base_file - # metadata. - # \param base_file: The base file of the material to duplicate. - # \param new_base_id A new material ID for the base material. The IDs of - # the submaterials will be based off this one. If not provided, a material - # ID will be generated automatically. - # \param new_metadata Metadata for the new material. If not provided, this - # will be duplicated from the original material. - # \return The root material ID of the duplicate material. def duplicateMaterialByBaseFile(self, base_file: str, new_base_id: Optional[str] = None, new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]: + """Creates a duplicate of a material with the same GUID and base_file metadata + + :param base_file: The base file of the material to duplicate. + :param new_base_id: A new material ID for the base material. The IDs of the submaterials will be based off this + one. If not provided, a material ID will be generated automatically. + :param new_metadata: Metadata for the new material. If not provided, this will be duplicated from the original + material. + + :return: The root material ID of the duplicate material. + """ + container_registry = CuraContainerRegistry.getInstance() root_materials = container_registry.findContainers(id = base_file) @@ -171,29 +181,32 @@ class MaterialManagementModel(QObject): return new_base_id - ## Creates a duplicate of a material with the same GUID and base_file - # metadata. - # \param material_node The node representing the material to duplicate. - # \param new_base_id A new material ID for the base material. The IDs of - # the submaterials will be based off this one. If not provided, a material - # ID will be generated automatically. - # \param new_metadata Metadata for the new material. If not provided, this - # will be duplicated from the original material. - # \return The root material ID of the duplicate material. @pyqtSlot("QVariant", result = str) def duplicateMaterial(self, material_node: "MaterialNode", new_base_id: Optional[str] = None, new_metadata: Optional[Dict[str, Any]] = None) -> Optional[str]: + """Creates a duplicate of a material with the same GUID and base_file metadata + + :param material_node: The node representing the material to duplicate. + :param new_base_id: A new material ID for the base material. The IDs of the submaterials will be based off this + one. If not provided, a material ID will be generated automatically. + :param new_metadata: Metadata for the new material. If not provided, this will be duplicated from the original + material. + + :return: The root material ID of the duplicate material. + """ return self.duplicateMaterialByBaseFile(material_node.base_file, new_base_id, new_metadata) - ## Create a new material by cloning the preferred material for the current - # material diameter and generate a new GUID. - # - # The material type is explicitly left to be the one from the preferred - # material, since this allows the user to still have SOME profiles to work - # with. - # \return The ID of the newly created material. @pyqtSlot(result = str) def createMaterial(self) -> str: + """Create a new material by cloning the preferred material for the current material diameter and generate a new + GUID. + + The material type is explicitly left to be the one from the preferred material, since this allows the user to + still have SOME profiles to work with. + + :return: The ID of the newly created material. + """ + # Ensure all settings are saved. application = cura.CuraApplication.CuraApplication.getInstance() application.saveSettings() @@ -218,10 +231,13 @@ class MaterialManagementModel(QObject): self.duplicateMaterial(preferred_material_node, new_base_id = new_id, new_metadata = new_metadata) return new_id - ## Adds a certain material to the favorite materials. - # \param material_base_file The base file of the material to add. @pyqtSlot(str) def addFavorite(self, material_base_file: str) -> None: + """Adds a certain material to the favorite materials. + + :param material_base_file: The base file of the material to add. + """ + application = cura.CuraApplication.CuraApplication.getInstance() favorites = application.getPreferences().getValue("cura/favorite_materials").split(";") if material_base_file not in favorites: @@ -230,11 +246,13 @@ class MaterialManagementModel(QObject): application.saveSettings() self.favoritesChanged.emit(material_base_file) - ## Removes a certain material from the favorite materials. - # - # If the material was not in the favorite materials, nothing happens. @pyqtSlot(str) def removeFavorite(self, material_base_file: str) -> None: + """Removes a certain material from the favorite materials. + + If the material was not in the favorite materials, nothing happens. + """ + application = cura.CuraApplication.CuraApplication.getInstance() favorites = application.getPreferences().getValue("cura/favorite_materials").split(";") try: diff --git a/cura/Machines/Models/MultiBuildPlateModel.py b/cura/Machines/Models/MultiBuildPlateModel.py index add960a545..8e2f086e3b 100644 --- a/cura/Machines/Models/MultiBuildPlateModel.py +++ b/cura/Machines/Models/MultiBuildPlateModel.py @@ -9,11 +9,11 @@ from UM.Scene.Selection import Selection from UM.Qt.ListModel import ListModel -# -# This is the model for multi build plate feature. -# This has nothing to do with the build plate types you can choose on the sidebar for a machine. -# class MultiBuildPlateModel(ListModel): + """This is the model for multi build plate feature. + + This has nothing to do with the build plate types you can choose on the sidebar for a machine. + """ maxBuildPlateChanged = pyqtSignal() activeBuildPlateChanged = pyqtSignal() @@ -39,9 +39,10 @@ class MultiBuildPlateModel(ListModel): self._max_build_plate = max_build_plate self.maxBuildPlateChanged.emit() - ## Return the highest build plate number @pyqtProperty(int, notify = maxBuildPlateChanged) def maxBuildPlate(self): + """Return the highest build plate number""" + return self._max_build_plate def setActiveBuildPlate(self, nr): diff --git a/cura/Machines/Models/QualityManagementModel.py b/cura/Machines/Models/QualityManagementModel.py index 74dc8649d0..6dfed60f7b 100644 --- a/cura/Machines/Models/QualityManagementModel.py +++ b/cura/Machines/Models/QualityManagementModel.py @@ -26,10 +26,9 @@ if TYPE_CHECKING: from cura.Settings.GlobalStack import GlobalStack -# -# This the QML model for the quality management page. -# class QualityManagementModel(ListModel): + """This the QML model for the quality management page.""" + NameRole = Qt.UserRole + 1 IsReadOnlyRole = Qt.UserRole + 2 QualityGroupRole = Qt.UserRole + 3 @@ -74,11 +73,13 @@ class QualityManagementModel(ListModel): def _onChange(self) -> None: self._update_timer.start() - ## Deletes a custom profile. It will be gone forever. - # \param quality_changes_group The quality changes group representing the - # profile to delete. @pyqtSlot(QObject) def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None: + """Deletes a custom profile. It will be gone forever. + + :param quality_changes_group: The quality changes group representing the profile to delete. + """ + Logger.log("i", "Removing quality changes group {group_name}".format(group_name = quality_changes_group.name)) removed_quality_changes_ids = set() container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry() @@ -95,16 +96,19 @@ class QualityManagementModel(ListModel): if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids: extruder_stack.qualityChanges = empty_quality_changes_container - ## Rename a custom profile. - # - # Because the names must be unique, the new name may not actually become - # the name that was given. The actual name is returned by this function. - # \param quality_changes_group The custom profile that must be renamed. - # \param new_name The desired name for the profile. - # \return The actual new name of the profile, after making the name - # unique. @pyqtSlot(QObject, str, result = str) def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str: + """Rename a custom profile. + + Because the names must be unique, the new name may not actually become the name that was given. The actual + name is returned by this function. + + :param quality_changes_group: The custom profile that must be renamed. + :param new_name: The desired name for the profile. + + :return: The actual new name of the profile, after making the name unique. + """ + Logger.log("i", "Renaming QualityChangesGroup {old_name} to {new_name}.".format(old_name = quality_changes_group.name, new_name = new_name)) if new_name == quality_changes_group.name: Logger.log("i", "QualityChangesGroup name {name} unchanged.".format(name = quality_changes_group.name)) @@ -138,13 +142,16 @@ class QualityManagementModel(ListModel): return new_name - ## Duplicates a given quality profile OR quality changes profile. - # \param new_name The desired name of the new profile. This will be made - # unique, so it might end up with a different name. - # \param quality_model_item The item of this model to duplicate, as - # dictionary. See the descriptions of the roles of this list model. @pyqtSlot(str, "QVariantMap") def duplicateQualityChanges(self, new_name: str, quality_model_item: Dict[str, Any]) -> None: + """Duplicates a given quality profile OR quality changes profile. + + :param new_name: The desired name of the new profile. This will be made unique, so it might end up with a + different name. + :param quality_model_item: The item of this model to duplicate, as dictionary. See the descriptions of the + roles of this list model. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if not global_stack: Logger.log("i", "No active global stack, cannot duplicate quality (changes) profile.") @@ -170,18 +177,18 @@ class QualityManagementModel(ListModel): new_id = container_registry.uniqueName(container.getId()) container_registry.addContainer(container.duplicate(new_id, new_name)) - ## Create quality changes containers from the user containers in the active - # stacks. - # - # This will go through the global and extruder stacks and create - # quality_changes containers from the user containers in each stack. These - # then replace the quality_changes containers in the stack and clear the - # user settings. - # \param base_name The new name for the quality changes profile. The final - # name of the profile might be different from this, because it needs to be - # made unique. @pyqtSlot(str) def createQualityChanges(self, base_name: str) -> None: + """Create quality changes containers from the user containers in the active stacks. + + This will go through the global and extruder stacks and create quality_changes containers from the user + containers in each stack. These then replace the quality_changes containers in the stack and clear the user + settings. + + :param base_name: The new name for the quality changes profile. The final name of the profile might be + different from this, because it needs to be made unique. + """ + machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager() global_stack = machine_manager.activeMachine @@ -220,14 +227,16 @@ class QualityManagementModel(ListModel): container_registry.addContainer(new_changes) - ## Create a quality changes container with the given set-up. - # \param quality_type The quality type of the new container. - # \param intent_category The intent category of the new container. - # \param new_name The name of the container. This name must be unique. - # \param machine The global stack to create the profile for. - # \param extruder_stack The extruder stack to create the profile for. If - # not provided, only a global container will be created. def _createQualityChanges(self, quality_type: str, intent_category: Optional[str], new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer": + """Create a quality changes container with the given set-up. + + :param quality_type: The quality type of the new container. + :param intent_category: The intent category of the new container. + :param new_name: The name of the container. This name must be unique. + :param machine: The global stack to create the profile for. + :param extruder_stack: The extruder stack to create the profile for. If not provided, only a global container will be created. + """ + container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry() base_id = machine.definition.getId() if extruder_stack is None else extruder_stack.getId() new_id = base_id + "_" + new_name @@ -253,11 +262,13 @@ class QualityManagementModel(ListModel): quality_changes.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.getInstance().SettingVersion) return quality_changes - ## Triggered when any container changed. - # - # This filters the updates to the container manager: When it applies to - # the list of quality changes, we need to update our list. def _qualityChangesListChanged(self, container: "ContainerInterface") -> None: + """Triggered when any container changed. + + This filters the updates to the container manager: When it applies to the list of quality changes, we need to + update our list. + """ + if container.getMetaDataEntry("type") == "quality_changes": self._update() @@ -366,18 +377,19 @@ class QualityManagementModel(ListModel): self.setItems(item_list) - # TODO: Duplicated code here from InstanceContainersModel. Refactor and remove this later. - # - ## Gets a list of the possible file filters that the plugins have - # registered they can read or write. The convenience meta-filters - # "All Supported Types" and "All Files" are added when listing - # readers, but not when listing writers. - # - # \param io_type \type{str} name of the needed IO type - # \return A list of strings indicating file name filters for a file - # dialog. @pyqtSlot(str, result = "QVariantList") def getFileNameFilters(self, io_type): + """Gets a list of the possible file filters that the plugins have registered they can read or write. + + The convenience meta-filters "All Supported Types" and "All Files" are added when listing readers, + but not when listing writers. + + :param io_type: name of the needed IO type + :return: A list of strings indicating file name filters for a file dialog. + + TODO: Duplicated code here from InstanceContainersModel. Refactor and remove this later. + """ + from UM.i18n import i18nCatalog catalog = i18nCatalog("uranium") #TODO: This function should be in UM.Resources! @@ -394,9 +406,11 @@ class QualityManagementModel(ListModel): filters.append(catalog.i18nc("@item:inlistbox", "All Files (*)")) # Also allow arbitrary files, if the user so prefers. return filters - ## Gets a list of profile reader or writer plugins - # \return List of tuples of (plugin_id, meta_data). def _getIOPlugins(self, io_type): + """Gets a list of profile reader or writer plugins + + :return: List of tuples of (plugin_id, meta_data). + """ from UM.PluginRegistry import PluginRegistry pr = PluginRegistry.getInstance() active_plugin_ids = pr.getActivePlugins() diff --git a/cura/Machines/Models/QualityProfilesDropDownMenuModel.py b/cura/Machines/Models/QualityProfilesDropDownMenuModel.py index 3a79ceeaf1..7aa30c6f82 100644 --- a/cura/Machines/Models/QualityProfilesDropDownMenuModel.py +++ b/cura/Machines/Models/QualityProfilesDropDownMenuModel.py @@ -10,10 +10,9 @@ from cura.Machines.ContainerTree import ContainerTree from cura.Machines.Models.MachineModelUtils import fetchLayerHeight -# -# QML Model for all built-in quality profiles. This model is used for the drop-down quality menu. -# class QualityProfilesDropDownMenuModel(ListModel): + """QML Model for all built-in quality profiles. This model is used for the drop-down quality menu.""" + NameRole = Qt.UserRole + 1 QualityTypeRole = Qt.UserRole + 2 LayerHeightRole = Qt.UserRole + 3 diff --git a/cura/Machines/Models/QualitySettingsModel.py b/cura/Machines/Models/QualitySettingsModel.py index 8a956263e7..04af5aff7b 100644 --- a/cura/Machines/Models/QualitySettingsModel.py +++ b/cura/Machines/Models/QualitySettingsModel.py @@ -10,10 +10,9 @@ from UM.Qt.ListModel import ListModel from UM.Settings.ContainerRegistry import ContainerRegistry -# -# This model is used to show details settings of the selected quality in the quality management page. -# class QualitySettingsModel(ListModel): + """This model is used to show details settings of the selected quality in the quality management page.""" + KeyRole = Qt.UserRole + 1 LabelRole = Qt.UserRole + 2 UnitRole = Qt.UserRole + 3 diff --git a/cura/Machines/QualityChangesGroup.py b/cura/Machines/QualityChangesGroup.py index 655060070b..ecaaa67b7a 100644 --- a/cura/Machines/QualityChangesGroup.py +++ b/cura/Machines/QualityChangesGroup.py @@ -6,12 +6,12 @@ from typing import Any, Dict, Optional from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal -## Data struct to group several quality changes instance containers together. -# -# Each group represents one "custom profile" as the user sees it, which -# contains an instance container for the global stack and one instance -# container per extruder. class QualityChangesGroup(QObject): + """Data struct to group several quality changes instance containers together. + + Each group represents one "custom profile" as the user sees it, which contains an instance container for the + global stack and one instance container per extruder. + """ def __init__(self, name: str, quality_type: str, intent_category: str, parent: Optional["QObject"] = None) -> None: super().__init__(parent) diff --git a/cura/Machines/QualityGroup.py b/cura/Machines/QualityGroup.py index 97b5e28b41..8e470e0102 100644 --- a/cura/Machines/QualityGroup.py +++ b/cura/Machines/QualityGroup.py @@ -9,28 +9,34 @@ from UM.Util import parseBool from cura.Machines.ContainerNode import ContainerNode -## A QualityGroup represents a group of quality containers that must be applied -# to each ContainerStack when it's used. -# -# A concrete example: When there are two extruders and the user selects the -# quality type "normal", this quality type must be applied to all stacks in a -# machine, although each stack can have different containers. So one global -# profile gets put on the global stack and one extruder profile gets put on -# each extruder stack. This quality group then contains the following -# profiles (for instance): -# GlobalStack ExtruderStack 1 ExtruderStack 2 -# quality container: um3_global_normal um3_aa04_pla_normal um3_aa04_abs_normal -# -# The purpose of these quality groups is to group the containers that can be -# applied to a configuration, so that when a quality level is selected, the -# container can directly be applied to each stack instead of looking them up -# again. class QualityGroup: - ## Constructs a new group. - # \param name The user-visible name for the group. - # \param quality_type The quality level that each profile in this group - # has. + """A QualityGroup represents a group of quality containers that must be applied to each ContainerStack when it's + used. + + A concrete example: When there are two extruders and the user selects the quality type "normal", this quality + type must be applied to all stacks in a machine, although each stack can have different containers. So one global + profile gets put on the global stack and one extruder profile gets put on each extruder stack. This quality group + then contains the following profiles (for instance): + - GlobalStack + - ExtruderStack 1 + - ExtruderStack 2 + quality container: + - um3_global_normal + - um3_aa04_pla_normal + - um3_aa04_abs_normal + + The purpose of these quality groups is to group the containers that can be applied to a configuration, + so that when a quality level is selected, the container can directly be applied to each stack instead of looking + them up again. + """ + def __init__(self, name: str, quality_type: str) -> None: + """Constructs a new group. + + :param name: The user-visible name for the group. + :param quality_type: The quality level that each profile in this group has. + """ + self.name = name self.node_for_global = None # type: Optional[ContainerNode] self.nodes_for_extruders = {} # type: Dict[int, ContainerNode] diff --git a/cura/Machines/QualityNode.py b/cura/Machines/QualityNode.py index 7696dfb117..85e55c7a2d 100644 --- a/cura/Machines/QualityNode.py +++ b/cura/Machines/QualityNode.py @@ -13,12 +13,14 @@ if TYPE_CHECKING: from cura.Machines.MachineNode import MachineNode -## Represents a quality profile in the container tree. -# -# This may either be a normal quality profile or a global quality profile. -# -# Its subcontainers are intent profiles. class QualityNode(ContainerNode): + """Represents a quality profile in the container tree. + + This may either be a normal quality profile or a global quality profile. + + Its subcontainers are intent profiles. + """ + def __init__(self, container_id: str, parent: Union["MaterialNode", "MachineNode"]) -> None: super().__init__(container_id) self.parent = parent diff --git a/cura/Machines/VariantNode.py b/cura/Machines/VariantNode.py index 0f30782a91..72372b5a45 100644 --- a/cura/Machines/VariantNode.py +++ b/cura/Machines/VariantNode.py @@ -17,16 +17,16 @@ if TYPE_CHECKING: from cura.Machines.MachineNode import MachineNode -## This class represents an extruder variant in the container tree. -# -# The subnodes of these nodes are materials. -# -# This node contains materials with ALL filament diameters underneath it. The -# tree of this variant is not specific to one global stack, so because the -# list of materials can be different per stack depending on the compatible -# material diameter setting, we cannot filter them here. Filtering must be -# done in the model. class VariantNode(ContainerNode): + """This class represents an extruder variant in the container tree. + + The subnodes of these nodes are materials. + + This node contains materials with ALL filament diameters underneath it. The tree of this variant is not specific + to one global stack, so because the list of materials can be different per stack depending on the compatible + material diameter setting, we cannot filter them here. Filtering must be done in the model. + """ + def __init__(self, container_id: str, machine: "MachineNode") -> None: super().__init__(container_id) self.machine = machine @@ -39,9 +39,10 @@ class VariantNode(ContainerNode): container_registry.containerRemoved.connect(self._materialRemoved) self._loadAll() - ## (Re)loads all materials under this variant. @UM.FlameProfiler.profile def _loadAll(self) -> None: + """(Re)loads all materials under this variant.""" + container_registry = ContainerRegistry.getInstance() if not self.machine.has_materials: @@ -69,18 +70,18 @@ class VariantNode(ContainerNode): if not self.materials: self.materials["empty_material"] = MaterialNode("empty_material", variant = self) - ## Finds the preferred material for this printer with this nozzle in one of - # the extruders. - # - # If the preferred material is not available, an arbitrary material is - # returned. If there is a configuration mistake (like a typo in the - # preferred material) this returns a random available material. If there - # are no available materials, this will return the empty material node. - # \param approximate_diameter The desired approximate diameter of the - # material. - # \return The node for the preferred material, or any arbitrary material - # if there is no match. def preferredMaterial(self, approximate_diameter: int) -> MaterialNode: + """Finds the preferred material for this printer with this nozzle in one of the extruders. + + If the preferred material is not available, an arbitrary material is returned. If there is a configuration + mistake (like a typo in the preferred material) this returns a random available material. If there are no + available materials, this will return the empty material node. + + :param approximate_diameter: The desired approximate diameter of the material. + + :return: The node for the preferred material, or any arbitrary material if there is no match. + """ + for base_material, material_node in self.materials.items(): if self.machine.preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")): return material_node @@ -107,10 +108,10 @@ class VariantNode(ContainerNode): )) return fallback - ## When a material gets added to the set of profiles, we need to update our - # tree here. @UM.FlameProfiler.profile def _materialAdded(self, container: ContainerInterface) -> None: + """When a material gets added to the set of profiles, we need to update our tree here.""" + if container.getMetaDataEntry("type") != "material": return # Not interested. if not ContainerRegistry.getInstance().findContainersMetadata(id = container.getId()): From b032101e55a2ef48421d1cdc663ac9b85eea0e14 Mon Sep 17 00:00:00 2001 From: jelle Spijker Date: Mon, 11 May 2020 13:16:29 +0200 Subject: [PATCH 15/27] Updated comments in cura/OAuth2 Used DOX_2_RST.py to convert doxygen style comments to sphinx style comments --- cura/OAuth2/AuthorizationHelpers.py | 63 ++++++++++++++-------- cura/OAuth2/AuthorizationRequestHandler.py | 22 +++++--- cura/OAuth2/AuthorizationRequestServer.py | 19 ++++--- cura/OAuth2/AuthorizationService.py | 47 ++++++++++------ cura/OAuth2/LocalAuthorizationServer.py | 35 +++++++----- cura/OAuth2/Models.py | 20 ++++--- 6 files changed, 135 insertions(+), 71 deletions(-) diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index cc809abf05..612c00de2f 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -16,23 +16,27 @@ from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settin catalog = i18nCatalog("cura") TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S" -## Class containing several helpers to deal with the authorization flow. class AuthorizationHelpers: + """Class containing several helpers to deal with the authorization flow.""" + def __init__(self, settings: "OAuth2Settings") -> None: self._settings = settings self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL) @property - ## The OAuth2 settings object. def settings(self) -> "OAuth2Settings": + """The OAuth2 settings object.""" + return self._settings - ## Request the access token from the authorization server. - # \param authorization_code: The authorization code from the 1st step. - # \param verification_code: The verification code needed for the PKCE - # extension. - # \return An AuthenticationResponse object. def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse": + """Request the access token from the authorization server. + + :param authorization_code: The authorization code from the 1st step. + :param verification_code: The verification code needed for the PKCE extension. + :return: An AuthenticationResponse object. + """ + data = { "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "", @@ -46,10 +50,13 @@ class AuthorizationHelpers: except requests.exceptions.ConnectionError: return AuthenticationResponse(success=False, err_message="Unable to connect to remote server") - ## Request the access token from the authorization server using a refresh token. - # \param refresh_token: - # \return An AuthenticationResponse object. def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse": + """Request the access token from the authorization server using a refresh token. + + :param refresh_token: + :return: An AuthenticationResponse object. + """ + Logger.log("d", "Refreshing the access token.") data = { "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", @@ -64,10 +71,13 @@ class AuthorizationHelpers: return AuthenticationResponse(success=False, err_message="Unable to connect to remote server") @staticmethod - ## Parse the token response from the authorization server into an AuthenticationResponse object. - # \param token_response: The JSON string data response from the authorization server. - # \return An AuthenticationResponse object. def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse": + """Parse the token response from the authorization server into an AuthenticationResponse object. + + :param token_response: The JSON string data response from the authorization server. + :return: An AuthenticationResponse object. + """ + token_data = None try: @@ -89,10 +99,13 @@ class AuthorizationHelpers: scope=token_data["scope"], received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT)) - ## Calls the authentication API endpoint to get the token data. - # \param access_token: The encoded JWT token. - # \return Dict containing some profile data. def parseJWT(self, access_token: str) -> Optional["UserProfile"]: + """Calls the authentication API endpoint to get the token data. + + :param access_token: The encoded JWT token. + :return: Dict containing some profile data. + """ + try: token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = { "Authorization": "Bearer {}".format(access_token) @@ -115,16 +128,22 @@ class AuthorizationHelpers: ) @staticmethod - ## Generate a verification code of arbitrary length. - # \param code_length: How long should the code be? This should never be lower than 16, but it's probably better to - # leave it at 32 def generateVerificationCode(code_length: int = 32) -> str: + """Generate a verification code of arbitrary length. + + :param code_length:: How long should the code be? This should never be lower than 16, but it's probably + better to leave it at 32 + """ + return "".join(random.choice("0123456789ABCDEF") for i in range(code_length)) @staticmethod - ## Generates a base64 encoded sha512 encrypted version of a given string. - # \param verification_code: - # \return The encrypted code in base64 format. def generateVerificationCodeChallenge(verification_code: str) -> str: + """Generates a base64 encoded sha512 encrypted version of a given string. + + :param verification_code: + :return: The encrypted code in base64 format. + """ + encoded = sha512(verification_code.encode()).digest() return b64encode(encoded, altchars = b"_-").decode() diff --git a/cura/OAuth2/AuthorizationRequestHandler.py b/cura/OAuth2/AuthorizationRequestHandler.py index b002039491..9bcfbfc805 100644 --- a/cura/OAuth2/AuthorizationRequestHandler.py +++ b/cura/OAuth2/AuthorizationRequestHandler.py @@ -14,9 +14,12 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## This handler handles all HTTP requests on the local web server. -# It also requests the access token for the 2nd stage of the OAuth flow. class AuthorizationRequestHandler(BaseHTTPRequestHandler): + """This handler handles all HTTP requests on the local web server. + + It also requests the access token for the 2nd stage of the OAuth flow. + """ + def __init__(self, request, client_address, server) -> None: super().__init__(request, client_address, server) @@ -55,10 +58,13 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): # This will cause the server to shut down, so we do it at the very end of the request handling. self.authorization_callback(token_response) - ## Handler for the callback URL redirect. - # \param query Dict containing the HTTP query parameters. - # \return HTTP ResponseData containing a success page to show to the user. def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]: + """Handler for the callback URL redirect. + + :param query: Dict containing the HTTP query parameters. + :return: HTTP ResponseData containing a success page to show to the user. + """ + code = self._queryGet(query, "code") state = self._queryGet(query, "state") if state != self.state: @@ -95,9 +101,10 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): self.authorization_helpers.settings.AUTH_FAILED_REDIRECT ), token_response - ## Handle all other non-existing server calls. @staticmethod def _handleNotFound() -> ResponseData: + """Handle all other non-existing server calls.""" + return ResponseData(status = HTTP_STATUS["NOT_FOUND"], content_type = "text/html", data_stream = b"Not found.") def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None: @@ -110,7 +117,8 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): def _sendData(self, data: bytes) -> None: self.wfile.write(data) - ## Convenience helper for getting values from a pre-parsed query string @staticmethod def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str] = None) -> Optional[str]: + """Convenience helper for getting values from a pre-parsed query string""" + return query_data.get(key, [default])[0] diff --git a/cura/OAuth2/AuthorizationRequestServer.py b/cura/OAuth2/AuthorizationRequestServer.py index 687bbf5ad8..e2f9dddc32 100644 --- a/cura/OAuth2/AuthorizationRequestServer.py +++ b/cura/OAuth2/AuthorizationRequestServer.py @@ -9,21 +9,26 @@ if TYPE_CHECKING: from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers -## The authorization request callback handler server. -# This subclass is needed to be able to pass some data to the request handler. -# This cannot be done on the request handler directly as the HTTPServer -# creates an instance of the handler after init. class AuthorizationRequestServer(HTTPServer): - ## Set the authorization helpers instance on the request handler. + """The authorization request callback handler server. + + This subclass is needed to be able to pass some data to the request handler. This cannot be done on the request + handler directly as the HTTPServer creates an instance of the handler after init. + """ + def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None: + """Set the authorization helpers instance on the request handler.""" + self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore - ## Set the authorization callback on the request handler. def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None: + """Set the authorization callback on the request handler.""" + self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore - ## Set the verification code on the request handler. def setVerificationCode(self, verification_code: str) -> None: + """Set the verification code on the request handler.""" + self.RequestHandlerClass.verification_code = verification_code # type: ignore def setState(self, state: str) -> None: diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index 2f865456b6..5de7ff2dce 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -25,9 +25,11 @@ if TYPE_CHECKING: from UM.Preferences import Preferences -## The authorization service is responsible for handling the login flow, -# storing user credentials and providing account information. class AuthorizationService: + """The authorization service is responsible for handling the login flow, storing user credentials and providing + account information. + """ + # Emit signal when authentication is completed. onAuthStateChanged = Signal() @@ -59,11 +61,16 @@ class AuthorizationService: if self._preferences: self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}") - ## Get the user profile as obtained from the JWT (JSON Web Token). - # If the JWT is not yet parsed, calling this will take care of that. - # \return UserProfile if a user is logged in, None otherwise. - # \sa _parseJWT def getUserProfile(self) -> Optional["UserProfile"]: + """Get the user profile as obtained from the JWT (JSON Web Token). + + If the JWT is not yet parsed, calling this will take care of that. + + :return: UserProfile if a user is logged in, None otherwise. + + See also: :py:method:`cura.OAuth2.AuthorizationService.AuthorizationService._parseJWT` + """ + if not self._user_profile: # If no user profile was stored locally, we try to get it from JWT. try: @@ -81,9 +88,12 @@ class AuthorizationService: return self._user_profile - ## Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there. - # \return UserProfile if it was able to parse, None otherwise. def _parseJWT(self) -> Optional["UserProfile"]: + """Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there. + + :return: UserProfile if it was able to parse, None otherwise. + """ + if not self._auth_data or self._auth_data.access_token is None: # If no auth data exists, we should always log in again. Logger.log("d", "There was no auth data or access token") @@ -106,8 +116,9 @@ class AuthorizationService: self._storeAuthData(self._auth_data) return self._auth_helpers.parseJWT(self._auth_data.access_token) - ## Get the access token as provided by the repsonse data. def getAccessToken(self) -> Optional[str]: + """Get the access token as provided by the repsonse data.""" + if self._auth_data is None: Logger.log("d", "No auth data to retrieve the access_token from") return None @@ -122,8 +133,9 @@ class AuthorizationService: return self._auth_data.access_token if self._auth_data else None - ## Try to refresh the access token. This should be used when it has expired. def refreshAccessToken(self) -> None: + """Try to refresh the access token. This should be used when it has expired.""" + if self._auth_data is None or self._auth_data.refresh_token is None: Logger.log("w", "Unable to refresh access token, since there is no refresh token.") return @@ -135,14 +147,16 @@ class AuthorizationService: Logger.log("w", "Failed to get a new access token from the server.") self.onAuthStateChanged.emit(logged_in = False) - ## Delete the authentication data that we have stored locally (eg; logout) def deleteAuthData(self) -> None: + """Delete the authentication data that we have stored locally (eg; logout)""" + if self._auth_data is not None: self._storeAuthData() self.onAuthStateChanged.emit(logged_in = False) - ## Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login. def startAuthorizationFlow(self) -> None: + """Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.""" + Logger.log("d", "Starting new OAuth2 flow...") # Create the tokens needed for the code challenge (PKCE) extension for OAuth2. @@ -177,8 +191,9 @@ class AuthorizationService: QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string))) - ## Callback method for the authentication flow. def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None: + """Callback method for the authentication flow.""" + if auth_response.success: self._storeAuthData(auth_response) self.onAuthStateChanged.emit(logged_in = True) @@ -186,8 +201,9 @@ class AuthorizationService: self.onAuthenticationError.emit(logged_in = False, error_message = auth_response.err_message) self._server.stop() # Stop the web server at all times. - ## Load authentication data from preferences. def loadAuthDataFromPreferences(self) -> None: + """Load authentication data from preferences.""" + if self._preferences is None: Logger.log("e", "Unable to load authentication data, since no preference has been set!") return @@ -208,8 +224,9 @@ class AuthorizationService: except ValueError: Logger.logException("w", "Could not load auth data from preferences") - ## Store authentication data in preferences. def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None: + """Store authentication data in preferences.""" + Logger.log("d", "Attempting to store the auth data") if self._preferences is None: Logger.log("e", "Unable to save authentication data, since no preference has been set!") diff --git a/cura/OAuth2/LocalAuthorizationServer.py b/cura/OAuth2/LocalAuthorizationServer.py index 0e4e491e46..942967301a 100644 --- a/cura/OAuth2/LocalAuthorizationServer.py +++ b/cura/OAuth2/LocalAuthorizationServer.py @@ -20,18 +20,23 @@ if TYPE_CHECKING: class LocalAuthorizationServer: - ## The local LocalAuthorizationServer takes care of the oauth2 callbacks. - # Once the flow is completed, this server should be closed down again by - # calling stop() - # \param auth_helpers An instance of the authorization helpers class. - # \param auth_state_changed_callback A callback function to be called when - # the authorization state changes. - # \param daemon Whether the server thread should be run in daemon mode. - # Note: Daemon threads are abruptly stopped at shutdown. Their resources - # (e.g. open files) may never be released. def __init__(self, auth_helpers: "AuthorizationHelpers", auth_state_changed_callback: Callable[["AuthenticationResponse"], Any], daemon: bool) -> None: + """The local LocalAuthorizationServer takes care of the oauth2 callbacks. + + Once the flow is completed, this server should be closed down again by calling + :py:meth:`cura.OAuth2.LocalAuthorizationServer.LocalAuthorizationServer.stop()` + + :param auth_helpers: An instance of the authorization helpers class. + :param auth_state_changed_callback: A callback function to be called when the authorization state changes. + :param daemon: Whether the server thread should be run in daemon mode. + + .. note:: + + Daemon threads are abruptly stopped at shutdown. Their resources (e.g. open files) may never be released. + """ + self._web_server = None # type: Optional[AuthorizationRequestServer] self._web_server_thread = None # type: Optional[threading.Thread] self._web_server_port = auth_helpers.settings.CALLBACK_PORT @@ -39,10 +44,13 @@ class LocalAuthorizationServer: self._auth_state_changed_callback = auth_state_changed_callback self._daemon = daemon - ## Starts the local web server to handle the authorization callback. - # \param verification_code The verification code part of the OAuth2 client identification. - # \param state The unique state code (to ensure that the request we get back is really from the server. def start(self, verification_code: str, state: str) -> None: + """Starts the local web server to handle the authorization callback. + + :param verification_code: The verification code part of the OAuth2 client identification. + :param state: The unique state code (to ensure that the request we get back is really from the server. + """ + if self._web_server: # If the server is already running (because of a previously aborted auth flow), we don't have to start it. # We still inject the new verification code though. @@ -66,8 +74,9 @@ class LocalAuthorizationServer: self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) self._web_server_thread.start() - ## Stops the web server if it was running. It also does some cleanup. def stop(self) -> None: + """Stops the web server if it was running. It also does some cleanup.""" + Logger.log("d", "Stopping local oauth2 web server...") if self._web_server: diff --git a/cura/OAuth2/Models.py b/cura/OAuth2/Models.py index dd935fef6e..93b44e8057 100644 --- a/cura/OAuth2/Models.py +++ b/cura/OAuth2/Models.py @@ -8,8 +8,9 @@ class BaseModel: self.__dict__.update(kwargs) -## OAuth OAuth2Settings data template. class OAuth2Settings(BaseModel): + """OAuth OAuth2Settings data template.""" + CALLBACK_PORT = None # type: Optional[int] OAUTH_SERVER_URL = None # type: Optional[str] CLIENT_ID = None # type: Optional[str] @@ -20,16 +21,18 @@ class OAuth2Settings(BaseModel): AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str -## User profile data template. class UserProfile(BaseModel): + """User profile data template.""" + user_id = None # type: Optional[str] username = None # type: Optional[str] profile_image_url = None # type: Optional[str] -## Authentication data template. class AuthenticationResponse(BaseModel): - """Data comes from the token response with success flag and error message added.""" + """Authentication data template.""" + + # Data comes from the token response with success flag and error message added. success = True # type: bool token_type = None # type: Optional[str] access_token = None # type: Optional[str] @@ -40,22 +43,25 @@ class AuthenticationResponse(BaseModel): received_at = None # type: Optional[str] -## Response status template. class ResponseStatus(BaseModel): + """Response status template.""" + code = 200 # type: int message = "" # type: str -## Response data template. class ResponseData(BaseModel): + """Response data template.""" + status = None # type: ResponseStatus data_stream = None # type: Optional[bytes] redirect_uri = None # type: Optional[str] content_type = "text/html" # type: str -## Possible HTTP responses. HTTP_STATUS = { +"""Possible HTTP responses.""" + "OK": ResponseStatus(code = 200, message = "OK"), "NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"), "REDIRECT": ResponseStatus(code = 302, message = "REDIRECT") From 58e43c0a07cbd349e3df433352f5f56aa5e0c26d Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 15 May 2020 13:34:58 +0200 Subject: [PATCH 16/27] Convert doxygen to rst for SentryLogger, SimulationView, SliceInfo, SolidView --- plugins/SentryLogger/SentryLogger.py | 8 +++--- plugins/SimulationView/SimulationView.py | 32 +++++++++++++++--------- plugins/SliceInfoPlugin/SliceInfo.py | 15 +++++++---- plugins/SolidView/SolidView.py | 3 ++- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/plugins/SentryLogger/SentryLogger.py b/plugins/SentryLogger/SentryLogger.py index 51e77ad589..55bcc7e806 100644 --- a/plugins/SentryLogger/SentryLogger.py +++ b/plugins/SentryLogger/SentryLogger.py @@ -33,10 +33,12 @@ class SentryLogger(LogOutput): super().__init__() self._show_once = set() # type: Set[str] - ## Log the message to the sentry hub as a breadcrumb - # \param log_type "e" (error), "i"(info), "d"(debug), "w"(warning) or "c"(critical) (can postfix with "_once") - # \param message String containing message to be logged def log(self, log_type: str, message: str) -> None: + """Log the message to the sentry hub as a breadcrumb + + :param log_type: "e" (error), "i"(info), "d"(debug), "w"(warning) or "c"(critical) (can postfix with "_once") + :param message: String containing message to be logged + """ level = self._translateLogType(log_type) message = CrashHandler.pruneSensitiveData(message) if level is None: diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 28d10c5f40..c0dd7ea820 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -48,8 +48,9 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## The preview layer view. It is used to display g-code paths. class SimulationView(CuraView): + """The preview layer view. It is used to display g-code paths.""" + # Must match SimulationViewMenuComponent.qml LAYER_VIEW_TYPE_MATERIAL_TYPE = 0 LAYER_VIEW_TYPE_LINE_TYPE = 1 @@ -294,23 +295,28 @@ class SimulationView(CuraView): self.currentPathNumChanged.emit() - ## Set the layer view type - # - # \param layer_view_type integer as in SimulationView.qml and this class def setSimulationViewType(self, layer_view_type: int) -> None: + """Set the layer view type + + :param layer_view_type: integer as in SimulationView.qml and this class + """ + if layer_view_type != self._layer_view_type: self._layer_view_type = layer_view_type self.currentLayerNumChanged.emit() - ## Return the layer view type, integer as in SimulationView.qml and this class def getSimulationViewType(self) -> int: + """Return the layer view type, integer as in SimulationView.qml and this class""" + return self._layer_view_type - ## Set the extruder opacity - # - # \param extruder_nr 0..3 - # \param opacity 0.0 .. 1.0 def setExtruderOpacity(self, extruder_nr: int, opacity: float) -> None: + """Set the extruder opacity + + :param extruder_nr: 0..3 + :param opacity: 0.0 .. 1.0 + """ + if 0 <= extruder_nr <= 3: self._extruder_opacity[extruder_nr] = opacity self.currentLayerNumChanged.emit() @@ -372,8 +378,8 @@ class SimulationView(CuraView): scene = self.getController().getScene() self._old_max_layers = self._max_layers - ## Recalculate num max layers new_max_layers = -1 + """Recalculate num max layers""" for node in DepthFirstIterator(scene.getRoot()): # type: ignore layer_data = node.callDecoration("getLayerData") if not layer_data: @@ -449,9 +455,11 @@ class SimulationView(CuraView): busyChanged = Signal() activityChanged = Signal() - ## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created - # as this caused some issues. def getProxy(self, engine, script_engine): + """Hackish way to ensure the proxy is already created + + which ensures that the layerview.qml is already created as this caused some issues. + """ if self._proxy is None: self._proxy = SimulationViewProxy(self) return self._proxy diff --git a/plugins/SliceInfoPlugin/SliceInfo.py b/plugins/SliceInfoPlugin/SliceInfo.py index c21d70819a..a4b17236dd 100755 --- a/plugins/SliceInfoPlugin/SliceInfo.py +++ b/plugins/SliceInfoPlugin/SliceInfo.py @@ -26,10 +26,13 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## This Extension runs in the background and sends several bits of information to the Ultimaker servers. -# The data is only sent when the user in question gave permission to do so. All data is anonymous and -# no model files are being sent (Just a SHA256 hash of the model). class SliceInfo(QObject, Extension): + """This Extension runs in the background and sends several bits of information to the Ultimaker servers. + + The data is only sent when the user in question gave permission to do so. All data is anonymous and + no model files are being sent (Just a SHA256 hash of the model). + """ + info_url = "https://stats.ultimaker.com/api/cura" def __init__(self, parent = None): @@ -54,9 +57,11 @@ class SliceInfo(QObject, Extension): if self._more_info_dialog is None: self._more_info_dialog = self._createDialog("MoreInfoWindow.qml") - ## Perform action based on user input. - # Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it. def messageActionTriggered(self, message_id, action_id): + """Perform action based on user input. + + Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it. + """ self._application.getPreferences().setValue("info/asked_send_slice_info", True) if action_id == "MoreInfo": self.showMoreInfoDialog() diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index bfe803f224..dc88265d68 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -33,9 +33,10 @@ import math catalog = i18nCatalog("cura") -## Standard view for mesh models. class SolidView(View): + """Standard view for mesh models.""" + _show_xray_warning_preference = "view/show_xray_warning" def __init__(self): From d96359f208dc6585886e58ce67c877f0c9c27f16 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 15 May 2020 13:53:10 +0200 Subject: [PATCH 17/27] Convert doxygen to rst for Toolbox, TrimeshReader --- plugins/Toolbox/src/AuthorsModel.py | 12 ++++-- plugins/Toolbox/src/CloudApiModel.py | 2 +- .../src/CloudSync/DiscrepanciesPresenter.py | 6 ++- .../src/CloudSync/DownloadPresenter.py | 6 ++- .../Toolbox/src/CloudSync/LicensePresenter.py | 8 ++-- .../CloudSync/RestartApplicationPresenter.py | 6 ++- .../Toolbox/src/CloudSync/SyncOrchestrator.py | 40 +++++++++++-------- plugins/Toolbox/src/ConfigsModel.py | 3 +- plugins/Toolbox/src/PackagesModel.py | 12 ++++-- plugins/Toolbox/src/Toolbox.py | 32 ++++++++++----- plugins/TrimeshReader/TrimeshReader.py | 27 ++++++++----- 11 files changed, 98 insertions(+), 56 deletions(-) diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py index 81158978b0..4d1c812981 100644 --- a/plugins/Toolbox/src/AuthorsModel.py +++ b/plugins/Toolbox/src/AuthorsModel.py @@ -9,8 +9,12 @@ from PyQt5.QtCore import Qt, pyqtProperty from UM.Qt.ListModel import ListModel -## Model that holds cura packages. By setting the filter property the instances held by this model can be changed. class AuthorsModel(ListModel): + """Model that holds cura packages. + + By setting the filter property the instances held by this model can be changed. + """ + def __init__(self, parent = None) -> None: super().__init__(parent) @@ -67,9 +71,11 @@ class AuthorsModel(ListModel): filtered_items.sort(key = lambda k: k["name"]) self.setItems(filtered_items) - ## Set the filter of this model based on a string. - # \param filter_dict \type{Dict} Dictionary to do the filtering by. def setFilter(self, filter_dict: Dict[str, str]) -> None: + """Set the filter of this model based on a string. + + :param filter_dict: Dictionary to do the filtering by. + """ if filter_dict != self._filter: self._filter = filter_dict self._update() diff --git a/plugins/Toolbox/src/CloudApiModel.py b/plugins/Toolbox/src/CloudApiModel.py index 3386cffb51..b4ff00c6cd 100644 --- a/plugins/Toolbox/src/CloudApiModel.py +++ b/plugins/Toolbox/src/CloudApiModel.py @@ -20,9 +20,9 @@ class CloudApiModel: cloud_api_version=cloud_api_version, ) - ## https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id} @classmethod def userPackageUrl(cls, package_id: str) -> str: + """https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}""" return (CloudApiModel.api_url_user_packages + "/{package_id}").format( package_id=package_id diff --git a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py index ddf1a39e78..a168381efa 100644 --- a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py +++ b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py @@ -8,9 +8,11 @@ from UM.Signal import Signal from .SubscribedPackagesModel import SubscribedPackagesModel -## Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's -# choices are emitted on the `packageMutations` Signal. class DiscrepanciesPresenter(QObject): + """Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's + + choices are emitted on the `packageMutations` Signal. + """ def __init__(self, app: QtApplication) -> None: super().__init__(app) diff --git a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py index a5d6eee0b6..11c1dffcb4 100644 --- a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py +++ b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py @@ -16,9 +16,11 @@ from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from .SubscribedPackagesModel import SubscribedPackagesModel -## Downloads a set of packages from the Ultimaker Cloud Marketplace -# use download() exactly once: should not be used for multiple sets of downloads since this class contains state class DownloadPresenter: + """Downloads a set of packages from the Ultimaker Cloud Marketplace + + use download() exactly once: should not be used for multiple sets of downloads since this class contains state + """ DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB diff --git a/plugins/Toolbox/src/CloudSync/LicensePresenter.py b/plugins/Toolbox/src/CloudSync/LicensePresenter.py index 778a36fbde..8701120cc5 100644 --- a/plugins/Toolbox/src/CloudSync/LicensePresenter.py +++ b/plugins/Toolbox/src/CloudSync/LicensePresenter.py @@ -43,10 +43,12 @@ class LicensePresenter(QObject): self._compatibility_dialog_path = "resources/qml/dialogs/ToolboxLicenseDialog.qml" - ## Show a license dialog for multiple packages where users can read a license and accept or decline them - # \param plugin_path: Root directory of the Toolbox plugin - # \param packages: Dict[package id, file path] def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None: + """Show a license dialog for multiple packages where users can read a license and accept or decline them + + :param plugin_path: Root directory of the Toolbox plugin + :param packages: Dict[package id, file path] + """ if self._presented: Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__) return diff --git a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py b/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py index 6e2bc53e7e..1242bd3b49 100644 --- a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py +++ b/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py @@ -3,9 +3,11 @@ from UM.Message import Message from cura.CuraApplication import CuraApplication -## Presents a dialog telling the user that a restart is required to apply changes -# Since we cannot restart Cura, the app is closed instead when the button is clicked class RestartApplicationPresenter: + """Presents a dialog telling the user that a restart is required to apply changes + + Since we cannot restart Cura, the app is closed instead when the button is clicked + """ def __init__(self, app: CuraApplication) -> None: self._app = app self._i18n_catalog = i18nCatalog("cura") diff --git a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py index fc3dfaea38..89f20815bf 100644 --- a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py +++ b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py @@ -16,20 +16,23 @@ from .RestartApplicationPresenter import RestartApplicationPresenter from .SubscribedPackagesModel import SubscribedPackagesModel -## Orchestrates the synchronizing of packages from the user account to the installed packages -# Example flow: -# - CloudPackageChecker compares a list of packages the user `subscribed` to in their account -# If there are `discrepancies` between the account and locally installed packages, they are emitted -# - DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations` -# the user selected to be performed -# - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed -# - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads -# - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to -# be installed. It emits the `licenseAnswers` signal for accept or declines -# - The CloudApiClient removes the declined packages from the account -# - The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files. -# - The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect class SyncOrchestrator(Extension): + """Orchestrates the synchronizing of packages from the user account to the installed packages + + Example flow: + + - CloudPackageChecker compares a list of packages the user `subscribed` to in their account + If there are `discrepancies` between the account and locally installed packages, they are emitted + - DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations` + the user selected to be performed + - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed + - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads + - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to + be installed. It emits the `licenseAnswers` signal for accept or declines + - The CloudApiClient removes the declined packages from the account + - The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files. + - The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect + """ def __init__(self, app: CuraApplication) -> None: super().__init__() @@ -63,10 +66,12 @@ class SyncOrchestrator(Extension): self._download_presenter.done.connect(self._onDownloadFinished) self._download_presenter.download(mutations) - ## Called when a set of packages have finished downloading - # \param success_items: Dict[package_id, Dict[str, str]] - # \param error_items: List[package_id] def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None: + """Called when a set of packages have finished downloading + + :param success_items:: Dict[package_id, Dict[str, str]] + :param error_items:: List[package_id] + """ if error_items: message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items))) self._showErrorMessage(message) @@ -96,7 +101,8 @@ class SyncOrchestrator(Extension): if has_changes: self._restart_presenter.present() - ## Logs an error and shows it to the user def _showErrorMessage(self, text: str): + """Logs an error and shows it to the user""" + Logger.error(text) Message(text, lifetime=0).show() diff --git a/plugins/Toolbox/src/ConfigsModel.py b/plugins/Toolbox/src/ConfigsModel.py index a92f9c0d93..a53817653f 100644 --- a/plugins/Toolbox/src/ConfigsModel.py +++ b/plugins/Toolbox/src/ConfigsModel.py @@ -6,8 +6,9 @@ from PyQt5.QtCore import Qt from UM.Qt.ListModel import ListModel -## Model that holds supported configurations (for material/quality packages). class ConfigsModel(ListModel): + """Model that holds supported configurations (for material/quality packages).""" + def __init__(self, parent = None): super().__init__(parent) diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py index c84e0da5d0..4cbabde07e 100644 --- a/plugins/Toolbox/src/PackagesModel.py +++ b/plugins/Toolbox/src/PackagesModel.py @@ -12,8 +12,12 @@ from UM.Qt.ListModel import ListModel from .ConfigsModel import ConfigsModel -## Model that holds Cura packages. By setting the filter property the instances held by this model can be changed. class PackagesModel(ListModel): + """Model that holds Cura packages. + + By setting the filter property the instances held by this model can be changed. + """ + def __init__(self, parent = None): super().__init__(parent) @@ -131,9 +135,11 @@ class PackagesModel(ListModel): filtered_items.sort(key = lambda k: k["name"]) self.setItems(filtered_items) - ## Set the filter of this model based on a string. - # \param filter_dict \type{Dict} Dictionary to do the filtering by. def setFilter(self, filter_dict: Dict[str, str]) -> None: + """Set the filter of this model based on a string. + + :param filter_dict: Dictionary to do the filtering by. + """ if filter_dict != self._filter: self._filter = filter_dict self._update() diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 3b1f85a69e..632af456b5 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -37,10 +37,10 @@ try: except ImportError: CuraMarketplaceRoot = DEFAULT_MARKETPLACE_ROOT -# todo Remove license and download dialog, use SyncOrchestrator instead -## Provides a marketplace for users to download plugins an materials class Toolbox(QObject, Extension): + """Provides a marketplace for users to download plugins an materials""" + def __init__(self, application: CuraApplication) -> None: super().__init__() @@ -135,8 +135,9 @@ class Toolbox(QObject, Extension): closeLicenseDialog = pyqtSignal() uninstallVariablesChanged = pyqtSignal() - ## Go back to the start state (welcome screen or loading if no login required) def _restart(self): + """Go back to the start state (welcome screen or loading if no login required)""" + # For an Essentials build, login is mandatory if not self._application.getCuraAPI().account.isLoggedIn and ApplicationMetadata.IsEnterpriseVersion: self.setViewPage("welcome") @@ -311,10 +312,13 @@ class Toolbox(QObject, Extension): self.restartRequiredChanged.emit() return package_id - ## Check package usage and uninstall - # If the package is in use, you'll get a confirmation dialog to set everything to default @pyqtSlot(str) def checkPackageUsageAndUninstall(self, package_id: str) -> None: + """Check package usage and uninstall + + If the package is in use, you'll get a confirmation dialog to set everything to default + """ + package_used_materials, package_used_qualities = self._package_manager.getMachinesUsingPackage(package_id) if package_used_materials or package_used_qualities: # Set up "uninstall variables" for resetMaterialsQualitiesAndUninstall @@ -352,10 +356,13 @@ class Toolbox(QObject, Extension): if self._confirm_reset_dialog is not None: self._confirm_reset_dialog.close() - ## Uses "uninstall variables" to reset qualities and materials, then uninstall - # It's used as an action on Confirm reset on Uninstall @pyqtSlot() def resetMaterialsQualitiesAndUninstall(self) -> None: + """Uses "uninstall variables" to reset qualities and materials, then uninstall + + It's used as an action on Confirm reset on Uninstall + """ + application = CuraApplication.getInstance() machine_manager = application.getMachineManager() container_tree = ContainerTree.getInstance() @@ -418,8 +425,9 @@ class Toolbox(QObject, Extension): self._restart_required = True self.restartRequiredChanged.emit() - ## Actual update packages that are in self._to_update def _update(self) -> None: + """Actual update packages that are in self._to_update""" + if self._to_update: plugin_id = self._to_update.pop(0) remote_package = self.getRemotePackage(plugin_id) @@ -433,9 +441,10 @@ class Toolbox(QObject, Extension): if self._to_update: self._application.callLater(self._update) - ## Update a plugin by plugin_id @pyqtSlot(str) def update(self, plugin_id: str) -> None: + """Update a plugin by plugin_id""" + self._to_update.append(plugin_id) self._application.callLater(self._update) @@ -714,9 +723,10 @@ class Toolbox(QObject, Extension): self._active_package = package self.activePackageChanged.emit() - ## The active package is the package that is currently being downloaded @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged) def activePackage(self) -> Optional[QObject]: + """The active package is the package that is currently being downloaded""" + return self._active_package def setViewCategory(self, category: str = "plugin") -> None: @@ -724,7 +734,7 @@ class Toolbox(QObject, Extension): self._view_category = category self.viewChanged.emit() - ## Function explicitly defined so that it can be called through the callExtensionsMethod + # Function explicitly defined so that it can be called through the callExtensionsMethod # which cannot receive arguments. def setViewCategoryToMaterials(self) -> None: self.setViewCategory("material") diff --git a/plugins/TrimeshReader/TrimeshReader.py b/plugins/TrimeshReader/TrimeshReader.py index 6ed7435f88..f746c55cc5 100644 --- a/plugins/TrimeshReader/TrimeshReader.py +++ b/plugins/TrimeshReader/TrimeshReader.py @@ -22,8 +22,10 @@ from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator # Adde if TYPE_CHECKING: from UM.Scene.SceneNode import SceneNode -## Class that leverages Trimesh to import files. + class TrimeshReader(MeshReader): + """Class that leverages Trimesh to import files.""" + def __init__(self) -> None: super().__init__() @@ -79,11 +81,13 @@ class TrimeshReader(MeshReader): ) ) - ## Reads a file using Trimesh. - # \param file_name The file path. This is assumed to be one of the file - # types that Trimesh can read. It will not be checked again. - # \return A scene node that contains the file's contents. def _read(self, file_name: str) -> Union["SceneNode", List["SceneNode"]]: + """Reads a file using Trimesh. + + :param file_name: The file path. This is assumed to be one of the file + types that Trimesh can read. It will not be checked again. + :return: A scene node that contains the file's contents. + """ # CURA-6739 # GLTF files are essentially JSON files. If you directly give a file name to trimesh.load(), it will # try to figure out the format, but for GLTF, it loads it as a binary file with flags "rb", and the json.load() @@ -130,13 +134,14 @@ class TrimeshReader(MeshReader): node.setParent(group_node) return group_node - ## Converts a Trimesh to Uranium's MeshData. - # \param tri_node A Trimesh containing the contents of a file that was - # just read. - # \param file_name The full original filename used to watch for changes - # \return Mesh data from the Trimesh in a way that Uranium can understand - # it. def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData: + """Converts a Trimesh to Uranium's MeshData. + + :param tri_node: A Trimesh containing the contents of a file that was just read. + :param file_name: The full original filename used to watch for changes + :return: Mesh data from the Trimesh in a way that Uranium can understand it. + """ + tri_faces = tri_node.faces tri_vertices = tri_node.vertices From de824067820a0e2871b601886ce470db8ffad34c Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 15 May 2020 14:00:44 +0200 Subject: [PATCH 18/27] Convert doxygen to rst for UltimakerMachineActions, USBPrinting --- plugins/USBPrinting/USBPrinterOutputDevice.py | 29 +++++++++++-------- .../USBPrinterOutputDeviceManager.py | 15 ++++++---- .../BedLevelMachineAction.py | 7 +++-- .../UMOUpgradeSelection.py | 7 +++-- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 6be3827e5e..2e52a89713 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -110,20 +110,22 @@ class USBPrinterOutputDevice(PrinterOutputDevice): application = CuraApplication.getInstance() application.triggerNextExitCheck() - ## Reset USB device settings - # def resetDeviceSettings(self) -> None: + """Reset USB device settings""" + self._firmware_name = None - ## Request the current scene to be sent to a USB-connected printer. - # - # \param nodes A collection of scene nodes to send. This is ignored. - # \param file_name A suggestion for a file name to write. - # \param filter_by_machine Whether to filter MIME types by machine. This - # is ignored. - # \param kwargs Keyword arguments. def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: + """Request the current scene to be sent to a USB-connected printer. + + :param nodes: A collection of scene nodes to send. This is ignored. + :param file_name: A suggestion for a file name to write. + :param filter_by_machine: Whether to filter MIME types by machine. This + is ignored. + :param kwargs: Keyword arguments. + """ + if self._is_printing: message = Message(text = catalog.i18nc("@message", "A print is still in progress. Cura cannot start another print via USB until the previous print has completed."), title = catalog.i18nc("@message", "Print in Progress")) message.show() @@ -144,9 +146,11 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._printGCode(gcode_textio.getvalue()) - ## Start a print based on a g-code. - # \param gcode The g-code to print. def _printGCode(self, gcode: str): + """Start a print based on a g-code. + + :param gcode: The g-code to print. + """ self._gcode.clear() self._paused = False @@ -219,8 +223,9 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._update_thread = Thread(target=self._update, daemon=True, name = "USBPrinterUpdate") self._serial = None - ## Send a command to printer. def sendCommand(self, command: Union[str, bytes]): + """Send a command to printer.""" + if not self._command_received.is_set(): self._command_queue.put(command) else: diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index a0585adf51..3ee43e071e 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -20,9 +20,10 @@ from . import USBPrinterOutputDevice i18n_catalog = i18nCatalog("cura") -## Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer. @signalemitter class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): + """Manager class that ensures that an USBPrinterOutput device is created for every connected USB printer.""" + addUSBOutputDeviceSignal = Signal() progressChanged = pyqtSignal() @@ -85,8 +86,9 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): self._addRemovePorts(port_list) time.sleep(5) - ## Helper to identify serial ports (and scan for them) def _addRemovePorts(self, serial_ports): + """Helper to identify serial ports (and scan for them)""" + # First, find and add all new or changed keys for serial_port in list(serial_ports): if serial_port not in self._serial_port_list: @@ -98,16 +100,19 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): if port not in self._serial_port_list: device.close() - ## Because the model needs to be created in the same thread as the QMLEngine, we use a signal. def addOutputDevice(self, serial_port): + """Because the model needs to be created in the same thread as the QMLEngine, we use a signal.""" + device = USBPrinterOutputDevice.USBPrinterOutputDevice(serial_port) device.connectionStateChanged.connect(self._onConnectionStateChanged) self._usb_output_devices[serial_port] = device device.connect() - ## Create a list of serial ports on the system. - # \param only_list_usb If true, only usb ports are listed def getSerialPortList(self, only_list_usb = False): + """Create a list of serial ports on the system. + + :param only_list_usb: If true, only usb ports are listed + """ base_list = [] for port in serial.tools.list_ports.comports(): if not isinstance(port, tuple): diff --git a/plugins/UltimakerMachineActions/BedLevelMachineAction.py b/plugins/UltimakerMachineActions/BedLevelMachineAction.py index 818ad0e4f0..7d2eb74f84 100644 --- a/plugins/UltimakerMachineActions/BedLevelMachineAction.py +++ b/plugins/UltimakerMachineActions/BedLevelMachineAction.py @@ -14,9 +14,12 @@ from UM.Logger import Logger catalog = i18nCatalog("cura") -## A simple action to handle manual bed leveling procedure for printers that don't have it on the firmware. -# This is currently only used by the Ultimaker Original+ class BedLevelMachineAction(MachineAction): + """A simple action to handle manual bed leveling procedure for printers that don't have it on the firmware. + + This is currently only used by the Ultimaker Original+ + """ + def __init__(self): super().__init__("BedLevel", catalog.i18nc("@action", "Level build plate")) self._qml_url = "BedLevelMachineAction.qml" diff --git a/plugins/UltimakerMachineActions/UMOUpgradeSelection.py b/plugins/UltimakerMachineActions/UMOUpgradeSelection.py index f6275e5b56..62eab75986 100644 --- a/plugins/UltimakerMachineActions/UMOUpgradeSelection.py +++ b/plugins/UltimakerMachineActions/UMOUpgradeSelection.py @@ -11,9 +11,12 @@ catalog = i18nCatalog("cura") from cura.Settings.CuraStackBuilder import CuraStackBuilder -## The Ultimaker Original can have a few revisions & upgrades. This action helps with selecting them, so they are added -# as a variant. + class UMOUpgradeSelection(MachineAction): + """The Ultimaker Original can have a few revisions & upgrades. + This action helps with selecting them, so they are added as a variant. + """ + def __init__(self): super().__init__("UMOUpgradeSelection", catalog.i18nc("@action", "Select upgrades")) self._qml_url = "UMOUpgradeSelectionMachineAction.qml" From 5eb5ffd916e0964d5c4cabfa110f6fbdde3aa022 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 15 May 2020 15:05:38 +0200 Subject: [PATCH 19/27] Convert doxygen to rst for UM3NetworkPrinting --- .../src/Cloud/CloudApiClient.py | 122 +++++++++++------- .../src/Cloud/CloudOutputDevice.py | 89 ++++++++----- .../src/Cloud/ToolPathUploader.py | 58 ++++++--- .../UM3NetworkPrinting/src/ExportFileJob.py | 8 +- .../src/MeshFormatHandler.py | 45 ++++--- .../LegacyDeviceNoLongerSupportedMessage.py | 8 +- .../src/Messages/MaterialSyncMessage.py | 4 +- .../src/Messages/NotClusterHostMessage.py | 4 +- .../Messages/PrintJobUploadBlockedMessage.py | 4 +- .../Messages/PrintJobUploadErrorMessage.py | 4 +- .../Messages/PrintJobUploadProgressMessage.py | 12 +- .../Messages/PrintJobUploadSuccessMessage.py | 4 +- .../src/Models/BaseModel.py | 47 ++++--- .../src/Models/Http/CloudClusterResponse.py | 26 ++-- .../src/Models/Http/CloudClusterStatus.py | 14 +- .../src/Models/Http/CloudError.py | 23 ++-- .../src/Models/Http/CloudPrintJobResponse.py | 23 ++-- .../Models/Http/CloudPrintJobUploadRequest.py | 11 +- .../src/Models/Http/CloudPrintResponse.py | 13 +- .../src/Models/Http/ClusterBuildPlate.py | 8 +- .../Http/ClusterPrintCoreConfiguration.py | 37 ++++-- .../ClusterPrintJobConfigurationChange.py | 25 ++-- .../Models/Http/ClusterPrintJobConstraint.py | 10 +- .../Models/Http/ClusterPrintJobImpediment.py | 13 +- .../src/Models/Http/ClusterPrintJobStatus.py | 77 ++++++----- .../ClusterPrinterConfigurationMaterial.py | 30 +++-- .../Http/ClusterPrinterMaterialStation.py | 13 +- .../Http/ClusterPrinterMaterialStationSlot.py | 17 ++- .../src/Models/Http/ClusterPrinterStatus.py | 70 ++++++---- .../src/Models/Http/PrinterSystemStatus.py | 5 +- .../src/Network/ClusterApiClient.py | 102 +++++++++------ .../src/Network/LocalClusterOutputDevice.py | 64 ++++++--- .../LocalClusterOutputDeviceManager.py | 63 ++++++--- .../src/Network/SendMaterialJob.py | 68 ++++++---- .../src/Network/ZeroConfClient.py | 40 ++++-- .../src/UM3OutputDevicePlugin.py | 30 +++-- .../src/UltimakerNetworkedPrinterAction.py | 36 ++++-- .../UltimakerNetworkedPrinterOutputDevice.py | 57 +++++--- 38 files changed, 797 insertions(+), 487 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 1c9670d87f..65dde1e519 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -20,13 +20,15 @@ from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest from ..Models.Http.CloudPrintResponse import CloudPrintResponse -## The generic type variable used to document the methods below. CloudApiClientModel = TypeVar("CloudApiClientModel", bound=BaseModel) +"""The generic type variable used to document the methods below.""" -## The cloud API client is responsible for handling the requests and responses from the cloud. -# Each method should only handle models instead of exposing Any HTTP details. class CloudApiClient: + """The cloud API client is responsible for handling the requests and responses from the cloud. + + Each method should only handle models instead of exposing Any HTTP details. + """ # The cloud URL to use for this remote cluster. ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot @@ -36,54 +38,70 @@ class CloudApiClient: # In order to avoid garbage collection we keep the callbacks in this list. _anti_gc_callbacks = [] # type: List[Callable[[], None]] - ## Initializes a new cloud API client. - # \param account: The user's account object - # \param on_error: The callback to be called whenever we receive errors from the server. def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> None: + """Initializes a new cloud API client. + + :param account: The user's account object + :param on_error: The callback to be called whenever we receive errors from the server. + """ super().__init__() self._manager = QNetworkAccessManager() self._account = account self._on_error = on_error self._upload = None # type: Optional[ToolPathUploader] - ## Gets the account used for the API. @property def account(self) -> Account: + """Gets the account used for the API.""" + return self._account - ## Retrieves all the clusters for the user that is currently logged in. - # \param on_finished: The function to be called after the result is parsed. def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None: + """Retrieves all the clusters for the user that is currently logged in. + + :param on_finished: The function to be called after the result is parsed. + """ + url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, CloudClusterResponse, failed) - ## Retrieves the status of the given cluster. - # \param cluster_id: The ID of the cluster. - # \param on_finished: The function to be called after the result is parsed. def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None: + """Retrieves the status of the given cluster. + + :param cluster_id: The ID of the cluster. + :param on_finished: The function to be called after the result is parsed. + """ + url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, CloudClusterStatus) - ## Requests the cloud to register the upload of a print job mesh. - # \param request: The request object. - # \param on_finished: The function to be called after the result is parsed. def requestUpload(self, request: CloudPrintJobUploadRequest, on_finished: Callable[[CloudPrintJobResponse], Any]) -> None: + + """Requests the cloud to register the upload of a print job mesh. + + :param request: The request object. + :param on_finished: The function to be called after the result is parsed. + """ + url = "{}/jobs/upload".format(self.CURA_API_ROOT) body = json.dumps({"data": request.toDict()}) reply = self._manager.put(self._createEmptyRequest(url), body.encode()) self._addCallback(reply, on_finished, CloudPrintJobResponse) - ## Uploads a print job tool path to the cloud. - # \param print_job: The object received after requesting an upload with `self.requestUpload`. - # \param mesh: The tool path data to be uploaded. - # \param on_finished: The function to be called after the upload is successful. - # \param on_progress: A function to be called during upload progress. It receives a percentage (0-100). - # \param on_error: A function to be called if the upload fails. def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]): + """Uploads a print job tool path to the cloud. + + :param print_job: The object received after requesting an upload with `self.requestUpload`. + :param mesh: The tool path data to be uploaded. + :param on_finished: The function to be called after the upload is successful. + :param on_progress: A function to be called during upload progress. It receives a percentage (0-100). + :param on_error: A function to be called if the upload fails. + """ + self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error) self._upload.start() @@ -96,20 +114,27 @@ class CloudApiClient: reply = self._manager.post(self._createEmptyRequest(url), b"") self._addCallback(reply, on_finished, CloudPrintResponse) - ## Send a print job action to the cluster for the given print job. - # \param cluster_id: The ID of the cluster. - # \param cluster_job_id: The ID of the print job within the cluster. - # \param action: The name of the action to execute. def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, data: Optional[Dict[str, Any]] = None) -> None: + + """Send a print job action to the cluster for the given print job. + + :param cluster_id: The ID of the cluster. + :param cluster_job_id: The ID of the print job within the cluster. + :param action: The name of the action to execute. + """ + body = json.dumps({"data": data}).encode() if data else b"" url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action) self._manager.post(self._createEmptyRequest(url), body) - ## We override _createEmptyRequest in order to add the user credentials. - # \param url: The URL to request - # \param content_type: The type of the body contents. def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest: + """We override _createEmptyRequest in order to add the user credentials. + + :param url: The URL to request + :param content_type: The type of the body contents. + """ + request = QNetworkRequest(QUrl(path)) if content_type: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) @@ -118,11 +143,14 @@ class CloudApiClient: request.setRawHeader(b"Authorization", "Bearer {}".format(access_token).encode()) return request - ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. - # \param reply: The reply from the server. - # \return A tuple with a status code and a dictionary. @staticmethod def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: + """Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. + + :param reply: The reply from the server. + :return: A tuple with a status code and a dictionary. + """ + status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) try: response = bytes(reply.readAll()).decode() @@ -133,14 +161,15 @@ class CloudApiClient: Logger.logException("e", "Could not parse the stardust response: %s", error.toDict()) return status_code, {"errors": [error.toDict()]} - ## Parses the given models and calls the correct callback depending on the result. - # \param response: The response from the server, after being converted to a dict. - # \param on_finished: The callback in case the response is successful. - # \param model_class: The type of the model to convert the response to. It may either be a single record or a list. - def _parseModels(self, response: Dict[str, Any], - on_finished: Union[Callable[[CloudApiClientModel], Any], - Callable[[List[CloudApiClientModel]], Any]], - model_class: Type[CloudApiClientModel]) -> None: + def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any], + Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None: + """Parses the given models and calls the correct callback depending on the result. + + :param response: The response from the server, after being converted to a dict. + :param on_finished: The callback in case the response is successful. + :param model_class: The type of the model to convert the response to. It may either be a single record or a list. + """ + if "data" in response: data = response["data"] if isinstance(data, list): @@ -156,18 +185,21 @@ class CloudApiClient: else: Logger.log("e", "Cannot find data or errors in the cloud response: %s", response) - ## Creates a callback function so that it includes the parsing of the response into the correct model. - # The callback is added to the 'finished' signal of the reply. - # \param reply: The reply that should be listened to. - # \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either - # a list or a single item. - # \param model: The type of the model to convert the response to. def _addCallback(self, reply: QNetworkReply, on_finished: Union[Callable[[CloudApiClientModel], Any], Callable[[List[CloudApiClientModel]], Any]], model: Type[CloudApiClientModel], on_error: Optional[Callable] = None) -> None: + """Creates a callback function so that it includes the parsing of the response into the correct model. + + The callback is added to the 'finished' signal of the reply. + :param reply: The reply that should be listened to. + :param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either + a list or a single item. + :param model: The type of the model to convert the response to. + """ + def parse() -> None: self._anti_gc_callbacks.remove(parse) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 64638a0a1e..939b9cce23 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -35,11 +35,13 @@ from ..Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus I18N_CATALOG = i18nCatalog("cura") -## The cloud output device is a network output device that works remotely but has limited functionality. -# Currently it only supports viewing the printer and print job status and adding a new job to the queue. -# As such, those methods have been implemented here. -# Note that this device represents a single remote cluster, not a list of multiple clusters. class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): + """The cloud output device is a network output device that works remotely but has limited functionality. + + Currently it only supports viewing the printer and print job status and adding a new job to the queue. + As such, those methods have been implemented here. + Note that this device represents a single remote cluster, not a list of multiple clusters. + """ # The interval with which the remote cluster is checked. # We can do this relatively often as this API call is quite fast. @@ -56,11 +58,13 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): # Therefore we create a private signal used to trigger the printersChanged signal. _cloudClusterPrintersChanged = pyqtSignal() - ## Creates a new cloud output device - # \param api_client: The client that will run the API calls - # \param cluster: The device response received from the cloud API. - # \param parent: The optional parent of this output device. def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None: + """Creates a new cloud output device + + :param api_client: The client that will run the API calls + :param cluster: The device response received from the cloud API. + :param parent: The optional parent of this output device. + """ # The following properties are expected on each networked output device. # Because the cloud connection does not off all of these, we manually construct this version here. @@ -99,8 +103,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._tool_path = None # type: Optional[bytes] self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse] - ## Connects this device. def connect(self) -> None: + """Connects this device.""" + if self.isConnected(): return super().connect() @@ -108,21 +113,24 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange) self._update() - ## Disconnects the device def disconnect(self) -> None: + """Disconnects the device""" + if not self.isConnected(): return super().disconnect() Logger.log("i", "Disconnected from cluster %s", self.key) CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange) - ## Resets the print job that was uploaded to force a new upload, runs whenever the user re-slices. def _onBackendStateChange(self, _: BackendState) -> None: + """Resets the print job that was uploaded to force a new upload, runs whenever the user re-slices.""" + self._tool_path = None self._uploaded_print_job = None - ## Checks whether the given network key is found in the cloud's host name def matchesNetworkKey(self, network_key: str) -> bool: + """Checks whether the given network key is found in the cloud's host name""" + # Typically, a network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local." # the host name should then be "ultimakersystem-aabbccdd0011" if network_key.startswith(str(self.clusterData.host_name or "")): @@ -133,15 +141,17 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): return True return False - ## Set all the interface elements and texts for this output device. def _setInterfaceElements(self) -> None: + """Set all the interface elements and texts for this output device.""" + self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'. self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via Cloud")) self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud")) self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud")) - ## Called when the network data should be updated. def _update(self) -> None: + """Called when the network data should be updated.""" + super()._update() if time() - self._time_of_last_request < self.CHECK_CLUSTER_INTERVAL: return # avoid calling the cloud too often @@ -153,9 +163,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): else: self.setAuthenticationState(AuthState.NotAuthenticated) - ## Method called when HTTP request to status endpoint is finished. - # Contains both printers and print jobs statuses in a single response. def _onStatusCallFinished(self, status: CloudClusterStatus) -> None: + """Method called when HTTP request to status endpoint is finished. + + Contains both printers and print jobs statuses in a single response. + """ self._responseReceived() if status.printers != self._received_printers: self._received_printers = status.printers @@ -164,10 +176,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._received_print_jobs = status.print_jobs self._updatePrintJobs(status.print_jobs) - ## Called when Cura requests an output device to receive a (G-code) file. def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: + """Called when Cura requests an output device to receive a (G-code) file.""" + # Show an error message if we're already sending a job. if self._progress.visible: PrintJobUploadBlockedMessage().show() @@ -187,9 +200,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): job.finished.connect(self._onPrintJobCreated) job.start() - ## Handler for when the print job was created locally. - # It can now be sent over the cloud. def _onPrintJobCreated(self, job: ExportFileJob) -> None: + """Handler for when the print job was created locally. + + It can now be sent over the cloud. + """ output = job.getOutput() self._tool_path = output # store the tool path to prevent re-uploading when printing the same file again file_name = job.getFileName() @@ -200,9 +215,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): ) self._api.requestUpload(request, self._uploadPrintJob) - ## Uploads the mesh when the print job was registered with the cloud API. - # \param job_response: The response received from the cloud API. def _uploadPrintJob(self, job_response: CloudPrintJobResponse) -> None: + """Uploads the mesh when the print job was registered with the cloud API. + + :param job_response: The response received from the cloud API. + """ if not self._tool_path: return self._onUploadError() self._progress.show() @@ -210,38 +227,45 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update, self._onUploadError) - ## Requests the print to be sent to the printer when we finished uploading the mesh. def _onPrintJobUploaded(self) -> None: + """Requests the print to be sent to the printer when we finished uploading the mesh.""" + self._progress.update(100) print_job = cast(CloudPrintJobResponse, self._uploaded_print_job) self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted) - ## Shows a message when the upload has succeeded - # \param response: The response from the cloud API. def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None: + """Shows a message when the upload has succeeded + + :param response: The response from the cloud API. + """ self._progress.hide() PrintJobUploadSuccessMessage().show() self.writeFinished.emit() - ## Displays the given message if uploading the mesh has failed - # \param message: The message to display. def _onUploadError(self, message: str = None) -> None: + """Displays the given message if uploading the mesh has failed + + :param message: The message to display. + """ self._progress.hide() self._uploaded_print_job = None PrintJobUploadErrorMessage(message).show() self.writeError.emit() - ## Whether the printer that this output device represents supports print job actions via the cloud. @pyqtProperty(bool, notify=_cloudClusterPrintersChanged) def supportsPrintJobActions(self) -> bool: + """Whether the printer that this output device represents supports print job actions via the cloud.""" + if not self._printers: return False version_number = self.printers[0].firmwareVersion.split(".") firmware_version = Version([version_number[0], version_number[1], version_number[2]]) return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION - ## Set the remote print job state. def setJobState(self, print_job_uuid: str, state: str) -> None: + """Set the remote print job state.""" + self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, state) @pyqtSlot(str, name="sendJobToTop") @@ -265,18 +289,21 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): def openPrinterControlPanel(self) -> None: QDesktopServices.openUrl(QUrl(self.clusterCloudUrl)) - ## Gets the cluster response from which this device was created. @property def clusterData(self) -> CloudClusterResponse: + """Gets the cluster response from which this device was created.""" + return self._cluster - ## Updates the cluster data from the cloud. @clusterData.setter def clusterData(self, value: CloudClusterResponse) -> None: + """Updates the cluster data from the cloud.""" + self._cluster = value - ## Gets the URL on which to monitor the cluster via the cloud. @property def clusterCloudUrl(self) -> str: + """Gets the URL on which to monitor the cluster via the cloud.""" + root_url_prefix = "-staging" if self._account.is_staging else "" return "https://mycloud{}.ultimaker.com/app/jobs/{}".format(root_url_prefix, self.clusterData.cluster_id) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py index 6aa341c0e5..5de2378042 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py @@ -10,8 +10,9 @@ from UM.Logger import Logger from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse -## Class responsible for uploading meshes to the cloud in separate requests. class ToolPathUploader: + """Class responsible for uploading meshes to the cloud in separate requests.""" + # The maximum amount of times to retry if the server returns one of the RETRY_HTTP_CODES MAX_RETRIES = 10 @@ -22,16 +23,19 @@ class ToolPathUploader: # The amount of bytes to send per request BYTES_PER_REQUEST = 256 * 1024 - ## Creates a mesh upload object. - # \param manager: The network access manager that will handle the HTTP requests. - # \param print_job: The print job response that was returned by the cloud after registering the upload. - # \param data: The mesh bytes to be uploaded. - # \param on_finished: The method to be called when done. - # \param on_progress: The method to be called when the progress changes (receives a percentage 0-100). - # \param on_error: The method to be called when an error occurs. def __init__(self, manager: QNetworkAccessManager, print_job: CloudPrintJobResponse, data: bytes, on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any] ) -> None: + """Creates a mesh upload object. + + :param manager: The network access manager that will handle the HTTP requests. + :param print_job: The print job response that was returned by the cloud after registering the upload. + :param data: The mesh bytes to be uploaded. + :param on_finished: The method to be called when done. + :param on_progress: The method to be called when the progress changes (receives a percentage 0-100). + :param on_error: The method to be called when an error occurs. + """ + self._manager = manager self._print_job = print_job self._data = data @@ -45,13 +49,15 @@ class ToolPathUploader: self._finished = False self._reply = None # type: Optional[QNetworkReply] - ## Returns the print job for which this object was created. @property def printJob(self): + """Returns the print job for which this object was created.""" + return self._print_job - ## Creates a network request to the print job upload URL, adding the needed content range header. def _createRequest(self) -> QNetworkRequest: + """Creates a network request to the print job upload URL, adding the needed content range header.""" + request = QNetworkRequest(QUrl(self._print_job.upload_url)) request.setHeader(QNetworkRequest.ContentTypeHeader, self._print_job.content_type) @@ -62,14 +68,17 @@ class ToolPathUploader: return request - ## Determines the bytes that should be uploaded next. - # \return: A tuple with the first and the last byte to upload. def _chunkRange(self) -> Tuple[int, int]: + """Determines the bytes that should be uploaded next. + + :return: A tuple with the first and the last byte to upload. + """ last_byte = min(len(self._data), self._sent_bytes + self.BYTES_PER_REQUEST) return self._sent_bytes, last_byte - ## Starts uploading the mesh. def start(self) -> None: + """Starts uploading the mesh.""" + if self._finished: # reset state. self._sent_bytes = 0 @@ -77,13 +86,15 @@ class ToolPathUploader: self._finished = False self._uploadChunk() - ## Stops uploading the mesh, marking it as finished. def stop(self): + """Stops uploading the mesh, marking it as finished.""" + Logger.log("i", "Stopped uploading") self._finished = True - ## Uploads a chunk of the mesh to the cloud. def _uploadChunk(self) -> None: + """Uploads a chunk of the mesh to the cloud.""" + if self._finished: raise ValueError("The upload is already finished") @@ -96,25 +107,29 @@ class ToolPathUploader: self._reply.uploadProgress.connect(self._progressCallback) self._reply.error.connect(self._errorCallback) - ## Handles an update to the upload progress - # \param bytes_sent: The amount of bytes sent in the current request. - # \param bytes_total: The amount of bytes to send in the current request. def _progressCallback(self, bytes_sent: int, bytes_total: int) -> None: + """Handles an update to the upload progress + + :param bytes_sent: The amount of bytes sent in the current request. + :param bytes_total: The amount of bytes to send in the current request. + """ Logger.log("i", "Progress callback %s / %s", bytes_sent, bytes_total) if bytes_total: total_sent = self._sent_bytes + bytes_sent self._on_progress(int(total_sent / len(self._data) * 100)) - ## Handles an error uploading. def _errorCallback(self) -> None: + """Handles an error uploading.""" + reply = cast(QNetworkReply, self._reply) body = bytes(reply.readAll()).decode() Logger.log("e", "Received error while uploading: %s", body) self.stop() self._on_error() - ## Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed. def _finishedCallback(self) -> None: + """Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed.""" + reply = cast(QNetworkReply, self._reply) Logger.log("i", "Finished callback %s %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString()) @@ -140,8 +155,9 @@ class ToolPathUploader: [bytes(header).decode() for header in reply.rawHeaderList()], bytes(reply.readAll()).decode()) self._chunkUploaded() - ## Handles a chunk of data being uploaded, starting the next chunk if needed. def _chunkUploaded(self) -> None: + """Handles a chunk of data being uploaded, starting the next chunk if needed.""" + # We got a successful response. Let's start the next chunk or report the upload is finished. first_byte, last_byte = self._chunkRange() self._sent_bytes += last_byte - first_byte diff --git a/plugins/UM3NetworkPrinting/src/ExportFileJob.py b/plugins/UM3NetworkPrinting/src/ExportFileJob.py index 56d15bc835..6fde08cc5f 100644 --- a/plugins/UM3NetworkPrinting/src/ExportFileJob.py +++ b/plugins/UM3NetworkPrinting/src/ExportFileJob.py @@ -9,8 +9,8 @@ from cura.CuraApplication import CuraApplication from .MeshFormatHandler import MeshFormatHandler -## Job that exports the build plate to the correct file format for the target cluster. class ExportFileJob(WriteFileJob): + """Job that exports the build plate to the correct file format for the target cluster.""" def __init__(self, file_handler: Optional[FileHandler], nodes: List[SceneNode], firmware_version: str) -> None: @@ -27,12 +27,14 @@ class ExportFileJob(WriteFileJob): extension = self._mesh_format_handler.preferred_format.get("extension", "") self.setFileName("{}.{}".format(job_name, extension)) - ## Get the mime type of the selected export file type. def getMimeType(self) -> str: + """Get the mime type of the selected export file type.""" + return self._mesh_format_handler.mime_type - ## Get the job result as bytes as that is what we need to upload to the cluster. def getOutput(self) -> bytes: + """Get the job result as bytes as that is what we need to upload to the cluster.""" + output = self.getStream().getvalue() if isinstance(output, str): output = output.encode("utf-8") diff --git a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py index 9927bf744e..0287d72eb6 100644 --- a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py +++ b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py @@ -16,8 +16,9 @@ from cura.CuraApplication import CuraApplication I18N_CATALOG = i18nCatalog("cura") -## This class is responsible for choosing the formats used by the connected clusters. class MeshFormatHandler: + """This class is responsible for choosing the formats used by the connected clusters.""" + def __init__(self, file_handler: Optional[FileHandler], firmware_version: str) -> None: self._file_handler = file_handler or CuraApplication.getInstance().getMeshFileHandler() @@ -28,42 +29,50 @@ class MeshFormatHandler: def is_valid(self) -> bool: return bool(self._writer) - ## Chooses the preferred file format. - # \return A dict with the file format details, with the following keys: - # {id: str, extension: str, description: str, mime_type: str, mode: int, hide_in_file_dialog: bool} @property def preferred_format(self) -> Dict[str, Union[str, int, bool]]: + """Chooses the preferred file format. + + :return: A dict with the file format details, with the following keys: + {id: str, extension: str, description: str, mime_type: str, mode: int, hide_in_file_dialog: bool} + """ return self._preferred_format - ## Gets the file writer for the given file handler and mime type. - # \return A file writer. @property def writer(self) -> Optional[FileWriter]: + """Gets the file writer for the given file handler and mime type. + + :return: A file writer. + """ return self._writer @property def mime_type(self) -> str: return cast(str, self._preferred_format["mime_type"]) - ## Gets the file mode (FileWriter.OutputMode.TextMode or FileWriter.OutputMode.BinaryMode) @property def file_mode(self) -> int: + """Gets the file mode (FileWriter.OutputMode.TextMode or FileWriter.OutputMode.BinaryMode)""" + return cast(int, self._preferred_format["mode"]) - ## Gets the file extension @property def file_extension(self) -> str: + """Gets the file extension""" + return cast(str, self._preferred_format["extension"]) - ## Creates the right kind of stream based on the preferred format. def createStream(self) -> Union[io.BytesIO, io.StringIO]: + """Creates the right kind of stream based on the preferred format.""" + if self.file_mode == FileWriter.OutputMode.TextMode: return io.StringIO() else: return io.BytesIO() - ## Writes the mesh and returns its value. def getBytes(self, nodes: List[SceneNode]) -> bytes: + """Writes the mesh and returns its value.""" + if self.writer is None: raise ValueError("There is no writer for the mesh format handler.") stream = self.createStream() @@ -73,10 +82,12 @@ class MeshFormatHandler: value = value.encode() return value - ## Chooses the preferred file format for the given file handler. - # \param firmware_version: The version of the firmware. - # \return A dict with the file format details. def _getPreferredFormat(self, firmware_version: str) -> Dict[str, Union[str, int, bool]]: + """Chooses the preferred file format for the given file handler. + + :param firmware_version: The version of the firmware. + :return: A dict with the file format details. + """ # Formats supported by this application (file types that we can actually write). application = CuraApplication.getInstance() @@ -108,9 +119,11 @@ class MeshFormatHandler: ) return file_formats[0] - ## Gets the file writer for the given file handler and mime type. - # \param mime_type: The mine type. - # \return A file writer. def _getWriter(self, mime_type: str) -> Optional[FileWriter]: + """Gets the file writer for the given file handler and mime type. + + :param mime_type: The mine type. + :return: A file writer. + """ # Just take the first file format available. return self._file_handler.getWriterByMimeType(mime_type) diff --git a/plugins/UM3NetworkPrinting/src/Messages/LegacyDeviceNoLongerSupportedMessage.py b/plugins/UM3NetworkPrinting/src/Messages/LegacyDeviceNoLongerSupportedMessage.py index f4132dbcbc..146767467a 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/LegacyDeviceNoLongerSupportedMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/LegacyDeviceNoLongerSupportedMessage.py @@ -7,12 +7,12 @@ from UM.Message import Message I18N_CATALOG = i18nCatalog("cura") -## Message shown when trying to connect to a legacy printer device. class LegacyDeviceNoLongerSupportedMessage(Message): - - # Singleton used to prevent duplicate messages of this type at the same time. + """Message shown when trying to connect to a legacy printer device.""" + __is_visible = False - + """Singleton used to prevent duplicate messages of this type at the same time.""" + def __init__(self) -> None: super().__init__( text = I18N_CATALOG.i18nc("@info:status", "You are attempting to connect to a printer that is not " diff --git a/plugins/UM3NetworkPrinting/src/Messages/MaterialSyncMessage.py b/plugins/UM3NetworkPrinting/src/Messages/MaterialSyncMessage.py index e021b2ae99..6b481ff4a1 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/MaterialSyncMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/MaterialSyncMessage.py @@ -13,11 +13,11 @@ if TYPE_CHECKING: I18N_CATALOG = i18nCatalog("cura") -## Message shown when sending material files to cluster host. class MaterialSyncMessage(Message): + """Message shown when sending material files to cluster host.""" - # Singleton used to prevent duplicate messages of this type at the same time. __is_visible = False + """Singleton used to prevent duplicate messages of this type at the same time.""" def __init__(self, device: "UltimakerNetworkedPrinterOutputDevice") -> None: super().__init__( diff --git a/plugins/UM3NetworkPrinting/src/Messages/NotClusterHostMessage.py b/plugins/UM3NetworkPrinting/src/Messages/NotClusterHostMessage.py index 77d7995fc7..70bfa769ee 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/NotClusterHostMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/NotClusterHostMessage.py @@ -16,11 +16,11 @@ if TYPE_CHECKING: I18N_CATALOG = i18nCatalog("cura") -## Message shown when trying to connect to a printer that is not a host. class NotClusterHostMessage(Message): + """Message shown when trying to connect to a printer that is not a host.""" - # Singleton used to prevent duplicate messages of this type at the same time. __is_visible = False + """Singleton used to prevent duplicate messages of this type at the same time.""" def __init__(self, device: "UltimakerNetworkedPrinterOutputDevice") -> None: super().__init__( diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadBlockedMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadBlockedMessage.py index be00292559..39dc985cb8 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadBlockedMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadBlockedMessage.py @@ -7,9 +7,9 @@ from UM.Message import Message I18N_CATALOG = i18nCatalog("cura") -## Message shown when uploading a print job to a cluster is blocked because another upload is already in progress. class PrintJobUploadBlockedMessage(Message): - + """Message shown when uploading a print job to a cluster is blocked because another upload is already in progress.""" + def __init__(self) -> None: super().__init__( text = I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."), diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadErrorMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadErrorMessage.py index bb26a84953..5145844ea7 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadErrorMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadErrorMessage.py @@ -7,9 +7,9 @@ from UM.Message import Message I18N_CATALOG = i18nCatalog("cura") -## Message shown when uploading a print job to a cluster failed. class PrintJobUploadErrorMessage(Message): - + """Message shown when uploading a print job to a cluster failed.""" + def __init__(self, message: str = None) -> None: super().__init__( text = message or I18N_CATALOG.i18nc("@info:text", "Could not upload the data to the printer."), diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py index bdbab008e3..b7ddf7f550 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py @@ -7,8 +7,9 @@ from UM.Message import Message I18N_CATALOG = i18nCatalog("cura") -## Class responsible for showing a progress message while a mesh is being uploaded to the cloud. class PrintJobUploadProgressMessage(Message): + """Class responsible for showing a progress message while a mesh is being uploaded to the cloud.""" + def __init__(self): super().__init__( title = I18N_CATALOG.i18nc("@info:status", "Sending Print Job"), @@ -19,14 +20,17 @@ class PrintJobUploadProgressMessage(Message): use_inactivity_timer = False ) - ## Shows the progress message. def show(self): + """Shows the progress message.""" + self.setProgress(0) super().show() - ## Updates the percentage of the uploaded. - # \param percentage: The percentage amount (0-100). def update(self, percentage: int) -> None: + """Updates the percentage of the uploaded. + + :param percentage: The percentage amount (0-100). + """ if not self._visible: super().show() self.setProgress(percentage) diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py index c9be28d57f..aa64f338dd 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadSuccessMessage.py @@ -7,9 +7,9 @@ from UM.Message import Message I18N_CATALOG = i18nCatalog("cura") -## Message shown when uploading a print job to a cluster succeeded. class PrintJobUploadSuccessMessage(Message): - + """Message shown when uploading a print job to a cluster succeeded.""" + def __init__(self) -> None: super().__init__( text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."), diff --git a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py index 3d38a4b116..f1385a0270 100644 --- a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py @@ -18,45 +18,56 @@ class BaseModel: def validate(self) -> None: pass - ## Checks whether the two models are equal. - # \param other: The other model. - # \return True if they are equal, False if they are different. def __eq__(self, other): + """Checks whether the two models are equal. + + :param other: The other model. + :return: True if they are equal, False if they are different. + """ return type(self) == type(other) and self.toDict() == other.toDict() - ## Checks whether the two models are different. - # \param other: The other model. - # \return True if they are different, False if they are the same. def __ne__(self, other) -> bool: + """Checks whether the two models are different. + + :param other: The other model. + :return: True if they are different, False if they are the same. + """ return type(self) != type(other) or self.toDict() != other.toDict() - ## Converts the model into a serializable dictionary def toDict(self) -> Dict[str, Any]: + """Converts the model into a serializable dictionary""" + return self.__dict__ - ## Parses a single model. - # \param model_class: The model class. - # \param values: The value of the model, which is usually a dictionary, but may also be already parsed. - # \return An instance of the model_class given. @staticmethod def parseModel(model_class: Type[T], values: Union[T, Dict[str, Any]]) -> T: + """Parses a single model. + + :param model_class: The model class. + :param values: The value of the model, which is usually a dictionary, but may also be already parsed. + :return: An instance of the model_class given. + """ if isinstance(values, dict): return model_class(**values) return values - ## Parses a list of models. - # \param model_class: The model class. - # \param values: The value of the list. Each value is usually a dictionary, but may also be already parsed. - # \return A list of instances of the model_class given. @classmethod def parseModels(cls, model_class: Type[T], values: List[Union[T, Dict[str, Any]]]) -> List[T]: + """Parses a list of models. + + :param model_class: The model class. + :param values: The value of the list. Each value is usually a dictionary, but may also be already parsed. + :return: A list of instances of the model_class given. + """ return [cls.parseModel(model_class, value) for value in values] - ## Parses the given date string. - # \param date: The date to parse. - # \return The parsed date. @staticmethod def parseDate(date: Union[str, datetime]) -> datetime: + """Parses the given date string. + + :param date: The date to parse. + :return: The parsed date. + """ if isinstance(date, datetime): return date return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py index 7ecfe8b0a3..a108b8dc87 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py @@ -5,22 +5,26 @@ from typing import Optional from ..BaseModel import BaseModel -## Class representing a cloud connected cluster. class CloudClusterResponse(BaseModel): + """Class representing a cloud connected cluster.""" + - ## Creates a new cluster response object. - # \param cluster_id: The secret unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. - # \param host_guid: The unique identifier of the print cluster host, e.g. 'e90ae0ac-1257-4403-91ee-a44c9b7e8050'. - # \param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users. - # \param is_online: Whether this cluster is currently connected to the cloud. - # \param status: The status of the cluster authentication (active or inactive). - # \param host_version: The firmware version of the cluster host. This is where the Stardust client is running on. - # \param host_internal_ip: The internal IP address of the host printer. - # \param friendly_name: The human readable name of the host printer. - # \param printer_type: The machine type of the host printer. def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str, host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", **kwargs) -> None: + """Creates a new cluster response object. + + :param cluster_id: The secret unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. + :param host_guid: The unique identifier of the print cluster host, e.g. 'e90ae0ac-1257-4403-91ee-a44c9b7e8050'. + :param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users. + :param is_online: Whether this cluster is currently connected to the cloud. + :param status: The status of the cluster authentication (active or inactive). + :param host_version: The firmware version of the cluster host. This is where the Stardust client is running on. + :param host_internal_ip: The internal IP address of the host printer. + :param friendly_name: The human readable name of the host printer. + :param printer_type: The machine type of the host printer. + """ + self.cluster_id = cluster_id self.host_guid = host_guid self.host_name = host_name diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py index 330e61d343..10f7b0ce6b 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py @@ -11,15 +11,17 @@ from .ClusterPrintJobStatus import ClusterPrintJobStatus # Model that represents the status of the cluster for the cloud class CloudClusterStatus(BaseModel): - ## Creates a new cluster status model object. - # \param printers: The latest status of each printer in the cluster. - # \param print_jobs: The latest status of each print job in the cluster. - # \param generated_time: The datetime when the object was generated on the server-side. - def __init__(self, - printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]], + def __init__(self, printers: List[Union[ClusterPrinterStatus, Dict[str, Any]]], print_jobs: List[Union[ClusterPrintJobStatus, Dict[str, Any]]], generated_time: Union[str, datetime], **kwargs) -> None: + """Creates a new cluster status model object. + + :param printers: The latest status of each printer in the cluster. + :param print_jobs: The latest status of each print job in the cluster. + :param generated_time: The datetime when the object was generated on the server-side. + """ + self.generated_time = self.parseDate(generated_time) self.printers = self.parseModels(ClusterPrinterStatus, printers) self.print_jobs = self.parseModels(ClusterPrintJobStatus, print_jobs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py index 9381e4b8cf..97e7862ff6 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py @@ -5,20 +5,23 @@ from typing import Dict, Optional, Any from ..BaseModel import BaseModel -## Class representing errors generated by the cloud servers, according to the JSON-API standard. class CloudError(BaseModel): + """Class representing errors generated by the cloud servers, according to the JSON-API standard.""" - ## Creates a new error object. - # \param id: Unique identifier for this particular occurrence of the problem. - # \param title: A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence - # of the problem, except for purposes of localization. - # \param code: An application-specific error code, expressed as a string value. - # \param detail: A human-readable explanation specific to this occurrence of the problem. Like title, this field's - # value can be localized. - # \param http_status: The HTTP status code applicable to this problem, converted to string. - # \param meta: Non-standard meta-information about the error, depending on the error code. def __init__(self, id: str, code: str, title: str, http_status: str, detail: Optional[str] = None, meta: Optional[Dict[str, Any]] = None, **kwargs) -> None: + """Creates a new error object. + + :param id: Unique identifier for this particular occurrence of the problem. + :param title: A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence + of the problem, except for purposes of localization. + :param code: An application-specific error code, expressed as a string value. + :param detail: A human-readable explanation specific to this occurrence of the problem. Like title, this field's + value can be localized. + :param http_status: The HTTP status code applicable to this problem, converted to string. + :param meta: Non-standard meta-information about the error, depending on the error code. + """ + self.id = id self.code = code self.http_status = http_status diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py index a1880e8751..ccc9ffb2fc 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py @@ -8,19 +8,22 @@ from ..BaseModel import BaseModel # Model that represents the response received from the cloud after requesting to upload a print job class CloudPrintJobResponse(BaseModel): - ## Creates a new print job response model. - # \param job_id: The job unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. - # \param status: The status of the print job. - # \param status_description: Contains more details about the status, e.g. the cause of failures. - # \param download_url: A signed URL to download the resulting status. Only available when the job is finished. - # \param job_name: The name of the print job. - # \param slicing_details: Model for slice information. - # \param upload_url: The one-time use URL where the toolpath must be uploaded to (only if status is uploading). - # \param content_type: The content type of the print job (e.g. text/plain or application/gzip) - # \param generated_time: The datetime when the object was generated on the server-side. def __init__(self, job_id: str, status: str, download_url: Optional[str] = None, job_name: Optional[str] = None, upload_url: Optional[str] = None, content_type: Optional[str] = None, status_description: Optional[str] = None, slicing_details: Optional[dict] = None, **kwargs) -> None: + """Creates a new print job response model. + + :param job_id: The job unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. + :param status: The status of the print job. + :param status_description: Contains more details about the status, e.g. the cause of failures. + :param download_url: A signed URL to download the resulting status. Only available when the job is finished. + :param job_name: The name of the print job. + :param slicing_details: Model for slice information. + :param upload_url: The one-time use URL where the toolpath must be uploaded to (only if status is uploading). + :param content_type: The content type of the print job (e.g. text/plain or application/gzip) + :param generated_time: The datetime when the object was generated on the server-side. + """ + self.job_id = job_id self.status = status self.download_url = download_url diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py index ff705ae495..efa1efb7e4 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py @@ -6,11 +6,14 @@ from ..BaseModel import BaseModel # Model that represents the request to upload a print job to the cloud class CloudPrintJobUploadRequest(BaseModel): - ## Creates a new print job upload request. - # \param job_name: The name of the print job. - # \param file_size: The size of the file in bytes. - # \param content_type: The content type of the print job (e.g. text/plain or application/gzip) def __init__(self, job_name: str, file_size: int, content_type: str, **kwargs) -> None: + """Creates a new print job upload request. + + :param job_name: The name of the print job. + :param file_size: The size of the file in bytes. + :param content_type: The content type of the print job (e.g. text/plain or application/gzip) + """ + self.job_name = job_name self.file_size = file_size self.content_type = content_type diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py index b108f40e27..ee2b8a307f 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py @@ -9,13 +9,16 @@ from ..BaseModel import BaseModel # Model that represents the responses received from the cloud after requesting a job to be printed. class CloudPrintResponse(BaseModel): - ## Creates a new print response object. - # \param job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect. - # \param status: The status of the print request (queued or failed). - # \param generated_time: The datetime when the object was generated on the server-side. - # \param cluster_job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect. def __init__(self, job_id: str, status: str, generated_time: Union[str, datetime], cluster_job_id: Optional[str] = None, **kwargs) -> None: + """Creates a new print response object. + + :param job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect. + :param status: The status of the print request (queued or failed). + :param generated_time: The datetime when the object was generated on the server-side. + :param cluster_job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect. + """ + self.job_id = job_id self.status = status self.cluster_job_id = cluster_job_id diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py index a5a392488d..c81e0a372c 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py @@ -3,11 +3,13 @@ from ..BaseModel import BaseModel -## Class representing a cluster printer class ClusterBuildPlate(BaseModel): + """Class representing a cluster printer""" - ## Create a new build plate - # \param type: The type of build plate glass or aluminium def __init__(self, type: str = "glass", **kwargs) -> None: + """Create a new build plate + + :param type: The type of build plate glass or aluminium + """ self.type = type super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py index e11d2be2d2..75ce234e23 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py @@ -9,26 +9,33 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate from ..BaseModel import BaseModel -## Class representing a cloud cluster printer configuration -# Also used for representing slots in a Material Station (as from Cura's perspective these are the same). class ClusterPrintCoreConfiguration(BaseModel): + """Class representing a cloud cluster printer configuration + + Also used for representing slots in a Material Station (as from Cura's perspective these are the same). + """ + + def __init__(self, extruder_index: int, material: Union[None, Dict[str, Any], + ClusterPrinterConfigurationMaterial] = None, print_core_id: Optional[str] = None, **kwargs) -> None: + """Creates a new cloud cluster printer configuration object + + :param extruder_index: The position of the extruder on the machine as list index. Numbered from left to right. + :param material: The material of a configuration object in a cluster printer. May be in a dict or an object. + :param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'. + :param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'. + """ - ## Creates a new cloud cluster printer configuration object - # \param extruder_index: The position of the extruder on the machine as list index. Numbered from left to right. - # \param material: The material of a configuration object in a cluster printer. May be in a dict or an object. - # \param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'. - # \param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'. - def __init__(self, extruder_index: int, - material: Union[None, Dict[str, Any], ClusterPrinterConfigurationMaterial] = None, - print_core_id: Optional[str] = None, **kwargs) -> None: self.extruder_index = extruder_index self.material = self.parseModel(ClusterPrinterConfigurationMaterial, material) if material else None self.print_core_id = print_core_id super().__init__(**kwargs) - ## Updates the given output model. - # \param model - The output model to update. def updateOutputModel(self, model: ExtruderOutputModel) -> None: + """Updates the given output model. + + :param model: The output model to update. + """ + if self.print_core_id is not None: model.updateHotendID(self.print_core_id) @@ -40,14 +47,16 @@ class ClusterPrintCoreConfiguration(BaseModel): else: model.updateActiveMaterial(None) - ## Creates a configuration model def createConfigurationModel(self) -> ExtruderConfigurationModel: + """Creates a configuration model""" + model = ExtruderConfigurationModel(position = self.extruder_index) self.updateConfigurationModel(model) return model - ## Creates a configuration model def updateConfigurationModel(self, model: ExtruderConfigurationModel) -> ExtruderConfigurationModel: + """Creates a configuration model""" + model.setHotendID(self.print_core_id) if self.material: model.setMaterial(self.material.createOutputModel()) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py index 88251bbf53..cdfa633170 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py @@ -5,19 +5,22 @@ from typing import Optional from ..BaseModel import BaseModel -## Model for the types of changes that are needed before a print job can start class ClusterPrintJobConfigurationChange(BaseModel): + """Model for the types of changes that are needed before a print job can start""" + + + def __init__(self, type_of_change: str, target_id: str, origin_id: str, index: Optional[int] = None, + target_name: Optional[str] = None, origin_name: Optional[str] = None, **kwargs) -> None: + """Creates a new print job constraint. + + :param type_of_change: The type of configuration change, one of: "material", "print_core_change" + :param index: The hotend slot or extruder index to change + :param target_id: Target material guid or hotend id + :param origin_id: Original/current material guid or hotend id + :param target_name: Target material name or hotend id + :param origin_name: Original/current material name or hotend id + """ - ## Creates a new print job constraint. - # \param type_of_change: The type of configuration change, one of: "material", "print_core_change" - # \param index: The hotend slot or extruder index to change - # \param target_id: Target material guid or hotend id - # \param origin_id: Original/current material guid or hotend id - # \param target_name: Target material name or hotend id - # \param origin_name: Original/current material name or hotend id - def __init__(self, type_of_change: str, target_id: str, origin_id: str, - index: Optional[int] = None, target_name: Optional[str] = None, origin_name: Optional[str] = None, - **kwargs) -> None: self.type_of_change = type_of_change self.index = index self.target_id = target_id diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py index 9239004b18..258d940a03 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py @@ -5,12 +5,14 @@ from typing import Optional from ..BaseModel import BaseModel -## Class representing a cloud cluster print job constraint class ClusterPrintJobConstraints(BaseModel): + """Class representing a cloud cluster print job constraint""" - ## Creates a new print job constraint. - # \param require_printer_name: Unique name of the printer that this job should be printed on. - # Should be one of the unique_name field values in the cluster, e.g. 'ultimakersystem-ccbdd30044ec' def __init__(self, require_printer_name: Optional[str] = None, **kwargs) -> None: + """Creates a new print job constraint. + + :param require_printer_name: Unique name of the printer that this job should be printed on. + Should be one of the unique_name field values in the cluster, e.g. 'ultimakersystem-ccbdd30044ec' + """ self.require_printer_name = require_printer_name super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py index 5a8f0aa46d..7beaf6f61f 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py @@ -3,14 +3,17 @@ from ..BaseModel import BaseModel -## Class representing the reasons that prevent this job from being printed on the associated printer class ClusterPrintJobImpediment(BaseModel): + """Class representing the reasons that prevent this job from being printed on the associated printer""" - ## Creates a new print job constraint. - # \param translation_key: A string indicating a reason the print cannot be printed, - # such as 'does_not_fit_in_build_volume' - # \param severity: A number indicating the severity of the problem, with higher being more severe def __init__(self, translation_key: str, severity: int, **kwargs) -> None: + """Creates a new print job constraint. + + :param translation_key: A string indicating a reason the print cannot be printed, + such as 'does_not_fit_in_build_volume' + :param severity: A number indicating the severity of the problem, with higher being more severe + """ + self.translation_key = translation_key self.severity = severity super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py index 22fb9bb37a..9fb94ab12f 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py @@ -15,36 +15,9 @@ from ..BaseModel import BaseModel from ...ClusterOutputController import ClusterOutputController -## Model for the status of a single print job in a cluster. class ClusterPrintJobStatus(BaseModel): + """Model for the status of a single print job in a cluster.""" - ## Creates a new cloud print job status model. - # \param assigned_to: The name of the printer this job is assigned to while being queued. - # \param configuration: The required print core configurations of this print job. - # \param constraints: Print job constraints object. - # \param created_at: The timestamp when the job was created in Cura Connect. - # \param force: Allow this job to be printed despite of mismatching configurations. - # \param last_seen: The number of seconds since this job was checked. - # \param machine_variant: The machine type that this job should be printed on.Coincides with the machine_type field - # of the printer object. - # \param name: The name of the print job. Usually the name of the .gcode file. - # \param network_error_count: The number of errors encountered when requesting data for this print job. - # \param owner: The name of the user who added the print job to Cura Connect. - # \param printer_uuid: UUID of the printer that the job is currently printing on or assigned to. - # \param started: Whether the job has started printing or not. - # \param status: The status of the print job. - # \param time_elapsed: The remaining printing time in seconds. - # \param time_total: The total printing time in seconds. - # \param uuid: UUID of this print job. Should be used for identification purposes. - # \param deleted_at: The time when this print job was deleted. - # \param printed_on_uuid: UUID of the printer used to print this job. - # \param configuration_changes_required: List of configuration changes the printer this job is associated with - # needs to make in order to be able to print this job - # \param build_plate: The build plate (type) this job needs to be printed on. - # \param compatible_machine_families: Family names of machines suitable for this print job - # \param impediments_to_printing: A list of reasons that prevent this job from being printed on the associated - # printer - # \param preview_url: URL to the preview image (same as wou;d've been included in the ufp). def __init__(self, created_at: str, force: bool, machine_variant: str, name: str, started: bool, status: str, time_total: int, uuid: str, configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], @@ -60,6 +33,37 @@ class ClusterPrintJobStatus(BaseModel): impediments_to_printing: List[Union[Dict[str, Any], ClusterPrintJobImpediment]] = None, preview_url: Optional[str] = None, **kwargs) -> None: + + """Creates a new cloud print job status model. + + :param assigned_to: The name of the printer this job is assigned to while being queued. + :param configuration: The required print core configurations of this print job. + :param constraints: Print job constraints object. + :param created_at: The timestamp when the job was created in Cura Connect. + :param force: Allow this job to be printed despite of mismatching configurations. + :param last_seen: The number of seconds since this job was checked. + :param machine_variant: The machine type that this job should be printed on.Coincides with the machine_type field + of the printer object. + :param name: The name of the print job. Usually the name of the .gcode file. + :param network_error_count: The number of errors encountered when requesting data for this print job. + :param owner: The name of the user who added the print job to Cura Connect. + :param printer_uuid: UUID of the printer that the job is currently printing on or assigned to. + :param started: Whether the job has started printing or not. + :param status: The status of the print job. + :param time_elapsed: The remaining printing time in seconds. + :param time_total: The total printing time in seconds. + :param uuid: UUID of this print job. Should be used for identification purposes. + :param deleted_at: The time when this print job was deleted. + :param printed_on_uuid: UUID of the printer used to print this job. + :param configuration_changes_required: List of configuration changes the printer this job is associated with + needs to make in order to be able to print this job + :param build_plate: The build plate (type) this job needs to be printed on. + :param compatible_machine_families: Family names of machines suitable for this print job + :param impediments_to_printing: A list of reasons that prevent this job from being printed on the associated + printer + :param preview_url: URL to the preview image (same as wou;d've been included in the ufp). + """ + self.assigned_to = assigned_to self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) self.constraints = self.parseModels(ClusterPrintJobConstraints, constraints) @@ -90,24 +94,31 @@ class ClusterPrintJobStatus(BaseModel): super().__init__(**kwargs) - ## Creates an UM3 print job output model based on this cloud cluster print job. - # \param printer: The output model of the printer def createOutputModel(self, controller: ClusterOutputController) -> UM3PrintJobOutputModel: + """Creates an UM3 print job output model based on this cloud cluster print job. + + :param printer: The output model of the printer + """ + model = UM3PrintJobOutputModel(controller, self.uuid, self.name) self.updateOutputModel(model) return model - ## Creates a new configuration model def _createConfigurationModel(self) -> PrinterConfigurationModel: + """Creates a new configuration model""" + extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()] configuration = PrinterConfigurationModel() configuration.setExtruderConfigurations(extruders) configuration.setPrinterType(self.machine_variant) return configuration - ## Updates an UM3 print job output model based on this cloud cluster print job. - # \param model: The model to update. def updateOutputModel(self, model: UM3PrintJobOutputModel) -> None: + """Updates an UM3 print job output model based on this cloud cluster print job. + + :param model: The model to update. + """ + model.updateConfiguration(self._createConfigurationModel()) model.updateTimeTotal(self.time_total) model.updateTimeElapsed(self.time_elapsed) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py index 8edb9fb808..62f99293d1 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py @@ -9,29 +9,35 @@ from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel from ..BaseModel import BaseModel -## Class representing a cloud cluster printer configuration class ClusterPrinterConfigurationMaterial(BaseModel): + """Class representing a cloud cluster printer configuration""" - ## Creates a new material configuration model. - # \param brand: The brand of material in this print core, e.g. 'Ultimaker'. - # \param color: The color of material in this print core, e.g. 'Blue'. - # \param guid: he GUID of the material in this print core, e.g. '506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9'. - # \param material: The type of material in this print core, e.g. 'PLA'. def __init__(self, brand: Optional[str] = None, color: Optional[str] = None, guid: Optional[str] = None, material: Optional[str] = None, **kwargs) -> None: + + """Creates a new material configuration model. + + :param brand: The brand of material in this print core, e.g. 'Ultimaker'. + :param color: The color of material in this print core, e.g. 'Blue'. + :param guid: he GUID of the material in this print core, e.g. '506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9'. + :param material: The type of material in this print core, e.g. 'PLA'. + """ + self.guid = guid self.brand = brand self.color = color self.material = material super().__init__(**kwargs) - ## Creates a material output model based on this cloud printer material. - # - # A material is chosen that matches the current GUID. If multiple such - # materials are available, read-only materials are preferred and the - # material with the earliest alphabetical name will be selected. - # \return A material output model that matches the current GUID. def createOutputModel(self) -> MaterialOutputModel: + """Creates a material output model based on this cloud printer material. + + A material is chosen that matches the current GUID. If multiple such + materials are available, read-only materials are preferred and the + material with the earliest alphabetical name will be selected. + :return: A material output model that matches the current GUID. + """ + container_registry = ContainerRegistry.getInstance() same_guid = container_registry.findInstanceContainersMetadata(GUID = self.guid) if same_guid: diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py index c51e07bcfc..1929c2a388 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py @@ -6,16 +6,19 @@ from ..BaseModel import BaseModel from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot -## Class representing the data of a Material Station in the cluster. class ClusterPrinterMaterialStation(BaseModel): + """Class representing the data of a Material Station in the cluster.""" - ## Creates a new Material Station status. - # \param status: The status of the material station. - # \param: supported: Whether the material station is supported on this machine or not. - # \param material_slots: The active slots configurations of this material station. def __init__(self, status: str, supported: bool = False, material_slots: List[Union[ClusterPrinterMaterialStationSlot, Dict[str, Any]]] = None, **kwargs) -> None: + """Creates a new Material Station status. + + :param status: The status of the material station. + :param: supported: Whether the material station is supported on this machine or not. + :param material_slots: The active slots configurations of this material station. + """ + self.status = status self.supported = supported self.material_slots = self.parseModels(ClusterPrinterMaterialStationSlot, material_slots)\ diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py index b9c40592e5..d41d6c14fc 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py @@ -5,16 +5,19 @@ from typing import Optional from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration -## Class representing the data of a single slot in the material station. class ClusterPrinterMaterialStationSlot(ClusterPrintCoreConfiguration): - - ## Create a new material station slot object. - # \param slot_index: The index of the slot in the material station (ranging 0 to 5). - # \param compatible: Whether the configuration is compatible with the print core. - # \param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data). - # \param material_empty: Whether the material spool is too empty to be used. + """Class representing the data of a single slot in the material station.""" + def __init__(self, slot_index: int, compatible: bool, material_remaining: float, material_empty: Optional[bool] = False, **kwargs) -> None: + """Create a new material station slot object. + + :param slot_index: The index of the slot in the material station (ranging 0 to 5). + :param compatible: Whether the configuration is compatible with the print core. + :param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data). + :param material_empty: Whether the material spool is too empty to be used. + """ + self.slot_index = slot_index self.compatible = compatible self.material_remaining = material_remaining diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 2e0912f057..3d342a519d 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -17,26 +17,10 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate from ..BaseModel import BaseModel -## Class representing a cluster printer class ClusterPrinterStatus(BaseModel): + """Class representing a cluster printer""" + - ## Creates a new cluster printer status - # \param enabled: A printer can be disabled if it should not receive new jobs. By default every printer is enabled. - # \param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster. - # \param friendly_name: Human readable name of the printer. Can be used for identification purposes. - # \param ip_address: The IP address of the printer in the local network. - # \param machine_variant: The type of printer. Can be 'Ultimaker 3' or 'Ultimaker 3ext'. - # \param status: The status of the printer. - # \param unique_name: The unique name of the printer in the network. - # \param uuid: The unique ID of the printer, also known as GUID. - # \param configuration: The active print core configurations of this printer. - # \param reserved_by: A printer can be claimed by a specific print job. - # \param maintenance_required: Indicates if maintenance is necessary. - # \param firmware_update_status: Whether the printer's firmware is up-to-date, value is one of: "up_to_date", - # "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible". - # \param latest_available_firmware: The version of the latest firmware that is available. - # \param build_plate: The build plate that is on the printer. - # \param material_station: The material station that is on the printer. def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str, status: str, unique_name: str, uuid: str, configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], @@ -44,6 +28,25 @@ class ClusterPrinterStatus(BaseModel): firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None, build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None: + """Creates a new cluster printer status + + :param enabled: A printer can be disabled if it should not receive new jobs. By default every printer is enabled. + :param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster. + :param friendly_name: Human readable name of the printer. Can be used for identification purposes. + :param ip_address: The IP address of the printer in the local network. + :param machine_variant: The type of printer. Can be 'Ultimaker 3' or 'Ultimaker 3ext'. + :param status: The status of the printer. + :param unique_name: The unique name of the printer in the network. + :param uuid: The unique ID of the printer, also known as GUID. + :param configuration: The active print core configurations of this printer. + :param reserved_by: A printer can be claimed by a specific print job. + :param maintenance_required: Indicates if maintenance is necessary. + :param firmware_update_status: Whether the printer's firmware is up-to-date, value is one of: "up_to_date", + "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible". + :param latest_available_firmware: The version of the latest firmware that is available. + :param build_plate: The build plate that is on the printer. + :param material_station: The material station that is on the printer. + """ self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) self.enabled = enabled @@ -63,9 +66,12 @@ class ClusterPrinterStatus(BaseModel): material_station) if material_station else None super().__init__(**kwargs) - ## Creates a new output model. - # \param controller - The controller of the model. def createOutputModel(self, controller: PrinterOutputController) -> PrinterOutputModel: + """Creates a new output model. + + :param controller: - The controller of the model. + """ + # FIXME # Note that we're using '2' here as extruder count. We have hardcoded this for now to prevent issues where the # amount of extruders coming back from the API is actually lower (which it can be if a printer was just added @@ -74,9 +80,12 @@ class ClusterPrinterStatus(BaseModel): self.updateOutputModel(model) return model - ## Updates the given output model. - # \param model - The output model to update. def updateOutputModel(self, model: PrinterOutputModel) -> None: + """Updates the given output model. + + :param model: - The output model to update. + """ + model.updateKey(self.uuid) model.updateName(self.friendly_name) model.updateUniqueName(self.unique_name) @@ -110,9 +119,12 @@ class ClusterPrinterStatus(BaseModel): ) for left_slot, right_slot in product(self._getSlotsForExtruder(0), self._getSlotsForExtruder(1))] model.setAvailableConfigurations(available_configurations) - ## Create a list of Material Station slots for the given extruder index. - # Returns a list with a single empty material slot if none are found to ensure we don't miss configurations. def _getSlotsForExtruder(self, extruder_index: int) -> List[ClusterPrinterMaterialStationSlot]: + """Create a list of Material Station slots for the given extruder index. + + Returns a list with a single empty material slot if none are found to ensure we don't miss configurations. + """ + if not self.material_station: # typing guard return [] slots = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration( @@ -121,15 +133,19 @@ class ClusterPrinterStatus(BaseModel): )] return slots or [self._createEmptyMaterialSlot(extruder_index)] - ## Check if a configuration is supported in order to make it selectable by the user. - # We filter out any slot that is not supported by the extruder index, print core type or if the material is empty. @staticmethod def _isSupportedConfiguration(slot: ClusterPrinterMaterialStationSlot, extruder_index: int) -> bool: + """Check if a configuration is supported in order to make it selectable by the user. + + We filter out any slot that is not supported by the extruder index, print core type or if the material is empty. + """ + return slot.extruder_index == extruder_index and slot.compatible and not slot.material_empty - ## Create an empty material slot with a fake empty material. @staticmethod def _createEmptyMaterialSlot(extruder_index: int) -> ClusterPrinterMaterialStationSlot: + """Create an empty material slot with a fake empty material.""" + empty_material = ClusterPrinterConfigurationMaterial(guid = "", material = "empty", brand = "", color = "") return ClusterPrinterMaterialStationSlot(slot_index = 0, extruder_index = extruder_index, compatible = True, material_remaining = 0, material = empty_material) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py index ad7b9c8698..58d6c94ee1 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/PrinterSystemStatus.py @@ -5,12 +5,11 @@ from typing import Dict, Any from ..BaseModel import BaseModel -## Class representing the system status of a printer. class PrinterSystemStatus(BaseModel): + """Class representing the system status of a printer.""" def __init__(self, guid: str, firmware: str, hostname: str, name: str, platform: str, variant: str, - hardware: Dict[str, Any], **kwargs - ) -> None: + hardware: Dict[str, Any], **kwargs) -> None: self.guid = guid self.firmware = firmware self.hostname = hostname diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index 6a8b9f625c..7d6b260b90 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -16,12 +16,13 @@ from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus from ..Models.Http.ClusterMaterial import ClusterMaterial -## The generic type variable used to document the methods below. ClusterApiClientModel = TypeVar("ClusterApiClientModel", bound=BaseModel) +"""The generic type variable used to document the methods below.""" -## The ClusterApiClient is responsible for all network calls to local network clusters. class ClusterApiClient: + """The ClusterApiClient is responsible for all network calls to local network clusters.""" + PRINTER_API_PREFIX = "/api/v1" CLUSTER_API_PREFIX = "/cluster-api/v1" @@ -29,75 +30,92 @@ class ClusterApiClient: # In order to avoid garbage collection we keep the callbacks in this list. _anti_gc_callbacks = [] # type: List[Callable[[], None]] - ## Initializes a new cluster API client. - # \param address: The network address of the cluster to call. - # \param on_error: The callback to be called whenever we receive errors from the server. def __init__(self, address: str, on_error: Callable) -> None: + """Initializes a new cluster API client. + + :param address: The network address of the cluster to call. + :param on_error: The callback to be called whenever we receive errors from the server. + """ super().__init__() self._manager = QNetworkAccessManager() self._address = address self._on_error = on_error - ## Get printer system information. - # \param on_finished: The callback in case the response is successful. def getSystem(self, on_finished: Callable) -> None: + """Get printer system information. + + :param on_finished: The callback in case the response is successful. + """ url = "{}/system".format(self.PRINTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, PrinterSystemStatus) - ## Get the installed materials on the printer. - # \param on_finished: The callback in case the response is successful. def getMaterials(self, on_finished: Callable[[List[ClusterMaterial]], Any]) -> None: + """Get the installed materials on the printer. + + :param on_finished: The callback in case the response is successful. + """ url = "{}/materials".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterMaterial) - ## Get the printers in the cluster. - # \param on_finished: The callback in case the response is successful. def getPrinters(self, on_finished: Callable[[List[ClusterPrinterStatus]], Any]) -> None: + """Get the printers in the cluster. + + :param on_finished: The callback in case the response is successful. + """ url = "{}/printers".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterPrinterStatus) - ## Get the print jobs in the cluster. - # \param on_finished: The callback in case the response is successful. def getPrintJobs(self, on_finished: Callable[[List[ClusterPrintJobStatus]], Any]) -> None: + """Get the print jobs in the cluster. + + :param on_finished: The callback in case the response is successful. + """ url = "{}/print_jobs".format(self.CLUSTER_API_PREFIX) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, ClusterPrintJobStatus) - ## Move a print job to the top of the queue. def movePrintJobToTop(self, print_job_uuid: str) -> None: + """Move a print job to the top of the queue.""" + url = "{}/print_jobs/{}/action/move".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.post(self._createEmptyRequest(url), json.dumps({"to_position": 0, "list": "queued"}).encode()) - ## Override print job configuration and force it to be printed. def forcePrintJob(self, print_job_uuid: str) -> None: + """Override print job configuration and force it to be printed.""" + url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.put(self._createEmptyRequest(url), json.dumps({"force": True}).encode()) - ## Delete a print job from the queue. def deletePrintJob(self, print_job_uuid: str) -> None: + """Delete a print job from the queue.""" + url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.deleteResource(self._createEmptyRequest(url)) - ## Set the state of a print job. def setPrintJobState(self, print_job_uuid: str, state: str) -> None: + """Set the state of a print job.""" + url = "{}/print_jobs/{}/action".format(self.CLUSTER_API_PREFIX, print_job_uuid) # We rewrite 'resume' to 'print' here because we are using the old print job action endpoints. action = "print" if state == "resume" else state self._manager.put(self._createEmptyRequest(url), json.dumps({"action": action}).encode()) - ## Get the preview image data of a print job. def getPrintJobPreviewImage(self, print_job_uuid: str, on_finished: Callable) -> None: + """Get the preview image data of a print job.""" + url = "{}/print_jobs/{}/preview_image".format(self.CLUSTER_API_PREFIX, print_job_uuid) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished) - ## We override _createEmptyRequest in order to add the user credentials. - # \param url: The URL to request - # \param content_type: The type of the body contents. def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest: + """We override _createEmptyRequest in order to add the user credentials. + + :param url: The URL to request + :param content_type: The type of the body contents. + """ url = QUrl("http://" + self._address + path) request = QNetworkRequest(url) request.setAttribute(QNetworkRequest.FollowRedirectsAttribute, True) @@ -105,11 +123,13 @@ class ClusterApiClient: request.setHeader(QNetworkRequest.ContentTypeHeader, content_type) return request - ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. - # \param reply: The reply from the server. - # \return A tuple with a status code and a dictionary. @staticmethod def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: + """Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. + + :param reply: The reply from the server. + :return: A tuple with a status code and a dictionary. + """ status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) try: response = bytes(reply.readAll()).decode() @@ -118,14 +138,15 @@ class ClusterApiClient: Logger.logException("e", "Could not parse the cluster response: %s", err) return status_code, {"errors": [err]} - ## Parses the given models and calls the correct callback depending on the result. - # \param response: The response from the server, after being converted to a dict. - # \param on_finished: The callback in case the response is successful. - # \param model_class: The type of the model to convert the response to. It may either be a single record or a list. - def _parseModels(self, response: Dict[str, Any], - on_finished: Union[Callable[[ClusterApiClientModel], Any], - Callable[[List[ClusterApiClientModel]], Any]], - model_class: Type[ClusterApiClientModel]) -> None: + def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[ClusterApiClientModel], Any], + Callable[[List[ClusterApiClientModel]], Any]], model_class: Type[ClusterApiClientModel]) -> None: + """Parses the given models and calls the correct callback depending on the result. + + :param response: The response from the server, after being converted to a dict. + :param on_finished: The callback in case the response is successful. + :param model_class: The type of the model to convert the response to. It may either be a single record or a list. + """ + try: if isinstance(response, list): results = [model_class(**c) for c in response] # type: List[ClusterApiClientModel] @@ -138,16 +159,15 @@ class ClusterApiClient: except (JSONDecodeError, TypeError, ValueError): Logger.log("e", "Could not parse response from network: %s", str(response)) - ## Creates a callback function so that it includes the parsing of the response into the correct model. - # The callback is added to the 'finished' signal of the reply. - # \param reply: The reply that should be listened to. - # \param on_finished: The callback in case the response is successful. - def _addCallback(self, - reply: QNetworkReply, - on_finished: Union[Callable[[ClusterApiClientModel], Any], - Callable[[List[ClusterApiClientModel]], Any]], - model: Type[ClusterApiClientModel] = None, + def _addCallback(self, reply: QNetworkReply, on_finished: Union[Callable[[ClusterApiClientModel], Any], + Callable[[List[ClusterApiClientModel]], Any]], model: Type[ClusterApiClientModel] = None, ) -> None: + """Creates a callback function so that it includes the parsing of the response into the correct model. + + The callback is added to the 'finished' signal of the reply. + :param reply: The reply that should be listened to. + :param on_finished: The callback in case the response is successful. + """ def parse() -> None: self._anti_gc_callbacks.remove(parse) diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index 1266afcca8..ea120f8978 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -51,15 +51,17 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._setInterfaceElements() self._active_camera_url = QUrl() # type: QUrl - ## Set all the interface elements and texts for this output device. def _setInterfaceElements(self) -> None: + """Set all the interface elements and texts for this output device.""" + self.setPriority(3) # Make sure the output device gets selected above local file output self.setShortDescription(I18N_CATALOG.i18nc("@action:button Preceded by 'Ready to'.", "Print over network")) self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print over network")) self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected over the network")) - ## Called when the connection to the cluster changes. def connect(self) -> None: + """Called when the connection to the cluster changes.""" + super().connect() self._update() self.sendMaterialProfiles() @@ -94,10 +96,13 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): def forceSendJob(self, print_job_uuid: str) -> None: self._getApiClient().forcePrintJob(print_job_uuid) - ## Set the remote print job state. - # \param print_job_uuid: The UUID of the print job to set the state for. - # \param action: The action to undertake ('pause', 'resume', 'abort'). def setJobState(self, print_job_uuid: str, action: str) -> None: + """Set the remote print job state. + + :param print_job_uuid: The UUID of the print job to set the state for. + :param action: The action to undertake ('pause', 'resume', 'abort'). + """ + self._getApiClient().setPrintJobState(print_job_uuid, action) def _update(self) -> None: @@ -106,19 +111,22 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._getApiClient().getPrintJobs(self._updatePrintJobs) self._updatePrintJobPreviewImages() - ## Get a list of materials that are installed on the cluster host. def getMaterials(self, on_finished: Callable[[List[ClusterMaterial]], Any]) -> None: + """Get a list of materials that are installed on the cluster host.""" + self._getApiClient().getMaterials(on_finished = on_finished) - ## Sync the material profiles in Cura with the printer. - # This gets called when connecting to a printer as well as when sending a print. def sendMaterialProfiles(self) -> None: + """Sync the material profiles in Cura with the printer. + + This gets called when connecting to a printer as well as when sending a print. + """ job = SendMaterialJob(device = self) job.run() - ## Send a print job to the cluster. def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None: + """Send a print job to the cluster.""" # Show an error message if we're already sending a job. if self._progress.visible: @@ -132,15 +140,20 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): job.finished.connect(self._onPrintJobCreated) job.start() - ## Allows the user to choose a printer to print with from the printer selection dialogue. - # \param unique_name: The unique name of the printer to target. @pyqtSlot(str, name="selectTargetPrinter") def selectTargetPrinter(self, unique_name: str = "") -> None: + """Allows the user to choose a printer to print with from the printer selection dialogue. + + :param unique_name: The unique name of the printer to target. + """ self._startPrintJobUpload(unique_name if unique_name != "" else None) - ## Handler for when the print job was created locally. - # It can now be sent over the network. def _onPrintJobCreated(self, job: ExportFileJob) -> None: + """Handler for when the print job was created locally. + + It can now be sent over the network. + """ + self._active_exported_job = job # TODO: add preference to enable/disable this feature? if self.clusterSize > 1: @@ -148,8 +161,9 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): return self._startPrintJobUpload() - ## Shows a dialog allowing the user to select which printer in a group to send a job to. def _showPrinterSelectionDialog(self) -> None: + """Shows a dialog allowing the user to select which printer in a group to send a job to.""" + if not self._printer_select_dialog: plugin_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "" path = os.path.join(plugin_path, "resources", "qml", "PrintWindow.qml") @@ -157,8 +171,9 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): if self._printer_select_dialog is not None: self._printer_select_dialog.show() - ## Upload the print job to the group. def _startPrintJobUpload(self, unique_name: str = None) -> None: + """Upload the print job to the group.""" + if not self._active_exported_job: Logger.log("e", "No active exported job to upload!") return @@ -177,33 +192,40 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): on_progress=self._onPrintJobUploadProgress) self._active_exported_job = None - ## Handler for print job upload progress. def _onPrintJobUploadProgress(self, bytes_sent: int, bytes_total: int) -> None: + """Handler for print job upload progress.""" + percentage = (bytes_sent / bytes_total) if bytes_total else 0 self._progress.setProgress(percentage * 100) self.writeProgress.emit() - ## Handler for when the print job was fully uploaded to the cluster. def _onPrintUploadCompleted(self, _: QNetworkReply) -> None: + """Handler for when the print job was fully uploaded to the cluster.""" + self._progress.hide() PrintJobUploadSuccessMessage().show() self.writeFinished.emit() - ## Displays the given message if uploading the mesh has failed - # \param message: The message to display. def _onUploadError(self, message: str = None) -> None: + """Displays the given message if uploading the mesh has failed + + :param message: The message to display. + """ + self._progress.hide() PrintJobUploadErrorMessage(message).show() self.writeError.emit() - ## Download all the images from the cluster and load their data in the print job models. def _updatePrintJobPreviewImages(self): + """Download all the images from the cluster and load their data in the print job models.""" + for print_job in self._print_jobs: if print_job.getPreviewImage() is None: self._getApiClient().getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData) - ## Get the API client instance. def _getApiClient(self) -> ClusterApiClient: + """Get the API client instance.""" + if not self._cluster_api: self._cluster_api = ClusterApiClient(self.address, on_error = lambda error: Logger.log("e", str(error))) return self._cluster_api diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py index 273c64ef4d..68500d777e 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py @@ -24,8 +24,9 @@ from ..Models.Http.PrinterSystemStatus import PrinterSystemStatus I18N_CATALOG = i18nCatalog("cura") -## The LocalClusterOutputDeviceManager is responsible for discovering and managing local networked clusters. class LocalClusterOutputDeviceManager: + """The LocalClusterOutputDeviceManager is responsible for discovering and managing local networked clusters.""" + META_NETWORK_KEY = "um_network_key" @@ -49,30 +50,35 @@ class LocalClusterOutputDeviceManager: self._zero_conf_client.addedNetworkCluster.connect(self._onDeviceDiscovered) self._zero_conf_client.removedNetworkCluster.connect(self._onDiscoveredDeviceRemoved) - ## Start the network discovery. def start(self) -> None: + """Start the network discovery.""" + self._zero_conf_client.start() for address in self._getStoredManualAddresses(): self.addManualDevice(address) - ## Stop network discovery and clean up discovered devices. def stop(self) -> None: + """Stop network discovery and clean up discovered devices.""" + self._zero_conf_client.stop() for instance_name in list(self._discovered_devices): self._onDiscoveredDeviceRemoved(instance_name) - ## Restart discovery on the local network. def startDiscovery(self): + """Restart discovery on the local network.""" + self.stop() self.start() - ## Add a networked printer manually by address. def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: + """Add a networked printer manually by address.""" + api_client = ClusterApiClient(address, lambda error: Logger.log("e", str(error))) api_client.getSystem(lambda status: self._onCheckManualDeviceResponse(address, status, callback)) - ## Remove a manually added networked printer. def removeManualDevice(self, device_id: str, address: Optional[str] = None) -> None: + """Remove a manually added networked printer.""" + if device_id not in self._discovered_devices and address is not None: device_id = "manual:{}".format(address) @@ -83,16 +89,19 @@ class LocalClusterOutputDeviceManager: if address in self._getStoredManualAddresses(): self._removeStoredManualAddress(address) - ## Force reset all network device connections. def refreshConnections(self) -> None: + """Force reset all network device connections.""" + self._connectToActiveMachine() - ## Get the discovered devices. def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]: + """Get the discovered devices.""" + return self._discovered_devices - ## Connect the active machine to a given device. def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: + """Connect the active machine to a given device.""" + active_machine = CuraApplication.getInstance().getGlobalContainerStack() if not active_machine: return @@ -106,8 +115,9 @@ class LocalClusterOutputDeviceManager: return CuraApplication.getInstance().getMachineManager().switchPrinterType(definitions[0].getName()) - ## Callback for when the active machine was changed by the user or a new remote cluster was found. def _connectToActiveMachine(self) -> None: + """Callback for when the active machine was changed by the user or a new remote cluster was found.""" + active_machine = CuraApplication.getInstance().getGlobalContainerStack() if not active_machine: return @@ -122,9 +132,10 @@ class LocalClusterOutputDeviceManager: # Remove device if it is not meant for the active machine. output_device_manager.removeOutputDevice(device.key) - ## Callback for when a manual device check request was responded to. def _onCheckManualDeviceResponse(self, address: str, status: PrinterSystemStatus, callback: Optional[Callable[[bool, str], None]] = None) -> None: + """Callback for when a manual device check request was responded to.""" + self._onDeviceDiscovered("manual:{}".format(address), address, { b"name": status.name.encode("utf-8"), b"address": address.encode("utf-8"), @@ -137,10 +148,13 @@ class LocalClusterOutputDeviceManager: if callback is not None: CuraApplication.getInstance().callLater(callback, True, address) - ## Returns a dict of printer BOM numbers to machine types. - # These numbers are available in the machine definition already so we just search for them here. @staticmethod def _getPrinterTypeIdentifiers() -> Dict[str, str]: + """Returns a dict of printer BOM numbers to machine types. + + These numbers are available in the machine definition already so we just search for them here. + """ + container_registry = CuraApplication.getInstance().getContainerRegistry() ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.") found_machine_type_identifiers = {} # type: Dict[str, str] @@ -154,8 +168,9 @@ class LocalClusterOutputDeviceManager: found_machine_type_identifiers[str(bom_number)] = machine_type return found_machine_type_identifiers - ## Add a new device. def _onDeviceDiscovered(self, key: str, address: str, properties: Dict[bytes, bytes]) -> None: + """Add a new device.""" + machine_identifier = properties.get(b"machine", b"").decode("utf-8") printer_type_identifiers = self._getPrinterTypeIdentifiers() @@ -189,8 +204,9 @@ class LocalClusterOutputDeviceManager: self.discoveredDevicesChanged.emit() self._connectToActiveMachine() - ## Remove a device. def _onDiscoveredDeviceRemoved(self, device_id: str) -> None: + """Remove a device.""" + device = self._discovered_devices.pop(device_id, None) # type: Optional[LocalClusterOutputDevice] if not device: return @@ -198,8 +214,9 @@ class LocalClusterOutputDeviceManager: CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.address) self.discoveredDevicesChanged.emit() - ## Create a machine instance based on the discovered network printer. def _createMachineFromDiscoveredDevice(self, device_id: str) -> None: + """Create a machine instance based on the discovered network printer.""" + device = self._discovered_devices.get(device_id) if device is None: return @@ -216,8 +233,9 @@ class LocalClusterOutputDeviceManager: self._connectToOutputDevice(device, new_machine) self._showCloudFlowMessage(device) - ## Add an address to the stored preferences. def _storeManualAddress(self, address: str) -> None: + """Add an address to the stored preferences.""" + stored_addresses = self._getStoredManualAddresses() if address in stored_addresses: return # Prevent duplicates. @@ -225,8 +243,9 @@ class LocalClusterOutputDeviceManager: new_value = ",".join(stored_addresses) CuraApplication.getInstance().getPreferences().setValue(self.MANUAL_DEVICES_PREFERENCE_KEY, new_value) - ## Remove an address from the stored preferences. def _removeStoredManualAddress(self, address: str) -> None: + """Remove an address from the stored preferences.""" + stored_addresses = self._getStoredManualAddresses() try: stored_addresses.remove(address) # Can throw a ValueError @@ -235,15 +254,16 @@ class LocalClusterOutputDeviceManager: except ValueError: Logger.log("w", "Could not remove address from stored_addresses, it was not there") - ## Load the user-configured manual devices from Cura preferences. def _getStoredManualAddresses(self) -> List[str]: + """Load the user-configured manual devices from Cura preferences.""" + preferences = CuraApplication.getInstance().getPreferences() preferences.addPreference(self.MANUAL_DEVICES_PREFERENCE_KEY, "") manual_instances = preferences.getValue(self.MANUAL_DEVICES_PREFERENCE_KEY).split(",") return manual_instances - ## Add a device to the current active machine. def _connectToOutputDevice(self, device: UltimakerNetworkedPrinterOutputDevice, machine: GlobalStack) -> None: + """Add a device to the current active machine.""" # Make sure users know that we no longer support legacy devices. if Version(device.firmwareVersion) < self.MIN_SUPPORTED_CLUSTER_VERSION: @@ -262,9 +282,10 @@ class LocalClusterOutputDeviceManager: if device.key not in output_device_manager.getOutputDeviceIds(): output_device_manager.addOutputDevice(device) - ## Nudge the user to start using Ultimaker Cloud. @staticmethod def _showCloudFlowMessage(device: LocalClusterOutputDevice) -> None: + """Nudge the user to start using Ultimaker Cloud.""" + if CuraApplication.getInstance().getMachineManager().activeMachineIsUsingCloudConnection: # This printer is already cloud connected, so we do not bother the user anymore. return diff --git a/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py index 49e088100d..90aa68eedb 100644 --- a/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py @@ -16,27 +16,33 @@ if TYPE_CHECKING: from .LocalClusterOutputDevice import LocalClusterOutputDevice -## Asynchronous job to send material profiles to the printer. -# -# This way it won't freeze up the interface while sending those materials. class SendMaterialJob(Job): + """Asynchronous job to send material profiles to the printer. + + This way it won't freeze up the interface while sending those materials. + """ + def __init__(self, device: "LocalClusterOutputDevice") -> None: super().__init__() self.device = device # type: LocalClusterOutputDevice - ## Send the request to the printer and register a callback def run(self) -> None: + """Send the request to the printer and register a callback""" + self.device.getMaterials(on_finished = self._onGetMaterials) - ## Callback for when the remote materials were returned. def _onGetMaterials(self, materials: List[ClusterMaterial]) -> None: + """Callback for when the remote materials were returned.""" + remote_materials_by_guid = {material.guid: material for material in materials} self._sendMissingMaterials(remote_materials_by_guid) - ## Determine which materials should be updated and send them to the printer. - # \param remote_materials_by_guid The remote materials by GUID. def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None: + """Determine which materials should be updated and send them to the printer. + + :param remote_materials_by_guid: The remote materials by GUID. + """ local_materials_by_guid = self._getLocalMaterials() if len(local_materials_by_guid) == 0: Logger.log("d", "There are no local materials to synchronize with the printer.") @@ -47,25 +53,31 @@ class SendMaterialJob(Job): return self._sendMaterials(material_ids_to_send) - ## From the local and remote materials, determine which ones should be synchronized. - # Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that - # are newer in Cura. - # \param local_materials The local materials by GUID. - # \param remote_materials The remote materials by GUID. @staticmethod def _determineMaterialsToSend(local_materials: Dict[str, LocalMaterial], remote_materials: Dict[str, ClusterMaterial]) -> Set[str]: + """From the local and remote materials, determine which ones should be synchronized. + + Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that + are newer in Cura. + :param local_materials: The local materials by GUID. + :param remote_materials: The remote materials by GUID. + """ + return { local_material.id for guid, local_material in local_materials.items() if guid not in remote_materials.keys() or local_material.version > remote_materials[guid].version } - ## Send the materials to the printer. - # The given materials will be loaded from disk en sent to to printer. - # The given id's will be matched with filenames of the locally stored materials. - # \param materials_to_send A set with id's of materials that must be sent. def _sendMaterials(self, materials_to_send: Set[str]) -> None: + """Send the materials to the printer. + + The given materials will be loaded from disk en sent to to printer. + The given id's will be matched with filenames of the locally stored materials. + :param materials_to_send: A set with id's of materials that must be sent. + """ + container_registry = CuraApplication.getInstance().getContainerRegistry() all_materials = container_registry.findInstanceContainersMetadata(type = "material") all_base_files = {material["base_file"] for material in all_materials if "base_file" in material} # Filters out uniques by making it a set. Don't include files without base file (i.e. empty material). @@ -83,12 +95,14 @@ class SendMaterialJob(Job): file_name = os.path.basename(file_path) self._sendMaterialFile(file_path, file_name, root_material_id) - ## Send a single material file to the printer. - # Also add the material signature file if that is available. - # \param file_path The path of the material file. - # \param file_name The name of the material file. - # \param material_id The ID of the material in the file. def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None: + """Send a single material file to the printer. + + Also add the material signature file if that is available. + :param file_path: The path of the material file. + :param file_name: The name of the material file. + :param material_id: The ID of the material in the file. + """ parts = [] # Add the material file. @@ -112,8 +126,9 @@ class SendMaterialJob(Job): self.device.postFormWithParts(target = "/cluster-api/v1/materials/", parts = parts, on_finished = self._sendingFinished) - ## Check a reply from an upload to the printer and log an error when the call failed def _sendingFinished(self, reply: QNetworkReply) -> None: + """Check a reply from an upload to the printer and log an error when the call failed""" + if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: Logger.log("w", "Error while syncing material: %s", reply.errorString()) return @@ -125,11 +140,14 @@ class SendMaterialJob(Job): # Because of the guards above it is not shown when syncing failed (which is not always an actual problem). MaterialSyncMessage(self.device).show() - ## Retrieves a list of local materials - # Only the new newest version of the local materials is returned - # \return a dictionary of LocalMaterial objects by GUID @staticmethod def _getLocalMaterials() -> Dict[str, LocalMaterial]: + """Retrieves a list of local materials + + Only the new newest version of the local materials is returned + :return: a dictionary of LocalMaterial objects by GUID + """ + result = {} # type: Dict[str, LocalMaterial] all_materials = CuraApplication.getInstance().getContainerRegistry().findInstanceContainersMetadata(type = "material") all_base_files = [material for material in all_materials if material["id"] == material.get("base_file")] # Don't send materials without base_file: The empty material doesn't need to be sent. diff --git a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py index 466638d99e..b41cd7d151 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py @@ -12,9 +12,11 @@ from UM.Signal import Signal from cura.CuraApplication import CuraApplication -## The ZeroConfClient handles all network discovery logic. -# It emits signals when new network services were found or disappeared. class ZeroConfClient: + """The ZeroConfClient handles all network discovery logic. + + It emits signals when new network services were found or disappeared. + """ # The discovery protocol name for Ultimaker printers. ZERO_CONF_NAME = u"_ultimaker._tcp.local." @@ -30,10 +32,13 @@ class ZeroConfClient: self._service_changed_request_event = None # type: Optional[Event] self._service_changed_request_thread = None # type: Optional[Thread] - ## The ZeroConf service changed requests are handled in a separate thread so we don't block the UI. - # We can also re-schedule the requests when they fail to get detailed service info. - # Any new or re-reschedule requests will be appended to the request queue and the thread will process them. def start(self) -> None: + """The ZeroConf service changed requests are handled in a separate thread so we don't block the UI. + + We can also re-schedule the requests when they fail to get detailed service info. + Any new or re-reschedule requests will be appended to the request queue and the thread will process them. + """ + self._service_changed_request_queue = Queue() self._service_changed_request_event = Event() try: @@ -56,16 +61,18 @@ class ZeroConfClient: self._zero_conf_browser.cancel() self._zero_conf_browser = None - ## Handles a change is discovered network services. def _queueService(self, zeroconf: Zeroconf, service_type, name: str, state_change: ServiceStateChange) -> None: + """Handles a change is discovered network services.""" + item = (zeroconf, service_type, name, state_change) if not self._service_changed_request_queue or not self._service_changed_request_event: return self._service_changed_request_queue.put(item) self._service_changed_request_event.set() - ## Callback for when a ZeroConf service has changes. def _handleOnServiceChangedRequests(self) -> None: + """Callback for when a ZeroConf service has changes.""" + if not self._service_changed_request_queue or not self._service_changed_request_event: return @@ -98,19 +105,23 @@ class ZeroConfClient: for request in reschedule_requests: self._service_changed_request_queue.put(request) - ## Handler for zeroConf detection. - # Return True or False indicating if the process succeeded. - # Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread. - def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange - ) -> bool: + def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, + state_change: ServiceStateChange) -> bool: + """Handler for zeroConf detection. + + Return True or False indicating if the process succeeded. + Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread. + """ + if state_change == ServiceStateChange.Added: return self._onServiceAdded(zero_conf, service_type, name) elif state_change == ServiceStateChange.Removed: return self._onServiceRemoved(name) return True - ## Handler for when a ZeroConf service was added. def _onServiceAdded(self, zero_conf: Zeroconf, service_type: str, name: str) -> bool: + """Handler for when a ZeroConf service was added.""" + # First try getting info from zero-conf cache info = ServiceInfo(service_type, name, properties={}) for record in zero_conf.cache.entries_with_name(name.lower()): @@ -141,8 +152,9 @@ class ZeroConfClient: return True - ## Handler for when a ZeroConf service was removed. def _onServiceRemoved(self, name: str) -> bool: + """Handler for when a ZeroConf service was removed.""" + Logger.log("d", "ZeroConf service removed: %s" % name) self.removedNetworkCluster.emit(str(name)) return True diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 3ab37297b5..72b2da33cc 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -13,11 +13,11 @@ from .Network.LocalClusterOutputDeviceManager import LocalClusterOutputDeviceMan from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager -## This plugin handles the discovery and networking for Ultimaker 3D printers that support network and cloud printing. class UM3OutputDevicePlugin(OutputDevicePlugin): - - # Signal emitted when the list of discovered devices changed. Used by printer action in this plugin. + """This plugin handles the discovery and networking for Ultimaker 3D printers""" + discoveredDevicesChanged = Signal() + """Signal emitted when the list of discovered devices changed. Used by printer action in this plugin.""" def __init__(self) -> None: super().__init__() @@ -33,8 +33,9 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): # This ensures no output devices are still connected that do not belong to the new active machine. CuraApplication.getInstance().globalContainerStackChanged.connect(self.refreshConnections) - ## Start looking for devices in the network and cloud. def start(self): + """Start looking for devices in the network and cloud.""" + self._network_output_device_manager.start() self._cloud_output_device_manager.start() @@ -43,31 +44,38 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._network_output_device_manager.stop() self._cloud_output_device_manager.stop() - ## Restart network discovery. def startDiscovery(self) -> None: + """Restart network discovery.""" + self._network_output_device_manager.startDiscovery() - ## Force refreshing the network connections. def refreshConnections(self) -> None: + """Force refreshing the network connections.""" + self._network_output_device_manager.refreshConnections() self._cloud_output_device_manager.refreshConnections() - ## Indicate that this plugin supports adding networked printers manually. def canAddManualDevice(self, address: str = "") -> ManualDeviceAdditionAttempt: + """Indicate that this plugin supports adding networked printers manually.""" + return ManualDeviceAdditionAttempt.PRIORITY - ## Add a networked printer manually based on its network address. def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None: + """Add a networked printer manually based on its network address.""" + self._network_output_device_manager.addManualDevice(address, callback) - ## Remove a manually connected networked printer. def removeManualDevice(self, key: str, address: Optional[str] = None) -> None: + """Remove a manually connected networked printer.""" + self._network_output_device_manager.removeManualDevice(key, address) - ## Get the discovered devices from the local network. def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]: + """Get the discovered devices from the local network.""" + return self._network_output_device_manager.getDiscoveredDevices() - ## Connect the active machine to a device. def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: + """Connect the active machine to a device.""" + self._network_output_device_manager.associateActiveMachineWithPrinterDevice(device) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py index 8c5f5c12ea..dd3f3939ed 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py @@ -15,9 +15,11 @@ from .Network.LocalClusterOutputDevice import LocalClusterOutputDevice I18N_CATALOG = i18nCatalog("cura") -## Machine action that allows to connect the active machine to a networked devices. -# TODO: in the future this should be part of the new discovery workflow baked into Cura. class UltimakerNetworkedPrinterAction(MachineAction): + """Machine action that allows to connect the active machine to a networked devices. + + TODO: in the future this should be part of the new discovery workflow baked into Cura. + """ # Signal emitted when discovered devices have changed. discoveredDevicesChanged = pyqtSignal() @@ -27,59 +29,69 @@ class UltimakerNetworkedPrinterAction(MachineAction): self._qml_url = "resources/qml/DiscoverUM3Action.qml" self._network_plugin = None # type: Optional[UM3OutputDevicePlugin] - ## Override the default value. def needsUserInteraction(self) -> bool: + """Override the default value.""" + return False - ## Start listening to network discovery events via the plugin. @pyqtSlot(name = "startDiscovery") def startDiscovery(self) -> None: + """Start listening to network discovery events via the plugin.""" + self._networkPlugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged) self.discoveredDevicesChanged.emit() # trigger at least once to populate the list - ## Reset the discovered devices. @pyqtSlot(name = "reset") def reset(self) -> None: + """Reset the discovered devices.""" + self.discoveredDevicesChanged.emit() # trigger to reset the list - ## Reset the discovered devices. @pyqtSlot(name = "restartDiscovery") def restartDiscovery(self) -> None: + """Reset the discovered devices.""" + self._networkPlugin.startDiscovery() self.discoveredDevicesChanged.emit() # trigger to reset the list - ## Remove a manually added device. @pyqtSlot(str, str, name = "removeManualDevice") def removeManualDevice(self, key: str, address: str) -> None: + """Remove a manually added device.""" + self._networkPlugin.removeManualDevice(key, address) - ## Add a new manual device. Can replace an existing one by key. @pyqtSlot(str, str, name = "setManualDevice") def setManualDevice(self, key: str, address: str) -> None: + """Add a new manual device. Can replace an existing one by key.""" + if key != "": self._networkPlugin.removeManualDevice(key) if address != "": self._networkPlugin.addManualDevice(address) - ## Get the devices discovered in the local network sorted by name. @pyqtProperty("QVariantList", notify = discoveredDevicesChanged) def foundDevices(self): + """Get the devices discovered in the local network sorted by name.""" + discovered_devices = list(self._networkPlugin.getDiscoveredDevices().values()) discovered_devices.sort(key = lambda d: d.name) return discovered_devices - ## Connect a device selected in the list with the active machine. @pyqtSlot(QObject, name = "associateActiveMachineWithPrinterDevice") def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: + """Connect a device selected in the list with the active machine.""" + self._networkPlugin.associateActiveMachineWithPrinterDevice(device) - ## Callback for when the list of discovered devices in the plugin was changed. def _onDeviceDiscoveryChanged(self) -> None: + """Callback for when the list of discovered devices in the plugin was changed.""" + self.discoveredDevicesChanged.emit() - ## Get the network manager from the plugin. @property def _networkPlugin(self) -> UM3OutputDevicePlugin: + """Get the network manager from the plugin.""" + if not self._network_plugin: output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() network_plugin = output_device_manager.getOutputDevicePlugin("UM3NetworkPrinting") diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index f50bab8a1f..8090177b83 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -22,10 +22,12 @@ from .Models.Http.ClusterPrinterStatus import ClusterPrinterStatus from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus -## Output device class that forms the basis of Ultimaker networked printer output devices. -# Currently used for local networking and cloud printing using Ultimaker Connect. -# This base class primarily contains all the Qt properties and slots needed for the monitor page to work. class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): + """Output device class that forms the basis of Ultimaker networked printer output devices. + + Currently used for local networking and cloud printing using Ultimaker Connect. + This base class primarily contains all the Qt properties and slots needed for the monitor page to work. + """ META_NETWORK_KEY = "um_network_key" META_CLUSTER_ID = "um_cloud_cluster_id" @@ -85,14 +87,16 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # The job upload progress message modal. self._progress = PrintJobUploadProgressMessage() - ## The IP address of the printer. @pyqtProperty(str, constant=True) def address(self) -> str: + """The IP address of the printer.""" + return self._address - ## The display name of the printer. @pyqtProperty(str, constant=True) def printerTypeName(self) -> str: + """The display name of the printer.""" + return self._printer_type_name # Get all print jobs for this cluster. @@ -157,13 +161,15 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self._active_printer = printer self.activePrinterChanged.emit() - ## Whether the printer that this output device represents supports print job actions via the local network. @pyqtProperty(bool, constant=True) def supportsPrintJobActions(self) -> bool: + """Whether the printer that this output device represents supports print job actions via the local network.""" + return True - ## Set the remote print job state. def setJobState(self, print_job_uuid: str, state: str) -> None: + """Set the remote print job state.""" + raise NotImplementedError("setJobState must be implemented") @pyqtSlot(str, name="sendJobToTop") @@ -210,11 +216,13 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self._checkStillConnected() super()._update() - ## Check if we're still connected by comparing the last timestamps for network response and the current time. - # This implementation is similar to the base NetworkedPrinterOutputDevice, but is tweaked slightly. - # Re-connecting is handled automatically by the output device managers in this plugin. - # TODO: it would be nice to have this logic in the managers, but connecting those with signals causes crashes. def _checkStillConnected(self) -> None: + """Check if we're still connected by comparing the last timestamps for network response and the current time. + + This implementation is similar to the base NetworkedPrinterOutputDevice, but is tweaked slightly. + Re-connecting is handled automatically by the output device managers in this plugin. + TODO: it would be nice to have this logic in the managers, but connecting those with signals causes crashes. + """ time_since_last_response = time() - self._time_of_last_response if time_since_last_response > self.NETWORK_RESPONSE_CONSIDER_OFFLINE: self.setConnectionState(ConnectionState.Closed) @@ -223,9 +231,11 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): elif self.connectionState == ConnectionState.Closed: self._reconnectForActiveMachine() - ## Reconnect for the active output device. - # Does nothing if the device is not meant for the active machine. def _reconnectForActiveMachine(self) -> None: + """Reconnect for the active output device. + + Does nothing if the device is not meant for the active machine. + """ active_machine = CuraApplication.getInstance().getGlobalContainerStack() if not active_machine: return @@ -281,16 +291,19 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self.printersChanged.emit() self._checkIfClusterHost() - ## Check is this device is a cluster host and takes the needed actions when it is not. def _checkIfClusterHost(self): + """Check is this device is a cluster host and takes the needed actions when it is not.""" + if len(self._printers) < 1 and self.isConnected(): NotClusterHostMessage(self).show() self.close() CuraApplication.getInstance().getOutputDeviceManager().removeOutputDevice(self.key) - ## Updates the local list of print jobs with the list received from the cluster. - # \param remote_jobs: The print jobs received from the cluster. def _updatePrintJobs(self, remote_jobs: List[ClusterPrintJobStatus]) -> None: + """Updates the local list of print jobs with the list received from the cluster. + + :param remote_jobs: The print jobs received from the cluster. + """ self._responseReceived() # Keep track of the new print jobs to show. @@ -321,9 +334,11 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): self._print_jobs = new_print_jobs self.printJobsChanged.emit() - ## Create a new print job model based on the remote status of the job. - # \param remote_job: The remote print job data. def _createPrintJobModel(self, remote_job: ClusterPrintJobStatus) -> UM3PrintJobOutputModel: + """Create a new print job model based on the remote status of the job. + + :param remote_job: The remote print job data. + """ model = remote_job.createOutputModel(ClusterOutputController(self)) if remote_job.printer_uuid: self._updateAssignedPrinter(model, remote_job.printer_uuid) @@ -333,16 +348,18 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): model.loadPreviewImageFromUrl(remote_job.preview_url) return model - ## Updates the printer assignment for the given print job model. def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None: + """Updates the printer assignment for the given print job model.""" + printer = next((p for p in self._printers if printer_uuid == p.key), None) if not printer: return printer.updateActivePrintJob(model) model.updateAssignedPrinter(printer) - ## Load Monitor tab QML. def _loadMonitorTab(self) -> None: + """Load Monitor tab QML.""" + plugin_registry = CuraApplication.getInstance().getPluginRegistry() if not plugin_registry: Logger.log("e", "Could not get plugin registry") From 6035adb96356344ef43ff54f5f2d6d9f8271a84d Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Wed, 27 May 2020 14:24:28 +0200 Subject: [PATCH 20/27] Fix return type in GCodeProfileReader --- plugins/GCodeProfileReader/GCodeProfileReader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/GCodeProfileReader/GCodeProfileReader.py b/plugins/GCodeProfileReader/GCodeProfileReader.py index 2b80e80085..93bba1db4d 100644 --- a/plugins/GCodeProfileReader/GCodeProfileReader.py +++ b/plugins/GCodeProfileReader/GCodeProfileReader.py @@ -3,6 +3,7 @@ import re #Regular expressions for parsing escape characters in the settings. import json +from typing import Optional from UM.Settings.ContainerFormatError import ContainerFormatError from UM.Settings.InstanceContainer import InstanceContainer @@ -117,7 +118,7 @@ def unescapeGcodeComment(string: str) -> str: return pattern.sub(lambda m: GCodeProfileReader.escape_characters[re.escape(m.group(0))], string) -def readQualityProfileFromString(profile_string) -> InstanceContainer: +def readQualityProfileFromString(profile_string) -> Optional[InstanceContainer]: """Read in a profile from a serialized string. :param profile_string: The profile data in serialized form. From 5af9faf5e52dc3024c4b9253a39ee49a1eff8cb3 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 28 May 2020 16:08:09 +0200 Subject: [PATCH 21/27] Convert doxygen to rst for X3DReader, XmlMaterialProfile, XRayView --- plugins/XRayView/XRayView.py | 3 +- .../XmlMaterialProfile/XmlMaterialProfile.py | 82 +++++++++++-------- .../XmlMaterialValidator.py | 7 +- 3 files changed, 56 insertions(+), 36 deletions(-) diff --git a/plugins/XRayView/XRayView.py b/plugins/XRayView/XRayView.py index 0144f4c176..be4fe5ea76 100644 --- a/plugins/XRayView/XRayView.py +++ b/plugins/XRayView/XRayView.py @@ -23,8 +23,9 @@ from cura.Scene.ConvexHullNode import ConvexHullNode from cura import XRayPass -## View used to display a see-through version of objects with errors highlighted. class XRayView(CuraView): + """View used to display a see-through version of objects with errors highlighted.""" + def __init__(self): super().__init__(parent = None, use_empty_menu_placeholder = True) diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 4e0d1e09f5..0b21b57fef 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -25,8 +25,9 @@ except (ImportError, SystemError): import XmlMaterialValidator # type: ignore # This fixes the tests not being able to import. -## Handles serializing and deserializing material containers from an XML file class XmlMaterialProfile(InstanceContainer): + """Handles serializing and deserializing material containers from an XML file""" + CurrentFdmMaterialVersion = "1.3" Version = 1 @@ -34,17 +35,18 @@ class XmlMaterialProfile(InstanceContainer): super().__init__(container_id, *args, **kwargs) self._inherited_files = [] - ## Translates the version number in the XML files to the setting_version - # metadata entry. - # - # Since the two may increment independently we need a way to say which - # versions of the XML specification are compatible with our setting data - # version numbers. - # - # \param xml_version: The version number found in an XML file. - # \return The corresponding setting_version. @staticmethod def xmlVersionToSettingVersion(xml_version: str) -> int: + """Translates the version number in the XML files to the setting_version metadata entry. + + Since the two may increment independently we need a way to say which + versions of the XML specification are compatible with our setting data + version numbers. + + :param xml_version: The version number found in an XML file. + :return: The corresponding setting_version. + """ + if xml_version == "1.3": return CuraApplication.SettingVersion return 0 # Older than 1.3. @@ -52,15 +54,17 @@ class XmlMaterialProfile(InstanceContainer): def getInheritedFiles(self): return self._inherited_files - ## Overridden from InstanceContainer - # set the meta data for all machine / variant combinations - # - # The "apply_to_all" flag indicates whether this piece of metadata should be applied to all material containers - # or just this specific container. - # For example, when you change the material name, you want to apply it to all its derived containers, but for - # some specific settings, they should only be applied to a machine/variant-specific container. - # def setMetaDataEntry(self, key, value, apply_to_all = True): + """set the meta data for all machine / variant combinations + + The "apply_to_all" flag indicates whether this piece of metadata should be applied to all material containers + or just this specific container. + For example, when you change the material name, you want to apply it to all its derived containers, but for + some specific settings, they should only be applied to a machine/variant-specific container. + + Overridden from InstanceContainer + """ + registry = ContainerRegistry.getInstance() if registry.isReadOnly(self.getId()): Logger.log("w", "Can't change metadata {key} of material {material_id} because it's read-only.".format(key = key, material_id = self.getId())) @@ -89,10 +93,13 @@ class XmlMaterialProfile(InstanceContainer): for k, v in new_setting_values_dict.items(): self.setProperty(k, "value", v) - ## Overridden from InstanceContainer, similar to setMetaDataEntry. - # without this function the setName would only set the name of the specific nozzle / material / machine combination container - # The function is a bit tricky. It will not set the name of all containers if it has the correct name itself. def setName(self, new_name): + """Overridden from InstanceContainer, similar to setMetaDataEntry. + + without this function the setName would only set the name of the specific nozzle / material / machine combination container + The function is a bit tricky. It will not set the name of all containers if it has the correct name itself. + """ + registry = ContainerRegistry.getInstance() if registry.isReadOnly(self.getId()): return @@ -110,8 +117,9 @@ class XmlMaterialProfile(InstanceContainer): for container in containers: container.setName(new_name) - ## Overridden from InstanceContainer, to set dirty to base file as well. def setDirty(self, dirty): + """Overridden from InstanceContainer, to set dirty to base file as well.""" + super().setDirty(dirty) base_file = self.getMetaDataEntry("base_file", None) registry = ContainerRegistry.getInstance() @@ -120,10 +128,12 @@ class XmlMaterialProfile(InstanceContainer): if containers: containers[0].setDirty(dirty) - ## Overridden from InstanceContainer - # base file: common settings + supported machines - # machine / variant combination: only changes for itself. def serialize(self, ignored_metadata_keys: Optional[Set[str]] = None): + """Overridden from InstanceContainer + + base file: common settings + supported machines + machine / variant combination: only changes for itself. + """ registry = ContainerRegistry.getInstance() base_file = self.getMetaDataEntry("base_file", "") @@ -467,8 +477,9 @@ class XmlMaterialProfile(InstanceContainer): return version * 1000000 + setting_version - ## Overridden from InstanceContainer def deserialize(self, serialized, file_name = None): + """Overridden from InstanceContainer""" + containers_to_add = [] # update the serialized data first from UM.Settings.Interfaces import ContainerInterface @@ -1061,12 +1072,13 @@ class XmlMaterialProfile(InstanceContainer): id_list = list(id_list) return id_list - ## Gets a mapping from product names in the XML files to their definition - # IDs. - # - # This loads the mapping from a file. @classmethod def getProductIdMap(cls) -> Dict[str, List[str]]: + """Gets a mapping from product names in the XML files to their definition IDs. + + This loads the mapping from a file. + """ + plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath("XmlMaterialProfile")) product_to_id_file = os.path.join(plugin_path, "product_to_id.json") with open(product_to_id_file, encoding = "utf-8") as f: @@ -1076,13 +1088,15 @@ class XmlMaterialProfile(InstanceContainer): #However it is not always loaded with that default; this mapping is also used in serialize() without that default. return product_to_id_map - ## Parse the value of the "material compatible" property. @staticmethod def _parseCompatibleValue(value: str): + """Parse the value of the "material compatible" property.""" + return value in {"yes", "unknown"} - ## Small string representation for debugging. def __str__(self): + """Small string representation for debugging.""" + return "".format(my_id = self.getId(), name = self.getName(), base_file = self.getMetaDataEntry("base_file")) _metadata_tags_that_have_cura_namespace = {"pva_compatible", "breakaway_compatible"} @@ -1132,8 +1146,10 @@ class XmlMaterialProfile(InstanceContainer): "cura": "http://www.ultimaker.com/cura" } -## Helper function for pretty-printing XML because ETree is stupid + def _indent(elem, level = 0): + """Helper function for pretty-printing XML because ETree is stupid""" + i = "\n" + level * " " if len(elem): if not elem.text or not elem.text.strip(): diff --git a/plugins/XmlMaterialProfile/XmlMaterialValidator.py b/plugins/XmlMaterialProfile/XmlMaterialValidator.py index a23022854b..c191577a88 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialValidator.py +++ b/plugins/XmlMaterialProfile/XmlMaterialValidator.py @@ -3,11 +3,14 @@ from typing import Any, Dict -## Makes sure that the required metadata is present for a material. + class XmlMaterialValidator: - ## Makes sure that the required metadata is present for a material. + """Makes sure that the required metadata is present for a material.""" + @classmethod def validateMaterialMetaData(cls, validation_metadata: Dict[str, Any]): + """Makes sure that the required metadata is present for a material.""" + if validation_metadata.get("GUID") is None: return "Missing GUID" From bb2a176e36dba2d5963f09e539986da3ae2ec5ec Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 28 May 2020 16:21:49 +0200 Subject: [PATCH 22/27] Convert doxygen to rst for VersionUpgrades 40+ --- .../VersionUpgrade35to40.py | 7 ++--- .../VersionUpgrade40to41.py | 16 +++++++----- .../VersionUpgrade41to42.py | 26 ++++++++++++------- .../VersionUpgrade42to43.py | 17 +++++++----- .../VersionUpgrade43to44.py | 19 ++++++++------ .../VersionUpgrade44to45.py | 19 ++++++++------ 6 files changed, 62 insertions(+), 42 deletions(-) diff --git a/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py b/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py index fdca7d15df..85a7e3135a 100644 --- a/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py +++ b/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py @@ -34,8 +34,9 @@ class VersionUpgrade35to40(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades Preferences to have the new version number. def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades Preferences to have the new version number.""" + parser = configparser.ConfigParser(interpolation=None) parser.read_string(serialized) @@ -48,9 +49,9 @@ class VersionUpgrade35to40(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades instance containers to have the new version - # number. def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number.""" + parser = configparser.ConfigParser(interpolation=None) parser.read_string(serialized) diff --git a/plugins/VersionUpgrade/VersionUpgrade40to41/VersionUpgrade40to41.py b/plugins/VersionUpgrade/VersionUpgrade40to41/VersionUpgrade40to41.py index 1cccbef7da..c1df23e040 100644 --- a/plugins/VersionUpgrade/VersionUpgrade40to41/VersionUpgrade40to41.py +++ b/plugins/VersionUpgrade/VersionUpgrade40to41/VersionUpgrade40to41.py @@ -20,12 +20,14 @@ _renamed_quality_profiles = { } # type: Dict[str, str] -## Upgrades configurations from the state they were in at version 4.0 to the -# state they should be in at version 4.1. class VersionUpgrade40to41(VersionUpgrade): - ## Upgrades instance containers to have the new version - # number. + """Upgrades configurations from the state they were in at version 4.0 to the + state they should be in at version 4.1. + """ + def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -46,8 +48,9 @@ class VersionUpgrade40to41(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades Preferences to have the new version number. def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades Preferences to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -66,8 +69,9 @@ class VersionUpgrade40to41(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades stacks to have the new version number. def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades stacks to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) diff --git a/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py b/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py index 43d8561567..39d02c078c 100644 --- a/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py +++ b/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py @@ -214,14 +214,17 @@ _creality_limited_quality_type = { } -## Upgrades configurations from the state they were in at version 4.1 to the -# state they should be in at version 4.2. class VersionUpgrade41to42(VersionUpgrade): - ## Upgrades instance containers to have the new version - # number. - # - # This renames the renamed settings in the containers. + """Upgrades configurations from the state they were in at version 4.1 to the + + state they should be in at version 4.2. + """ + def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number. + + This renames the renamed settings in the containers. + """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) parser.read_string(serialized) @@ -257,10 +260,12 @@ class VersionUpgrade41to42(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades Preferences to have the new version number. - # - # This renames the renamed settings in the list of visible settings. def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades Preferences to have the new version number. + + This renames the renamed settings in the list of visible settings. + """ + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -284,8 +289,9 @@ class VersionUpgrade41to42(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades stacks to have the new version number. def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades stacks to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) diff --git a/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py b/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py index b139ede83c..7359e88179 100644 --- a/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py +++ b/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py @@ -57,9 +57,11 @@ _renamed_settings = { } # type: Dict[str, str] -## Upgrades configurations from the state they were in at version 4.2 to the -# state they should be in at version 4.3. class VersionUpgrade42to43(VersionUpgrade): + """Upgrades configurations from the state they were in at version 4.2 to the + + state they should be in at version 4.3. + """ def upgradePreferences(self, serialized: str, filename: str): parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -82,11 +84,11 @@ class VersionUpgrade42to43(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades instance containers to have the new version - # number. - # - # This renames the renamed settings in the containers. def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number. + + This renames the renamed settings in the containers. + """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) parser.read_string(serialized) @@ -111,8 +113,9 @@ class VersionUpgrade42to43(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades stacks to have the new version number. def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades stacks to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) diff --git a/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py b/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py index b5825af62e..249f55a42c 100644 --- a/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py +++ b/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py @@ -27,10 +27,12 @@ _renamed_container_id_map = { class VersionUpgrade43to44(VersionUpgrade): - ## Upgrades Preferences to have the new version number. - # - # This renames the renamed settings in the list of visible settings. def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades Preferences to have the new version number. + + This renames the renamed settings in the list of visible settings. + """ + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -41,11 +43,11 @@ class VersionUpgrade43to44(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades instance containers to have the new version - # number. - # - # This renames the renamed settings in the containers. def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number. + + This renames the renamed settings in the containers. + """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) parser.read_string(serialized) @@ -72,8 +74,9 @@ class VersionUpgrade43to44(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades stacks to have the new version number. def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades stacks to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) diff --git a/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py b/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py index 0747ad280b..df4f95d74d 100644 --- a/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py +++ b/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py @@ -122,10 +122,12 @@ class VersionUpgrade44to45(VersionUpgrade): except OSError: # Is a directory, file not found, or insufficient rights. continue - ## Upgrades Preferences to have the new version number. - # - # This renames the renamed settings in the list of visible settings. def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades Preferences to have the new version number. + + This renames the renamed settings in the list of visible settings. + """ + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) @@ -136,11 +138,11 @@ class VersionUpgrade44to45(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades instance containers to have the new version - # number. - # - # This renames the renamed settings in the containers. def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades instance containers to have the new version number. + + This renames the renamed settings in the containers. + """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) parser.read_string(serialized) @@ -166,8 +168,9 @@ class VersionUpgrade44to45(VersionUpgrade): parser.write(result) return [filename], [result.getvalue()] - ## Upgrades stacks to have the new version number. def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: + """Upgrades stacks to have the new version number.""" + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) From fe779d95016333d99190034ece2869ec7e0b92a1 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 28 May 2020 16:25:26 +0200 Subject: [PATCH 23/27] Convert doxygen to rst for Cura scripts --- scripts/check_gcode_buffer.py | 61 ++++++++++++++++++++++------------- scripts/lionbridge_import.py | 52 ++++++++++++++++++----------- 2 files changed, 71 insertions(+), 42 deletions(-) diff --git a/scripts/check_gcode_buffer.py b/scripts/check_gcode_buffer.py index 2024ce2214..f35dd20c24 100755 --- a/scripts/check_gcode_buffer.py +++ b/scripts/check_gcode_buffer.py @@ -31,16 +31,19 @@ MACHINE_MAX_JERK_E = 5 MACHINE_MINIMUM_FEEDRATE = 0.001 MACHINE_ACCELERATION = 3000 -## Gets the code and number from the given g-code line. + def get_code_and_num(gcode_line: str) -> Tuple[str, str]: + """Gets the code and number from the given g-code line.""" + gcode_line = gcode_line.strip() cmd_code = gcode_line[0].upper() cmd_num = str(gcode_line[1:]) return cmd_code, cmd_num -## Fetches arguments such as X1 Y2 Z3 from the given part list and returns a -# dict. + def get_value_dict(parts: List[str]) -> Dict[str, str]: + """Fetches arguments such as X1 Y2 Z3 from the given part list and returns a dict""" + value_dict = {} for p in parts: p = p.strip() @@ -63,39 +66,49 @@ def calc_distance(pos1, pos2): distance = math.sqrt(distance) return distance -## Given the initial speed, the target speed, and the acceleration, calculate -# the distance that's neede for the acceleration to finish. + def calc_acceleration_distance(init_speed: float, target_speed: float, acceleration: float) -> float: + """Given the initial speed, the target speed, and the acceleration + + calculate the distance that's neede for the acceleration to finish. + """ if acceleration == 0: return 0.0 return (target_speed ** 2 - init_speed ** 2) / (2 * acceleration) -## Gives the time it needs to accelerate from an initial speed to reach a final -# distance. + def calc_acceleration_time_from_distance(initial_feedrate: float, distance: float, acceleration: float) -> float: + """Gives the time it needs to accelerate from an initial speed to reach a final distance.""" + discriminant = initial_feedrate ** 2 - 2 * acceleration * -distance #If the discriminant is negative, we're moving in the wrong direction. #Making the discriminant 0 then gives the extremum of the parabola instead of the intersection. discriminant = max(0, discriminant) return (-initial_feedrate + math.sqrt(discriminant)) / acceleration -## Calculates the point at which you must start braking. -# -# This gives the distance from the start of a line at which you must start -# decelerating (at a rate of `-acceleration`) if you started at speed -# `initial_feedrate` and accelerated until this point and want to end at the -# `final_feedrate` after a total travel of `distance`. This can be used to -# compute the intersection point between acceleration and deceleration in the -# cases where the trapezoid has no plateau (i.e. never reaches maximum speed). + def calc_intersection_distance(initial_feedrate: float, final_feedrate: float, acceleration: float, distance: float) -> float: + """Calculates the point at which you must start braking. + + This gives the distance from the start of a line at which you must start + decelerating (at a rate of `-acceleration`) if you started at speed + `initial_feedrate` and accelerated until this point and want to end at the + `final_feedrate` after a total travel of `distance`. This can be used to + compute the intersection point between acceleration and deceleration in the + cases where the trapezoid has no plateau (i.e. never reaches maximum speed). + """ + if acceleration == 0: return 0 return (2 * acceleration * distance - initial_feedrate * initial_feedrate + final_feedrate * final_feedrate) / (4 * acceleration) -## Calculates the maximum speed that is allowed at this point when you must be -# able to reach target_velocity using the acceleration within the allotted -# distance. + def calc_max_allowable_speed(acceleration: float, target_velocity: float, distance: float) -> float: + """Calculates the maximum speed that is allowed at this point when you must be + able to reach target_velocity using the acceleration within the allotted + distance. + """ + return math.sqrt(target_velocity * target_velocity - 2 * acceleration * distance) @@ -130,10 +143,12 @@ class Command: self._delta = [0, 0, 0] self._abs_delta = [0, 0, 0] - ## Calculate the velocity-time trapezoid function for this move. - # - # Each move has a three-part function mapping time to velocity. def calculate_trapezoid(self, entry_factor, exit_factor): + """Calculate the velocity-time trapezoid function for this move. + + Each move has a three-part function mapping time to velocity. + """ + initial_feedrate = self._nominal_feedrate * entry_factor final_feedrate = self._nominal_feedrate * exit_factor @@ -169,9 +184,9 @@ class Command: return self._cmd_str.strip() + " ; --- " + info + os.linesep - ## Estimates the execution time of this command and calculates the state - # after this command is executed. def parse(self) -> None: + """Estimates the execution time of this command and calculates the state after this command is executed.""" + line = self._cmd_str.strip() if not line: self._is_empty = True diff --git a/scripts/lionbridge_import.py b/scripts/lionbridge_import.py index 83f53403ea..0bd6c5a8af 100644 --- a/scripts/lionbridge_import.py +++ b/scripts/lionbridge_import.py @@ -9,14 +9,16 @@ import os.path #To find files from the source and the destination path. cura_files = {"cura", "fdmprinter.def.json", "fdmextruder.def.json"} uranium_files = {"uranium"} -## Imports translation files from Lionbridge. -# -# Lionbridge has a bit of a weird export feature. It exports it to the same -# file type as what we imported, so that's a .pot file. However this .pot file -# only contains the translations, so the header is completely empty. We need -# to merge those translations into our existing files so that the header is -# preserved. def lionbridge_import(source: str) -> None: + """Imports translation files from Lionbridge. + + Lionbridge has a bit of a weird export feature. It exports it to the same + file type as what we imported, so that's a .pot file. However this .pot file + only contains the translations, so the header is completely empty. We need + to merge those translations into our existing files so that the header is + preserved. + """ + print("Importing from:", source) print("Importing to Cura:", destination_cura()) print("Importing to Uranium:", destination_uranium()) @@ -43,14 +45,20 @@ def lionbridge_import(source: str) -> None: with io.open(destination_file, "w", encoding = "utf8") as f: f.write(result) -## Gets the destination path to copy the translations for Cura to. -# \return Destination path for Cura. + def destination_cura() -> str: + """Gets the destination path to copy the translations for Cura to. + + :return: Destination path for Cura. + """ return os.path.abspath(os.path.join(__file__, "..", "..", "resources", "i18n")) -## Gets the destination path to copy the translations for Uranium to. -# \return Destination path for Uranium. + def destination_uranium() -> str: + """Gets the destination path to copy the translations for Uranium to. + + :return: Destination path for Uranium. + """ try: import UM except ImportError: @@ -64,11 +72,14 @@ def destination_uranium() -> str: raise Exception("Can't find Uranium. Please put UM on the PYTHONPATH or put the Uranium folder next to the Cura folder. Looked for: " + absolute_path) return os.path.abspath(os.path.join(UM.__file__, "..", "..", "resources", "i18n")) -## Merges translations from the source file into the destination file if they -# were missing in the destination file. -# \param source The contents of the source .po file. -# \param destination The contents of the destination .po file. + def merge(source: str, destination: str) -> str: + """Merges translations from the source file into the destination file if they + + were missing in the destination file. + :param source: The contents of the source .po file. + :param destination: The contents of the destination .po file. + """ result_lines = [] last_destination = { "msgctxt": "\"\"\n", @@ -119,11 +130,14 @@ def merge(source: str, destination: str) -> str: result_lines.append(line) #This line itself. return "\n".join(result_lines) -## Finds a translation in the source file. -# \param source The contents of the source .po file. -# \param msgctxt The ctxt of the translation to find. -# \param msgid The id of the translation to find. + def find_translation(source: str, msgctxt: str, msgid: str) -> str: + """Finds a translation in the source file. + + :param source: The contents of the source .po file. + :param msgctxt: The ctxt of the translation to find. + :param msgid: The id of the translation to find. + """ last_source = { "msgctxt": "\"\"\n", "msgid": "\"\"\n", From c2c96faf5fcbad942f8cf257e75c94a623ac5eaa Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 28 May 2020 17:13:44 +0200 Subject: [PATCH 24/27] Convert remaining doxygen to rst --- cmake/mod_bundled_packages_json.py | 22 +- cura/Operations/PlatformPhysicsOperation.py | 3 +- .../SetBuildPlateNumberOperation.py | 3 +- cura/Operations/SetParentOperation.py | 36 +- cura/PrinterOutput/FirmwareUpdater.py | 3 +- .../Models/ExtruderConfigurationModel.py | 9 +- .../Models/ExtruderOutputModel.py | 18 +- .../Models/PrinterConfigurationModel.py | 12 +- .../Models/PrinterOutputModel.py | 18 +- cura/PrinterOutput/NetworkMJPGImage.py | 3 +- .../NetworkedPrinterOutputDevice.py | 82 +- cura/PrinterOutput/Peripheral.py | 20 +- cura/PrinterOutput/PrinterOutputDevice.py | 51 +- cura/ReaderWriters/ProfileReader.py | 16 +- cura/ReaderWriters/ProfileWriter.py | 34 +- cura/Scene/BuildPlateDecorator.py | 3 +- cura/Scene/ConvexHullDecorator.py | 89 +- cura/Scene/ConvexHullNode.py | 10 +- cura/Scene/CuraSceneController.py | 3 +- cura/Scene/CuraSceneNode.py | 24 +- cura/Scene/ZOffsetDecorator.py | 3 +- cura/Settings/ContainerManager.py | 136 +- cura/Settings/CuraContainerRegistry.py | 1549 +++++++++-------- cura/Settings/CuraContainerStack.py | 284 +-- cura/Settings/CuraStackBuilder.py | 79 +- cura/Settings/Exceptions.py | 12 +- cura/Settings/ExtruderManager.py | 160 +- cura/Settings/ExtruderStack.py | 61 +- cura/Settings/GlobalStack.py | 91 +- cura/Settings/IntentManager.py | 88 +- cura/Settings/MachineManager.py | 194 ++- cura/Settings/MachineNameValidator.py | 23 +- cura/Settings/SetObjectExtruderOperation.py | 4 +- cura/Settings/SettingInheritanceManager.py | 14 +- cura/Settings/SettingOverrideDecorator.py | 65 +- cura/UI/MachineActionManager.py | 66 +- cura/UI/ObjectsModel.py | 3 +- cura/UI/PrintInformation.py | 24 +- cura/Utils/Decorators.py | 14 +- tests/Machines/TestMachineNode.py | 31 +- tests/Machines/TestQualityNode.py | 2 +- tests/Machines/TestVariantNode.py | 41 +- tests/Settings/MockContainer.py | 122 +- tests/Settings/TestCuraContainerRegistry.py | 6 +- tests/Settings/TestDefinitionContainer.py | 71 +- tests/Settings/TestExtruderStack.py | 52 +- tests/Settings/TestGlobalStack.py | 73 +- tests/Settings/TestProfiles.py | 6 +- tests/TestArrange.py | 87 +- 49 files changed, 2163 insertions(+), 1657 deletions(-) diff --git a/cmake/mod_bundled_packages_json.py b/cmake/mod_bundled_packages_json.py index 6423591f57..66038b30e2 100755 --- a/cmake/mod_bundled_packages_json.py +++ b/cmake/mod_bundled_packages_json.py @@ -11,11 +11,13 @@ import os import sys -## Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths. -# -# \param work_dir The directory to look for JSON files recursively. -# \return A list of JSON files in absolute paths that are found in the given directory. def find_json_files(work_dir: str) -> list: + """Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths. + + :param work_dir: The directory to look for JSON files recursively. + :return: A list of JSON files in absolute paths that are found in the given directory. + """ + json_file_list = [] for root, dir_names, file_names in os.walk(work_dir): for file_name in file_names: @@ -24,12 +26,14 @@ def find_json_files(work_dir: str) -> list: return json_file_list -## Removes the given entries from the given JSON file. The file will modified in-place. -# -# \param file_path The JSON file to modify. -# \param entries A list of strings as entries to remove. -# \return None def remove_entries_from_json_file(file_path: str, entries: list) -> None: + """Removes the given entries from the given JSON file. The file will modified in-place. + + :param file_path: The JSON file to modify. + :param entries: A list of strings as entries to remove. + :return: None + """ + try: with open(file_path, "r", encoding = "utf-8") as f: package_dict = json.load(f, object_hook = collections.OrderedDict) diff --git a/cura/Operations/PlatformPhysicsOperation.py b/cura/Operations/PlatformPhysicsOperation.py index 0d69320eec..e433b67a7b 100644 --- a/cura/Operations/PlatformPhysicsOperation.py +++ b/cura/Operations/PlatformPhysicsOperation.py @@ -6,8 +6,9 @@ from UM.Operations.GroupedOperation import GroupedOperation from UM.Scene.SceneNode import SceneNode -## A specialised operation designed specifically to modify the previous operation. class PlatformPhysicsOperation(Operation): + """A specialised operation designed specifically to modify the previous operation.""" + def __init__(self, node: SceneNode, translation: Vector) -> None: super().__init__() self._node = node diff --git a/cura/Operations/SetBuildPlateNumberOperation.py b/cura/Operations/SetBuildPlateNumberOperation.py index fd48cf47d9..8a5bdeb442 100644 --- a/cura/Operations/SetBuildPlateNumberOperation.py +++ b/cura/Operations/SetBuildPlateNumberOperation.py @@ -7,8 +7,9 @@ from UM.Operations.Operation import Operation from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -## Simple operation to set the buildplate number of a scenenode. class SetBuildPlateNumberOperation(Operation): + """Simple operation to set the buildplate number of a scenenode.""" + def __init__(self, node: SceneNode, build_plate_nr: int) -> None: super().__init__() self._node = node diff --git a/cura/Operations/SetParentOperation.py b/cura/Operations/SetParentOperation.py index 6d603c1d82..52bef4aabc 100644 --- a/cura/Operations/SetParentOperation.py +++ b/cura/Operations/SetParentOperation.py @@ -6,31 +6,37 @@ from UM.Scene.SceneNode import SceneNode from UM.Operations import Operation - -## An operation that parents a scene node to another scene node. class SetParentOperation(Operation.Operation): - ## Initialises this SetParentOperation. - # - # \param node The node which will be reparented. - # \param parent_node The node which will be the parent. + """An operation that parents a scene node to another scene node.""" + def __init__(self, node: SceneNode, parent_node: Optional[SceneNode]) -> None: + """Initialises this SetParentOperation. + + :param node: The node which will be reparented. + :param parent_node: The node which will be the parent. + """ + super().__init__() self._node = node self._parent = parent_node self._old_parent = node.getParent() # To restore the previous parent in case of an undo. - ## Undoes the set-parent operation, restoring the old parent. def undo(self) -> None: + """Undoes the set-parent operation, restoring the old parent.""" + self._set_parent(self._old_parent) - ## Re-applies the set-parent operation. def redo(self) -> None: + """Re-applies the set-parent operation.""" + self._set_parent(self._parent) - ## Sets the parent of the node while applying transformations to the world-transform of the node stays the same. - # - # \param new_parent The new parent. Note: this argument can be None, which would hide the node from the scene. def _set_parent(self, new_parent: Optional[SceneNode]) -> None: + """Sets the parent of the node while applying transformations to the world-transform of the node stays the same. + + :param new_parent: The new parent. Note: this argument can be None, which would hide the node from the scene. + """ + if new_parent: current_parent = self._node.getParent() if current_parent: @@ -56,8 +62,10 @@ class SetParentOperation(Operation.Operation): self._node.setParent(new_parent) - ## Returns a programmer-readable representation of this operation. - # - # \return A programmer-readable representation of this operation. def __repr__(self) -> str: + """Returns a programmer-readable representation of this operation. + + :return: A programmer-readable representation of this operation. + """ + return "SetParentOperation(node = {0}, parent_node={1})".format(self._node, self._parent) diff --git a/cura/PrinterOutput/FirmwareUpdater.py b/cura/PrinterOutput/FirmwareUpdater.py index 80269b97a3..2794bf5c65 100644 --- a/cura/PrinterOutput/FirmwareUpdater.py +++ b/cura/PrinterOutput/FirmwareUpdater.py @@ -44,8 +44,9 @@ class FirmwareUpdater(QObject): def _updateFirmware(self) -> None: raise NotImplementedError("_updateFirmware needs to be implemented") - ## Cleanup after a succesful update def _cleanupAfterUpdate(self) -> None: + """Cleanup after a succesful update""" + # Clean up for next attempt. self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True, name = "FirmwareUpdateThread") self._firmware_file = "" diff --git a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py index 4a1cf4916f..b80a652163 100644 --- a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py +++ b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py @@ -47,10 +47,13 @@ class ExtruderConfigurationModel(QObject): def hotendID(self) -> Optional[str]: return self._hotend_id - ## This method is intended to indicate whether the configuration is valid or not. - # The method checks if the mandatory fields are or not set - # At this moment is always valid since we allow to have empty material and variants. def isValid(self) -> bool: + """This method is intended to indicate whether the configuration is valid or not. + + The method checks if the mandatory fields are or not set + At this moment is always valid since we allow to have empty material and variants. + """ + return True def __str__(self) -> str: diff --git a/cura/PrinterOutput/Models/ExtruderOutputModel.py b/cura/PrinterOutput/Models/ExtruderOutputModel.py index 889e140312..9e74e9520c 100644 --- a/cura/PrinterOutput/Models/ExtruderOutputModel.py +++ b/cura/PrinterOutput/Models/ExtruderOutputModel.py @@ -54,8 +54,9 @@ class ExtruderOutputModel(QObject): def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]) -> None: self._extruder_configuration.setMaterial(material) - ## Update the hotend temperature. This only changes it locally. def updateHotendTemperature(self, temperature: float) -> None: + """Update the hotend temperature. This only changes it locally.""" + if self._hotend_temperature != temperature: self._hotend_temperature = temperature self.hotendTemperatureChanged.emit() @@ -65,9 +66,10 @@ class ExtruderOutputModel(QObject): self._target_hotend_temperature = temperature self.targetHotendTemperatureChanged.emit() - ## Set the target hotend temperature. This ensures that it's actually sent to the remote. @pyqtSlot(float) def setTargetHotendTemperature(self, temperature: float) -> None: + """Set the target hotend temperature. This ensures that it's actually sent to the remote.""" + self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature) self.updateTargetHotendTemperature(temperature) @@ -101,13 +103,15 @@ class ExtruderOutputModel(QObject): def isPreheating(self) -> bool: return self._is_preheating - ## Pre-heats the extruder before printer. - # - # \param temperature The temperature to heat the extruder to, in degrees - # Celsius. - # \param duration How long the bed should stay warm, in seconds. @pyqtSlot(float, float) def preheatHotend(self, temperature: float, duration: float) -> None: + """Pre-heats the extruder before printer. + + :param temperature: The temperature to heat the extruder to, in degrees + Celsius. + :param duration: How long the bed should stay warm, in seconds. + """ + self._printer._controller.preheatHotend(self, temperature, duration) @pyqtSlot() diff --git a/cura/PrinterOutput/Models/PrinterConfigurationModel.py b/cura/PrinterOutput/Models/PrinterConfigurationModel.py index 52c7b6f960..c5aa949ff3 100644 --- a/cura/PrinterOutput/Models/PrinterConfigurationModel.py +++ b/cura/PrinterOutput/Models/PrinterConfigurationModel.py @@ -48,9 +48,11 @@ class PrinterConfigurationModel(QObject): def buildplateConfiguration(self) -> str: return self._buildplate_configuration - ## This method is intended to indicate whether the configuration is valid or not. - # The method checks if the mandatory fields are or not set def isValid(self) -> bool: + """This method is intended to indicate whether the configuration is valid or not. + + The method checks if the mandatory fields are or not set + """ if not self._extruder_configurations: return False for configuration in self._extruder_configurations: @@ -97,9 +99,11 @@ class PrinterConfigurationModel(QObject): return True - ## The hash function is used to compare and create unique sets. The configuration is unique if the configuration - # of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same. def __hash__(self): + """The hash function is used to compare and create unique sets. The configuration is unique if the configuration + + of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same. + """ extruder_hash = hash(0) first_extruder = None for configuration in self._extruder_configurations: diff --git a/cura/PrinterOutput/Models/PrinterOutputModel.py b/cura/PrinterOutput/Models/PrinterOutputModel.py index 37135bf663..8b716a1958 100644 --- a/cura/PrinterOutput/Models/PrinterOutputModel.py +++ b/cura/PrinterOutput/Models/PrinterOutputModel.py @@ -163,13 +163,15 @@ class PrinterOutputModel(QObject): def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None: self._controller.moveHead(self, x, y, z, speed) - ## Pre-heats the heated bed of the printer. - # - # \param temperature The temperature to heat the bed to, in degrees - # Celsius. - # \param duration How long the bed should stay warm, in seconds. @pyqtSlot(float, float) def preheatBed(self, temperature: float, duration: float) -> None: + """Pre-heats the heated bed of the printer. + + :param temperature: The temperature to heat the bed to, in degrees + Celsius. + :param duration: How long the bed should stay warm, in seconds. + """ + self._controller.preheatBed(self, temperature, duration) @pyqtSlot() @@ -200,8 +202,9 @@ class PrinterOutputModel(QObject): self._unique_name = unique_name self.nameChanged.emit() - ## Update the bed temperature. This only changes it locally. def updateBedTemperature(self, temperature: float) -> None: + """Update the bed temperature. This only changes it locally.""" + if self._bed_temperature != temperature: self._bed_temperature = temperature self.bedTemperatureChanged.emit() @@ -211,9 +214,10 @@ class PrinterOutputModel(QObject): self._target_bed_temperature = temperature self.targetBedTemperatureChanged.emit() - ## Set the target bed temperature. This ensures that it's actually sent to the remote. @pyqtSlot(float) def setTargetBedTemperature(self, temperature: float) -> None: + """Set the target bed temperature. This ensures that it's actually sent to the remote.""" + self._controller.setTargetBedTemperature(self, temperature) self.updateTargetBedTemperature(temperature) diff --git a/cura/PrinterOutput/NetworkMJPGImage.py b/cura/PrinterOutput/NetworkMJPGImage.py index 42132a7880..0bfcfab764 100644 --- a/cura/PrinterOutput/NetworkMJPGImage.py +++ b/cura/PrinterOutput/NetworkMJPGImage.py @@ -32,8 +32,9 @@ class NetworkMJPGImage(QQuickPaintedItem): self.setAntialiasing(True) - ## Ensure that close gets called when object is destroyed def __del__(self) -> None: + """Ensure that close gets called when object is destroyed""" + self.stop() diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 60be5bc8f3..e57e461dde 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -84,8 +84,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def _compressGCode(self) -> Optional[bytes]: self._compressing_gcode = True - ## Mash the data into single string max_chars_per_line = int(1024 * 1024 / 4) # 1/4 MB per line. + """Mash the data into single string""" file_data_bytes_list = [] batched_lines = [] batched_lines_count = 0 @@ -145,9 +145,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) return request - ## This method was only available privately before, but it was actually called from SendMaterialJob.py. - # We now have a public equivalent as well. We did not remove the private one as plugins might be using that. def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: + """This method was only available privately before, but it was actually called from SendMaterialJob.py. + + We now have a public equivalent as well. We did not remove the private one as plugins might be using that. + """ return self._createFormPart(content_header, data, content_type) def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: @@ -163,8 +165,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): part.setBody(data) return part - ## Convenience function to get the username, either from the cloud or from the OS. def _getUserName(self) -> str: + """Convenience function to get the username, either from the cloud or from the OS.""" + # check first if we are logged in with the Ultimaker Account account = CuraApplication.getInstance().getCuraAPI().account # type: Account if account and account.isLoggedIn: @@ -187,15 +190,17 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._createNetworkManager() assert (self._manager is not None) - ## Sends a put request to the given path. - # \param url: The path after the API prefix. - # \param data: The data to be sent in the body - # \param content_type: The content type of the body data. - # \param on_finished: The function to call when the response is received. - # \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. def put(self, url: str, data: Union[str, bytes], content_type: Optional[str] = "application/json", on_finished: Optional[Callable[[QNetworkReply], None]] = None, on_progress: Optional[Callable[[int, int], None]] = None) -> None: + """Sends a put request to the given path. + + :param url: The path after the API prefix. + :param data: The data to be sent in the body + :param content_type: The content type of the body data. + :param on_finished: The function to call when the response is received. + :param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. + """ self._validateManager() request = self._createEmptyRequest(url, content_type = content_type) @@ -212,10 +217,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if on_progress is not None: reply.uploadProgress.connect(on_progress) - ## Sends a delete request to the given path. - # \param url: The path after the API prefix. - # \param on_finished: The function to be call when the response is received. def delete(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: + """Sends a delete request to the given path. + + :param url: The path after the API prefix. + :param on_finished: The function to be call when the response is received. + """ self._validateManager() request = self._createEmptyRequest(url) @@ -228,10 +235,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): reply = self._manager.deleteResource(request) self._registerOnFinishedCallback(reply, on_finished) - ## Sends a get request to the given path. - # \param url: The path after the API prefix. - # \param on_finished: The function to be call when the response is received. def get(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: + """Sends a get request to the given path. + + :param url: The path after the API prefix. + :param on_finished: The function to be call when the response is received. + """ self._validateManager() request = self._createEmptyRequest(url) @@ -244,14 +253,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): reply = self._manager.get(request) self._registerOnFinishedCallback(reply, on_finished) - ## Sends a post request to the given path. - # \param url: The path after the API prefix. - # \param data: The data to be sent in the body - # \param on_finished: The function to call when the response is received. - # \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. def post(self, url: str, data: Union[str, bytes], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Optional[Callable[[int, int], None]] = None) -> None: + + """Sends a post request to the given path. + + :param url: The path after the API prefix. + :param data: The data to be sent in the body + :param on_finished: The function to call when the response is received. + :param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total. + """ + self._validateManager() request = self._createEmptyRequest(url) @@ -318,10 +331,13 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if on_finished is not None: self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished - ## This method checks if the name of the group stored in the definition container is correct. - # After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group - # then all the container stacks are updated, both the current and the hidden ones. def _checkCorrectGroupName(self, device_id: str, group_name: str) -> None: + """This method checks if the name of the group stored in the definition container is correct. + + After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group + then all the container stacks are updated, both the current and the hidden ones. + """ + global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() active_machine_network_name = CuraApplication.getInstance().getMachineManager().activeMachineNetworkKey() if global_container_stack and device_id == active_machine_network_name: @@ -366,32 +382,38 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def getProperties(self): return self._properties - ## Get the unique key of this machine - # \return key String containing the key of the machine. @pyqtProperty(str, constant = True) def key(self) -> str: + """Get the unique key of this machine + + :return: key String containing the key of the machine. + """ return self._id - ## The IP address of the printer. @pyqtProperty(str, constant = True) def address(self) -> str: + """The IP address of the printer.""" + return self._properties.get(b"address", b"").decode("utf-8") - ## Name of the printer (as returned from the ZeroConf properties) @pyqtProperty(str, constant = True) def name(self) -> str: + """Name of the printer (as returned from the ZeroConf properties)""" + return self._properties.get(b"name", b"").decode("utf-8") - ## Firmware version (as returned from the ZeroConf properties) @pyqtProperty(str, constant = True) def firmwareVersion(self) -> str: + """Firmware version (as returned from the ZeroConf properties)""" + return self._properties.get(b"firmware_version", b"").decode("utf-8") @pyqtProperty(str, constant = True) def printerType(self) -> str: return self._properties.get(b"printer_type", b"Unknown").decode("utf-8") - ## IP adress of this printer @pyqtProperty(str, constant = True) def ipAddress(self) -> str: + """IP adress of this printer""" + return self._address diff --git a/cura/PrinterOutput/Peripheral.py b/cura/PrinterOutput/Peripheral.py index 2693b82c36..e9a283ba2b 100644 --- a/cura/PrinterOutput/Peripheral.py +++ b/cura/PrinterOutput/Peripheral.py @@ -2,15 +2,19 @@ # Cura is released under the terms of the LGPLv3 or higher. -## Data class that represents a peripheral for a printer. -# -# Output device plug-ins may specify that the printer has a certain set of -# peripherals. This set is then possibly shown in the interface of the monitor -# stage. class Peripheral: - ## Constructs the peripheral. - # \param type A unique ID for the type of peripheral. - # \param name A human-readable name for the peripheral. + """Data class that represents a peripheral for a printer. + + Output device plug-ins may specify that the printer has a certain set of + peripherals. This set is then possibly shown in the interface of the monitor + stage. + """ + def __init__(self, peripheral_type: str, name: str) -> None: + """Constructs the peripheral. + + :param peripheral_type: A unique ID for the type of peripheral. + :param name: A human-readable name for the peripheral. + """ self.type = peripheral_type self.name = name diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index 0e0ad488b1..9b10e6abec 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -24,8 +24,9 @@ if MYPY: i18n_catalog = i18nCatalog("cura") -## The current processing state of the backend. class ConnectionState(IntEnum): + """The current processing state of the backend.""" + Closed = 0 Connecting = 1 Connected = 2 @@ -40,17 +41,19 @@ class ConnectionType(IntEnum): CloudConnection = 3 -## Printer output device adds extra interface options on top of output device. -# -# The assumption is made the printer is a FDM printer. -# -# Note that a number of settings are marked as "final". This is because decorators -# are not inherited by children. To fix this we use the private counter part of those -# functions to actually have the implementation. -# -# For all other uses it should be used in the same way as a "regular" OutputDevice. @signalemitter class PrinterOutputDevice(QObject, OutputDevice): + """Printer output device adds extra interface options on top of output device. + + The assumption is made the printer is a FDM printer. + + Note that a number of settings are marked as "final". This is because decorators + are not inherited by children. To fix this we use the private counter part of those + functions to actually have the implementation. + + For all other uses it should be used in the same way as a "regular" OutputDevice. + """ + printersChanged = pyqtSignal() connectionStateChanged = pyqtSignal(str) @@ -184,26 +187,30 @@ class PrinterOutputDevice(QObject, OutputDevice): if self._monitor_item is None: self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self}) - ## Attempt to establish connection def connect(self) -> None: + """Attempt to establish connection""" + self.setConnectionState(ConnectionState.Connecting) self._update_timer.start() - ## Attempt to close the connection def close(self) -> None: + """Attempt to close the connection""" + self._update_timer.stop() self.setConnectionState(ConnectionState.Closed) - ## Ensure that close gets called when object is destroyed def __del__(self) -> None: + """Ensure that close gets called when object is destroyed""" + self.close() @pyqtProperty(bool, notify = acceptsCommandsChanged) def acceptsCommands(self) -> bool: return self._accepts_commands - ## Set a flag to signal the UI that the printer is not (yet) ready to receive commands def _setAcceptsCommands(self, accepts_commands: bool) -> None: + """Set a flag to signal the UI that the printer is not (yet) ready to receive commands""" + if self._accepts_commands != accepts_commands: self._accepts_commands = accepts_commands @@ -241,16 +248,20 @@ class PrinterOutputDevice(QObject, OutputDevice): # At this point there may be non-updated configurations self._updateUniqueConfigurations() - ## Set the device firmware name - # - # \param name The name of the firmware. def _setFirmwareName(self, name: str) -> None: + """Set the device firmware name + + :param name: The name of the firmware. + """ + self._firmware_name = name - ## Get the name of device firmware - # - # This name can be used to define device type def getFirmwareName(self) -> Optional[str]: + """Get the name of device firmware + + This name can be used to define device type + """ + return self._firmware_name def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]: diff --git a/cura/ReaderWriters/ProfileReader.py b/cura/ReaderWriters/ProfileReader.py index 460fce823e..3d80411713 100644 --- a/cura/ReaderWriters/ProfileReader.py +++ b/cura/ReaderWriters/ProfileReader.py @@ -10,15 +10,19 @@ class NoProfileException(Exception): pass -## A type of plug-ins that reads profiles from a file. -# -# The profile is then stored as instance container of the type user profile. class ProfileReader(PluginObject): + """A type of plug-ins that reads profiles from a file. + + The profile is then stored as instance container of the type user profile. + """ + def __init__(self): super().__init__() - ## Read profile data from a file and return a filled profile. - # - # \return \type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles. def read(self, file_name): + """Read profile data from a file and return a filled profile. + + :return: :type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles. + """ + raise NotImplementedError("Profile reader plug-in was not correctly implemented. The read function was not implemented.") diff --git a/cura/ReaderWriters/ProfileWriter.py b/cura/ReaderWriters/ProfileWriter.py index 5f81dc28c3..0dd787335e 100644 --- a/cura/ReaderWriters/ProfileWriter.py +++ b/cura/ReaderWriters/ProfileWriter.py @@ -3,23 +3,29 @@ from UM.PluginObject import PluginObject -## Base class for profile writer plugins. -# -# This class defines a write() function to write profiles to files with. + class ProfileWriter(PluginObject): - ## Initialises the profile writer. - # - # This currently doesn't do anything since the writer is basically static. + """Base class for profile writer plugins. + + This class defines a write() function to write profiles to files with. + """ + def __init__(self): + """Initialises the profile writer. + + This currently doesn't do anything since the writer is basically static. + """ + super().__init__() - ## Writes a profile to the specified file path. - # - # The profile writer may write its own file format to the specified file. - # - # \param path \type{string} The file to output to. - # \param profiles \type{Profile} or \type{List} The profile(s) to write to the file. - # \return \code True \endcode if the writing was successful, or \code - # False \endcode if it wasn't. def write(self, path, profiles): + """Writes a profile to the specified file path. + + The profile writer may write its own file format to the specified file. + + :param path: :type{string} The file to output to. + :param profiles: :type{Profile} or :type{List} The profile(s) to write to the file. + :return: True if the writing was successful, or False if it wasn't. + """ + raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.") diff --git a/cura/Scene/BuildPlateDecorator.py b/cura/Scene/BuildPlateDecorator.py index cff9f88f62..9dd9d3dc24 100644 --- a/cura/Scene/BuildPlateDecorator.py +++ b/cura/Scene/BuildPlateDecorator.py @@ -2,8 +2,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from cura.Scene.CuraSceneNode import CuraSceneNode -## Make a SceneNode build plate aware CuraSceneNode objects all have this decorator. class BuildPlateDecorator(SceneNodeDecorator): + """Make a SceneNode build plate aware CuraSceneNode objects all have this decorator.""" + def __init__(self, build_plate_number: int = -1) -> None: super().__init__() self._build_plate_number = build_plate_number diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index b5f5fb4540..3dee409761 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -23,9 +23,12 @@ if TYPE_CHECKING: from UM.Math.Matrix import Matrix -## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. -# If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. class ConvexHullDecorator(SceneNodeDecorator): + """The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. + + If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. + """ + def __init__(self) -> None: super().__init__() @@ -74,13 +77,16 @@ class ConvexHullDecorator(SceneNodeDecorator): self._onChanged() - ## Force that a new (empty) object is created upon copy. def __deepcopy__(self, memo): + """Force that a new (empty) object is created upon copy.""" + return ConvexHullDecorator() - ## The polygon representing the 2D adhesion area. - # If no adhesion is used, the regular convex hull is returned def getAdhesionArea(self) -> Optional[Polygon]: + """The polygon representing the 2D adhesion area. + + If no adhesion is used, the regular convex hull is returned + """ if self._node is None: return None @@ -90,9 +96,11 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._add2DAdhesionMargin(hull) - ## Get the unmodified 2D projected convex hull of the node (if any) - # In case of one-at-a-time, this includes adhesion and head+fans clearance def getConvexHull(self) -> Optional[Polygon]: + """Get the unmodified 2D projected convex hull of the node (if any) + + In case of one-at-a-time, this includes adhesion and head+fans clearance + """ if self._node is None: return None if self._node.callDecoration("isNonPrintingMesh"): @@ -108,9 +116,11 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._compute2DConvexHull() - ## For one at the time this is the convex hull of the node with the full head size - # In case of printing all at once this is None. def getConvexHullHeadFull(self) -> Optional[Polygon]: + """For one at the time this is the convex hull of the node with the full head size + + In case of printing all at once this is None. + """ if self._node is None: return None @@ -126,10 +136,12 @@ class ConvexHullDecorator(SceneNodeDecorator): return False return bool(parent.callDecoration("isGroup")) - ## Get convex hull of the object + head size - # In case of printing all at once this is None. - # For one at the time this is area with intersection of mirrored head def getConvexHullHead(self) -> Optional[Polygon]: + """Get convex hull of the object + head size + + In case of printing all at once this is None. + For one at the time this is area with intersection of mirrored head + """ if self._node is None: return None if self._node.callDecoration("isNonPrintingMesh"): @@ -142,10 +154,12 @@ class ConvexHullDecorator(SceneNodeDecorator): return head_with_fans_with_adhesion_margin return None - ## Get convex hull of the node - # In case of printing all at once this None?? - # For one at the time this is the area without the head. def getConvexHullBoundary(self) -> Optional[Polygon]: + """Get convex hull of the node + + In case of printing all at once this None?? + For one at the time this is the area without the head. + """ if self._node is None: return None @@ -157,10 +171,12 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._compute2DConvexHull() return None - ## Get the buildplate polygon where will be printed - # In case of printing all at once this is the same as convex hull (no individual adhesion) - # For one at the time this includes the adhesion area def getPrintingArea(self) -> Optional[Polygon]: + """Get the buildplate polygon where will be printed + + In case of printing all at once this is the same as convex hull (no individual adhesion) + For one at the time this includes the adhesion area + """ if self._isSingularOneAtATimeNode(): # In one-at-a-time mode, every printed object gets it's own adhesion printing_area = self.getAdhesionArea() @@ -168,8 +184,9 @@ class ConvexHullDecorator(SceneNodeDecorator): printing_area = self.getConvexHull() return printing_area - ## The same as recomputeConvexHull, but using a timer if it was set. def recomputeConvexHullDelayed(self) -> None: + """The same as recomputeConvexHull, but using a timer if it was set.""" + if self._recompute_convex_hull_timer is not None: self._recompute_convex_hull_timer.start() else: @@ -325,9 +342,11 @@ class ConvexHullDecorator(SceneNodeDecorator): return convex_hull.getMinkowskiHull(head_and_fans) return None - ## Compensate given 2D polygon with adhesion margin - # \return 2D polygon with added margin def _add2DAdhesionMargin(self, poly: Polygon) -> Polygon: + """Compensate given 2D polygon with adhesion margin + + :return: 2D polygon with added margin + """ if not self._global_stack: return Polygon() # Compensate for raft/skirt/brim @@ -358,12 +377,14 @@ class ConvexHullDecorator(SceneNodeDecorator): poly = poly.getMinkowskiHull(extra_margin_polygon) return poly - ## Offset the convex hull with settings that influence the collision area. - # - # \param convex_hull Polygon of the original convex hull. - # \return New Polygon instance that is offset with everything that - # influences the collision area. def _offsetHull(self, convex_hull: Polygon) -> Polygon: + """Offset the convex hull with settings that influence the collision area. + + :param convex_hull: Polygon of the original convex hull. + :return: New Polygon instance that is offset with everything that + influences the collision area. + """ + horizontal_expansion = max( self._getSettingProperty("xy_offset", "value"), self._getSettingProperty("xy_offset_layer_0", "value") @@ -409,8 +430,9 @@ class ConvexHullDecorator(SceneNodeDecorator): self._onChanged() - ## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property). def _getSettingProperty(self, setting_key: str, prop: str = "value") -> Any: + """Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).""" + if self._global_stack is None or self._node is None: return None per_mesh_stack = self._node.callDecoration("getStack") @@ -430,16 +452,18 @@ class ConvexHullDecorator(SceneNodeDecorator): # Limit_to_extruder is set. The global stack handles this then return self._global_stack.getProperty(setting_key, prop) - ## Returns True if node is a descendant or the same as the root node. def __isDescendant(self, root: "SceneNode", node: Optional["SceneNode"]) -> bool: + """Returns True if node is a descendant or the same as the root node.""" + if node is None: return False if root is node: return True return self.__isDescendant(root, node.getParent()) - ## True if print_sequence is one_at_a_time and _node is not part of a group def _isSingularOneAtATimeNode(self) -> bool: + """True if print_sequence is one_at_a_time and _node is not part of a group""" + if self._node is None: return False return self._global_stack is not None \ @@ -450,7 +474,8 @@ class ConvexHullDecorator(SceneNodeDecorator): "adhesion_type", "raft_margin", "print_sequence", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "skirt_distance", "brim_line_count"] - ## Settings that change the convex hull. - # - # If these settings change, the convex hull should be recalculated. _influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh"} + """Settings that change the convex hull. + + If these settings change, the convex hull should be recalculated. + """ diff --git a/cura/Scene/ConvexHullNode.py b/cura/Scene/ConvexHullNode.py index da2713a522..cd0951cba6 100644 --- a/cura/Scene/ConvexHullNode.py +++ b/cura/Scene/ConvexHullNode.py @@ -18,11 +18,13 @@ if TYPE_CHECKING: class ConvexHullNode(SceneNode): shader = None # To prevent the shader from being re-built over and over again, only load it once. - ## Convex hull node is a special type of scene node that is used to display an area, to indicate the - # location an object uses on the buildplate. This area (or area's in case of one at a time printing) is - # then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded - # to represent the raft as well. def __init__(self, node: SceneNode, hull: Optional[Polygon], thickness: float, parent: Optional[SceneNode] = None) -> None: + """Convex hull node is a special type of scene node that is used to display an area, to indicate the + + location an object uses on the buildplate. This area (or area's in case of one at a time printing) is + then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded + to represent the raft as well. + """ super().__init__(parent) self.setCalculateBoundingBox(False) diff --git a/cura/Scene/CuraSceneController.py b/cura/Scene/CuraSceneController.py index 36d9e68c8f..2fd05db87a 100644 --- a/cura/Scene/CuraSceneController.py +++ b/cura/Scene/CuraSceneController.py @@ -72,9 +72,10 @@ class CuraSceneController(QObject): max_build_plate = max(build_plate_number, max_build_plate) return max_build_plate - ## Either select or deselect an item @pyqtSlot(int) def changeSelection(self, index): + """Either select or deselect an item""" + modifiers = QApplication.keyboardModifiers() ctrl_is_active = modifiers & Qt.ControlModifier shift_is_active = modifiers & Qt.ShiftModifier diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index eb609def5a..b9f2279414 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -15,9 +15,11 @@ from cura.Settings.ExtruderStack import ExtruderStack # For typing. from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings. -## Scene nodes that are models are only seen when selecting the corresponding build plate -# Note that many other nodes can just be UM SceneNode objects. class CuraSceneNode(SceneNode): + """Scene nodes that are models are only seen when selecting the corresponding build plate + + Note that many other nodes can just be UM SceneNode objects. + """ def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None: super().__init__(parent = parent, visible = visible, name = name) if not no_setting_override: @@ -36,9 +38,11 @@ class CuraSceneNode(SceneNode): def isSelectable(self) -> bool: return super().isSelectable() and self.callDecoration("getBuildPlateNumber") == cura.CuraApplication.CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate - ## Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned - # TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded def getPrintingExtruder(self) -> Optional[ExtruderStack]: + """Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned + + TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded + """ global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack is None: return None @@ -69,8 +73,9 @@ class CuraSceneNode(SceneNode): # This point should never be reached return None - ## Return the color of the material used to print this model def getDiffuseColor(self) -> List[float]: + """Return the color of the material used to print this model""" + printing_extruder = self.getPrintingExtruder() material_color = "#808080" # Fallback color @@ -86,8 +91,9 @@ class CuraSceneNode(SceneNode): 1.0 ] - ## Return if any area collides with the convex hull of this scene node def collidesWithAreas(self, areas: List[Polygon]) -> bool: + """Return if any area collides with the convex hull of this scene node""" + convex_hull = self.callDecoration("getPrintingArea") if convex_hull: if not convex_hull.isValid(): @@ -101,8 +107,9 @@ class CuraSceneNode(SceneNode): return True return False - ## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box def _calculateAABB(self) -> None: + """Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box""" + self._aabb = None if self._mesh_data: self._aabb = self._mesh_data.getExtents(self.getWorldTransformation()) @@ -122,8 +129,9 @@ class CuraSceneNode(SceneNode): else: self._aabb = self._aabb + child.getBoundingBox() - ## Taken from SceneNode, but replaced SceneNode with CuraSceneNode def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode": + """Taken from SceneNode, but replaced SceneNode with CuraSceneNode""" + copy = CuraSceneNode(no_setting_override = True) # Setting override will be added later copy.setTransformation(self.getLocalTransformation()) copy.setMeshData(self._mesh_data) diff --git a/cura/Scene/ZOffsetDecorator.py b/cura/Scene/ZOffsetDecorator.py index b35b17a412..1f1f5a9b1f 100644 --- a/cura/Scene/ZOffsetDecorator.py +++ b/cura/Scene/ZOffsetDecorator.py @@ -1,8 +1,9 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator -## A decorator that stores the amount an object has been moved below the platform. class ZOffsetDecorator(SceneNodeDecorator): + """A decorator that stores the amount an object has been moved below the platform.""" + def __init__(self) -> None: super().__init__() self._z_offset = 0. diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 4d972ba87e..29be16dcce 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -33,12 +33,14 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## Manager class that contains common actions to deal with containers in Cura. -# -# This is primarily intended as a class to be able to perform certain actions -# from within QML. We want to be able to trigger things like removing a container -# when a certain action happens. This can be done through this class. class ContainerManager(QObject): + """Manager class that contains common actions to deal with containers in Cura. + + This is primarily intended as a class to be able to perform certain actions + from within QML. We want to be able to trigger things like removing a container + when a certain action happens. This can be done through this class. + """ + def __init__(self, application: "CuraApplication") -> None: if ContainerManager.__instance is not None: @@ -67,21 +69,23 @@ class ContainerManager(QObject): return "" return str(result) - ## Set a metadata entry of the specified container. - # - # This will set the specified entry of the container's metadata to the specified - # value. Note that entries containing dictionaries can have their entries changed - # by using "/" as a separator. For example, to change an entry "foo" in a - # dictionary entry "bar", you can specify "bar/foo" as entry name. - # - # \param container_node \type{ContainerNode} - # \param entry_name \type{str} The name of the metadata entry to change. - # \param entry_value The new value of the entry. - # - # TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this. - # Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want? @pyqtSlot("QVariant", str, str) def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool: + """Set a metadata entry of the specified container. + + This will set the specified entry of the container's metadata to the specified + value. Note that entries containing dictionaries can have their entries changed + by using "/" as a separator. For example, to change an entry "foo" in a + dictionary entry "bar", you can specify "bar/foo" as entry name. + + :param container_node: :type{ContainerNode} + :param entry_name: :type{str} The name of the metadata entry to change. + :param entry_value: The new value of the entry. + + TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this. + Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want? + """ + if container_node.container is None: Logger.log("w", "Container node {0} doesn't have a container.".format(container_node.container_id)) return False @@ -124,18 +128,20 @@ class ContainerManager(QObject): def makeUniqueName(self, original_name: str) -> str: return cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry().uniqueName(original_name) - ## Get a list of string that can be used as name filters for a Qt File Dialog - # - # This will go through the list of available container types and generate a list of strings - # out of that. The strings are formatted as "description (*.extension)" and can be directly - # passed to a nameFilters property of a Qt File Dialog. - # - # \param type_name Which types of containers to list. These types correspond to the "type" - # key of the plugin metadata. - # - # \return A string list with name filters. @pyqtSlot(str, result = "QStringList") def getContainerNameFilters(self, type_name: str) -> List[str]: + """Get a list of string that can be used as name filters for a Qt File Dialog + + This will go through the list of available container types and generate a list of strings + out of that. The strings are formatted as "description (*.extension)" and can be directly + passed to a nameFilters property of a Qt File Dialog. + + :param type_name: Which types of containers to list. These types correspond to the "type" + key of the plugin metadata. + + :return: A string list with name filters. + """ + if not self._container_name_filters: self._updateContainerNameFilters() @@ -147,17 +153,18 @@ class ContainerManager(QObject): filters.append("All Files (*)") return filters - ## Export a container to a file - # - # \param container_id The ID of the container to export - # \param file_type The type of file to save as. Should be in the form of "description (*.extension, *.ext)" - # \param file_url_or_string The URL where to save the file. - # - # \return A dictionary containing a key "status" with a status code and a key "message" with a message - # explaining the status. - # The status code can be one of "error", "cancelled", "success" @pyqtSlot(str, str, QUrl, result = "QVariantMap") def exportContainer(self, container_id: str, file_type: str, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: + """Export a container to a file + + :param container_id: The ID of the container to export + :param file_type: The type of file to save as. Should be in the form of "description (*.extension, *.ext)" + :param file_url_or_string: The URL where to save the file. + + :return: A dictionary containing a key "status" with a status code and a key "message" with a message + explaining the status. The status code can be one of "error", "cancelled", "success" + """ + if not container_id or not file_type or not file_url_or_string: return {"status": "error", "message": "Invalid arguments"} @@ -214,14 +221,16 @@ class ContainerManager(QObject): return {"status": "success", "message": "Successfully exported container", "path": file_url} - ## Imports a profile from a file - # - # \param file_url A URL that points to the file to import. - # - # \return \type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key - # containing a message for the user @pyqtSlot(QUrl, result = "QVariantMap") def importMaterialContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: + """Imports a profile from a file + + :param file_url: A URL that points to the file to import. + + :return: :type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key + containing a message for the user + """ + if not file_url_or_string: return {"status": "error", "message": "Invalid path"} @@ -266,14 +275,16 @@ class ContainerManager(QObject): return {"status": "success", "message": "Successfully imported container {0}".format(container.getName())} - ## Update the current active quality changes container with the settings from the user container. - # - # This will go through the active global stack and all active extruder stacks and merge the changes from the user - # container into the quality_changes container. After that, the user container is cleared. - # - # \return \type{bool} True if successful, False if not. @pyqtSlot(result = bool) def updateQualityChanges(self) -> bool: + """Update the current active quality changes container with the settings from the user container. + + This will go through the active global stack and all active extruder stacks and merge the changes from the user + container into the quality_changes container. After that, the user container is cleared. + + :return: :type{bool} True if successful, False if not. + """ + application = cura.CuraApplication.CuraApplication.getInstance() global_stack = application.getMachineManager().activeMachine if not global_stack: @@ -313,9 +324,10 @@ class ContainerManager(QObject): return True - ## Clear the top-most (user) containers of the active stacks. @pyqtSlot() def clearUserContainers(self) -> None: + """Clear the top-most (user) containers of the active stacks.""" + machine_manager = cura.CuraApplication.CuraApplication.getInstance().getMachineManager() machine_manager.blurSettings.emit() @@ -335,25 +347,28 @@ class ContainerManager(QObject): for container in send_emits_containers: container.sendPostponedEmits() - ## Get a list of materials that have the same GUID as the reference material - # - # \param material_node The node representing the material for which to get - # the same GUID. - # \param exclude_self Whether to include the name of the material you - # provided. - # \return A list of names of materials with the same GUID. @pyqtSlot("QVariant", bool, result = "QStringList") def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]: + """Get a list of materials that have the same GUID as the reference material + + :param material_node: The node representing the material for which to get + the same GUID. + :param exclude_self: Whether to include the name of the material you provided. + :return: A list of names of materials with the same GUID. + """ + same_guid = ContainerRegistry.getInstance().findInstanceContainersMetadata(GUID = material_node.guid) if exclude_self: return list({meta["name"] for meta in same_guid if meta["base_file"] != material_node.base_file}) else: return list({meta["name"] for meta in same_guid}) - ## Unlink a material from all other materials by creating a new GUID - # \param material_id \type{str} the id of the material to create a new GUID for. @pyqtSlot("QVariant") def unlinkMaterial(self, material_node: "MaterialNode") -> None: + """Unlink a material from all other materials by creating a new GUID + + :param material_id: :type{str} the id of the material to create a new GUID for. + """ # Get the material group if material_node.container is None: # Failed to lazy-load this container. return @@ -428,9 +443,10 @@ class ContainerManager(QObject): name_filter = "{0} ({1})".format(mime_type.comment, suffix_list) self._container_name_filters[name_filter] = entry - ## Import single profile, file_url does not have to end with curaprofile @pyqtSlot(QUrl, result = "QVariantMap") def importProfile(self, file_url: QUrl) -> Dict[str, str]: + """Import single profile, file_url does not have to end with curaprofile""" + if not file_url.isValid(): return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)} path = file_url.toLocalFile() diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 0ef09a1fac..1ada51af59 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -1,765 +1,784 @@ -# Copyright (c) 2019 Ultimaker B.V. -# Cura is released under the terms of the LGPLv3 or higher. - -import os -import re -import configparser - -from typing import Any, cast, Dict, Optional, List, Union -from PyQt5.QtWidgets import QMessageBox - -from UM.Decorators import override -from UM.Settings.ContainerFormatError import ContainerFormatError -from UM.Settings.Interfaces import ContainerInterface -from UM.Settings.ContainerRegistry import ContainerRegistry -from UM.Settings.ContainerStack import ContainerStack -from UM.Settings.InstanceContainer import InstanceContainer -from UM.Settings.SettingInstance import SettingInstance -from UM.Logger import Logger -from UM.Message import Message -from UM.Platform import Platform -from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with. -from UM.Resources import Resources -from UM.Util import parseBool -from cura.ReaderWriters.ProfileWriter import ProfileWriter - -from . import ExtruderStack -from . import GlobalStack - -import cura.CuraApplication -from cura.Settings.cura_empty_instance_containers import empty_quality_container -from cura.Machines.ContainerTree import ContainerTree -from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader - -from UM.i18n import i18nCatalog -catalog = i18nCatalog("cura") - - -class CuraContainerRegistry(ContainerRegistry): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack - # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack - # is added, we check to see if an extruder stack needs to be added. - self.containerAdded.connect(self._onContainerAdded) - - ## Overridden from ContainerRegistry - # - # Adds a container to the registry. - # - # This will also try to convert a ContainerStack to either Extruder or - # Global stack based on metadata information. - @override(ContainerRegistry) - def addContainer(self, container: ContainerInterface) -> None: - # Note: Intentional check with type() because we want to ignore subclasses - if type(container) == ContainerStack: - container = self._convertContainerStack(cast(ContainerStack, container)) - - if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()): - # Check against setting version of the definition. - required_setting_version = cura.CuraApplication.CuraApplication.SettingVersion - actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0)) - if required_setting_version != actual_setting_version: - Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version)) - return # Don't add. - - super().addContainer(container) - - ## Create a name that is not empty and unique - # \param container_type \type{string} Type of the container (machine, quality, ...) - # \param current_name \type{} Current name of the container, which may be an acceptable option - # \param new_name \type{string} Base name, which may not be unique - # \param fallback_name \type{string} Name to use when (stripped) new_name is empty - # \return \type{string} Name that is unique for the specified type and name/id - def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str: - new_name = new_name.strip() - num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name) - if num_check: - new_name = num_check.group(1) - if new_name == "": - new_name = fallback_name - - unique_name = new_name - i = 1 - # In case we are renaming, the current name of the container is also a valid end-result - while self._containerExists(container_type, unique_name) and unique_name != current_name: - i += 1 - unique_name = "%s #%d" % (new_name, i) - - return unique_name - - ## Check if a container with of a certain type and a certain name or id exists - # Both the id and the name are checked, because they may not be the same and it is better if they are both unique - # \param container_type \type{string} Type of the container (machine, quality, ...) - # \param container_name \type{string} Name to check - def _containerExists(self, container_type: str, container_name: str): - container_class = ContainerStack if container_type == "machine" else InstanceContainer - - return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \ - self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type) - - ## Exports an profile to a file - # - # \param container_list \type{list} the containers to export. This is not - # necessarily in any order! - # \param file_name \type{str} the full path and filename to export to. - # \param file_type \type{str} the file type with the format " (*.)" - # \return True if the export succeeded, false otherwise. - def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool: - # Parse the fileType to deduce what plugin can save the file format. - # fileType has the format " (*.)" - split = file_type.rfind(" (*.") # Find where the description ends and the extension starts. - if split < 0: # Not found. Invalid format. - Logger.log("e", "Invalid file format identifier %s", file_type) - return False - description = file_type[:split] - extension = file_type[split + 4:-1] # Leave out the " (*." and ")". - if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any. - file_name += "." + extension - - # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself. - if not Platform.isWindows(): - if os.path.exists(file_name): - result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), - catalog.i18nc("@label Don't translate the XML tag !", "The file {0} already exists. Are you sure you want to overwrite it?").format(file_name)) - if result == QMessageBox.No: - return False - - profile_writer = self._findProfileWriter(extension, description) - try: - if profile_writer is None: - raise Exception("Unable to find a profile writer") - success = profile_writer.write(file_name, container_list) - except Exception as e: - Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e)) - m = Message(catalog.i18nc("@info:status Don't translate the XML tags or !", "Failed to export profile to {0}: {1}", file_name, str(e)), - lifetime = 0, - title = catalog.i18nc("@info:title", "Error")) - m.show() - return False - if not success: - Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name) - m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Failed to export profile to {0}: Writer plugin reported failure.", file_name), - lifetime = 0, - title = catalog.i18nc("@info:title", "Error")) - m.show() - return False - m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Exported profile to {0}", file_name), - title = catalog.i18nc("@info:title", "Export succeeded")) - m.show() - return True - - ## Gets the plugin object matching the criteria - # \param extension - # \param description - # \return The plugin object matching the given extension and description. - def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]: - plugin_registry = PluginRegistry.getInstance() - for plugin_id, meta_data in self._getIOPlugins("profile_writer"): - for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write. - supported_extension = supported_type.get("extension", None) - if supported_extension == extension: # This plugin supports a file type with the same extension. - supported_description = supported_type.get("description", None) - if supported_description == description: # The description is also identical. Assume it's the same file type. - return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id)) - return None - - ## Imports a profile from a file - # - # \param file_name The full path and filename of the profile to import. - # \return Dict with a 'status' key containing the string 'ok' or 'error', - # and a 'message' key containing a message for the user. - def importProfile(self, file_name: str) -> Dict[str, str]: - Logger.log("d", "Attempting to import profile %s", file_name) - if not file_name: - return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}: {1}", file_name, "Invalid path")} - - global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() - if not global_stack: - return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Can't import profile from {0} before a printer is added.", file_name)} - container_tree = ContainerTree.getInstance() - - machine_extruders = [] - for position in sorted(global_stack.extruders): - machine_extruders.append(global_stack.extruders[position]) - - plugin_registry = PluginRegistry.getInstance() - extension = file_name.split(".")[-1] - - for plugin_id, meta_data in self._getIOPlugins("profile_reader"): - if meta_data["profile_reader"][0]["extension"] != extension: - continue - profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id)) - try: - profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. - except NoProfileException: - return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "No custom profile to import in file {0}", file_name)} - except Exception as e: - # Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None. - Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e)) - return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}:", file_name) + "\n" + str(e) + ""} - - if profile_or_list: - # Ensure it is always a list of profiles - if not isinstance(profile_or_list, list): - profile_or_list = [profile_or_list] - - # First check if this profile is suitable for this machine - global_profile = None - extruder_profiles = [] - if len(profile_or_list) == 1: - global_profile = profile_or_list[0] - else: - for profile in profile_or_list: - if not profile.getMetaDataEntry("position"): - global_profile = profile - else: - extruder_profiles.append(profile) - extruder_profiles = sorted(extruder_profiles, key = lambda x: int(x.getMetaDataEntry("position"))) - profile_or_list = [global_profile] + extruder_profiles - - if not global_profile: - Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name) - return { "status": "error", - "message": catalog.i18nc("@info:status Don't translate the XML tags !", "This profile {0} contains incorrect data, could not import it.", file_name)} - profile_definition = global_profile.getMetaDataEntry("definition") - - # Make sure we have a profile_definition in the file: - if profile_definition is None: - break - machine_definitions = self.findContainers(id = profile_definition) - if not machine_definitions: - Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition) - return {"status": "error", - "message": catalog.i18nc("@info:status Don't translate the XML tags !", "This profile {0} contains incorrect data, could not import it.", file_name) - } - machine_definition = machine_definitions[0] - - # Get the expected machine definition. - # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... - has_machine_quality = parseBool(machine_definition.getMetaDataEntry("has_machine_quality", "false")) - profile_definition = machine_definition.getMetaDataEntry("quality_definition", machine_definition.getId()) if has_machine_quality else "fdmprinter" - expected_machine_definition = container_tree.machines[global_stack.definition.getId()].quality_definition - - # And check if the profile_definition matches either one (showing error if not): - if profile_definition != expected_machine_definition: - Logger.log("d", "Profile {file_name} is for machine {profile_definition}, but the current active machine is {expected_machine_definition}. Changing profile's definition.".format(file_name = file_name, profile_definition = profile_definition, expected_machine_definition = expected_machine_definition)) - global_profile.setMetaDataEntry("definition", expected_machine_definition) - for extruder_profile in extruder_profiles: - extruder_profile.setMetaDataEntry("definition", expected_machine_definition) - - quality_name = global_profile.getName() - quality_type = global_profile.getMetaDataEntry("quality_type") - - name_seed = os.path.splitext(os.path.basename(file_name))[0] - new_name = self.uniqueName(name_seed) - - # Ensure it is always a list of profiles - if type(profile_or_list) is not list: - profile_or_list = [profile_or_list] - - # Make sure that there are also extruder stacks' quality_changes, not just one for the global stack - if len(profile_or_list) == 1: - global_profile = profile_or_list[0] - extruder_profiles = [] - for idx, extruder in enumerate(global_stack.extruders.values()): - profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1)) - profile = InstanceContainer(profile_id) - profile.setName(quality_name) - profile.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion) - profile.setMetaDataEntry("type", "quality_changes") - profile.setMetaDataEntry("definition", expected_machine_definition) - profile.setMetaDataEntry("quality_type", quality_type) - profile.setDirty(True) - if idx == 0: - # Move all per-extruder settings to the first extruder's quality_changes - for qc_setting_key in global_profile.getAllKeys(): - settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = global_profile.getProperty(qc_setting_key, "value") - - setting_definition = global_stack.getSettingDefinition(qc_setting_key) - if setting_definition is not None: - new_instance = SettingInstance(setting_definition, profile) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - profile.addInstance(new_instance) - profile.setDirty(True) - - global_profile.removeInstance(qc_setting_key, postpone_emit = True) - extruder_profiles.append(profile) - - for profile in extruder_profiles: - profile_or_list.append(profile) - - # Import all profiles - profile_ids_added = [] # type: List[str] - for profile_index, profile in enumerate(profile_or_list): - if profile_index == 0: - # This is assumed to be the global profile - profile_id = (cast(ContainerInterface, global_stack.getBottom()).getId() + "_" + name_seed).lower().replace(" ", "_") - - elif profile_index < len(machine_extruders) + 1: - # This is assumed to be an extruder profile - extruder_id = machine_extruders[profile_index - 1].definition.getId() - extruder_position = str(profile_index - 1) - if not profile.getMetaDataEntry("position"): - profile.setMetaDataEntry("position", extruder_position) - else: - profile.setMetaDataEntry("position", extruder_position) - profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_") - - else: # More extruders in the imported file than in the machine. - continue # Delete the additional profiles. - - result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) - if result is not None: - # Remove any profiles that did got added. - for profile_id in profile_ids_added: - self.removeContainer(profile_id) - - return {"status": "error", "message": catalog.i18nc( - "@info:status Don't translate the XML tag !", - "Failed to import profile from {0}:", - file_name) + " " + result} - profile_ids_added.append(profile.getId()) - return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} - - # This message is throw when the profile reader doesn't find any profile in the file - return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)} - - # If it hasn't returned by now, none of the plugins loaded the profile successfully. - return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)} - - @override(ContainerRegistry) - def load(self) -> None: - super().load() - self._registerSingleExtrusionMachinesExtruderStacks() - self._connectUpgradedExtruderStacksToMachines() - - ## Check if the metadata for a container is okay before adding it. - # - # This overrides the one from UM.Settings.ContainerRegistry because we - # also require that the setting_version is correct. - @override(ContainerRegistry) - def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool: - if metadata is None: - return False - if "setting_version" not in metadata: - return False - try: - if int(metadata["setting_version"]) != cura.CuraApplication.CuraApplication.SettingVersion: - return False - except ValueError: #Not parsable as int. - return False - return True - - ## Update an imported profile to match the current machine configuration. - # - # \param profile The profile to configure. - # \param id_seed The base ID for the profile. May be changed so it does not conflict with existing containers. - # \param new_name The new name for the profile. - # - # \return None if configuring was successful or an error message if an error occurred. - def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]: - profile.setDirty(True) # Ensure the profiles are correctly saved - - new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile")) - profile.setMetaDataEntry("id", new_id) - profile.setName(new_name) - - # Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile - # It also solves an issue with importing profiles from G-Codes - profile.setMetaDataEntry("id", new_id) - profile.setMetaDataEntry("definition", machine_definition_id) - - if "type" in profile.getMetaData(): - profile.setMetaDataEntry("type", "quality_changes") - else: - profile.setMetaDataEntry("type", "quality_changes") - - quality_type = profile.getMetaDataEntry("quality_type") - if not quality_type: - return catalog.i18nc("@info:status", "Profile is missing a quality type.") - - global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() - if global_stack is None: - return None - definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition - profile.setDefinition(definition_id) - - # Check to make sure the imported profile actually makes sense in context of the current configuration. - # This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as - # successfully imported but then fail to show up. - quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups() - # "not_supported" profiles can be imported. - if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict: - return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type) - - ContainerRegistry.getInstance().addContainer(profile) - - return None - - @override(ContainerRegistry) - def saveDirtyContainers(self) -> None: - # Lock file for "more" atomically loading and saving to/from config dir. - with self.lockFile(): - # Save base files first - for instance in self.findDirtyContainers(container_type=InstanceContainer): - if instance.getMetaDataEntry("removed"): - continue - if instance.getId() == instance.getMetaData().get("base_file"): - self.saveContainer(instance) - - for instance in self.findDirtyContainers(container_type=InstanceContainer): - if instance.getMetaDataEntry("removed"): - continue - self.saveContainer(instance) - - for stack in self.findContainerStacks(): - self.saveContainer(stack) - - ## Gets a list of profile writer plugins - # \return List of tuples of (plugin_id, meta_data). - def _getIOPlugins(self, io_type): - plugin_registry = PluginRegistry.getInstance() - active_plugin_ids = plugin_registry.getActivePlugins() - - result = [] - for plugin_id in active_plugin_ids: - meta_data = plugin_registry.getMetaData(plugin_id) - if io_type in meta_data: - result.append( (plugin_id, meta_data) ) - return result - - ## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack. - def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]: - assert type(container) == ContainerStack - - container_type = container.getMetaDataEntry("type") - if container_type not in ("extruder_train", "machine"): - # It is not an extruder or machine, so do nothing with the stack - return container - - Logger.log("d", "Converting ContainerStack {stack} to {type}", stack = container.getId(), type = container_type) - - if container_type == "extruder_train": - new_stack = ExtruderStack.ExtruderStack(container.getId()) - else: - new_stack = GlobalStack.GlobalStack(container.getId()) - - container_contents = container.serialize() - new_stack.deserialize(container_contents) - - # Delete the old configuration file so we do not get double stacks - if os.path.isfile(container.getPath()): - os.remove(container.getPath()) - - return new_stack - - def _registerSingleExtrusionMachinesExtruderStacks(self) -> None: - machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"}) - for machine in machines: - extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId()) - if not extruder_stacks: - self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder") - - def _onContainerAdded(self, container: ContainerInterface) -> None: - # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack - # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack - # is added, we check to see if an extruder stack needs to be added. - if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine": - return - - machine_extruder_trains = container.getMetaDataEntry("machine_extruder_trains") - if machine_extruder_trains is not None and machine_extruder_trains != {"0": "fdmextruder"}: - return - - extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId()) - if not extruder_stacks: - self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder") - - # - # new_global_quality_changes is optional. It is only used in project loading for a scenario like this: - # - override the current machine - # - create new for custom quality profile - # new_global_quality_changes is the new global quality changes container in this scenario. - # create_new_ids indicates if new unique ids must be created - # - def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True): - new_extruder_id = extruder_id - - application = cura.CuraApplication.CuraApplication.getInstance() - - extruder_definitions = self.findDefinitionContainers(id = new_extruder_id) - if not extruder_definitions: - Logger.log("w", "Could not find definition containers for extruder %s", new_extruder_id) - return - - extruder_definition = extruder_definitions[0] - unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id - - extruder_stack = ExtruderStack.ExtruderStack(unique_name) - extruder_stack.setName(extruder_definition.getName()) - extruder_stack.setDefinition(extruder_definition) - extruder_stack.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) - - # create a new definition_changes container for the extruder stack - definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings") if create_new_ids else extruder_stack.getId() + "_settings" - definition_changes_name = definition_changes_id - definition_changes = InstanceContainer(definition_changes_id, parent = application) - definition_changes.setName(definition_changes_name) - definition_changes.setMetaDataEntry("setting_version", application.SettingVersion) - definition_changes.setMetaDataEntry("type", "definition_changes") - definition_changes.setMetaDataEntry("definition", extruder_definition.getId()) - - # move definition_changes settings if exist - for setting_key in definition_changes.getAllKeys(): - if machine.definition.getProperty(setting_key, "settable_per_extruder"): - setting_value = machine.definitionChanges.getProperty(setting_key, "value") - if setting_value is not None: - # move it to the extruder stack's definition_changes - setting_definition = machine.getSettingDefinition(setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - definition_changes.addInstance(new_instance) - definition_changes.setDirty(True) - - machine.definitionChanges.removeInstance(setting_key, postpone_emit = True) - - self.addContainer(definition_changes) - extruder_stack.setDefinitionChanges(definition_changes) - - # create empty user changes container otherwise - user_container_id = self.uniqueName(extruder_stack.getId() + "_user") if create_new_ids else extruder_stack.getId() + "_user" - user_container_name = user_container_id - user_container = InstanceContainer(user_container_id, parent = application) - user_container.setName(user_container_name) - user_container.setMetaDataEntry("type", "user") - user_container.setMetaDataEntry("machine", machine.getId()) - user_container.setMetaDataEntry("setting_version", application.SettingVersion) - user_container.setDefinition(machine.definition.getId()) - user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position")) - - if machine.userChanges: - # For the newly created extruder stack, we need to move all "per-extruder" settings to the user changes - # container to the extruder stack. - for user_setting_key in machine.userChanges.getAllKeys(): - settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = machine.getProperty(user_setting_key, "value") - - setting_definition = machine.getSettingDefinition(user_setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - user_container.addInstance(new_instance) - user_container.setDirty(True) - - machine.userChanges.removeInstance(user_setting_key, postpone_emit = True) - - self.addContainer(user_container) - extruder_stack.setUserChanges(user_container) - - empty_variant = application.empty_variant_container - empty_material = application.empty_material_container - empty_quality = application.empty_quality_container - - if machine.variant.getId() not in ("empty", "empty_variant"): - variant = machine.variant - else: - variant = empty_variant - extruder_stack.variant = variant - - if machine.material.getId() not in ("empty", "empty_material"): - material = machine.material - else: - material = empty_material - extruder_stack.material = material - - if machine.quality.getId() not in ("empty", "empty_quality"): - quality = machine.quality - else: - quality = empty_quality - extruder_stack.quality = quality - - machine_quality_changes = machine.qualityChanges - if new_global_quality_changes is not None: - machine_quality_changes = new_global_quality_changes - - if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): - extruder_quality_changes_container = self.findInstanceContainers(name = machine_quality_changes.getName(), extruder = extruder_id) - if extruder_quality_changes_container: - extruder_quality_changes_container = extruder_quality_changes_container[0] - - quality_changes_id = extruder_quality_changes_container.getId() - extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] - else: - # Some extruder quality_changes containers can be created at runtime as files in the qualities - # folder. Those files won't be loaded in the registry immediately. So we also need to search - # the folder to see if the quality_changes exists. - extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) - if extruder_quality_changes_container: - quality_changes_id = extruder_quality_changes_container.getId() - extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) - extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] - else: - # If we still cannot find a quality changes container for the extruder, create a new one - container_name = machine_quality_changes.getName() - container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name) - extruder_quality_changes_container = InstanceContainer(container_id, parent = application) - extruder_quality_changes_container.setName(container_name) - extruder_quality_changes_container.setMetaDataEntry("type", "quality_changes") - extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion) - extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) - extruder_quality_changes_container.setMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type")) - extruder_quality_changes_container.setMetaDataEntry("intent_category", "default") # Intent categories weren't a thing back then. - extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId()) - - self.addContainer(extruder_quality_changes_container) - extruder_stack.qualityChanges = extruder_quality_changes_container - - if not extruder_quality_changes_container: - Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", - machine_quality_changes.getName(), extruder_stack.getId()) - else: - # Move all per-extruder settings to the extruder's quality changes - for qc_setting_key in machine_quality_changes.getAllKeys(): - settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = machine_quality_changes.getProperty(qc_setting_key, "value") - - setting_definition = machine.getSettingDefinition(qc_setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - extruder_quality_changes_container.addInstance(new_instance) - extruder_quality_changes_container.setDirty(True) - - machine_quality_changes.removeInstance(qc_setting_key, postpone_emit=True) - else: - extruder_stack.qualityChanges = self.findInstanceContainers(id = "empty_quality_changes")[0] - - self.addContainer(extruder_stack) - - # Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have - # per-extruder settings in the container for the machine instead of the extruder. - if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): - quality_changes_machine_definition_id = machine_quality_changes.getDefinition().getId() - else: - whole_machine_definition = machine.definition - machine_entry = machine.definition.getMetaDataEntry("machine") - if machine_entry is not None: - container_registry = ContainerRegistry.getInstance() - whole_machine_definition = container_registry.findDefinitionContainers(id = machine_entry)[0] - - quality_changes_machine_definition_id = "fdmprinter" - if whole_machine_definition.getMetaDataEntry("has_machine_quality"): - quality_changes_machine_definition_id = machine.definition.getMetaDataEntry("quality_definition", - whole_machine_definition.getId()) - qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id) - qc_groups = {} # map of qc names -> qc containers - for qc in qcs: - qc_name = qc.getName() - if qc_name not in qc_groups: - qc_groups[qc_name] = [] - qc_groups[qc_name].append(qc) - # Try to find from the quality changes cura directory too - quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) - if quality_changes_container: - qc_groups[qc_name].append(quality_changes_container) - - for qc_name, qc_list in qc_groups.items(): - qc_dict = {"global": None, "extruders": []} - for qc in qc_list: - extruder_position = qc.getMetaDataEntry("position") - if extruder_position is not None: - qc_dict["extruders"].append(qc) - else: - qc_dict["global"] = qc - if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1: - # Move per-extruder settings - for qc_setting_key in qc_dict["global"].getAllKeys(): - settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") - if settable_per_extruder: - setting_value = qc_dict["global"].getProperty(qc_setting_key, "value") - - setting_definition = machine.getSettingDefinition(qc_setting_key) - new_instance = SettingInstance(setting_definition, definition_changes) - new_instance.setProperty("value", setting_value) - new_instance.resetState() # Ensure that the state is not seen as a user state. - qc_dict["extruders"][0].addInstance(new_instance) - qc_dict["extruders"][0].setDirty(True) - - qc_dict["global"].removeInstance(qc_setting_key, postpone_emit=True) - - # Set next stack at the end - extruder_stack.setNextStack(machine) - - return extruder_stack - - def _findQualityChangesContainerInCuraFolder(self, name: str) -> Optional[InstanceContainer]: - quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer) - - instance_container = None - - for item in os.listdir(quality_changes_dir): - file_path = os.path.join(quality_changes_dir, item) - if not os.path.isfile(file_path): - continue - - parser = configparser.ConfigParser(interpolation = None) - try: - parser.read([file_path]) - except Exception: - # Skip, it is not a valid stack file - continue - - if not parser.has_option("general", "name"): - continue - - if parser["general"]["name"] == name: - # Load the container - container_id = os.path.basename(file_path).replace(".inst.cfg", "") - if self.findInstanceContainers(id = container_id): - # This container is already in the registry, skip it - continue - - instance_container = InstanceContainer(container_id) - with open(file_path, "r", encoding = "utf-8") as f: - serialized = f.read() - try: - instance_container.deserialize(serialized, file_path) - except ContainerFormatError: - Logger.logException("e", "Unable to deserialize InstanceContainer %s", file_path) - continue - self.addContainer(instance_container) - break - - return instance_container - - # Fix the extruders that were upgraded to ExtruderStack instances during addContainer. - # The stacks are now responsible for setting the next stack on deserialize. However, - # due to problems with loading order, some stacks may not have the proper next stack - # set after upgrading, because the proper global stack was not yet loaded. This method - # makes sure those extruders also get the right stack set. - def _connectUpgradedExtruderStacksToMachines(self) -> None: - extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack) - for extruder_stack in extruder_stacks: - if extruder_stack.getNextStack(): - # Has the right next stack, so ignore it. - continue - - machines = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack.getMetaDataEntry("machine", "")) - if machines: - extruder_stack.setNextStack(machines[0]) - else: - Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId()) - - # Override just for the type. - @classmethod - @override(ContainerRegistry) - def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry": - return cast(CuraContainerRegistry, super().getInstance(*args, **kwargs)) +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import os +import re +import configparser + +from typing import Any, cast, Dict, Optional, List, Union +from PyQt5.QtWidgets import QMessageBox + +from UM.Decorators import override +from UM.Settings.ContainerFormatError import ContainerFormatError +from UM.Settings.Interfaces import ContainerInterface +from UM.Settings.ContainerRegistry import ContainerRegistry +from UM.Settings.ContainerStack import ContainerStack +from UM.Settings.InstanceContainer import InstanceContainer +from UM.Settings.SettingInstance import SettingInstance +from UM.Logger import Logger +from UM.Message import Message +from UM.Platform import Platform +from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with. +from UM.Resources import Resources +from UM.Util import parseBool +from cura.ReaderWriters.ProfileWriter import ProfileWriter + +from . import ExtruderStack +from . import GlobalStack + +import cura.CuraApplication +from cura.Settings.cura_empty_instance_containers import empty_quality_container +from cura.Machines.ContainerTree import ContainerTree +from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader + +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") + + +class CuraContainerRegistry(ContainerRegistry): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack + # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack + # is added, we check to see if an extruder stack needs to be added. + self.containerAdded.connect(self._onContainerAdded) + + @override(ContainerRegistry) + def addContainer(self, container: ContainerInterface) -> None: + """Overridden from ContainerRegistry + + Adds a container to the registry. + + This will also try to convert a ContainerStack to either Extruder or + Global stack based on metadata information. + """ + + # Note: Intentional check with type() because we want to ignore subclasses + if type(container) == ContainerStack: + container = self._convertContainerStack(cast(ContainerStack, container)) + + if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()): + # Check against setting version of the definition. + required_setting_version = cura.CuraApplication.CuraApplication.SettingVersion + actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0)) + if required_setting_version != actual_setting_version: + Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version)) + return # Don't add. + + super().addContainer(container) + + def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str: + """Create a name that is not empty and unique + + :param container_type: :type{string} Type of the container (machine, quality, ...) + :param current_name: :type{} Current name of the container, which may be an acceptable option + :param new_name: :type{string} Base name, which may not be unique + :param fallback_name: :type{string} Name to use when (stripped) new_name is empty + :return: :type{string} Name that is unique for the specified type and name/id + """ + new_name = new_name.strip() + num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name) + if num_check: + new_name = num_check.group(1) + if new_name == "": + new_name = fallback_name + + unique_name = new_name + i = 1 + # In case we are renaming, the current name of the container is also a valid end-result + while self._containerExists(container_type, unique_name) and unique_name != current_name: + i += 1 + unique_name = "%s #%d" % (new_name, i) + + return unique_name + + def _containerExists(self, container_type: str, container_name: str): + """Check if a container with of a certain type and a certain name or id exists + + Both the id and the name are checked, because they may not be the same and it is better if they are both unique + :param container_type: :type{string} Type of the container (machine, quality, ...) + :param container_name: :type{string} Name to check + """ + container_class = ContainerStack if container_type == "machine" else InstanceContainer + + return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \ + self.findContainersMetadata(container_type = container_class, name = container_name, type = container_type) + + def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool: + """Exports an profile to a file + + :param container_list: :type{list} the containers to export. This is not + necessarily in any order! + :param file_name: :type{str} the full path and filename to export to. + :param file_type: :type{str} the file type with the format " (*.)" + :return: True if the export succeeded, false otherwise. + """ + + # Parse the fileType to deduce what plugin can save the file format. + # fileType has the format " (*.)" + split = file_type.rfind(" (*.") # Find where the description ends and the extension starts. + if split < 0: # Not found. Invalid format. + Logger.log("e", "Invalid file format identifier %s", file_type) + return False + description = file_type[:split] + extension = file_type[split + 4:-1] # Leave out the " (*." and ")". + if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any. + file_name += "." + extension + + # On Windows, QML FileDialog properly asks for overwrite confirm, but not on other platforms, so handle those ourself. + if not Platform.isWindows(): + if os.path.exists(file_name): + result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"), + catalog.i18nc("@label Don't translate the XML tag !", "The file {0} already exists. Are you sure you want to overwrite it?").format(file_name)) + if result == QMessageBox.No: + return False + + profile_writer = self._findProfileWriter(extension, description) + try: + if profile_writer is None: + raise Exception("Unable to find a profile writer") + success = profile_writer.write(file_name, container_list) + except Exception as e: + Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e)) + m = Message(catalog.i18nc("@info:status Don't translate the XML tags or !", "Failed to export profile to {0}: {1}", file_name, str(e)), + lifetime = 0, + title = catalog.i18nc("@info:title", "Error")) + m.show() + return False + if not success: + Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name) + m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Failed to export profile to {0}: Writer plugin reported failure.", file_name), + lifetime = 0, + title = catalog.i18nc("@info:title", "Error")) + m.show() + return False + m = Message(catalog.i18nc("@info:status Don't translate the XML tag !", "Exported profile to {0}", file_name), + title = catalog.i18nc("@info:title", "Export succeeded")) + m.show() + return True + + def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]: + """Gets the plugin object matching the criteria + + :param extension: + :param description: + :return: The plugin object matching the given extension and description. + """ + plugin_registry = PluginRegistry.getInstance() + for plugin_id, meta_data in self._getIOPlugins("profile_writer"): + for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write. + supported_extension = supported_type.get("extension", None) + if supported_extension == extension: # This plugin supports a file type with the same extension. + supported_description = supported_type.get("description", None) + if supported_description == description: # The description is also identical. Assume it's the same file type. + return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id)) + return None + + def importProfile(self, file_name: str) -> Dict[str, str]: + """Imports a profile from a file + + :param file_name: The full path and filename of the profile to import. + :return: Dict with a 'status' key containing the string 'ok' or 'error', + and a 'message' key containing a message for the user. + """ + + Logger.log("d", "Attempting to import profile %s", file_name) + if not file_name: + return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}: {1}", file_name, "Invalid path")} + + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() + if not global_stack: + return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Can't import profile from {0} before a printer is added.", file_name)} + container_tree = ContainerTree.getInstance() + + machine_extruders = [] + for position in sorted(global_stack.extruders): + machine_extruders.append(global_stack.extruders[position]) + + plugin_registry = PluginRegistry.getInstance() + extension = file_name.split(".")[-1] + + for plugin_id, meta_data in self._getIOPlugins("profile_reader"): + if meta_data["profile_reader"][0]["extension"] != extension: + continue + profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id)) + try: + profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. + except NoProfileException: + return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "No custom profile to import in file {0}", file_name)} + except Exception as e: + # Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None. + Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e)) + return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags !", "Failed to import profile from {0}:", file_name) + "\n" + str(e) + ""} + + if profile_or_list: + # Ensure it is always a list of profiles + if not isinstance(profile_or_list, list): + profile_or_list = [profile_or_list] + + # First check if this profile is suitable for this machine + global_profile = None + extruder_profiles = [] + if len(profile_or_list) == 1: + global_profile = profile_or_list[0] + else: + for profile in profile_or_list: + if not profile.getMetaDataEntry("position"): + global_profile = profile + else: + extruder_profiles.append(profile) + extruder_profiles = sorted(extruder_profiles, key = lambda x: int(x.getMetaDataEntry("position"))) + profile_or_list = [global_profile] + extruder_profiles + + if not global_profile: + Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name) + return { "status": "error", + "message": catalog.i18nc("@info:status Don't translate the XML tags !", "This profile {0} contains incorrect data, could not import it.", file_name)} + profile_definition = global_profile.getMetaDataEntry("definition") + + # Make sure we have a profile_definition in the file: + if profile_definition is None: + break + machine_definitions = self.findContainers(id = profile_definition) + if not machine_definitions: + Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition) + return {"status": "error", + "message": catalog.i18nc("@info:status Don't translate the XML tags !", "This profile {0} contains incorrect data, could not import it.", file_name) + } + machine_definition = machine_definitions[0] + + # Get the expected machine definition. + # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... + has_machine_quality = parseBool(machine_definition.getMetaDataEntry("has_machine_quality", "false")) + profile_definition = machine_definition.getMetaDataEntry("quality_definition", machine_definition.getId()) if has_machine_quality else "fdmprinter" + expected_machine_definition = container_tree.machines[global_stack.definition.getId()].quality_definition + + # And check if the profile_definition matches either one (showing error if not): + if profile_definition != expected_machine_definition: + Logger.log("d", "Profile {file_name} is for machine {profile_definition}, but the current active machine is {expected_machine_definition}. Changing profile's definition.".format(file_name = file_name, profile_definition = profile_definition, expected_machine_definition = expected_machine_definition)) + global_profile.setMetaDataEntry("definition", expected_machine_definition) + for extruder_profile in extruder_profiles: + extruder_profile.setMetaDataEntry("definition", expected_machine_definition) + + quality_name = global_profile.getName() + quality_type = global_profile.getMetaDataEntry("quality_type") + + name_seed = os.path.splitext(os.path.basename(file_name))[0] + new_name = self.uniqueName(name_seed) + + # Ensure it is always a list of profiles + if type(profile_or_list) is not list: + profile_or_list = [profile_or_list] + + # Make sure that there are also extruder stacks' quality_changes, not just one for the global stack + if len(profile_or_list) == 1: + global_profile = profile_or_list[0] + extruder_profiles = [] + for idx, extruder in enumerate(global_stack.extruders.values()): + profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1)) + profile = InstanceContainer(profile_id) + profile.setName(quality_name) + profile.setMetaDataEntry("setting_version", cura.CuraApplication.CuraApplication.SettingVersion) + profile.setMetaDataEntry("type", "quality_changes") + profile.setMetaDataEntry("definition", expected_machine_definition) + profile.setMetaDataEntry("quality_type", quality_type) + profile.setDirty(True) + if idx == 0: + # Move all per-extruder settings to the first extruder's quality_changes + for qc_setting_key in global_profile.getAllKeys(): + settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = global_profile.getProperty(qc_setting_key, "value") + + setting_definition = global_stack.getSettingDefinition(qc_setting_key) + if setting_definition is not None: + new_instance = SettingInstance(setting_definition, profile) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + profile.addInstance(new_instance) + profile.setDirty(True) + + global_profile.removeInstance(qc_setting_key, postpone_emit = True) + extruder_profiles.append(profile) + + for profile in extruder_profiles: + profile_or_list.append(profile) + + # Import all profiles + profile_ids_added = [] # type: List[str] + for profile_index, profile in enumerate(profile_or_list): + if profile_index == 0: + # This is assumed to be the global profile + profile_id = (cast(ContainerInterface, global_stack.getBottom()).getId() + "_" + name_seed).lower().replace(" ", "_") + + elif profile_index < len(machine_extruders) + 1: + # This is assumed to be an extruder profile + extruder_id = machine_extruders[profile_index - 1].definition.getId() + extruder_position = str(profile_index - 1) + if not profile.getMetaDataEntry("position"): + profile.setMetaDataEntry("position", extruder_position) + else: + profile.setMetaDataEntry("position", extruder_position) + profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_") + + else: # More extruders in the imported file than in the machine. + continue # Delete the additional profiles. + + result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) + if result is not None: + # Remove any profiles that did got added. + for profile_id in profile_ids_added: + self.removeContainer(profile_id) + + return {"status": "error", "message": catalog.i18nc( + "@info:status Don't translate the XML tag !", + "Failed to import profile from {0}:", + file_name) + " " + result} + profile_ids_added.append(profile.getId()) + return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} + + # This message is throw when the profile reader doesn't find any profile in the file + return {"status": "error", "message": catalog.i18nc("@info:status", "File {0} does not contain any valid profile.", file_name)} + + # If it hasn't returned by now, none of the plugins loaded the profile successfully. + return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)} + + @override(ContainerRegistry) + def load(self) -> None: + super().load() + self._registerSingleExtrusionMachinesExtruderStacks() + self._connectUpgradedExtruderStacksToMachines() + + @override(ContainerRegistry) + def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool: + """Check if the metadata for a container is okay before adding it. + + This overrides the one from UM.Settings.ContainerRegistry because we + also require that the setting_version is correct. + """ + + if metadata is None: + return False + if "setting_version" not in metadata: + return False + try: + if int(metadata["setting_version"]) != cura.CuraApplication.CuraApplication.SettingVersion: + return False + except ValueError: #Not parsable as int. + return False + return True + + def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]: + """Update an imported profile to match the current machine configuration. + + :param profile: The profile to configure. + :param id_seed: The base ID for the profile. May be changed so it does not conflict with existing containers. + :param new_name: The new name for the profile. + + :return: None if configuring was successful or an error message if an error occurred. + """ + + profile.setDirty(True) # Ensure the profiles are correctly saved + + new_id = self.createUniqueName("quality_changes", "", id_seed, catalog.i18nc("@label", "Custom profile")) + profile.setMetaDataEntry("id", new_id) + profile.setName(new_name) + + # Set the unique Id to the profile, so it's generating a new one even if the user imports the same profile + # It also solves an issue with importing profiles from G-Codes + profile.setMetaDataEntry("id", new_id) + profile.setMetaDataEntry("definition", machine_definition_id) + + if "type" in profile.getMetaData(): + profile.setMetaDataEntry("type", "quality_changes") + else: + profile.setMetaDataEntry("type", "quality_changes") + + quality_type = profile.getMetaDataEntry("quality_type") + if not quality_type: + return catalog.i18nc("@info:status", "Profile is missing a quality type.") + + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() + if global_stack is None: + return None + definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition + profile.setDefinition(definition_id) + + # Check to make sure the imported profile actually makes sense in context of the current configuration. + # This prevents issues where importing a "draft" profile for a machine without "draft" qualities would report as + # successfully imported but then fail to show up. + quality_group_dict = ContainerTree.getInstance().getCurrentQualityGroups() + # "not_supported" profiles can be imported. + if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict: + return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type) + + ContainerRegistry.getInstance().addContainer(profile) + + return None + + @override(ContainerRegistry) + def saveDirtyContainers(self) -> None: + # Lock file for "more" atomically loading and saving to/from config dir. + with self.lockFile(): + # Save base files first + for instance in self.findDirtyContainers(container_type=InstanceContainer): + if instance.getMetaDataEntry("removed"): + continue + if instance.getId() == instance.getMetaData().get("base_file"): + self.saveContainer(instance) + + for instance in self.findDirtyContainers(container_type=InstanceContainer): + if instance.getMetaDataEntry("removed"): + continue + self.saveContainer(instance) + + for stack in self.findContainerStacks(): + self.saveContainer(stack) + + def _getIOPlugins(self, io_type): + """Gets a list of profile writer plugins + + :return: List of tuples of (plugin_id, meta_data). + """ + plugin_registry = PluginRegistry.getInstance() + active_plugin_ids = plugin_registry.getActivePlugins() + + result = [] + for plugin_id in active_plugin_ids: + meta_data = plugin_registry.getMetaData(plugin_id) + if io_type in meta_data: + result.append( (plugin_id, meta_data) ) + return result + + def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]: + """Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.""" + + assert type(container) == ContainerStack + + container_type = container.getMetaDataEntry("type") + if container_type not in ("extruder_train", "machine"): + # It is not an extruder or machine, so do nothing with the stack + return container + + Logger.log("d", "Converting ContainerStack {stack} to {type}", stack = container.getId(), type = container_type) + + if container_type == "extruder_train": + new_stack = ExtruderStack.ExtruderStack(container.getId()) + else: + new_stack = GlobalStack.GlobalStack(container.getId()) + + container_contents = container.serialize() + new_stack.deserialize(container_contents) + + # Delete the old configuration file so we do not get double stacks + if os.path.isfile(container.getPath()): + os.remove(container.getPath()) + + return new_stack + + def _registerSingleExtrusionMachinesExtruderStacks(self) -> None: + machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"}) + for machine in machines: + extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId()) + if not extruder_stacks: + self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder") + + def _onContainerAdded(self, container: ContainerInterface) -> None: + # We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack + # for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack + # is added, we check to see if an extruder stack needs to be added. + if not isinstance(container, ContainerStack) or container.getMetaDataEntry("type") != "machine": + return + + machine_extruder_trains = container.getMetaDataEntry("machine_extruder_trains") + if machine_extruder_trains is not None and machine_extruder_trains != {"0": "fdmextruder"}: + return + + extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = container.getId()) + if not extruder_stacks: + self.addExtruderStackForSingleExtrusionMachine(container, "fdmextruder") + + # + # new_global_quality_changes is optional. It is only used in project loading for a scenario like this: + # - override the current machine + # - create new for custom quality profile + # new_global_quality_changes is the new global quality changes container in this scenario. + # create_new_ids indicates if new unique ids must be created + # + def addExtruderStackForSingleExtrusionMachine(self, machine, extruder_id, new_global_quality_changes = None, create_new_ids = True): + new_extruder_id = extruder_id + + application = cura.CuraApplication.CuraApplication.getInstance() + + extruder_definitions = self.findDefinitionContainers(id = new_extruder_id) + if not extruder_definitions: + Logger.log("w", "Could not find definition containers for extruder %s", new_extruder_id) + return + + extruder_definition = extruder_definitions[0] + unique_name = self.uniqueName(machine.getName() + " " + new_extruder_id) if create_new_ids else machine.getName() + " " + new_extruder_id + + extruder_stack = ExtruderStack.ExtruderStack(unique_name) + extruder_stack.setName(extruder_definition.getName()) + extruder_stack.setDefinition(extruder_definition) + extruder_stack.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) + + # create a new definition_changes container for the extruder stack + definition_changes_id = self.uniqueName(extruder_stack.getId() + "_settings") if create_new_ids else extruder_stack.getId() + "_settings" + definition_changes_name = definition_changes_id + definition_changes = InstanceContainer(definition_changes_id, parent = application) + definition_changes.setName(definition_changes_name) + definition_changes.setMetaDataEntry("setting_version", application.SettingVersion) + definition_changes.setMetaDataEntry("type", "definition_changes") + definition_changes.setMetaDataEntry("definition", extruder_definition.getId()) + + # move definition_changes settings if exist + for setting_key in definition_changes.getAllKeys(): + if machine.definition.getProperty(setting_key, "settable_per_extruder"): + setting_value = machine.definitionChanges.getProperty(setting_key, "value") + if setting_value is not None: + # move it to the extruder stack's definition_changes + setting_definition = machine.getSettingDefinition(setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + definition_changes.addInstance(new_instance) + definition_changes.setDirty(True) + + machine.definitionChanges.removeInstance(setting_key, postpone_emit = True) + + self.addContainer(definition_changes) + extruder_stack.setDefinitionChanges(definition_changes) + + # create empty user changes container otherwise + user_container_id = self.uniqueName(extruder_stack.getId() + "_user") if create_new_ids else extruder_stack.getId() + "_user" + user_container_name = user_container_id + user_container = InstanceContainer(user_container_id, parent = application) + user_container.setName(user_container_name) + user_container.setMetaDataEntry("type", "user") + user_container.setMetaDataEntry("machine", machine.getId()) + user_container.setMetaDataEntry("setting_version", application.SettingVersion) + user_container.setDefinition(machine.definition.getId()) + user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position")) + + if machine.userChanges: + # For the newly created extruder stack, we need to move all "per-extruder" settings to the user changes + # container to the extruder stack. + for user_setting_key in machine.userChanges.getAllKeys(): + settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = machine.getProperty(user_setting_key, "value") + + setting_definition = machine.getSettingDefinition(user_setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + user_container.addInstance(new_instance) + user_container.setDirty(True) + + machine.userChanges.removeInstance(user_setting_key, postpone_emit = True) + + self.addContainer(user_container) + extruder_stack.setUserChanges(user_container) + + empty_variant = application.empty_variant_container + empty_material = application.empty_material_container + empty_quality = application.empty_quality_container + + if machine.variant.getId() not in ("empty", "empty_variant"): + variant = machine.variant + else: + variant = empty_variant + extruder_stack.variant = variant + + if machine.material.getId() not in ("empty", "empty_material"): + material = machine.material + else: + material = empty_material + extruder_stack.material = material + + if machine.quality.getId() not in ("empty", "empty_quality"): + quality = machine.quality + else: + quality = empty_quality + extruder_stack.quality = quality + + machine_quality_changes = machine.qualityChanges + if new_global_quality_changes is not None: + machine_quality_changes = new_global_quality_changes + + if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): + extruder_quality_changes_container = self.findInstanceContainers(name = machine_quality_changes.getName(), extruder = extruder_id) + if extruder_quality_changes_container: + extruder_quality_changes_container = extruder_quality_changes_container[0] + + quality_changes_id = extruder_quality_changes_container.getId() + extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] + else: + # Some extruder quality_changes containers can be created at runtime as files in the qualities + # folder. Those files won't be loaded in the registry immediately. So we also need to search + # the folder to see if the quality_changes exists. + extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) + if extruder_quality_changes_container: + quality_changes_id = extruder_quality_changes_container.getId() + extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) + extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] + else: + # If we still cannot find a quality changes container for the extruder, create a new one + container_name = machine_quality_changes.getName() + container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name) + extruder_quality_changes_container = InstanceContainer(container_id, parent = application) + extruder_quality_changes_container.setName(container_name) + extruder_quality_changes_container.setMetaDataEntry("type", "quality_changes") + extruder_quality_changes_container.setMetaDataEntry("setting_version", application.SettingVersion) + extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) + extruder_quality_changes_container.setMetaDataEntry("quality_type", machine_quality_changes.getMetaDataEntry("quality_type")) + extruder_quality_changes_container.setMetaDataEntry("intent_category", "default") # Intent categories weren't a thing back then. + extruder_quality_changes_container.setDefinition(machine_quality_changes.getDefinition().getId()) + + self.addContainer(extruder_quality_changes_container) + extruder_stack.qualityChanges = extruder_quality_changes_container + + if not extruder_quality_changes_container: + Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", + machine_quality_changes.getName(), extruder_stack.getId()) + else: + # Move all per-extruder settings to the extruder's quality changes + for qc_setting_key in machine_quality_changes.getAllKeys(): + settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = machine_quality_changes.getProperty(qc_setting_key, "value") + + setting_definition = machine.getSettingDefinition(qc_setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + extruder_quality_changes_container.addInstance(new_instance) + extruder_quality_changes_container.setDirty(True) + + machine_quality_changes.removeInstance(qc_setting_key, postpone_emit=True) + else: + extruder_stack.qualityChanges = self.findInstanceContainers(id = "empty_quality_changes")[0] + + self.addContainer(extruder_stack) + + # Also need to fix the other qualities that are suitable for this machine. Those quality changes may still have + # per-extruder settings in the container for the machine instead of the extruder. + if machine_quality_changes.getId() not in ("empty", "empty_quality_changes"): + quality_changes_machine_definition_id = machine_quality_changes.getDefinition().getId() + else: + whole_machine_definition = machine.definition + machine_entry = machine.definition.getMetaDataEntry("machine") + if machine_entry is not None: + container_registry = ContainerRegistry.getInstance() + whole_machine_definition = container_registry.findDefinitionContainers(id = machine_entry)[0] + + quality_changes_machine_definition_id = "fdmprinter" + if whole_machine_definition.getMetaDataEntry("has_machine_quality"): + quality_changes_machine_definition_id = machine.definition.getMetaDataEntry("quality_definition", + whole_machine_definition.getId()) + qcs = self.findInstanceContainers(type = "quality_changes", definition = quality_changes_machine_definition_id) + qc_groups = {} # map of qc names -> qc containers + for qc in qcs: + qc_name = qc.getName() + if qc_name not in qc_groups: + qc_groups[qc_name] = [] + qc_groups[qc_name].append(qc) + # Try to find from the quality changes cura directory too + quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) + if quality_changes_container: + qc_groups[qc_name].append(quality_changes_container) + + for qc_name, qc_list in qc_groups.items(): + qc_dict = {"global": None, "extruders": []} + for qc in qc_list: + extruder_position = qc.getMetaDataEntry("position") + if extruder_position is not None: + qc_dict["extruders"].append(qc) + else: + qc_dict["global"] = qc + if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1: + # Move per-extruder settings + for qc_setting_key in qc_dict["global"].getAllKeys(): + settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") + if settable_per_extruder: + setting_value = qc_dict["global"].getProperty(qc_setting_key, "value") + + setting_definition = machine.getSettingDefinition(qc_setting_key) + new_instance = SettingInstance(setting_definition, definition_changes) + new_instance.setProperty("value", setting_value) + new_instance.resetState() # Ensure that the state is not seen as a user state. + qc_dict["extruders"][0].addInstance(new_instance) + qc_dict["extruders"][0].setDirty(True) + + qc_dict["global"].removeInstance(qc_setting_key, postpone_emit=True) + + # Set next stack at the end + extruder_stack.setNextStack(machine) + + return extruder_stack + + def _findQualityChangesContainerInCuraFolder(self, name: str) -> Optional[InstanceContainer]: + quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer) + + instance_container = None + + for item in os.listdir(quality_changes_dir): + file_path = os.path.join(quality_changes_dir, item) + if not os.path.isfile(file_path): + continue + + parser = configparser.ConfigParser(interpolation = None) + try: + parser.read([file_path]) + except Exception: + # Skip, it is not a valid stack file + continue + + if not parser.has_option("general", "name"): + continue + + if parser["general"]["name"] == name: + # Load the container + container_id = os.path.basename(file_path).replace(".inst.cfg", "") + if self.findInstanceContainers(id = container_id): + # This container is already in the registry, skip it + continue + + instance_container = InstanceContainer(container_id) + with open(file_path, "r", encoding = "utf-8") as f: + serialized = f.read() + try: + instance_container.deserialize(serialized, file_path) + except ContainerFormatError: + Logger.logException("e", "Unable to deserialize InstanceContainer %s", file_path) + continue + self.addContainer(instance_container) + break + + return instance_container + + # Fix the extruders that were upgraded to ExtruderStack instances during addContainer. + # The stacks are now responsible for setting the next stack on deserialize. However, + # due to problems with loading order, some stacks may not have the proper next stack + # set after upgrading, because the proper global stack was not yet loaded. This method + # makes sure those extruders also get the right stack set. + def _connectUpgradedExtruderStacksToMachines(self) -> None: + extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack) + for extruder_stack in extruder_stacks: + if extruder_stack.getNextStack(): + # Has the right next stack, so ignore it. + continue + + machines = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack.getMetaDataEntry("machine", "")) + if machines: + extruder_stack.setNextStack(machines[0]) + else: + Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId()) + + # Override just for the type. + @classmethod + @override(ContainerRegistry) + def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry": + return cast(CuraContainerRegistry, super().getInstance(*args, **kwargs)) diff --git a/cura/Settings/CuraContainerStack.py b/cura/Settings/CuraContainerStack.py index 1455e140a8..1551e46ef2 100755 --- a/cura/Settings/CuraContainerStack.py +++ b/cura/Settings/CuraContainerStack.py @@ -18,25 +18,27 @@ from cura.Settings import cura_empty_instance_containers from . import Exceptions -## Base class for Cura related stacks that want to enforce certain containers are available. -# -# This class makes sure that the stack has the following containers set: user changes, quality -# changes, quality, material, variant, definition changes and finally definition. Initially, -# these will be equal to the empty instance container. -# -# The container types are determined based on the following criteria: -# - user: An InstanceContainer with the metadata entry "type" set to "user". -# - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes". -# - quality: An InstanceContainer with the metadata entry "type" set to "quality". -# - material: An InstanceContainer with the metadata entry "type" set to "material". -# - variant: An InstanceContainer with the metadata entry "type" set to "variant". -# - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes". -# - definition: A DefinitionContainer. -# -# Internally, this class ensures the mentioned containers are always there and kept in a specific order. -# This also means that operations on the stack that modifies the container ordering is prohibited and -# will raise an exception. class CuraContainerStack(ContainerStack): + """Base class for Cura related stacks that want to enforce certain containers are available. + + This class makes sure that the stack has the following containers set: user changes, quality + changes, quality, material, variant, definition changes and finally definition. Initially, + these will be equal to the empty instance container. + + The container types are determined based on the following criteria: + - user: An InstanceContainer with the metadata entry "type" set to "user". + - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes". + - quality: An InstanceContainer with the metadata entry "type" set to "quality". + - material: An InstanceContainer with the metadata entry "type" set to "material". + - variant: An InstanceContainer with the metadata entry "type" set to "variant". + - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes". + - definition: A DefinitionContainer. + + Internally, this class ensures the mentioned containers are always there and kept in a specific order. + This also means that operations on the stack that modifies the container ordering is prohibited and + will raise an exception. + """ + def __init__(self, container_id: str) -> None: super().__init__(container_id) @@ -61,101 +63,131 @@ class CuraContainerStack(ContainerStack): # This is emitted whenever the containersChanged signal from the ContainerStack base class is emitted. pyqtContainersChanged = pyqtSignal() - ## Set the user changes container. - # - # \param new_user_changes The new user changes container. It is expected to have a "type" metadata entry with the value "user". def setUserChanges(self, new_user_changes: InstanceContainer) -> None: + """Set the user changes container. + + :param new_user_changes: The new user changes container. It is expected to have a "type" metadata entry with the value "user". + """ + self.replaceContainer(_ContainerIndexes.UserChanges, new_user_changes) - ## Get the user changes container. - # - # \return The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged) def userChanges(self) -> InstanceContainer: + """Get the user changes container. + + :return: The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.UserChanges]) - ## Set the quality changes container. - # - # \param new_quality_changes The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes". def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None: + """Set the quality changes container. + + :param new_quality_changes: The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes". + """ + self.replaceContainer(_ContainerIndexes.QualityChanges, new_quality_changes, postpone_emit = postpone_emit) - ## Get the quality changes container. - # - # \return The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged) def qualityChanges(self) -> InstanceContainer: + """Get the quality changes container. + + :return: The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.QualityChanges]) - ## Set the intent container. - # - # \param new_intent The new intent container. It is expected to have a "type" metadata entry with the value "intent". def setIntent(self, new_intent: InstanceContainer, postpone_emit: bool = False) -> None: + """Set the intent container. + + :param new_intent: The new intent container. It is expected to have a "type" metadata entry with the value "intent". + """ + self.replaceContainer(_ContainerIndexes.Intent, new_intent, postpone_emit = postpone_emit) - ## Get the quality container. - # - # \return The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setIntent, notify = pyqtContainersChanged) def intent(self) -> InstanceContainer: + """Get the quality container. + + :return: The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Intent]) - ## Set the quality container. - # - # \param new_quality The new quality container. It is expected to have a "type" metadata entry with the value "quality". def setQuality(self, new_quality: InstanceContainer, postpone_emit: bool = False) -> None: + """Set the quality container. + + :param new_quality: The new quality container. It is expected to have a "type" metadata entry with the value "quality". + """ + self.replaceContainer(_ContainerIndexes.Quality, new_quality, postpone_emit = postpone_emit) - ## Get the quality container. - # - # \return The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged) def quality(self) -> InstanceContainer: + """Get the quality container. + + :return: The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Quality]) - ## Set the material container. - # - # \param new_material The new material container. It is expected to have a "type" metadata entry with the value "material". def setMaterial(self, new_material: InstanceContainer, postpone_emit: bool = False) -> None: + """Set the material container. + + :param new_material: The new material container. It is expected to have a "type" metadata entry with the value "material". + """ + self.replaceContainer(_ContainerIndexes.Material, new_material, postpone_emit = postpone_emit) - ## Get the material container. - # - # \return The material container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged) def material(self) -> InstanceContainer: + """Get the material container. + + :return: The material container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Material]) - ## Set the variant container. - # - # \param new_variant The new variant container. It is expected to have a "type" metadata entry with the value "variant". def setVariant(self, new_variant: InstanceContainer) -> None: + """Set the variant container. + + :param new_variant: The new variant container. It is expected to have a "type" metadata entry with the value "variant". + """ + self.replaceContainer(_ContainerIndexes.Variant, new_variant) - ## Get the variant container. - # - # \return The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged) def variant(self) -> InstanceContainer: + """Get the variant container. + + :return: The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.Variant]) - ## Set the definition changes container. - # - # \param new_definition_changes The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes". def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None: + """Set the definition changes container. + + :param new_definition_changes: The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes". + """ + self.replaceContainer(_ContainerIndexes.DefinitionChanges, new_definition_changes) - ## Get the definition changes container. - # - # \return The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. @pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged) def definitionChanges(self) -> InstanceContainer: + """Get the definition changes container. + + :return: The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. + """ + return cast(InstanceContainer, self._containers[_ContainerIndexes.DefinitionChanges]) - ## Set the definition container. - # - # \param new_definition The new definition container. It is expected to have a "type" metadata entry with the value "definition". def setDefinition(self, new_definition: DefinitionContainerInterface) -> None: + """Set the definition container. + + :param new_definition: The new definition container. It is expected to have a "type" metadata entry with the value "definition". + """ + self.replaceContainer(_ContainerIndexes.Definition, new_definition) def getDefinition(self) -> "DefinitionContainer": @@ -171,14 +203,16 @@ class CuraContainerStack(ContainerStack): def getTop(self) -> "InstanceContainer": return self.userChanges - ## Check whether the specified setting has a 'user' value. - # - # A user value here is defined as the setting having a value in either - # the UserChanges or QualityChanges container. - # - # \return True if the setting has a user value, False if not. @pyqtSlot(str, result = bool) def hasUserValue(self, key: str) -> bool: + """Check whether the specified setting has a 'user' value. + + A user value here is defined as the setting having a value in either + the UserChanges or QualityChanges container. + + :return: True if the setting has a user value, False if not. + """ + if self._containers[_ContainerIndexes.UserChanges].hasProperty(key, "value"): return True @@ -187,51 +221,61 @@ class CuraContainerStack(ContainerStack): return False - ## Set a property of a setting. - # - # This will set a property of a specified setting. Since the container stack does not contain - # any settings itself, it is required to specify a container to set the property on. The target - # container is matched by container type. - # - # \param key The key of the setting to set. - # \param property_name The name of the property to set. - # \param new_value The new value to set the property to. def setProperty(self, key: str, property_name: str, property_value: Any, container: "ContainerInterface" = None, set_from_cache: bool = False) -> None: + """Set a property of a setting. + + This will set a property of a specified setting. Since the container stack does not contain + any settings itself, it is required to specify a container to set the property on. The target + container is matched by container type. + + :param key: The key of the setting to set. + :param property_name: The name of the property to set. + :param new_value: The new value to set the property to. + """ + container_index = _ContainerIndexes.UserChanges self._containers[container_index].setProperty(key, property_name, property_value, container, set_from_cache) - ## Overridden from ContainerStack - # - # Since we have a fixed order of containers in the stack and this method would modify the container - # ordering, we disallow this operation. @override(ContainerStack) def addContainer(self, container: ContainerInterface) -> None: + """Overridden from ContainerStack + + Since we have a fixed order of containers in the stack and this method would modify the container + ordering, we disallow this operation. + """ + raise Exceptions.InvalidOperationError("Cannot add a container to Global stack") - ## Overridden from ContainerStack - # - # Since we have a fixed order of containers in the stack and this method would modify the container - # ordering, we disallow this operation. @override(ContainerStack) def insertContainer(self, index: int, container: ContainerInterface) -> None: + """Overridden from ContainerStack + + Since we have a fixed order of containers in the stack and this method would modify the container + ordering, we disallow this operation. + """ + raise Exceptions.InvalidOperationError("Cannot insert a container into Global stack") - ## Overridden from ContainerStack - # - # Since we have a fixed order of containers in the stack and this method would modify the container - # ordering, we disallow this operation. @override(ContainerStack) def removeContainer(self, index: int = 0) -> None: + """Overridden from ContainerStack + + Since we have a fixed order of containers in the stack and this method would modify the container + ordering, we disallow this operation. + """ + raise Exceptions.InvalidOperationError("Cannot remove a container from Global stack") - ## Overridden from ContainerStack - # - # Replaces the container at the specified index with another container. - # This version performs checks to make sure the new container has the expected metadata and type. - # - # \throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type. @override(ContainerStack) def replaceContainer(self, index: int, container: ContainerInterface, postpone_emit: bool = False) -> None: + """Overridden from ContainerStack + + Replaces the container at the specified index with another container. + This version performs checks to make sure the new container has the expected metadata and type. + + :throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type. + """ + expected_type = _ContainerIndexes.IndexTypeMap[index] if expected_type == "definition": if not isinstance(container, DefinitionContainer): @@ -245,16 +289,18 @@ class CuraContainerStack(ContainerStack): super().replaceContainer(index, container, postpone_emit) - ## Overridden from ContainerStack - # - # This deserialize will make sure the internal list of containers matches with what we expect. - # It will first check to see if the container at a certain index already matches with what we - # expect. If it does not, it will search for a matching container with the correct type. Should - # no container with the correct type be found, it will use the empty container. - # - # \throws InvalidContainerStackError Raised when no definition can be found for the stack. @override(ContainerStack) def deserialize(self, serialized: str, file_name: Optional[str] = None) -> str: + """Overridden from ContainerStack + + This deserialize will make sure the internal list of containers matches with what we expect. + It will first check to see if the container at a certain index already matches with what we + expect. If it does not, it will search for a matching container with the correct type. Should + no container with the correct type be found, it will use the empty container. + + :raise InvalidContainerStackError: Raised when no definition can be found for the stack. + """ + # update the serialized data first serialized = super().deserialize(serialized, file_name) @@ -298,10 +344,9 @@ class CuraContainerStack(ContainerStack): ## TODO; Deserialize the containers. return serialized - ## protected: - - # Helper to make sure we emit a PyQt signal on container changes. def _onContainersChanged(self, container: Any) -> None: + """Helper to make sure we emit a PyQt signal on container changes.""" + Application.getInstance().callLater(self.pyqtContainersChanged.emit) # Helper that can be overridden to get the "machine" definition, that is, the definition that defines the machine @@ -309,16 +354,18 @@ class CuraContainerStack(ContainerStack): def _getMachineDefinition(self) -> DefinitionContainer: return self.definition - ## Find the ID that should be used when searching for instance containers for a specified definition. - # - # This handles the situation where the definition specifies we should use a different definition when - # searching for instance containers. - # - # \param machine_definition The definition to find the "quality definition" for. - # - # \return The ID of the definition container to use when searching for instance containers. @classmethod def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str: + """Find the ID that should be used when searching for instance containers for a specified definition. + + This handles the situation where the definition specifies we should use a different definition when + searching for instance containers. + + :param machine_definition: The definition to find the "quality definition" for. + + :return: The ID of the definition container to use when searching for instance containers. + """ + quality_definition = machine_definition.getMetaDataEntry("quality_definition") if not quality_definition: return machine_definition.id #type: ignore @@ -330,17 +377,18 @@ class CuraContainerStack(ContainerStack): return cls._findInstanceContainerDefinitionId(definitions[0]) - ## getProperty for extruder positions, with translation from -1 to default extruder number def getExtruderPositionValueWithDefault(self, key): + """getProperty for extruder positions, with translation from -1 to default extruder number""" + value = self.getProperty(key, "value") if value == -1: value = int(Application.getInstance().getMachineManager().defaultExtruderPosition) return value -## private: -# Private helper class to keep track of container positions and their types. class _ContainerIndexes: + """Private helper class to keep track of container positions and their types.""" + UserChanges = 0 QualityChanges = 1 Intent = 2 diff --git a/cura/Settings/CuraStackBuilder.py b/cura/Settings/CuraStackBuilder.py index 257af78ecc..5dc32f6e24 100644 --- a/cura/Settings/CuraStackBuilder.py +++ b/cura/Settings/CuraStackBuilder.py @@ -13,17 +13,20 @@ from .GlobalStack import GlobalStack from .ExtruderStack import ExtruderStack -## Contains helper functions to create new machines. class CuraStackBuilder: + """Contains helper functions to create new machines.""" + - ## Create a new instance of a machine. - # - # \param name The name of the new machine. - # \param definition_id The ID of the machine definition to use. - # - # \return The new global stack or None if an error occurred. @classmethod def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]: + """Create a new instance of a machine. + + :param name: The name of the new machine. + :param definition_id: The ID of the machine definition to use. + + :return: The new global stack or None if an error occurred. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() @@ -71,12 +74,14 @@ class CuraStackBuilder: return new_global_stack - ## Create a default Extruder Stack - # - # \param global_stack The global stack this extruder refers to. - # \param extruder_position The position of the current extruder. @classmethod def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None: + """Create a default Extruder Stack + + :param global_stack: The global stack this extruder refers to. + :param extruder_position: The position of the current extruder. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() @@ -120,17 +125,6 @@ class CuraStackBuilder: registry.addContainer(new_extruder) - ## Create a new Extruder stack - # - # \param new_stack_id The ID of the new stack. - # \param extruder_definition The definition to base the new stack on. - # \param machine_definition_id The ID of the machine definition to use for the user container. - # \param position The position the extruder occupies in the machine. - # \param variant_container The variant selected for the current extruder. - # \param material_container The material selected for the current extruder. - # \param quality_container The quality selected for the current extruder. - # - # \return A new Extruder stack instance with the specified parameters. @classmethod def createExtruderStack(cls, new_stack_id: str, extruder_definition: DefinitionContainerInterface, machine_definition_id: str, @@ -139,6 +133,19 @@ class CuraStackBuilder: material_container: "InstanceContainer", quality_container: "InstanceContainer") -> ExtruderStack: + """Create a new Extruder stack + + :param new_stack_id: The ID of the new stack. + :param extruder_definition: The definition to base the new stack on. + :param machine_definition_id: The ID of the machine definition to use for the user container. + :param position: The position the extruder occupies in the machine. + :param variant_container: The variant selected for the current extruder. + :param material_container: The material selected for the current extruder. + :param quality_container: The quality selected for the current extruder. + + :return: A new Extruder stack instance with the specified parameters. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() @@ -167,29 +174,23 @@ class CuraStackBuilder: return stack - ## Create a new Global stack - # - # \param new_stack_id The ID of the new stack. - # \param definition The definition to base the new stack on. - # \param kwargs You can add keyword arguments to specify IDs of containers to use for a specific type, for example "variant": "0.4mm" - # - # \return A new Global stack instance with the specified parameters. - - ## Create a new Global stack - # - # \param new_stack_id The ID of the new stack. - # \param definition The definition to base the new stack on. - # \param variant_container The variant selected for the current stack. - # \param material_container The material selected for the current stack. - # \param quality_container The quality selected for the current stack. - # - # \return A new Global stack instance with the specified parameters. @classmethod def createGlobalStack(cls, new_stack_id: str, definition: DefinitionContainerInterface, variant_container: "InstanceContainer", material_container: "InstanceContainer", quality_container: "InstanceContainer") -> GlobalStack: + """Create a new Global stack + + :param new_stack_id: The ID of the new stack. + :param definition: The definition to base the new stack on. + :param variant_container: The variant selected for the current stack. + :param material_container: The material selected for the current stack. + :param quality_container: The quality selected for the current stack. + + :return: A new Global stack instance with the specified parameters. + """ + from cura.CuraApplication import CuraApplication application = CuraApplication.getInstance() registry = application.getContainerRegistry() diff --git a/cura/Settings/Exceptions.py b/cura/Settings/Exceptions.py index 0a869cf922..fbb130417c 100644 --- a/cura/Settings/Exceptions.py +++ b/cura/Settings/Exceptions.py @@ -2,21 +2,25 @@ # Cura is released under the terms of the LGPLv3 or higher. -## Raised when trying to perform an operation like add on a stack that does not allow that. class InvalidOperationError(Exception): + """Raised when trying to perform an operation like add on a stack that does not allow that.""" + pass -## Raised when trying to replace a container with a container that does not have the expected type. class InvalidContainerError(Exception): + """Raised when trying to replace a container with a container that does not have the expected type.""" + pass -## Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders. class TooManyExtrudersError(Exception): + """Raised when trying to add an extruder to a Global stack that already has the maximum number of extruders.""" + pass -## Raised when an extruder has no next stack set. class NoGlobalStackError(Exception): + """Raised when an extruder has no next stack set.""" + pass diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index 4610e6a454..f9ffde4872 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -19,13 +19,15 @@ if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack -## Manages all existing extruder stacks. -# -# This keeps a list of extruder stacks for each machine. class ExtruderManager(QObject): + """Manages all existing extruder stacks. + + This keeps a list of extruder stacks for each machine. + """ - ## Registers listeners and such to listen to changes to the extruders. def __init__(self, parent = None): + """Registers listeners and such to listen to changes to the extruders.""" + if ExtruderManager.__instance is not None: raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) ExtruderManager.__instance = self @@ -43,20 +45,22 @@ class ExtruderManager(QObject): Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) - ## Signal to notify other components when the list of extruders for a machine definition changes. extrudersChanged = pyqtSignal(QVariant) + """Signal to notify other components when the list of extruders for a machine definition changes.""" - ## Notify when the user switches the currently active extruder. activeExtruderChanged = pyqtSignal() + """Notify when the user switches the currently active extruder.""" - ## Gets the unique identifier of the currently active extruder stack. - # - # The currently active extruder stack is the stack that is currently being - # edited. - # - # \return The unique ID of the currently active extruder stack. @pyqtProperty(str, notify = activeExtruderChanged) def activeExtruderStackId(self) -> Optional[str]: + """Gets the unique identifier of the currently active extruder stack. + + The currently active extruder stack is the stack that is currently being + edited. + + :return: The unique ID of the currently active extruder stack. + """ + if not self._application.getGlobalContainerStack(): return None # No active machine, so no active extruder. try: @@ -64,9 +68,10 @@ class ExtruderManager(QObject): except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong. return None - ## Gets a dict with the extruder stack ids with the extruder number as the key. @pyqtProperty("QVariantMap", notify = extrudersChanged) def extruderIds(self) -> Dict[str, str]: + """Gets a dict with the extruder stack ids with the extruder number as the key.""" + extruder_stack_ids = {} # type: Dict[str, str] global_container_stack = self._application.getGlobalContainerStack() @@ -75,11 +80,13 @@ class ExtruderManager(QObject): return extruder_stack_ids - ## Changes the active extruder by index. - # - # \param index The index of the new active extruder. @pyqtSlot(int) def setActiveExtruderIndex(self, index: int) -> None: + """Changes the active extruder by index. + + :param index: The index of the new active extruder. + """ + if self._active_extruder_index != index: self._active_extruder_index = index self.activeExtruderChanged.emit() @@ -88,12 +95,13 @@ class ExtruderManager(QObject): def activeExtruderIndex(self) -> int: return self._active_extruder_index - ## Emitted whenever the selectedObjectExtruders property changes. selectedObjectExtrudersChanged = pyqtSignal() + """Emitted whenever the selectedObjectExtruders property changes.""" - ## Provides a list of extruder IDs used by the current selected objects. @pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged) def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]: + """Provides a list of extruder IDs used by the current selected objects.""" + if not self._selected_object_extruders: object_extruders = set() @@ -122,11 +130,13 @@ class ExtruderManager(QObject): return self._selected_object_extruders - ## Reset the internal list used for the selectedObjectExtruders property - # - # This will trigger a recalculation of the extruders used for the - # selection. def resetSelectedObjectExtruders(self) -> None: + """Reset the internal list used for the selectedObjectExtruders property + + This will trigger a recalculation of the extruders used for the + selection. + """ + self._selected_object_extruders = [] self.selectedObjectExtrudersChanged.emit() @@ -134,8 +144,9 @@ class ExtruderManager(QObject): def getActiveExtruderStack(self) -> Optional["ExtruderStack"]: return self.getExtruderStack(self.activeExtruderIndex) - ## Get an extruder stack by index def getExtruderStack(self, index) -> Optional["ExtruderStack"]: + """Get an extruder stack by index""" + global_container_stack = self._application.getGlobalContainerStack() if global_container_stack: if global_container_stack.getId() in self._extruder_trains: @@ -162,12 +173,14 @@ class ExtruderManager(QObject): if changed: self.extrudersChanged.emit(machine_id) - ## Gets a property of a setting for all extruders. - # - # \param setting_key \type{str} The setting to get the property of. - # \param property \type{str} The property to get. - # \return \type{List} the list of results def getAllExtruderSettings(self, setting_key: str, prop: str) -> List[Any]: + """Gets a property of a setting for all extruders. + + :param setting_key: :type{str} The setting to get the property of. + :param prop: :type{str} The property to get. + :return: :type{List} the list of results + """ + result = [] for extruder_stack in self.getActiveExtruderStacks(): @@ -182,17 +195,19 @@ class ExtruderManager(QObject): else: return value - ## Gets the extruder stacks that are actually being used at the moment. - # - # An extruder stack is being used if it is the extruder to print any mesh - # with, or if it is the support infill extruder, the support interface - # extruder, or the bed adhesion extruder. - # - # If there are no extruders, this returns the global stack as a singleton - # list. - # - # \return A list of extruder stacks. def getUsedExtruderStacks(self) -> List["ExtruderStack"]: + """Gets the extruder stacks that are actually being used at the moment. + + An extruder stack is being used if it is the extruder to print any mesh + with, or if it is the support infill extruder, the support interface + extruder, or the bed adhesion extruder. + + If there are no extruders, this returns the global stack as a singleton + list. + + :return: A list of extruder stacks. + """ + global_stack = self._application.getGlobalContainerStack() container_registry = ContainerRegistry.getInstance() @@ -277,11 +292,13 @@ class ExtruderManager(QObject): Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids) return [] - ## Get the extruder that the print will start with. - # - # This should mirror the implementation in CuraEngine of - # ``FffGcodeWriter::getStartExtruder()``. def getInitialExtruderNr(self) -> int: + """Get the extruder that the print will start with. + + This should mirror the implementation in CuraEngine of + ``FffGcodeWriter::getStartExtruder()``. + """ + application = cura.CuraApplication.CuraApplication.getInstance() global_stack = application.getGlobalContainerStack() @@ -296,28 +313,34 @@ class ExtruderManager(QObject): # REALLY no adhesion? Use the first used extruder. return self.getUsedExtruderStacks()[0].getProperty("extruder_nr", "value") - ## Removes the container stack and user profile for the extruders for a specific machine. - # - # \param machine_id The machine to remove the extruders for. def removeMachineExtruders(self, machine_id: str) -> None: + """Removes the container stack and user profile for the extruders for a specific machine. + + :param machine_id: The machine to remove the extruders for. + """ + for extruder in self.getMachineExtruders(machine_id): ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId()) ContainerRegistry.getInstance().removeContainer(extruder.getId()) if machine_id in self._extruder_trains: del self._extruder_trains[machine_id] - ## Returns extruders for a specific machine. - # - # \param machine_id The machine to get the extruders of. def getMachineExtruders(self, machine_id: str) -> List["ExtruderStack"]: + """Returns extruders for a specific machine. + + :param machine_id: The machine to get the extruders of. + """ + if machine_id not in self._extruder_trains: return [] return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]] - ## Returns the list of active extruder stacks, taking into account the machine extruder count. - # - # \return \type{List[ContainerStack]} a list of def getActiveExtruderStacks(self) -> List["ExtruderStack"]: + """Returns the list of active extruder stacks, taking into account the machine extruder count. + + :return: :type{List[ContainerStack]} a list of + """ + global_stack = self._application.getGlobalContainerStack() if not global_stack: return [] @@ -329,8 +352,9 @@ class ExtruderManager(QObject): self.resetSelectedObjectExtruders() - ## Adds the extruders to the selected machine. def addMachineExtruders(self, global_stack: GlobalStack) -> None: + """Adds the extruders to the selected machine.""" + extruders_changed = False container_registry = ContainerRegistry.getInstance() global_stack_id = global_stack.getId() @@ -396,26 +420,30 @@ class ExtruderManager(QObject): raise IndexError(msg) extruder_stack_0.definition = extruder_definition - ## Get all extruder values for a certain setting. - # - # This is exposed to qml for display purposes - # - # \param key The key of the setting to retrieve values for. - # - # \return String representing the extruder values @pyqtSlot(str, result="QVariant") def getInstanceExtruderValues(self, key: str) -> List: + """Get all extruder values for a certain setting. + + This is exposed to qml for display purposes + + :param key: The key of the setting to retrieve values for. + + :return: String representing the extruder values + """ + return self._application.getCuraFormulaFunctions().getValuesInAllExtruders(key) - ## Get the resolve value or value for a given key - # - # This is the effective value for a given key, it is used for values in the global stack. - # This is exposed to SettingFunction to use in value functions. - # \param key The key of the setting to get the value of. - # - # \return The effective value @staticmethod def getResolveOrValue(key: str) -> Any: + """Get the resolve value or value for a given key + + This is the effective value for a given key, it is used for values in the global stack. + This is exposed to SettingFunction to use in value functions. + :param key: The key of the setting to get the value of. + + :return: The effective value + """ + global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) resolved_value = global_stack.getProperty(key, "value") diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index 5d4b3e38b1..7369838baa 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -22,10 +22,9 @@ if TYPE_CHECKING: from cura.Settings.GlobalStack import GlobalStack -## Represents an Extruder and its related containers. -# -# class ExtruderStack(CuraContainerStack): + """Represents an Extruder and its related containers.""" + def __init__(self, container_id: str) -> None: super().__init__(container_id) @@ -35,11 +34,13 @@ class ExtruderStack(CuraContainerStack): enabledChanged = pyqtSignal() - ## Overridden from ContainerStack - # - # This will set the next stack and ensure that we register this stack as an extruder. @override(ContainerStack) def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None: + """Overridden from ContainerStack + + This will set the next stack and ensure that we register this stack as an extruder. + """ + super().setNextStack(stack) stack.addExtruder(self) self.setMetaDataEntry("machine", stack.id) @@ -71,11 +72,13 @@ class ExtruderStack(CuraContainerStack): compatibleMaterialDiameterChanged = pyqtSignal() - ## Return the filament diameter that the machine requires. - # - # If the machine has no requirement for the diameter, -1 is returned. - # \return The filament diameter for the printer def getCompatibleMaterialDiameter(self) -> float: + """Return the filament diameter that the machine requires. + + If the machine has no requirement for the diameter, -1 is returned. + :return: The filament diameter for the printer + """ + context = PropertyEvaluationContext(self) context.context["evaluate_from_container_index"] = _ContainerIndexes.Variant @@ -97,31 +100,35 @@ class ExtruderStack(CuraContainerStack): approximateMaterialDiameterChanged = pyqtSignal() - ## Return the approximate filament diameter that the machine requires. - # - # The approximate material diameter is the material diameter rounded to - # the nearest millimetre. - # - # If the machine has no requirement for the diameter, -1 is returned. - # - # \return The approximate filament diameter for the printer def getApproximateMaterialDiameter(self) -> float: + """Return the approximate filament diameter that the machine requires. + + The approximate material diameter is the material diameter rounded to + the nearest millimetre. + + If the machine has no requirement for the diameter, -1 is returned. + + :return: The approximate filament diameter for the printer + """ + return round(self.getCompatibleMaterialDiameter()) approximateMaterialDiameter = pyqtProperty(float, fget = getApproximateMaterialDiameter, notify = approximateMaterialDiameterChanged) - ## Overridden from ContainerStack - # - # It will perform a few extra checks when trying to get properties. - # - # The two extra checks it currently does is to ensure a next stack is set and to bypass - # the extruder when the property is not settable per extruder. - # - # \throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without - # having a next stack set. @override(ContainerStack) def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: + """Overridden from ContainerStack + + It will perform a few extra checks when trying to get properties. + + The two extra checks it currently does is to ensure a next stack is set and to bypass + the extruder when the property is not settable per extruder. + + :throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without + having a next stack set. + """ + if not self._next_stack: raise Exceptions.NoGlobalStackError("Extruder {id} is missing the next stack!".format(id = self.id)) diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index d3a8842aa3..e020221187 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -29,9 +29,9 @@ if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack -## Represents the Global or Machine stack and its related containers. -# class GlobalStack(CuraContainerStack): + """Represents the Global or Machine stack and its related containers.""" + def __init__(self, container_id: str) -> None: super().__init__(container_id) @@ -58,12 +58,14 @@ class GlobalStack(CuraContainerStack): extrudersChanged = pyqtSignal() configuredConnectionTypesChanged = pyqtSignal() - ## Get the list of extruders of this stack. - # - # \return The extruders registered with this stack. @pyqtProperty("QVariantMap", notify = extrudersChanged) @deprecated("Please use extruderList instead.", "4.4") def extruders(self) -> Dict[str, "ExtruderStack"]: + """Get the list of extruders of this stack. + + :return: The extruders registered with this stack. + """ + return self._extruders @pyqtProperty("QVariantList", notify = extrudersChanged) @@ -86,16 +88,18 @@ class GlobalStack(CuraContainerStack): def getLoadingPriority(cls) -> int: return 2 - ## The configured connection types can be used to find out if the global - # stack is configured to be connected with a printer, without having to - # know all the details as to how this is exactly done (and without - # actually setting the stack to be active). - # - # This data can then in turn also be used when the global stack is active; - # If we can't get a network connection, but it is configured to have one, - # we can display a different icon to indicate the difference. @pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged) def configuredConnectionTypes(self) -> List[int]: + """The configured connection types can be used to find out if the global + + stack is configured to be connected with a printer, without having to + know all the details as to how this is exactly done (and without + actually setting the stack to be active). + + This data can then in turn also be used when the global stack is active; + If we can't get a network connection, but it is configured to have one, + we can display a different icon to indicate the difference. + """ # Requesting it from the metadata actually gets them as strings (as that's what you get from serializing). # But we do want them returned as a list of ints (so the rest of the code can directly compare) connection_types = self.getMetaDataEntry("connection_type", "").split(",") @@ -122,16 +126,18 @@ class GlobalStack(CuraContainerStack): ConnectionType.CloudConnection.value] return has_remote_connection - ## \sa configuredConnectionTypes def addConfiguredConnectionType(self, connection_type: int) -> None: + """:sa configuredConnectionTypes""" + configured_connection_types = self.configuredConnectionTypes if connection_type not in configured_connection_types: # Store the values as a string. configured_connection_types.append(connection_type) self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types])) - ## \sa configuredConnectionTypes def removeConfiguredConnectionType(self, connection_type: int) -> None: + """:sa configuredConnectionTypes""" + configured_connection_types = self.configuredConnectionTypes if connection_type in configured_connection_types: # Store the values as a string. @@ -163,13 +169,15 @@ class GlobalStack(CuraContainerStack): def preferred_output_file_formats(self) -> str: return self.getMetaDataEntry("file_formats") - ## Add an extruder to the list of extruders of this stack. - # - # \param extruder The extruder to add. - # - # \throws Exceptions.TooManyExtrudersError Raised when trying to add an extruder while we - # already have the maximum number of extruders. def addExtruder(self, extruder: ContainerStack) -> None: + """Add an extruder to the list of extruders of this stack. + + :param extruder: The extruder to add. + + :raise Exceptions.TooManyExtrudersError: Raised when trying to add an extruder while we + already have the maximum number of extruders. + """ + position = extruder.getMetaDataEntry("position") if position is None: Logger.log("w", "No position defined for extruder {extruder}, cannot add it to stack {stack}", extruder = extruder.id, stack = self.id) @@ -183,19 +191,21 @@ class GlobalStack(CuraContainerStack): self.extrudersChanged.emit() Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position) - ## Overridden from ContainerStack - # - # This will return the value of the specified property for the specified setting, - # unless the property is "value" and that setting has a "resolve" function set. - # When a resolve is set, it will instead try and execute the resolve first and - # then fall back to the normal "value" property. - # - # \param key The setting key to get the property of. - # \param property_name The property to get the value of. - # - # \return The value of the property for the specified setting, or None if not found. @override(ContainerStack) def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: + """Overridden from ContainerStack + + This will return the value of the specified property for the specified setting, + unless the property is "value" and that setting has a "resolve" function set. + When a resolve is set, it will instead try and execute the resolve first and + then fall back to the normal "value" property. + + :param key: The setting key to get the property of. + :param property_name: The property to get the value of. + + :return: The value of the property for the specified setting, or None if not found. + """ + if not self.definition.findDefinitions(key = key): return None @@ -235,11 +245,13 @@ class GlobalStack(CuraContainerStack): context.popContainer() return result - ## Overridden from ContainerStack - # - # This will simply raise an exception since the Global stack cannot have a next stack. @override(ContainerStack) def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None: + """Overridden from ContainerStack + + This will simply raise an exception since the Global stack cannot have a next stack. + """ + raise Exceptions.InvalidOperationError("Global stack cannot have a next stack!") # protected: @@ -267,9 +279,11 @@ class GlobalStack(CuraContainerStack): return True - ## Perform some sanity checks on the global stack - # Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1 def isValid(self) -> bool: + """Perform some sanity checks on the global stack + + Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1 + """ container_registry = ContainerRegistry.getInstance() extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = self.getId()) @@ -299,9 +313,10 @@ class GlobalStack(CuraContainerStack): def hasVariantBuildplates(self) -> bool: return parseBool(self.getMetaDataEntry("has_variant_buildplates", False)) - ## Get default firmware file name if one is specified in the firmware @pyqtSlot(result = str) def getDefaultFirmwareName(self) -> str: + """Get default firmware file name if one is specified in the firmware""" + machine_has_heated_bed = self.getProperty("machine_heated_bed", "value") baudrate = 250000 diff --git a/cura/Settings/IntentManager.py b/cura/Settings/IntentManager.py index 10ea8dff6a..9f636c9cc1 100644 --- a/cura/Settings/IntentManager.py +++ b/cura/Settings/IntentManager.py @@ -15,29 +15,32 @@ if TYPE_CHECKING: from UM.Settings.InstanceContainer import InstanceContainer -## Front-end for querying which intents are available for a certain -# configuration. class IntentManager(QObject): + """Front-end for querying which intents are available for a certain configuration. + """ __instance = None - ## This class is a singleton. @classmethod def getInstance(cls): + """This class is a singleton.""" + if not cls.__instance: cls.__instance = IntentManager() return cls.__instance intentCategoryChanged = pyqtSignal() #Triggered when we switch categories. - ## Gets the metadata dictionaries of all intent profiles for a given - # configuration. - # - # \param definition_id ID of the printer. - # \param nozzle_name Name of the nozzle. - # \param material_base_file The base_file of the material. - # \return A list of metadata dictionaries matching the search criteria, or - # an empty list if nothing was found. def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]: + """Gets the metadata dictionaries of all intent profiles for a given + + configuration. + + :param definition_id: ID of the printer. + :param nozzle_name: Name of the nozzle. + :param material_base_file: The base_file of the material. + :return: A list of metadata dictionaries matching the search criteria, or + an empty list if nothing was found. + """ intent_metadatas = [] # type: List[Dict[str, Any]] try: materials = ContainerTree.getInstance().machines[definition_id].variants[nozzle_name].materials @@ -53,28 +56,32 @@ class IntentManager(QObject): intent_metadatas.append(intent_node.getMetadata()) return intent_metadatas - ## Collects and returns all intent categories available for the given - # parameters. Note that the 'default' category is always available. - # - # \param definition_id ID of the printer. - # \param nozzle_name Name of the nozzle. - # \param material_id ID of the material. - # \return A set of intent category names. def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]: + """Collects and returns all intent categories available for the given + + parameters. Note that the 'default' category is always available. + + :param definition_id: ID of the printer. + :param nozzle_name: Name of the nozzle. + :param material_id: ID of the material. + :return: A set of intent category names. + """ categories = set() for intent in self.intentMetadatas(definition_id, nozzle_id, material_id): categories.add(intent["intent_category"]) categories.add("default") #The "empty" intent is not an actual profile specific to the configuration but we do want it to appear in the categories list. return list(categories) - ## List of intents to be displayed in the interface. - # - # For the interface this will have to be broken up into the different - # intent categories. That is up to the model there. - # - # \return A list of tuples of intent_category and quality_type. The actual - # instance may vary per extruder. def getCurrentAvailableIntents(self) -> List[Tuple[str, str]]: + """List of intents to be displayed in the interface. + + For the interface this will have to be broken up into the different + intent categories. That is up to the model there. + + :return: A list of tuples of intent_category and quality_type. The actual + instance may vary per extruder. + """ + application = cura.CuraApplication.CuraApplication.getInstance() global_stack = application.getGlobalContainerStack() if global_stack is None: @@ -100,16 +107,18 @@ class IntentManager(QObject): result.add((intent_metadata["intent_category"], intent_metadata["quality_type"])) return list(result) - ## List of intent categories available in either of the extruders. - # - # This is purposefully inconsistent with the way that the quality types - # are listed. The quality types will show all quality types available in - # the printer using any configuration. This will only list the intent - # categories that are available using the current configuration (but the - # union over the extruders). - # \return List of all categories in the current configurations of all - # extruders. def currentAvailableIntentCategories(self) -> List[str]: + """List of intent categories available in either of the extruders. + + This is purposefully inconsistent with the way that the quality types + are listed. The quality types will show all quality types available in + the printer using any configuration. This will only list the intent + categories that are available using the current configuration (but the + union over the extruders). + :return: List of all categories in the current configurations of all + extruders. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return ["default"] @@ -123,10 +132,12 @@ class IntentManager(QObject): final_intent_categories.update(self.intentCategories(current_definition_id, nozzle_name, material_id)) return list(final_intent_categories) - ## The intent that gets selected by default when no intent is available for - # the configuration, an extruder can't match the intent that the user - # selects, or just when creating a new printer. def getDefaultIntent(self) -> "InstanceContainer": + """The intent that gets selected by default when no intent is available for + + the configuration, an extruder can't match the intent that the user + selects, or just when creating a new printer. + """ return empty_intent_container @pyqtProperty(str, notify = intentCategoryChanged) @@ -137,9 +148,10 @@ class IntentManager(QObject): return "" return active_extruder_stack.intent.getMetaDataEntry("intent_category", "") - ## Apply intent on the stacks. @pyqtSlot(str, str) def selectIntent(self, intent_category: str, quality_type: str) -> None: + """Apply intent on the stacks.""" + Logger.log("i", "Attempting to set intent_category to [%s] and quality type to [%s]", intent_category, quality_type) old_intent_category = self.currentIntentCategory application = cura.CuraApplication.CuraApplication.getInstance() diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 2866e3a494..e56a3e38f3 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -215,8 +215,9 @@ class MachineManager(QObject): return set() return general_definition_containers[0].getAllKeys() - ## Triggered when the global container stack is changed in CuraApplication. def _onGlobalContainerChanged(self) -> None: + """Triggered when the global container stack is changed in CuraApplication.""" + if self._global_container_stack: try: self._global_container_stack.containersChanged.disconnect(self._onContainersChanged) @@ -338,12 +339,15 @@ class MachineManager(QObject): Logger.log("w", "An extruder has an unknown material, switching it to the preferred material") self.setMaterialById(extruder.getMetaDataEntry("position"), machine_node.preferred_material) - ## Given a definition id, return the machine with this id. - # Optional: add a list of keys and values to filter the list of machines with the given definition id - # \param definition_id \type{str} definition id that needs to look for - # \param metadata_filter \type{dict} list of metadata keys and values used for filtering @staticmethod def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]: + """Given a definition id, return the machine with this id. + + Optional: add a list of keys and values to filter the list of machines with the given definition id + :param definition_id: :type{str} definition id that needs to look for + :param metadata_filter: :type{dict} list of metadata keys and values used for filtering + """ + if metadata_filter is None: metadata_filter = {} machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) @@ -397,9 +401,10 @@ class MachineManager(QObject): Logger.log("d", "Checking %s stacks for errors took %.2f s" % (count, time.time() - time_start)) return False - ## Check if the global_container has instances in the user container @pyqtProperty(bool, notify = activeStackValueChanged) def hasUserSettings(self) -> bool: + """Check if the global_container has instances in the user container""" + if not self._global_container_stack: return False @@ -422,10 +427,12 @@ class MachineManager(QObject): num_user_settings += stack.getTop().getNumInstances() return num_user_settings - ## Delete a user setting from the global stack and all extruder stacks. - # \param key \type{str} the name of the key to delete @pyqtSlot(str) def clearUserSettingAllCurrentStacks(self, key: str) -> None: + """Delete a user setting from the global stack and all extruder stacks. + + :param key: :type{str} the name of the key to delete + """ Logger.log("i", "Clearing the setting [%s] from all stacks", key) if not self._global_container_stack: return @@ -454,11 +461,13 @@ class MachineManager(QObject): for container in send_emits_containers: container.sendPostponedEmits() - ## Check if none of the stacks contain error states - # Note that the _stacks_have_errors is cached due to performance issues - # Calling _checkStack(s)ForErrors on every change is simply too expensive @pyqtProperty(bool, notify = stacksValidationChanged) def stacksHaveErrors(self) -> bool: + """Check if none of the stacks contain error states + + Note that the _stacks_have_errors is cached due to performance issues + Calling _checkStack(s)ForErrors on every change is simply too expensive + """ return bool(self._stacks_have_errors) @pyqtProperty(str, notify = globalContainerChanged) @@ -528,14 +537,16 @@ class MachineManager(QObject): return material.getId() return "" - ## Gets the layer height of the currently active quality profile. - # - # This is indicated together with the name of the active quality profile. - # - # \return The layer height of the currently active quality profile. If - # there is no quality profile, this returns the default layer height. @pyqtProperty(float, notify = activeQualityGroupChanged) def activeQualityLayerHeight(self) -> float: + """Gets the layer height of the currently active quality profile. + + This is indicated together with the name of the active quality profile. + + :return: The layer height of the currently active quality profile. If + there is no quality profile, this returns the default layer height. + """ + if not self._global_container_stack: return 0 value = self._global_container_stack.getRawProperty("layer_height", "value", skip_until_container = self._global_container_stack.qualityChanges.getId()) @@ -605,13 +616,15 @@ class MachineManager(QObject): return result - ## Returns whether there is anything unsupported in the current set-up. - # - # The current set-up signifies the global stack and all extruder stacks, - # so this indicates whether there is any container in any of the container - # stacks that is not marked as supported. @pyqtProperty(bool, notify = activeQualityChanged) def isCurrentSetupSupported(self) -> bool: + """Returns whether there is anything unsupported in the current set-up. + + The current set-up signifies the global stack and all extruder stacks, + so this indicates whether there is any container in any of the container + stacks that is not marked as supported. + """ + if not self._global_container_stack: return False for stack in [self._global_container_stack] + self._global_container_stack.extruderList: @@ -622,9 +635,10 @@ class MachineManager(QObject): return False return True - ## Copy the value of the setting of the current extruder to all other extruders as well as the global container. @pyqtSlot(str) def copyValueToExtruders(self, key: str) -> None: + """Copy the value of the setting of the current extruder to all other extruders as well as the global container.""" + if self._active_container_stack is None or self._global_container_stack is None: return new_value = self._active_container_stack.getProperty(key, "value") @@ -634,9 +648,10 @@ class MachineManager(QObject): if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value: extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved - ## Copy the value of all manually changed settings of the current extruder to all other extruders. @pyqtSlot() def copyAllValuesToExtruders(self) -> None: + """Copy the value of all manually changed settings of the current extruder to all other extruders.""" + if self._active_container_stack is None or self._global_container_stack is None: return @@ -648,19 +663,23 @@ class MachineManager(QObject): # Check if the value has to be replaced extruder_stack.userChanges.setProperty(key, "value", new_value) - ## Get the Definition ID to use to select quality profiles for the currently active machine - # \returns DefinitionID (string) if found, empty string otherwise @pyqtProperty(str, notify = globalContainerChanged) def activeQualityDefinitionId(self) -> str: + """Get the Definition ID to use to select quality profiles for the currently active machine + + :returns: DefinitionID (string) if found, empty string otherwise + """ global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if not global_stack: return "" return ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition - ## Gets how the active definition calls variants - # Caveat: per-definition-variant-title is currently not translated (though the fallback is) @pyqtProperty(str, notify = globalContainerChanged) def activeDefinitionVariantsName(self) -> str: + """Gets how the active definition calls variants + + Caveat: per-definition-variant-title is currently not translated (though the fallback is) + """ fallback_title = catalog.i18nc("@label", "Nozzle") if self._global_container_stack: return self._global_container_stack.definition.getMetaDataEntry("variants_name", fallback_title) @@ -708,9 +727,10 @@ class MachineManager(QObject): # This reuses the method and remove all printers recursively self.removeMachine(hidden_containers[0].getId()) - ## The selected buildplate is compatible if it is compatible with all the materials in all the extruders @pyqtProperty(bool, notify = activeMaterialChanged) def variantBuildplateCompatible(self) -> bool: + """The selected buildplate is compatible if it is compatible with all the materials in all the extruders""" + if not self._global_container_stack: return True @@ -727,10 +747,12 @@ class MachineManager(QObject): return buildplate_compatible - ## The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible - # for the other material but the buildplate is still usable @pyqtProperty(bool, notify = activeMaterialChanged) def variantBuildplateUsable(self) -> bool: + """The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible + + for the other material but the buildplate is still usable + """ if not self._global_container_stack: return True @@ -751,11 +773,13 @@ class MachineManager(QObject): return result - ## Get the Definition ID of a machine (specified by ID) - # \param machine_id string machine id to get the definition ID of - # \returns DefinitionID if found, None otherwise @pyqtSlot(str, result = str) def getDefinitionByMachineId(self, machine_id: str) -> Optional[str]: + """Get the Definition ID of a machine (specified by ID) + + :param machine_id: string machine id to get the definition ID of + :returns: DefinitionID if found, None otherwise + """ containers = CuraContainerRegistry.getInstance().findContainerStacks(id = machine_id) if containers: return containers[0].definition.getId() @@ -786,8 +810,9 @@ class MachineManager(QObject): Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value) return result - ## Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed def correctExtruderSettings(self) -> None: + """Update extruder number to a valid value when the number of extruders are changed, or when an extruder is changed""" + if self._global_container_stack is None: return for setting_key in self.getIncompatibleSettingsOnEnabledExtruders(self._global_container_stack.userChanges): @@ -803,9 +828,11 @@ class MachineManager(QObject): title = catalog.i18nc("@info:title", "Settings updated")) caution_message.show() - ## Set the amount of extruders on the active machine (global stack) - # \param extruder_count int the number of extruders to set def setActiveMachineExtruderCount(self, extruder_count: int) -> None: + """Set the amount of extruders on the active machine (global stack) + + :param extruder_count: int the number of extruders to set + """ if self._global_container_stack is None: return extruder_manager = self._application.getExtruderManager() @@ -902,9 +929,10 @@ class MachineManager(QObject): def defaultExtruderPosition(self) -> str: return self._default_extruder_position - ## This will fire the propertiesChanged for all settings so they will be updated in the front-end @pyqtSlot() def forceUpdateAllSettings(self) -> None: + """This will fire the propertiesChanged for all settings so they will be updated in the front-end""" + if self._global_container_stack is None: return with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): @@ -945,11 +973,13 @@ class MachineManager(QObject): def _onMaterialNameChanged(self) -> None: self.activeMaterialChanged.emit() - ## Get the signals that signal that the containers changed for all stacks. - # - # This includes the global stack and all extruder stacks. So if any - # container changed anywhere. def _getContainerChangedSignals(self) -> List[Signal]: + """Get the signals that signal that the containers changed for all stacks. + + This includes the global stack and all extruder stacks. So if any + container changed anywhere. + """ + if self._global_container_stack is None: return [] return [s.containersChanged for s in self._global_container_stack.extruderList + [self._global_container_stack]] @@ -962,18 +992,21 @@ class MachineManager(QObject): container = extruder.userChanges container.setProperty(setting_name, property_name, property_value) - ## Reset all setting properties of a setting for all extruders. - # \param setting_name The ID of the setting to reset. @pyqtSlot(str) def resetSettingForAllExtruders(self, setting_name: str) -> None: + """Reset all setting properties of a setting for all extruders. + + :param setting_name: The ID of the setting to reset. + """ if self._global_container_stack is None: return for extruder in self._global_container_stack.extruderList: container = extruder.userChanges container.removeInstance(setting_name) - ## Update _current_root_material_id when the current root material was changed. def _onRootMaterialChanged(self) -> None: + """Update _current_root_material_id when the current root material was changed.""" + self._current_root_material_id = {} changed = False @@ -1135,8 +1168,9 @@ class MachineManager(QObject): return False return True - ## Update current quality type and machine after setting material def _updateQualityWithMaterial(self, *args: Any) -> None: + """Update current quality type and machine after setting material""" + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return @@ -1177,8 +1211,9 @@ class MachineManager(QObject): current_quality_type, quality_type) self._setQualityGroup(candidate_quality_groups[quality_type], empty_quality_changes = True) - ## Update the current intent after the quality changed def _updateIntentWithQuality(self): + """Update the current intent after the quality changed""" + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return @@ -1205,12 +1240,14 @@ class MachineManager(QObject): category = current_category self.setIntentByCategory(category) - ## Update the material profile in the current stacks when the variant is - # changed. - # \param position The extruder stack to update. If provided with None, all - # extruder stacks will be updated. @pyqtSlot() def updateMaterialWithVariant(self, position: Optional[str] = None) -> None: + """Update the material profile in the current stacks when the variant is + + changed. + :param position: The extruder stack to update. If provided with None, all + extruder stacks will be updated. + """ if self._global_container_stack is None: return if position is None: @@ -1245,10 +1282,12 @@ class MachineManager(QObject): material_node = nozzle_node.preferredMaterial(approximate_material_diameter) self._setMaterial(position_item, material_node) - ## Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new - # instance with the same network key. @pyqtSlot(str) def switchPrinterType(self, machine_name: str) -> None: + """Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new + + instance with the same network key. + """ # Don't switch if the user tries to change to the same type of printer if self._global_container_stack is None or self._global_container_stack.definition.name == machine_name: return @@ -1400,10 +1439,12 @@ class MachineManager(QObject): material_node = ContainerTree.getInstance().machines[machine_definition_id].variants[nozzle_name].materials[root_material_id] self.setMaterial(position, material_node) - ## Global_stack: if you want to provide your own global_stack instead of the current active one - # if you update an active machine, special measures have to be taken. @pyqtSlot(str, "QVariant") def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None: + """Global_stack: if you want to provide your own global_stack instead of the current active one + + if you update an active machine, special measures have to be taken. + """ if global_stack is not None and global_stack != self._global_container_stack: global_stack.extruders[position].material = container_node.container return @@ -1449,10 +1490,12 @@ class MachineManager(QObject): # Get all the quality groups for this global stack and filter out by quality_type self.setQualityGroup(ContainerTree.getInstance().getCurrentQualityGroups()[quality_type]) - ## Optionally provide global_stack if you want to use your own - # The active global_stack is treated differently. @pyqtSlot(QObject) def setQualityGroup(self, quality_group: "QualityGroup", no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None: + """Optionally provide global_stack if you want to use your own + + The active global_stack is treated differently. + """ if global_stack is not None and global_stack != self._global_container_stack: if quality_group is None: Logger.log("e", "Could not set quality group because quality group is None") @@ -1514,15 +1557,17 @@ class MachineManager(QObject): return {"main": main_part, "suffix": suffix_part} - ## Change the intent category of the current printer. - # - # All extruders can change their profiles. If an intent profile is - # available with the desired intent category, that one will get chosen. - # Otherwise the intent profile will be left to the empty profile, which - # represents the "default" intent category. - # \param intent_category The intent category to change to. @pyqtSlot(str) def setIntentByCategory(self, intent_category: str) -> None: + """Change the intent category of the current printer. + + All extruders can change their profiles. If an intent profile is + available with the desired intent category, that one will get chosen. + Otherwise the intent profile will be left to the empty profile, which + represents the "default" intent category. + :param intent_category: The intent category to change to. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if global_stack is None: return @@ -1554,21 +1599,25 @@ class MachineManager(QObject): else: # No intent had the correct category. extruder.intent = empty_intent_container - ## Get the currently activated quality group. - # - # If no printer is added yet or the printer doesn't have quality profiles, - # this returns ``None``. - # \return The currently active quality group. def activeQualityGroup(self) -> Optional["QualityGroup"]: + """Get the currently activated quality group. + + If no printer is added yet or the printer doesn't have quality profiles, + this returns ``None``. + :return: The currently active quality group. + """ + global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() if not global_stack or global_stack.quality == empty_quality_container: return None return ContainerTree.getInstance().getCurrentQualityGroups().get(self.activeQualityType) - ## Get the name of the active quality group. - # \return The name of the active quality group. @pyqtProperty(str, notify = activeQualityGroupChanged) def activeQualityGroupName(self) -> str: + """Get the name of the active quality group. + + :return: The name of the active quality group. + """ quality_group = self.activeQualityGroup() if quality_group is None: return "" @@ -1641,9 +1690,10 @@ class MachineManager(QObject): self.updateMaterialWithVariant(None) self._updateQualityWithMaterial() - ## This function will translate any printer type name to an abbreviated printer type name @pyqtSlot(str, result = str) def getAbbreviatedMachineName(self, machine_type_name: str) -> str: + """This function will translate any printer type name to an abbreviated printer type name""" + abbr_machine = "" for word in re.findall(r"[\w']+", machine_type_name): if word.lower() == "ultimaker": diff --git a/cura/Settings/MachineNameValidator.py b/cura/Settings/MachineNameValidator.py index acdda4b0a0..2bb614f093 100644 --- a/cura/Settings/MachineNameValidator.py +++ b/cura/Settings/MachineNameValidator.py @@ -10,10 +10,13 @@ from UM.Resources import Resources from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.InstanceContainer import InstanceContainer -## Are machine names valid? -# -# Performs checks based on the length of the name. + class MachineNameValidator(QObject): + """Are machine names valid? + + Performs checks based on the length of the name. + """ + def __init__(self, parent = None): super().__init__(parent) @@ -32,12 +35,13 @@ class MachineNameValidator(QObject): validationChanged = pyqtSignal() - ## Check if a specified machine name is allowed. - # - # \param name The machine name to check. - # \return ``QValidator.Invalid`` if it's disallowed, or - # ``QValidator.Acceptable`` if it's allowed. def validate(self, name): + """Check if a specified machine name is allowed. + + :param name: The machine name to check. + :return: ``QValidator.Invalid`` if it's disallowed, or ``QValidator.Acceptable`` if it's allowed. + """ + #Check for file name length of the current settings container (which is the longest file we're saving with the name). try: filename_max_length = os.statvfs(Resources.getDataStoragePath()).f_namemax @@ -50,9 +54,10 @@ class MachineNameValidator(QObject): return QValidator.Acceptable #All checks succeeded. - ## Updates the validation state of a machine name text field. @pyqtSlot(str) def updateValidation(self, new_name): + """Updates the validation state of a machine name text field.""" + is_valid = self.validate(new_name) if is_valid == QValidator.Acceptable: self.validation_regex = "^.*$" #Matches anything. diff --git a/cura/Settings/SetObjectExtruderOperation.py b/cura/Settings/SetObjectExtruderOperation.py index 25c1c6b759..63227c58e3 100644 --- a/cura/Settings/SetObjectExtruderOperation.py +++ b/cura/Settings/SetObjectExtruderOperation.py @@ -6,8 +6,10 @@ from UM.Operations.Operation import Operation from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator -## Simple operation to set the extruder a certain object should be printed with. + class SetObjectExtruderOperation(Operation): + """Simple operation to set the extruder a certain object should be printed with.""" + def __init__(self, node: SceneNode, extruder_id: str) -> None: self._node = node self._extruder_id = extruder_id diff --git a/cura/Settings/SettingInheritanceManager.py b/cura/Settings/SettingInheritanceManager.py index 7db579bf3f..6179e76ab7 100644 --- a/cura/Settings/SettingInheritanceManager.py +++ b/cura/Settings/SettingInheritanceManager.py @@ -45,9 +45,10 @@ class SettingInheritanceManager(QObject): settingsWithIntheritanceChanged = pyqtSignal() - ## Get the keys of all children settings with an override. @pyqtSlot(str, result = "QStringList") def getChildrenKeysWithOverride(self, key: str) -> List[str]: + """Get the keys of all children settings with an override.""" + if self._global_container_stack is None: return [] definitions = self._global_container_stack.definition.findDefinitions(key=key) @@ -163,8 +164,9 @@ class SettingInheritanceManager(QObject): def settingsWithInheritanceWarning(self) -> List[str]: return self._settings_with_inheritance_warning - ## Check if a setting has an inheritance function that is overwritten def _settingIsOverwritingInheritance(self, key: str, stack: ContainerStack = None) -> bool: + """Check if a setting has an inheritance function that is overwritten""" + has_setting_function = False if not stack: stack = self._active_container_stack @@ -177,17 +179,19 @@ class SettingInheritanceManager(QObject): containers = [] # type: List[ContainerInterface] - ## Check if the setting has a user state. If not, it is never overwritten. has_user_state = stack.getProperty(key, "state") == InstanceState.User + """Check if the setting has a user state. If not, it is never overwritten.""" + if not has_user_state: return False - ## If a setting is not enabled, don't label it as overwritten (It's never visible anyway). + # If a setting is not enabled, don't label it as overwritten (It's never visible anyway). if not stack.getProperty(key, "enabled"): return False - ## Also check if the top container is not a setting function (this happens if the inheritance is restored). user_container = stack.getTop() + """Also check if the top container is not a setting function (this happens if the inheritance is restored).""" + if user_container and isinstance(user_container.getProperty(key, "value"), SettingFunction): return False diff --git a/cura/Settings/SettingOverrideDecorator.py b/cura/Settings/SettingOverrideDecorator.py index 03b4c181dd..d48bff042f 100644 --- a/cura/Settings/SettingOverrideDecorator.py +++ b/cura/Settings/SettingOverrideDecorator.py @@ -15,21 +15,24 @@ from UM.Application import Application from cura.Settings.PerObjectContainerStack import PerObjectContainerStack from cura.Settings.ExtruderManager import ExtruderManager -## A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding -# the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by -# this stack still resolve. @signalemitter class SettingOverrideDecorator(SceneNodeDecorator): - ## Event indicating that the user selected a different extruder. + """A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding + + the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by + this stack still resolve. + """ activeExtruderChanged = Signal() + """Event indicating that the user selected a different extruder.""" - ## Non-printing meshes - # - # If these settings are True for any mesh, the mesh does not need a convex hull, - # and is sent to the slicer regardless of whether it fits inside the build volume. - # Note that Support Mesh is not in here because it actually generates - # g-code in the volume of the mesh. _non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"} + """Non-printing meshes + + If these settings are True for any mesh, the mesh does not need a convex hull, + and is sent to the slicer regardless of whether it fits inside the build volume. + Note that Support Mesh is not in here because it actually generates + g-code in the volume of the mesh. + """ _non_thumbnail_visible_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh", "support_mesh"} def __init__(self): @@ -56,11 +59,11 @@ class SettingOverrideDecorator(SceneNodeDecorator): return "SettingOverrideInstanceContainer-%s" % uuid.uuid1() def __deepcopy__(self, memo): - ## Create a fresh decorator object deep_copy = SettingOverrideDecorator() + """Create a fresh decorator object""" - ## Copy the instance instance_container = copy.deepcopy(self._stack.getContainer(0), memo) + """Copy the instance""" # A unique name must be added, or replaceContainer will not replace it instance_container.setMetaDataEntry("id", self._generateUniqueName()) @@ -78,22 +81,28 @@ class SettingOverrideDecorator(SceneNodeDecorator): return deep_copy - ## Gets the currently active extruder to print this object with. - # - # \return An extruder's container stack. def getActiveExtruder(self): + """Gets the currently active extruder to print this object with. + + :return: An extruder's container stack. + """ + return self._extruder_stack - ## Gets the signal that emits if the active extruder changed. - # - # This can then be accessed via a decorator. def getActiveExtruderChangedSignal(self): + """Gets the signal that emits if the active extruder changed. + + This can then be accessed via a decorator. + """ + return self.activeExtruderChanged - ## Gets the currently active extruders position - # - # \return An extruder's position, or None if no position info is available. def getActiveExtruderPosition(self): + """Gets the currently active extruders position + + :return: An extruder's position, or None if no position info is available. + """ + # for support_meshes, always use the support_extruder if self.getStack().getProperty("support_mesh", "value"): global_container_stack = Application.getInstance().getGlobalContainerStack() @@ -126,9 +135,11 @@ class SettingOverrideDecorator(SceneNodeDecorator): Application.getInstance().getBackend().needsSlicing() Application.getInstance().getBackend().tickle() - ## Makes sure that the stack upon which the container stack is placed is - # kept up to date. def _updateNextStack(self): + """Makes sure that the stack upon which the container stack is placed is + + kept up to date. + """ if self._extruder_stack: extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack) if extruder_stack: @@ -147,10 +158,12 @@ class SettingOverrideDecorator(SceneNodeDecorator): else: self._stack.setNextStack(Application.getInstance().getGlobalContainerStack()) - ## Changes the extruder with which to print this node. - # - # \param extruder_stack_id The new extruder stack to print with. def setActiveExtruder(self, extruder_stack_id): + """Changes the extruder with which to print this node. + + :param extruder_stack_id: The new extruder stack to print with. + """ + self._extruder_stack = extruder_stack_id self._updateNextStack() ExtruderManager.getInstance().resetSelectedObjectExtruders() diff --git a/cura/UI/MachineActionManager.py b/cura/UI/MachineActionManager.py index 6efd3217a1..25234fd43f 100644 --- a/cura/UI/MachineActionManager.py +++ b/cura/UI/MachineActionManager.py @@ -15,13 +15,15 @@ if TYPE_CHECKING: from cura.MachineAction import MachineAction -## Raised when trying to add an unknown machine action as a required action class UnknownMachineActionError(Exception): + """Raised when trying to add an unknown machine action as a required action""" + pass -## Raised when trying to add a machine action that does not have an unique key. class NotUniqueMachineActionError(Exception): + """Raised when trying to add a machine action that does not have an unique key.""" + pass @@ -71,9 +73,11 @@ class MachineActionManager(QObject): self._definition_ids_with_default_actions_added.add(definition_id) Logger.log("i", "Default machine actions added for machine definition [%s]", definition_id) - ## Add a required action to a machine - # Raises an exception when the action is not recognised. def addRequiredAction(self, definition_id: str, action_key: str) -> None: + """Add a required action to a machine + + Raises an exception when the action is not recognised. + """ if action_key in self._machine_actions: if definition_id in self._required_actions: if self._machine_actions[action_key] not in self._required_actions[definition_id]: @@ -83,8 +87,9 @@ class MachineActionManager(QObject): else: raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id)) - ## Add a supported action to a machine. def addSupportedAction(self, definition_id: str, action_key: str) -> None: + """Add a supported action to a machine.""" + if action_key in self._machine_actions: if definition_id in self._supported_actions: if self._machine_actions[action_key] not in self._supported_actions[definition_id]: @@ -94,8 +99,9 @@ class MachineActionManager(QObject): else: Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id) - ## Add an action to the first start list of a machine. def addFirstStartAction(self, definition_id: str, action_key: str) -> None: + """Add an action to the first start list of a machine.""" + if action_key in self._machine_actions: if definition_id in self._first_start_actions: self._first_start_actions[definition_id].append(self._machine_actions[action_key]) @@ -104,57 +110,69 @@ class MachineActionManager(QObject): else: Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id) - ## Add a (unique) MachineAction - # if the Key of the action is not unique, an exception is raised. def addMachineAction(self, action: "MachineAction") -> None: + """Add a (unique) MachineAction + + if the Key of the action is not unique, an exception is raised. + """ if action.getKey() not in self._machine_actions: self._machine_actions[action.getKey()] = action else: raise NotUniqueMachineActionError("MachineAction with key %s was already added. Actions must have unique keys.", action.getKey()) - ## Get all actions supported by given machine - # \param definition_id The ID of the definition you want the supported actions of - # \returns set of supported actions. @pyqtSlot(str, result = "QVariantList") def getSupportedActions(self, definition_id: str) -> List["MachineAction"]: + """Get all actions supported by given machine + + :param definition_id: The ID of the definition you want the supported actions of + :returns: set of supported actions. + """ if definition_id in self._supported_actions: return list(self._supported_actions[definition_id]) else: return list() - ## Get all actions required by given machine - # \param definition_id The ID of the definition you want the required actions of - # \returns set of required actions. def getRequiredActions(self, definition_id: str) -> List["MachineAction"]: + """Get all actions required by given machine + + :param definition_id: The ID of the definition you want the required actions of + :returns: set of required actions. + """ if definition_id in self._required_actions: return self._required_actions[definition_id] else: return list() - ## Get all actions that need to be performed upon first start of a given machine. - # Note that contrary to required / supported actions a list is returned (as it could be required to run the same - # action multiple times). - # \param definition_id The ID of the definition that you want to get the "on added" actions for. - # \returns List of actions. @pyqtSlot(str, result = "QVariantList") def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]: + """Get all actions that need to be performed upon first start of a given machine. + + Note that contrary to required / supported actions a list is returned (as it could be required to run the same + action multiple times). + :param definition_id: The ID of the definition that you want to get the "on added" actions for. + :returns: List of actions. + """ if definition_id in self._first_start_actions: return self._first_start_actions[definition_id] else: return [] - ## Remove Machine action from manager - # \param action to remove def removeMachineAction(self, action: "MachineAction") -> None: + """Remove Machine action from manager + + :param action: to remove + """ try: del self._machine_actions[action.getKey()] except KeyError: Logger.log("w", "Trying to remove MachineAction (%s) that was already removed", action.getKey()) - ## Get MachineAction by key - # \param key String of key to select - # \return Machine action if found, None otherwise def getMachineAction(self, key: str) -> Optional["MachineAction"]: + """Get MachineAction by key + + :param key: String of key to select + :return: Machine action if found, None otherwise + """ if key in self._machine_actions: return self._machine_actions[key] else: diff --git a/cura/UI/ObjectsModel.py b/cura/UI/ObjectsModel.py index 659732e895..1383476665 100644 --- a/cura/UI/ObjectsModel.py +++ b/cura/UI/ObjectsModel.py @@ -31,8 +31,9 @@ class _NodeInfo: self.is_group = is_group # type: bool -## Keep track of all objects in the project class ObjectsModel(ListModel): + """Keep track of all objects in the project""" + NameRole = Qt.UserRole + 1 SelectedRole = Qt.UserRole + 2 OutsideAreaRole = Qt.UserRole + 3 diff --git a/cura/UI/PrintInformation.py b/cura/UI/PrintInformation.py index c39314dc02..cbe0900ec0 100644 --- a/cura/UI/PrintInformation.py +++ b/cura/UI/PrintInformation.py @@ -21,11 +21,13 @@ if TYPE_CHECKING: catalog = i18nCatalog("cura") -## A class for processing and the print times per build plate as well as managing the job name -# -# This class also mangles the current machine name and the filename of the first loaded mesh into a job name. -# This job name is requested by the JobSpecs qml file. class PrintInformation(QObject): + """A class for processing and the print times per build plate as well as managing the job name + + This class also mangles the current machine name and the filename of the first loaded mesh into a job name. + This job name is requested by the JobSpecs qml file. + """ + UNTITLED_JOB_NAME = "Untitled" @@ -380,10 +382,12 @@ class PrintInformation(QObject): def baseName(self): return self._base_name - ## Created an acronym-like abbreviated machine name from the currently - # active machine name. - # Called each time the global stack is switched. def _defineAbbreviatedMachineName(self) -> None: + """Created an acronym-like abbreviated machine name from the currently active machine name. + + Called each time the global stack is switched. + """ + global_container_stack = self._application.getGlobalContainerStack() if not global_container_stack: self._abbr_machine = "" @@ -392,8 +396,9 @@ class PrintInformation(QObject): self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name) - ## Utility method that strips accents from characters (eg: â -> a) def _stripAccents(self, to_strip: str) -> str: + """Utility method that strips accents from characters (eg: â -> a)""" + return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn') @pyqtSlot(result = "QVariantMap") @@ -431,6 +436,7 @@ class PrintInformation(QObject): return self._change_timer.start() - ## Listen to scene changes to check if we need to reset the print information def _onSceneChanged(self) -> None: + """Listen to scene changes to check if we need to reset the print information""" + self.setToZeroPrintInformation(self._active_build_plate) diff --git a/cura/Utils/Decorators.py b/cura/Utils/Decorators.py index 9275ee6ce9..7be718c51c 100644 --- a/cura/Utils/Decorators.py +++ b/cura/Utils/Decorators.py @@ -11,13 +11,15 @@ from typing import Callable SEMANTIC_VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+(\.[0-9]+)?$") -## Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported -# APIs, meaning that those APIs should be versioned and maintained. -# -# \param since_version The earliest version since when this API becomes supported. This means that since this version, -# this API function is supposed to behave the same. This parameter is not used. It's just a -# documentation. def api(since_version: str) -> Callable: + """Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported + + APIs, meaning that those APIs should be versioned and maintained. + + :param since_version: The earliest version since when this API becomes supported. This means that since this version, + this API function is supposed to behave the same. This parameter is not used. It's just a + documentation. + """ # Make sure that APi versions are semantic versions if not SEMANTIC_VERSION_REGEX.fullmatch(since_version): raise ValueError("API since_version [%s] is not a semantic version." % since_version) diff --git a/tests/Machines/TestMachineNode.py b/tests/Machines/TestMachineNode.py index 50d7bdafa0..3b0822770b 100644 --- a/tests/Machines/TestMachineNode.py +++ b/tests/Machines/TestMachineNode.py @@ -26,12 +26,14 @@ def container_registry(): result.findContainersMetadata = MagicMock(return_value = [metadata_dict]) return result -## Creates a machine node without anything underneath it. No sub-nodes. -# -# For testing stuff with machine nodes without testing _loadAll(). You'll need -# to add subnodes manually in your test. @pytest.fixture def empty_machine_node(): + """Creates a machine node without anything underneath it. No sub-nodes. + + For testing stuff with machine nodes without testing _loadAll(). You'll need + to add subnodes manually in your test. + """ + empty_container_registry = MagicMock() empty_container_registry.findContainersMetadata = MagicMock(return_value = [metadata_dict]) # Still contain the MachineNode's own metadata for the constructor. empty_container_registry.findInstanceContainersMetadata = MagicMock(return_value = []) @@ -77,9 +79,13 @@ def test_metadataProperties(container_registry): assert node.preferred_material == metadata_dict["preferred_material"] assert node.preferred_quality_type == metadata_dict["preferred_quality_type"] -## Test getting quality groups when there are quality profiles available for -# the requested configurations on two extruders. + def test_getQualityGroupsBothExtrudersAvailable(empty_machine_node): + """Test getting quality groups when there are quality profiles available for + + the requested configurations on two extruders. + """ + # Prepare a tree inside the machine node. extruder_0_node = MagicMock(quality_type = "quality_type_1") extruder_1_node = MagicMock(quality_type = "quality_type_1") # Same quality type, so this is the one that can be returned. @@ -121,12 +127,15 @@ def test_getQualityGroupsBothExtrudersAvailable(empty_machine_node): assert result["quality_type_1"].name == global_node.getMetaDataEntry("name", "Unnamed Profile") assert result["quality_type_1"].quality_type == "quality_type_1" -## Test the "is_available" flag on quality groups. -# -# If a profile is available for a quality type on an extruder but not on all -# extruders, there should be a quality group for it but it should not be made -# available. + def test_getQualityGroupsAvailability(empty_machine_node): + """Test the "is_available" flag on quality groups. + + If a profile is available for a quality type on an extruder but not on all + extruders, there should be a quality group for it but it should not be made + available. + """ + # Prepare a tree inside the machine node. extruder_0_both = MagicMock(quality_type = "quality_type_both") # This quality type is available for both extruders. extruder_1_both = MagicMock(quality_type = "quality_type_both") diff --git a/tests/Machines/TestQualityNode.py b/tests/Machines/TestQualityNode.py index ffe897d203..0501450b5d 100644 --- a/tests/Machines/TestQualityNode.py +++ b/tests/Machines/TestQualityNode.py @@ -6,7 +6,7 @@ import pytest from cura.Machines.QualityNode import QualityNode -## Metadata for hypothetical containers that get put in the registry. +# Metadata for hypothetical containers that get put in the registry. metadatas = [ { "id": "matching_intent", # Matches our query. diff --git a/tests/Machines/TestVariantNode.py b/tests/Machines/TestVariantNode.py index 9a0213ef99..96084001c1 100644 --- a/tests/Machines/TestVariantNode.py +++ b/tests/Machines/TestVariantNode.py @@ -49,12 +49,14 @@ def machine_node(): mocked_machine_node.preferred_material = "preferred_material" return mocked_machine_node -## Constructs a variant node without any subnodes. -# -# This is useful for performing tests on VariantNode without being dependent -# on how _loadAll works. @pytest.fixture def empty_variant_node(machine_node): + """Constructs a variant node without any subnodes. + + This is useful for performing tests on VariantNode without being dependent + on how _loadAll works. + """ + container_registry = MagicMock( findContainersMetadata = MagicMock(return_value = [{"name": "test variant name"}]) ) @@ -132,9 +134,12 @@ def test_materialAdded_update(container_registry, machine_node, metadata, change for key in changed_material_list: assert original_material_nodes[key] != variant_node.materials[key] -## Tests the preferred material when the exact base file is available in the -# materials list for this node. + def test_preferredMaterialExactMatch(empty_variant_node): + """Tests the preferred material when the exact base file is available in the + + materials list for this node. + """ empty_variant_node.materials = { "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "preferred_material": MagicMock(getMetaDataEntry = lambda x: 3) # Exact match. @@ -143,9 +148,12 @@ def test_preferredMaterialExactMatch(empty_variant_node): assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material"], "It should match exactly on this one since it's the preferred material." -## Tests the preferred material when a submaterial is available in the -# materials list for this node. + def test_preferredMaterialSubmaterial(empty_variant_node): + """Tests the preferred material when a submaterial is available in the + + materials list for this node. + """ empty_variant_node.materials = { "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "preferred_material_base_file_aa04": MagicMock(getMetaDataEntry = lambda x: 3) # This is a submaterial of the preferred material. @@ -154,9 +162,10 @@ def test_preferredMaterialSubmaterial(empty_variant_node): assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material_base_file_aa04"], "It should match on the submaterial just as well." -## Tests the preferred material matching on the approximate diameter of the -# filament. + def test_preferredMaterialDiameter(empty_variant_node): + """Tests the preferred material matching on the approximate diameter of the filament. + """ empty_variant_node.materials = { "some_different_material": MagicMock(getMetaDataEntry = lambda x: 3), "preferred_material_wrong_diameter": MagicMock(getMetaDataEntry = lambda x: 2), # Approximate diameter is 2 instead of 3. @@ -166,18 +175,22 @@ def test_preferredMaterialDiameter(empty_variant_node): assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["preferred_material_correct_diameter"], "It should match only on the material with correct diameter." -## Tests the preferred material matching on a different material if the -# diameter is wrong. + def test_preferredMaterialDiameterNoMatch(empty_variant_node): + """Tests the preferred material matching on a different material if the diameter is wrong.""" + empty_variant_node.materials = collections.OrderedDict() empty_variant_node.materials["some_different_material"] = MagicMock(getMetaDataEntry = lambda x: 3) # This one first so that it gets iterated over first. empty_variant_node.materials["preferred_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # Wrong diameter. assert empty_variant_node.preferredMaterial(approximate_diameter = 3) == empty_variant_node.materials["some_different_material"], "It should match on another material with the correct diameter if the preferred one is unavailable." -## Tests that the material diameter is considered more important to match than -# the preferred diameter. + def test_preferredMaterialDiameterPreference(empty_variant_node): + """Tests that the material diameter is considered more important to match than + the preferred diameter. + """ + empty_variant_node.materials = collections.OrderedDict() empty_variant_node.materials["some_different_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # This one first so that it gets iterated over first. empty_variant_node.materials["preferred_material"] = MagicMock(getMetaDataEntry = lambda x: 2) # Matches on ID but not diameter. diff --git a/tests/Settings/MockContainer.py b/tests/Settings/MockContainer.py index 0400359154..8bb570bae1 100644 --- a/tests/Settings/MockContainer.py +++ b/tests/Settings/MockContainer.py @@ -5,18 +5,21 @@ import UM.PluginObject from UM.Signal import Signal -## Fake container class to add to the container registry. -# -# This allows us to test the container registry without testing the container -# class. If something is wrong in the container class it won't influence this -# test. - class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): - ## Initialise a new definition container. - # - # The container will have the specified ID and all metadata in the - # provided dictionary. + """Fake container class to add to the container registry. + + This allows us to test the container registry without testing the container + class. If something is wrong in the container class it won't influence this + test. + """ + def __init__(self, metadata = None): + """Initialise a new definition container. + + The container will have the specified ID and all metadata in the + provided dictionary. + """ + super().__init__() if metadata is None: self._metadata = {} @@ -24,55 +27,69 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): self._metadata = metadata self._plugin_id = "MockContainerPlugin" - ## Gets the ID that was provided at initialisation. - # - # \return The ID of the container. def getId(self): + """Gets the ID that was provided at initialisation. + + :return: The ID of the container. + """ + return self._metadata["id"] - ## Gets all metadata of this container. - # - # This returns the metadata dictionary that was provided in the - # constructor of this mock container. - # - # \return The metadata for this container. def getMetaData(self): + """Gets all metadata of this container. + + This returns the metadata dictionary that was provided in the + constructor of this mock container. + + :return: The metadata for this container. + """ + return self._metadata - ## Gets a metadata entry from the metadata dictionary. - # - # \param key The key of the metadata entry. - # \return The value of the metadata entry, or None if there is no such - # entry. def getMetaDataEntry(self, entry, default = None): + """Gets a metadata entry from the metadata dictionary. + + :param key: The key of the metadata entry. + :return: The value of the metadata entry, or None if there is no such + entry. + """ + if entry in self._metadata: return self._metadata[entry] return default - ## Gets a human-readable name for this container. - # \return The name from the metadata, or "MockContainer" if there was no - # name provided. def getName(self): + """Gets a human-readable name for this container. + + :return: The name from the metadata, or "MockContainer" if there was no + name provided. + """ return self._metadata.get("name", "MockContainer") - ## Get whether a container stack is enabled or not. - # \return Always returns True. @property def isEnabled(self): + """Get whether a container stack is enabled or not. + + :return: Always returns True. + """ return True - ## Get whether the container item is stored on a read only location in the filesystem. - # - # \return Always returns False def isReadOnly(self): + """Get whether the container item is stored on a read only location in the filesystem. + + :return: Always returns False + """ + return False - ## Mock get path def getPath(self): + """Mock get path""" + return "/path/to/the/light/side" - ## Mock set path def setPath(self, path): + """Mock set path""" + pass def getAllKeys(self): @@ -91,31 +108,38 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): return None - ## Get the value of a container item. - # - # Since this mock container cannot contain any items, it always returns - # None. - # - # \return Always returns None. def getValue(self, key): + """Get the value of a container item. + + Since this mock container cannot contain any items, it always returns None. + + :return: Always returns None. + """ + pass - ## Get whether the container item has a specific property. - # - # This method is not implemented in the mock container. def hasProperty(self, key, property_name): + """Get whether the container item has a specific property. + + This method is not implemented in the mock container. + """ + return key in self.items - ## Serializes the container to a string representation. - # - # This method is not implemented in the mock container. def serialize(self, ignored_metadata_keys = None): + """Serializes the container to a string representation. + + This method is not implemented in the mock container. + """ + raise NotImplementedError() - ## Deserializes the container from a string representation. - # - # This method is not implemented in the mock container. def deserialize(self, serialized, file_name: Optional[str] = None): + """Deserializes the container from a string representation. + + This method is not implemented in the mock container. + """ + raise NotImplementedError() @classmethod diff --git a/tests/Settings/TestCuraContainerRegistry.py b/tests/Settings/TestCuraContainerRegistry.py index 8bd6fe0ccb..df4a1df0a9 100644 --- a/tests/Settings/TestCuraContainerRegistry.py +++ b/tests/Settings/TestCuraContainerRegistry.py @@ -42,8 +42,9 @@ def test_createUniqueName(container_registry): assert container_registry.createUniqueName("user", "test", "", "nope") == "nope" -## Tests whether addContainer properly converts to ExtruderStack. def test_addContainerExtruderStack(container_registry, definition_container, definition_changes_container): + """Tests whether addContainer properly converts to ExtruderStack.""" + container_registry.addContainer(definition_container) container_registry.addContainer(definition_changes_container) @@ -61,8 +62,9 @@ def test_addContainerExtruderStack(container_registry, definition_container, def assert type(mock_super_add_container.call_args_list[0][0][0]) == ExtruderStack -## Tests whether addContainer properly converts to GlobalStack. def test_addContainerGlobalStack(container_registry, definition_container, definition_changes_container): + """Tests whether addContainer properly converts to GlobalStack.""" + container_registry.addContainer(definition_container) container_registry.addContainer(definition_changes_container) diff --git a/tests/Settings/TestDefinitionContainer.py b/tests/Settings/TestDefinitionContainer.py index 2f2b343338..f2a9c0d245 100644 --- a/tests/Settings/TestDefinitionContainer.py +++ b/tests/Settings/TestDefinitionContainer.py @@ -57,9 +57,10 @@ def test_noCategory(file_path): metadata = DefinitionContainer.deserializeMetadata(json, "test_container_id") assert "category" not in metadata[0] -## Tests all definition containers @pytest.mark.parametrize("file_path", machine_filepaths) def test_validateMachineDefinitionContainer(file_path, definition_container): + """Tests all definition containers""" + file_name = os.path.basename(file_path) if file_name == "fdmprinter.def.json" or file_name == "fdmextruder.def.json": return # Stop checking, these are root files. @@ -85,13 +86,15 @@ def assertIsDefinitionValid(definition_container, file_path): if "platform_texture" in metadata[0]: assert metadata[0]["platform_texture"] in all_images -## Tests whether setting values are not being hidden by parent containers. -# -# When a definition container defines a "default_value" but inherits from a -# definition that defines a "value", the "default_value" is ineffective. This -# test fails on those things. @pytest.mark.parametrize("file_path", definition_filepaths) def test_validateOverridingDefaultValue(file_path: str): + """Tests whether setting values are not being hidden by parent containers. + + When a definition container defines a "default_value" but inherits from a + definition that defines a "value", the "default_value" is ineffective. This + test fails on those things. + """ + with open(file_path, encoding = "utf-8") as f: doc = json.load(f) @@ -107,12 +110,14 @@ def test_validateOverridingDefaultValue(file_path: str): faulty_keys.add(key) assert not faulty_keys, "Unnecessary default_values for {faulty_keys} in {file_name}".format(faulty_keys = sorted(faulty_keys), file_name = file_path) # If there is a value in the parent settings, then the default_value is not effective. -## Get all settings and their properties from a definition we're inheriting -# from. -# \param definition_id The definition we're inheriting from. -# \return A dictionary of settings by key. Each setting is a dictionary of -# properties. + def getInheritedSettings(definition_id: str) -> Dict[str, Any]: + """Get all settings and their properties from a definition we're inheriting from. + + :param definition_id: The definition we're inheriting from. + :return: A dictionary of settings by key. Each setting is a dictionary of properties. + """ + definition_path = os.path.join(os.path.dirname(__file__), "..", "..", "resources", "definitions", definition_id + ".def.json") with open(definition_path, encoding = "utf-8") as f: doc = json.load(f) @@ -127,13 +132,15 @@ def getInheritedSettings(definition_id: str) -> Dict[str, Any]: return result -## Put all settings in the main dictionary rather than in children dicts. -# \param settings Nested settings. The keys are the setting IDs. The values -# are dictionaries of properties per setting, including the "children" -# property. -# \return A dictionary of settings by key. Each setting is a dictionary of -# properties. + def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]: + """Put all settings in the main dictionary rather than in children dicts. + + :param settings: Nested settings. The keys are the setting IDs. The values + are dictionaries of properties per setting, including the "children" property. + :return: A dictionary of settings by key. Each setting is a dictionary of properties. + """ + result = {} for entry, contents in settings.items(): if "children" in contents: @@ -142,12 +149,16 @@ def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]: result[entry] = contents return result -## Make one dictionary override the other. Nested dictionaries override each -# other in the same way. -# \param base A dictionary of settings that will get overridden by the other. -# \param overrides A dictionary of settings that will override the other. -# \return Combined setting data. + def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, Any]: + """Make one dictionary override the other. Nested dictionaries override each + + other in the same way. + :param base: A dictionary of settings that will get overridden by the other. + :param overrides: A dictionary of settings that will override the other. + :return: Combined setting data. + """ + result = {} result.update(base) for key, val in overrides.items(): @@ -161,21 +172,25 @@ def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, An result[key] = val return result -## Verifies that definition contains don't have an ID field. -# -# ID fields are legacy. They should not be used any more. This is legacy that -# people don't seem to be able to get used to. + @pytest.mark.parametrize("file_path", definition_filepaths) def test_noId(file_path: str): + """Verifies that definition contains don't have an ID field. + + ID fields are legacy. They should not be used any more. This is legacy that + people don't seem to be able to get used to. + """ + with open(file_path, encoding = "utf-8") as f: doc = json.load(f) assert "id" not in doc, "Definitions should not have an ID field." -## Verifies that extruders say that they work on the same extruder_nr as what -# is listed in their machine definition. + @pytest.mark.parametrize("file_path", extruder_filepaths) def test_extruderMatch(file_path: str): + """Verifies that extruders say that they work on the same extruder_nr as what is listed in their machine definition.""" + extruder_id = os.path.basename(file_path).split(".")[0] with open(file_path, encoding = "utf-8") as f: doc = json.load(f) diff --git a/tests/Settings/TestExtruderStack.py b/tests/Settings/TestExtruderStack.py index 73d5f583b3..e9f167b6a1 100644 --- a/tests/Settings/TestExtruderStack.py +++ b/tests/Settings/TestExtruderStack.py @@ -14,11 +14,13 @@ from cura.Settings.Exceptions import InvalidContainerError, InvalidOperationErro from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.cura_empty_instance_containers import empty_container -## Gets an instance container with a specified container type. -# -# \param container_type The type metadata for the instance container. -# \return An instance container instance. def getInstanceContainer(container_type) -> InstanceContainer: + """Gets an instance container with a specified container type. + + :param container_type: The type metadata for the instance container. + :return: An instance container instance. + """ + container = InstanceContainer(container_id = "InstanceContainer") container.setMetaDataEntry("type", container_type) return container @@ -32,10 +34,12 @@ class InstanceContainerSubClass(InstanceContainer): super().__init__(container_id = "SubInstanceContainer") self.setMetaDataEntry("type", container_type) -#############################START OF TEST CASES################################ +############################START OF TEST CASES################################ + -## Tests whether adding a container is properly forbidden. def test_addContainer(extruder_stack): + """Tests whether adding a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): extruder_stack.addContainer(unittest.mock.MagicMock()) @@ -164,8 +168,10 @@ def test_constrainDefinitionInvalid(container, extruder_stack): def test_constrainDefinitionValid(container, extruder_stack): extruder_stack.definition = container #Should not give an error. -## Tests whether deserialising completes the missing containers with empty ones. + def test_deserializeCompletesEmptyContainers(extruder_stack): + """Tests whether deserialising completes the missing containers with empty ones.""" + extruder_stack._containers = [DefinitionContainer(container_id = "definition"), extruder_stack.definitionChanges] #Set the internal state of this stack manually. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -179,8 +185,10 @@ def test_deserializeCompletesEmptyContainers(extruder_stack): continue assert extruder_stack.getContainer(container_type_index) == empty_container #All others need to be empty. -## Tests whether an instance container with the wrong type gets removed when deserialising. + def test_deserializeRemovesWrongInstanceContainer(extruder_stack): + """Tests whether an instance container with the wrong type gets removed when deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "wrong type") extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -189,8 +197,10 @@ def test_deserializeRemovesWrongInstanceContainer(extruder_stack): assert extruder_stack.quality == extruder_stack._empty_instance_container #Replaced with empty. -## Tests whether a container with the wrong class gets removed when deserialising. + def test_deserializeRemovesWrongContainerClass(extruder_stack): + """Tests whether a container with the wrong class gets removed when deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = DefinitionContainer(container_id = "wrong class") extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -199,16 +209,20 @@ def test_deserializeRemovesWrongContainerClass(extruder_stack): assert extruder_stack.quality == extruder_stack._empty_instance_container #Replaced with empty. -## Tests whether an instance container in the definition spot results in an error. + def test_deserializeWrongDefinitionClass(extruder_stack): + """Tests whether an instance container in the definition spot results in an error.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = getInstanceContainer(container_type = "definition") #Correct type but wrong class. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. with pytest.raises(UM.Settings.ContainerStack.InvalidContainerStackError): #Must raise an error that there is no definition container. extruder_stack.deserialize("") -## Tests whether an instance container with the wrong type is moved into the correct slot by deserialising. + def test_deserializeMoveInstanceContainer(extruder_stack): + """Tests whether an instance container with the wrong type is moved into the correct slot by deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "material") #Not in the correct spot. extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -218,8 +232,10 @@ def test_deserializeMoveInstanceContainer(extruder_stack): assert extruder_stack.quality == empty_container assert extruder_stack.material != empty_container -## Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising. + def test_deserializeMoveDefinitionContainer(extruder_stack): + """Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising.""" + extruder_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Material] = DefinitionContainer(container_id = "some definition") #Not in the correct spot. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -228,8 +244,10 @@ def test_deserializeMoveDefinitionContainer(extruder_stack): assert extruder_stack.material == empty_container assert extruder_stack.definition != empty_container -## Tests whether getProperty properly applies the stack-like behaviour on its containers. + def test_getPropertyFallThrough(global_stack, extruder_stack): + """Tests whether getProperty properly applies the stack-like behaviour on its containers.""" + # ExtruderStack.setNextStack calls registerExtruder for backward compatibility, but we do not need a complete extruder manager ExtruderManager._ExtruderManager__instance = unittest.mock.MagicMock() @@ -273,13 +291,17 @@ def test_getPropertyFallThrough(global_stack, extruder_stack): extruder_stack.userChanges = mock_layer_heights[container_indices.UserChanges] assert extruder_stack.getProperty("layer_height", "value") == container_indices.UserChanges -## Tests whether inserting a container is properly forbidden. + def test_insertContainer(extruder_stack): + """Tests whether inserting a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): extruder_stack.insertContainer(0, unittest.mock.MagicMock()) -## Tests whether removing a container is properly forbidden. + def test_removeContainer(extruder_stack): + """Tests whether removing a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): extruder_stack.removeContainer(unittest.mock.MagicMock()) diff --git a/tests/Settings/TestGlobalStack.py b/tests/Settings/TestGlobalStack.py index c1044c9de6..391013233d 100755 --- a/tests/Settings/TestGlobalStack.py +++ b/tests/Settings/TestGlobalStack.py @@ -16,11 +16,13 @@ import UM.Settings.SettingDefinition #To add settings to the definition. from cura.Settings.cura_empty_instance_containers import empty_container -## Gets an instance container with a specified container type. -# -# \param container_type The type metadata for the instance container. -# \return An instance container instance. def getInstanceContainer(container_type) -> InstanceContainer: + """Gets an instance container with a specified container type. + + :param container_type: The type metadata for the instance container. + :return: An instance container instance. + """ + container = InstanceContainer(container_id = "InstanceContainer") container.setMetaDataEntry("type", container_type) return container @@ -37,17 +39,19 @@ class InstanceContainerSubClass(InstanceContainer): self.setMetaDataEntry("type", container_type) -#############################START OF TEST CASES################################ +############################START OF TEST CASES################################ -## Tests whether adding a container is properly forbidden. def test_addContainer(global_stack): + """Tests whether adding a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.addContainer(unittest.mock.MagicMock()) -## Tests adding extruders to the global stack. def test_addExtruder(global_stack): + """Tests adding extruders to the global stack.""" + mock_definition = unittest.mock.MagicMock() mock_definition.getProperty = lambda key, property, context = None: 2 if key == "machine_extruder_count" and property == "value" else None @@ -213,9 +217,12 @@ def test_constrainDefinitionValid(container, global_stack): global_stack.definition = container #Should not give an error. -## Tests whether deserialising completes the missing containers with empty ones. The initial containers are just the -# definition and the definition_changes (that cannot be empty after CURA-5281) def test_deserializeCompletesEmptyContainers(global_stack): + """Tests whether deserialising completes the missing containers with empty ones. The initial containers are just the + + definition and the definition_changes (that cannot be empty after CURA-5281) + """ + global_stack._containers = [DefinitionContainer(container_id = "definition"), global_stack.definitionChanges] #Set the internal state of this stack manually. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -229,8 +236,9 @@ def test_deserializeCompletesEmptyContainers(global_stack): assert global_stack.getContainer(container_type_index) == empty_container #All others need to be empty. -## Tests whether an instance container with the wrong type gets removed when deserialising. def test_deserializeRemovesWrongInstanceContainer(global_stack): + """Tests whether an instance container with the wrong type gets removed when deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "wrong type") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -240,8 +248,9 @@ def test_deserializeRemovesWrongInstanceContainer(global_stack): assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty. -## Tests whether a container with the wrong class gets removed when deserialising. def test_deserializeRemovesWrongContainerClass(global_stack): + """Tests whether a container with the wrong class gets removed when deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = DefinitionContainer(container_id = "wrong class") global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -251,8 +260,9 @@ def test_deserializeRemovesWrongContainerClass(global_stack): assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty. -## Tests whether an instance container in the definition spot results in an error. def test_deserializeWrongDefinitionClass(global_stack): + """Tests whether an instance container in the definition spot results in an error.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = getInstanceContainer(container_type = "definition") #Correct type but wrong class. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -260,8 +270,9 @@ def test_deserializeWrongDefinitionClass(global_stack): global_stack.deserialize("") -## Tests whether an instance container with the wrong type is moved into the correct slot by deserialising. def test_deserializeMoveInstanceContainer(global_stack): + """Tests whether an instance container with the wrong type is moved into the correct slot by deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "material") #Not in the correct spot. global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = DefinitionContainer(container_id = "some definition") @@ -272,8 +283,9 @@ def test_deserializeMoveInstanceContainer(global_stack): assert global_stack.material != empty_container -## Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising. def test_deserializeMoveDefinitionContainer(global_stack): + """Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising.""" + global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Material] = DefinitionContainer(container_id = "some definition") #Not in the correct spot. with unittest.mock.patch("UM.Settings.ContainerStack.ContainerStack.deserialize", unittest.mock.MagicMock()): #Prevent calling super().deserialize. @@ -283,8 +295,9 @@ def test_deserializeMoveDefinitionContainer(global_stack): assert global_stack.definition != empty_container -## Tests whether getProperty properly applies the stack-like behaviour on its containers. def test_getPropertyFallThrough(global_stack): + """Tests whether getProperty properly applies the stack-like behaviour on its containers.""" + #A few instance container mocks to put in the stack. mock_layer_heights = {} #For each container type, a mock container that defines layer height to something unique. mock_no_settings = {} #For each container type, a mock container that has no settings at all. @@ -326,8 +339,9 @@ def test_getPropertyFallThrough(global_stack): assert global_stack.getProperty("layer_height", "value") == container_indexes.UserChanges -## In definitions, test whether having no resolve allows us to find the value. def test_getPropertyNoResolveInDefinition(global_stack): + """In definitions, test whether having no resolve allows us to find the value.""" + value = unittest.mock.MagicMock() #Just sets the value for bed temperature. value.getProperty = lambda key, property, context = None: 10 if (key == "material_bed_temperature" and property == "value") else None @@ -336,8 +350,9 @@ def test_getPropertyNoResolveInDefinition(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 10 #No resolve, so fall through to value. -## In definitions, when the value is asked and there is a resolve function, it must get the resolve first. def test_getPropertyResolveInDefinition(global_stack): + """In definitions, when the value is asked and there is a resolve function, it must get the resolve first.""" + resolve_and_value = unittest.mock.MagicMock() #Sets the resolve and value for bed temperature. resolve_and_value.getProperty = lambda key, property, context = None: (7.5 if property == "resolve" else 5) if (key == "material_bed_temperature" and property in ("resolve", "value")) else None #7.5 resolve, 5 value. @@ -346,8 +361,9 @@ def test_getPropertyResolveInDefinition(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 7.5 #Resolve wins in the definition. -## In instance containers, when the value is asked and there is a resolve function, it must get the value first. def test_getPropertyResolveInInstance(global_stack): + """In instance containers, when the value is asked and there is a resolve function, it must get the value first.""" + container_indices = cura.Settings.CuraContainerStack._ContainerIndexes instance_containers = {} for container_type in container_indices.IndexTypeMap: @@ -373,8 +389,9 @@ def test_getPropertyResolveInInstance(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 5 -## Tests whether the value in instances gets evaluated before the resolve in definitions. def test_getPropertyInstancesBeforeResolve(global_stack): + """Tests whether the value in instances gets evaluated before the resolve in definitions.""" + def getValueProperty(key, property, context = None): if key != "material_bed_temperature": return None @@ -404,8 +421,9 @@ def test_getPropertyInstancesBeforeResolve(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 10 -## Tests whether the hasUserValue returns true for settings that are changed in the user-changes container. def test_hasUserValueUserChanges(global_stack): + """Tests whether the hasUserValue returns true for settings that are changed in the user-changes container.""" + container = unittest.mock.MagicMock() container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "user") container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. @@ -416,8 +434,9 @@ def test_hasUserValueUserChanges(global_stack): assert not global_stack.hasUserValue("") -## Tests whether the hasUserValue returns true for settings that are changed in the quality-changes container. def test_hasUserValueQualityChanges(global_stack): + """Tests whether the hasUserValue returns true for settings that are changed in the quality-changes container.""" + container = unittest.mock.MagicMock() container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality_changes") container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. @@ -428,8 +447,9 @@ def test_hasUserValueQualityChanges(global_stack): assert not global_stack.hasUserValue("") -## Tests whether a container in some other place on the stack is correctly not recognised as user value. def test_hasNoUserValue(global_stack): + """Tests whether a container in some other place on the stack is correctly not recognised as user value.""" + container = unittest.mock.MagicMock() container.getMetaDataEntry = unittest.mock.MagicMock(return_value = "quality") container.hasProperty = lambda key, property: key == "layer_height" #Only have the layer_height property set. @@ -438,20 +458,23 @@ def test_hasNoUserValue(global_stack): assert not global_stack.hasUserValue("layer_height") #However this container is quality, so it's not a user value. -## Tests whether inserting a container is properly forbidden. def test_insertContainer(global_stack): + """Tests whether inserting a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.insertContainer(0, unittest.mock.MagicMock()) -## Tests whether removing a container is properly forbidden. def test_removeContainer(global_stack): + """Tests whether removing a container is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.removeContainer(unittest.mock.MagicMock()) -## Tests whether changing the next stack is properly forbidden. def test_setNextStack(global_stack): + """Tests whether changing the next stack is properly forbidden.""" + with pytest.raises(InvalidOperationError): global_stack.setNextStack(unittest.mock.MagicMock()) diff --git a/tests/Settings/TestProfiles.py b/tests/Settings/TestProfiles.py index cf26ad7020..fba57c5eea 100644 --- a/tests/Settings/TestProfiles.py +++ b/tests/Settings/TestProfiles.py @@ -61,9 +61,10 @@ variant_filepaths = collectAllVariants() intent_filepaths = collectAllIntents() -## Attempt to load all the quality profiles. @pytest.mark.parametrize("file_name", quality_filepaths) def test_validateQualityProfiles(file_name): + """Attempt to load all the quality profiles.""" + try: with open(file_name, encoding = "utf-8") as data: serialized = data.read() @@ -114,9 +115,10 @@ def test_validateIntentProfiles(file_name): # File can't be read, header sections missing, whatever the case, this shouldn't happen! assert False, "Got an exception while reading the file {file_name}: {err}".format(file_name = file_name, err = str(e)) -## Attempt to load all the variant profiles. @pytest.mark.parametrize("file_name", variant_filepaths) def test_validateVariantProfiles(file_name): + """Attempt to load all the variant profiles.""" + try: with open(file_name, encoding = "utf-8") as data: serialized = data.read() diff --git a/tests/TestArrange.py b/tests/TestArrange.py index a00b544936..f37e48f19c 100755 --- a/tests/TestArrange.py +++ b/tests/TestArrange.py @@ -6,36 +6,43 @@ import numpy from cura.Arranging.Arrange import Arrange from cura.Arranging.ShapeArray import ShapeArray -## Triangle of area 12 def gimmeTriangle(): + """Triangle of area 12""" + return numpy.array([[-3, 1], [3, 1], [0, -3]], dtype=numpy.int32) -## Boring square def gimmeSquare(): + """Boring square""" + return numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32) -## Triangle of area 12 def gimmeShapeArray(scale = 1.0): + """Triangle of area 12""" + vertices = gimmeTriangle() shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) return shape_arr -## Boring square def gimmeShapeArraySquare(scale = 1.0): + """Boring square""" + vertices = gimmeSquare() shape_arr = ShapeArray.fromPolygon(vertices, scale = scale) return shape_arr -## Smoke test for Arrange def test_smoke_arrange(): + """Smoke test for Arrange""" + Arrange.create(fixed_nodes = []) -## Smoke test for ShapeArray def test_smoke_ShapeArray(): + """Smoke test for ShapeArray""" + gimmeShapeArray() -## Test ShapeArray def test_ShapeArray(): + """Test ShapeArray""" + scale = 1 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -44,8 +51,9 @@ def test_ShapeArray(): count = len(numpy.where(shape_arr.arr == 1)[0]) assert count >= 10 # should approach 12 -## Test ShapeArray with scaling def test_ShapeArray_scaling(): + """Test ShapeArray with scaling""" + scale = 2 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -54,8 +62,9 @@ def test_ShapeArray_scaling(): count = len(numpy.where(shape_arr.arr == 1)[0]) assert count >= 40 # should approach 2*2*12 = 48 -## Test ShapeArray with scaling def test_ShapeArray_scaling2(): + """Test ShapeArray with scaling""" + scale = 0.5 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -64,8 +73,9 @@ def test_ShapeArray_scaling2(): count = len(numpy.where(shape_arr.arr == 1)[0]) assert count >= 1 # should approach 3, but it can be inaccurate due to pixel rounding -## Test centerFirst def test_centerFirst(): + """Test centerFirst""" + ar = Arrange(300, 300, 150, 150, scale = 1) ar.centerFirst() assert ar._priority[150][150] < ar._priority[170][150] @@ -75,8 +85,9 @@ def test_centerFirst(): assert ar._priority[150][150] < ar._priority[150][130] assert ar._priority[150][150] < ar._priority[130][130] -## Test centerFirst def test_centerFirst_rectangular(): + """Test centerFirst""" + ar = Arrange(400, 300, 200, 150, scale = 1) ar.centerFirst() assert ar._priority[150][200] < ar._priority[150][220] @@ -86,15 +97,17 @@ def test_centerFirst_rectangular(): assert ar._priority[150][200] < ar._priority[130][200] assert ar._priority[150][200] < ar._priority[130][180] -## Test centerFirst def test_centerFirst_rectangular2(): + """Test centerFirst""" + ar = Arrange(10, 20, 5, 10, scale = 1) ar.centerFirst() assert ar._priority[10][5] < ar._priority[10][7] -## Test backFirst def test_backFirst(): + """Test backFirst""" + ar = Arrange(300, 300, 150, 150, scale = 1) ar.backFirst() assert ar._priority[150][150] < ar._priority[170][150] @@ -102,8 +115,9 @@ def test_backFirst(): assert ar._priority[150][150] > ar._priority[130][150] assert ar._priority[150][150] > ar._priority[130][130] -## See if the result of bestSpot has the correct form def test_smoke_bestSpot(): + """See if the result of bestSpot has the correct form""" + ar = Arrange(30, 30, 15, 15, scale = 1) ar.centerFirst() @@ -114,8 +128,9 @@ def test_smoke_bestSpot(): assert hasattr(best_spot, "penalty_points") assert hasattr(best_spot, "priority") -## Real life test def test_bestSpot(): + """Real life test""" + ar = Arrange(16, 16, 8, 8, scale = 1) ar.centerFirst() @@ -131,8 +146,9 @@ def test_bestSpot(): assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location ar.place(best_spot.x, best_spot.y, shape_arr) -## Real life test rectangular build plate def test_bestSpot_rectangular_build_plate(): + """Real life test rectangular build plate""" + ar = Arrange(16, 40, 8, 20, scale = 1) ar.centerFirst() @@ -164,8 +180,9 @@ def test_bestSpot_rectangular_build_plate(): best_spot_x = ar.bestSpot(shape_arr) ar.place(best_spot_x.x, best_spot_x.y, shape_arr) -## Real life test def test_bestSpot_scale(): + """Real life test""" + scale = 0.5 ar = Arrange(16, 16, 8, 8, scale = scale) ar.centerFirst() @@ -182,8 +199,9 @@ def test_bestSpot_scale(): assert best_spot.x != 0 or best_spot.y != 0 # it can't be on the same location ar.place(best_spot.x, best_spot.y, shape_arr) -## Real life test def test_bestSpot_scale_rectangular(): + """Real life test""" + scale = 0.5 ar = Arrange(16, 40, 8, 20, scale = scale) ar.centerFirst() @@ -205,8 +223,9 @@ def test_bestSpot_scale_rectangular(): best_spot = ar.bestSpot(shape_arr_square) ar.place(best_spot.x, best_spot.y, shape_arr_square) -## Try to place an object and see if something explodes def test_smoke_place(): + """Try to place an object and see if something explodes""" + ar = Arrange(30, 30, 15, 15) ar.centerFirst() @@ -216,8 +235,9 @@ def test_smoke_place(): ar.place(0, 0, shape_arr) assert numpy.any(ar._occupied) -## See of our center has less penalty points than out of the center def test_checkShape(): + """See of our center has less penalty points than out of the center""" + ar = Arrange(30, 30, 15, 15) ar.centerFirst() @@ -228,8 +248,9 @@ def test_checkShape(): assert points2 > points assert points3 > points -## See of our center has less penalty points than out of the center def test_checkShape_rectangular(): + """See of our center has less penalty points than out of the center""" + ar = Arrange(20, 30, 10, 15) ar.centerFirst() @@ -240,8 +261,9 @@ def test_checkShape_rectangular(): assert points2 > points assert points3 > points -## Check that placing an object on occupied place returns None. def test_checkShape_place(): + """Check that placing an object on occupied place returns None.""" + ar = Arrange(30, 30, 15, 15) ar.centerFirst() @@ -252,8 +274,9 @@ def test_checkShape_place(): assert points2 is None -## Test the whole sequence def test_smoke_place_objects(): + """Test the whole sequence""" + ar = Arrange(20, 20, 10, 10, scale = 1) ar.centerFirst() shape_arr = gimmeShapeArray() @@ -268,26 +291,30 @@ def test_compare_occupied_and_priority_tables(): ar.centerFirst() assert ar._priority.shape == ar._occupied.shape -## Polygon -> array def test_arrayFromPolygon(): + """Polygon -> array""" + vertices = numpy.array([[-3, 1], [3, 1], [0, -3]]) array = ShapeArray.arrayFromPolygon([5, 5], vertices) assert numpy.any(array) -## Polygon -> array def test_arrayFromPolygon2(): + """Polygon -> array""" + vertices = numpy.array([[-3, 1], [3, 1], [2, -3]]) array = ShapeArray.arrayFromPolygon([5, 5], vertices) assert numpy.any(array) -## Polygon -> array def test_fromPolygon(): + """Polygon -> array""" + vertices = numpy.array([[0, 0.5], [0, 0], [0.5, 0]]) array = ShapeArray.fromPolygon(vertices, scale=0.5) assert numpy.any(array.arr) -## Line definition -> array with true/false def test_check(): + """Line definition -> array with true/false""" + base_array = numpy.zeros([5, 5], dtype=float) p1 = numpy.array([0, 0]) p2 = numpy.array([4, 4]) @@ -296,8 +323,9 @@ def test_check(): assert check_array[3][0] assert not check_array[0][3] -## Line definition -> array with true/false def test_check2(): + """Line definition -> array with true/false""" + base_array = numpy.zeros([5, 5], dtype=float) p1 = numpy.array([0, 3]) p2 = numpy.array([4, 3]) @@ -306,8 +334,9 @@ def test_check2(): assert not check_array[3][0] assert check_array[3][4] -## Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM def test_parts_of_fromNode(): + """Just adding some stuff to ensure fromNode works as expected. Some parts should actually be in UM""" + from UM.Math.Polygon import Polygon p = Polygon(numpy.array([[-2, -2], [2, -2], [2, 2], [-2, 2]], dtype=numpy.int32)) offset = 1 From ff3f4c99a88a9fea2ee2b7d876f266b452957a83 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 28 May 2020 17:34:04 +0200 Subject: [PATCH 25/27] Remove incorrect comment --- tests/Settings/TestExtruderStack.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Settings/TestExtruderStack.py b/tests/Settings/TestExtruderStack.py index e9f167b6a1..0af0f1dcf3 100644 --- a/tests/Settings/TestExtruderStack.py +++ b/tests/Settings/TestExtruderStack.py @@ -248,7 +248,6 @@ def test_deserializeMoveDefinitionContainer(extruder_stack): def test_getPropertyFallThrough(global_stack, extruder_stack): """Tests whether getProperty properly applies the stack-like behaviour on its containers.""" - # ExtruderStack.setNextStack calls registerExtruder for backward compatibility, but we do not need a complete extruder manager ExtruderManager._ExtruderManager__instance = unittest.mock.MagicMock() #A few instance container mocks to put in the stack. From 1e33360c359373f7acbf95ca9706514d12c0dd7f Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 29 May 2020 14:12:47 +0200 Subject: [PATCH 26/27] Fix merge mistake in ToolPathUploader --- plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py index 8707b2ee3e..246c33b0c7 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py @@ -118,7 +118,6 @@ class ToolPathUploader: def _errorCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None: """Handles an error uploading.""" - reply = cast(QNetworkReply, self._reply) body = bytes(reply.readAll()).decode() Logger.log("e", "Received error while uploading: %s", body) self.stop() @@ -127,7 +126,6 @@ class ToolPathUploader: def _finishedCallback(self, reply: QNetworkReply) -> None: """Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed.""" - reply = cast(QNetworkReply, self._reply) Logger.log("i", "Finished callback %s %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString()) From 89f0970a887612a6819263633180d027d06224af Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 29 May 2020 14:30:33 +0200 Subject: [PATCH 27/27] Remove trailing whitespace from Python files --- cmake/mod_bundled_packages_json.py | 4 +- cura/API/Backups.py | 8 +-- cura/API/Interface/Settings.py | 8 +-- cura/API/Interface/__init__.py | 4 +- cura/Arranging/ShapeArray.py | 8 +-- cura/Backups/Backup.py | 10 +-- cura/BuildVolume.py | 22 +++---- cura/CrashHandler.py | 2 +- cura/CuraActions.py | 4 +- cura/CuraApplication.py | 14 ++-- cura/Layer.py | 8 +-- cura/LayerData.py | 2 +- cura/LayerDataBuilder.py | 2 +- cura/LayerPolygon.py | 24 +++---- cura/MachineAction.py | 4 +- cura/Machines/ContainerNode.py | 10 +-- cura/Machines/ContainerTree.py | 20 +++--- cura/Machines/IntentNode.py | 2 +- cura/Machines/MachineNode.py | 12 ++-- cura/Machines/MaterialGroup.py | 6 +- cura/Machines/MaterialNode.py | 4 +- cura/Machines/Models/BaseMaterialsModel.py | 4 +- .../Models/DiscoveredPrintersModel.py | 2 +- cura/Machines/Models/ExtrudersModel.py | 10 +-- cura/Machines/Models/IntentCategoryModel.py | 2 +- .../Models/MaterialManagementModel.py | 16 ++--- .../Machines/Models/QualityManagementModel.py | 16 ++--- cura/Machines/QualityChangesGroup.py | 2 +- cura/Machines/QualityGroup.py | 6 +- cura/Machines/QualityNode.py | 4 +- cura/Machines/VariantNode.py | 10 +-- cura/OAuth2/AuthorizationHelpers.py | 24 +++---- cura/OAuth2/AuthorizationRequestHandler.py | 8 +-- cura/OAuth2/AuthorizationRequestServer.py | 4 +- cura/OAuth2/AuthorizationService.py | 2 +- cura/OAuth2/LocalAuthorizationServer.py | 14 ++-- cura/OneAtATimeIterator.py | 6 +- cura/Operations/SetParentOperation.py | 6 +- cura/PickingPass.py | 4 +- cura/PlatformPhysics.py | 6 +- cura/PreviewPass.py | 4 +- cura/PrinterOutput/FirmwareUpdater.py | 2 +- .../Models/ExtruderConfigurationModel.py | 2 +- .../Models/ExtruderOutputModel.py | 2 +- .../Models/PrinterConfigurationModel.py | 4 +- .../Models/PrinterOutputModel.py | 2 +- .../NetworkedPrinterOutputDevice.py | 14 ++-- cura/PrinterOutput/Peripheral.py | 4 +- cura/PrinterOutput/PrinterOutputDevice.py | 10 +-- cura/ReaderWriters/ProfileReader.py | 4 +- cura/ReaderWriters/ProfileWriter.py | 8 +-- cura/Scene/ConvexHullDecorator.py | 20 +++--- cura/Scene/ConvexHullNode.py | 2 +- cura/Scene/CuraSceneNode.py | 4 +- cura/Scene/SliceableObjectDecorator.py | 2 +- cura/Settings/ContainerManager.py | 30 ++++----- cura/Settings/CuraContainerStack.py | 64 +++++++++---------- cura/Settings/CuraStackBuilder.py | 14 ++-- cura/Settings/ExtruderStack.py | 16 ++--- cura/Settings/GlobalStack.py | 20 +++--- cura/Settings/IntentManager.py | 16 ++--- cura/Settings/MachineManager.py | 44 ++++++------- cura/Settings/MachineNameValidator.py | 4 +- cura/Settings/SettingOverrideDecorator.py | 14 ++-- cura/UI/MachineActionManager.py | 14 ++-- cura/UI/PrintInformation.py | 2 +- cura/Utils/Decorators.py | 4 +- plugins/3MFReader/ThreeMFReader.py | 10 +-- plugins/3MFReader/ThreeMFWorkspaceReader.py | 12 ++-- plugins/3MFReader/__init__.py | 2 +- plugins/3MFWriter/ThreeMFWorkspaceWriter.py | 2 +- plugins/3MFWriter/ThreeMFWriter.py | 4 +- plugins/AMFReader/AMFReader.py | 2 +- .../CuraEngineBackend/CuraEngineBackend.py | 2 +- .../ProcessSlicedLayersJob.py | 4 +- .../CuraProfileReader/CuraProfileReader.py | 12 ++-- .../CuraProfileWriter/CuraProfileWriter.py | 2 +- .../FirmwareUpdateChecker.py | 6 +- plugins/GCodeGzReader/GCodeGzReader.py | 2 +- plugins/GCodeGzWriter/GCodeGzWriter.py | 6 +- .../GCodeProfileReader/GCodeProfileReader.py | 10 +-- plugins/GCodeReader/FlavorParser.py | 2 +- plugins/GCodeReader/RepRapFlavorParser.py | 4 +- plugins/GCodeWriter/GCodeWriter.py | 18 +++--- .../LegacyProfileReader.py | 16 ++--- .../MachineSettingsAction.py | 4 +- plugins/ModelChecker/ModelChecker.py | 2 +- .../PerObjectSettingVisibilityHandler.py | 2 +- .../PerObjectSettingsTool.py | 6 +- .../PostProcessingPlugin.py | 6 +- plugins/PostProcessingPlugin/Script.py | 8 +-- .../PostProcessingPlugin/scripts/ColorMix.py | 8 +-- .../scripts/DisplayFilenameAndLayerOnLCD.py | 4 +- .../scripts/FilamentChange.py | 2 +- .../scripts/SearchAndReplace.py | 2 +- .../scripts/UsePreviousProbeMeasurements.py | 2 +- plugins/PreviewStage/PreviewStage.py | 8 +-- .../LinuxRemovableDrivePlugin.py | 2 +- .../RemovableDriveOutputDevice.py | 8 +-- plugins/SentryLogger/SentryLogger.py | 6 +- plugins/SliceInfoPlugin/SliceInfo.py | 4 +- plugins/Toolbox/src/AuthorsModel.py | 2 +- .../src/CloudSync/DiscrepanciesPresenter.py | 2 +- .../src/CloudSync/DownloadPresenter.py | 2 +- .../Toolbox/src/CloudSync/LicensePresenter.py | 2 +- .../CloudSync/RestartApplicationPresenter.py | 2 +- .../Toolbox/src/CloudSync/SyncOrchestrator.py | 4 +- plugins/Toolbox/src/PackagesModel.py | 2 +- plugins/Toolbox/src/Toolbox.py | 4 +- plugins/TrimeshReader/TrimeshReader.py | 4 +- .../src/Cloud/CloudOutputDevice.py | 14 ++-- .../src/Cloud/CloudOutputDeviceManager.py | 2 +- .../src/MeshFormatHandler.py | 8 +-- .../Messages/PrintJobUploadProgressMessage.py | 2 +- .../src/Models/BaseModel.py | 10 +-- .../src/Models/Http/CloudClusterResponse.py | 2 +- .../src/Models/Http/CloudClusterStatus.py | 2 +- .../src/Models/Http/CloudError.py | 2 +- .../src/Models/Http/CloudPrintJobResponse.py | 2 +- .../Models/Http/CloudPrintJobUploadRequest.py | 2 +- .../src/Models/Http/CloudPrintResponse.py | 2 +- .../src/Models/Http/ClusterBuildPlate.py | 2 +- .../Http/ClusterPrintCoreConfiguration.py | 6 +- .../ClusterPrintJobConfigurationChange.py | 2 +- .../Models/Http/ClusterPrintJobConstraint.py | 2 +- .../Models/Http/ClusterPrintJobImpediment.py | 2 +- .../src/Models/Http/ClusterPrintJobStatus.py | 6 +- .../ClusterPrinterConfigurationMaterial.py | 4 +- .../Http/ClusterPrinterMaterialStation.py | 2 +- .../Http/ClusterPrinterMaterialStationSlot.py | 2 +- .../src/Models/Http/ClusterPrinterStatus.py | 10 +-- .../src/Network/ClusterApiClient.py | 18 +++--- .../src/Network/LocalClusterOutputDevice.py | 10 +-- .../src/Network/SendMaterialJob.py | 12 ++-- .../src/Network/ZeroConfClient.py | 6 +- .../src/UM3OutputDevicePlugin.py | 2 +- .../src/UltimakerNetworkedPrinterAction.py | 2 +- .../UltimakerNetworkedPrinterOutputDevice.py | 10 +-- plugins/USBPrinting/USBPrinterOutputDevice.py | 4 +- .../USBPrinterOutputDeviceManager.py | 2 +- plugins/USBPrinting/avr_isp/intelHex.py | 2 +- plugins/USBPrinting/avr_isp/ispBase.py | 2 +- plugins/USBPrinting/avr_isp/stk500v2.py | 2 +- .../BedLevelMachineAction.py | 2 +- .../VersionUpgrade41to42.py | 6 +- .../VersionUpgrade42to43.py | 6 +- .../VersionUpgrade43to44.py | 4 +- .../VersionUpgrade44to45.py | 4 +- .../XmlMaterialProfile/XmlMaterialProfile.py | 12 ++-- scripts/check_gcode_buffer.py | 4 +- scripts/lionbridge_import.py | 10 +-- tests/Machines/TestMachineNode.py | 6 +- tests/Machines/TestVariantNode.py | 6 +- tests/Settings/MockContainer.py | 28 ++++---- tests/Settings/TestDefinitionContainer.py | 8 +-- tests/Settings/TestExtruderStack.py | 2 +- tests/Settings/TestGlobalStack.py | 4 +- 157 files changed, 562 insertions(+), 562 deletions(-) diff --git a/cmake/mod_bundled_packages_json.py b/cmake/mod_bundled_packages_json.py index 66038b30e2..e03261b479 100755 --- a/cmake/mod_bundled_packages_json.py +++ b/cmake/mod_bundled_packages_json.py @@ -13,7 +13,7 @@ import sys def find_json_files(work_dir: str) -> list: """Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths. - + :param work_dir: The directory to look for JSON files recursively. :return: A list of JSON files in absolute paths that are found in the given directory. """ @@ -28,7 +28,7 @@ def find_json_files(work_dir: str) -> list: def remove_entries_from_json_file(file_path: str, entries: list) -> None: """Removes the given entries from the given JSON file. The file will modified in-place. - + :param file_path: The JSON file to modify. :param entries: A list of strings as entries to remove. :return: None diff --git a/cura/API/Backups.py b/cura/API/Backups.py index cb5f2aa487..1940d38a36 100644 --- a/cura/API/Backups.py +++ b/cura/API/Backups.py @@ -10,9 +10,9 @@ if TYPE_CHECKING: class Backups: """The back-ups API provides a version-proof bridge between Cura's - + BackupManager and plug-ins that hook into it. - + Usage: .. code-block:: python @@ -28,7 +28,7 @@ class Backups: def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]: """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. """ @@ -36,7 +36,7 @@ class Backups: def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None: """Restore a back-up using the BackupsManager. - + :param zip_file: A ZIP file containing the actual back-up data. :param meta_data: Some metadata needed for restoring a back-up, like the Cura version number. """ diff --git a/cura/API/Interface/Settings.py b/cura/API/Interface/Settings.py index 18653bc88e..706a6d8c74 100644 --- a/cura/API/Interface/Settings.py +++ b/cura/API/Interface/Settings.py @@ -10,9 +10,9 @@ if TYPE_CHECKING: class Settings: """The Interface.Settings API provides a version-proof bridge between Cura's - + (currently) sidebar UI and plug-ins that hook into it. - + Usage: .. code-block:: python @@ -34,7 +34,7 @@ class Settings: def addContextMenuItem(self, menu_item: dict) -> None: """Add items to the sidebar context menu. - + :param menu_item: dict containing the menu item to add. """ @@ -42,7 +42,7 @@ class Settings: def getContextMenuItems(self) -> list: """Get all custom items currently added to the sidebar context menu. - + :return: List containing all custom context menu items. """ diff --git a/cura/API/Interface/__init__.py b/cura/API/Interface/__init__.py index 13174070d1..61510d6262 100644 --- a/cura/API/Interface/__init__.py +++ b/cura/API/Interface/__init__.py @@ -11,9 +11,9 @@ if TYPE_CHECKING: class Interface: """The Interface class serves as a common root for the specific API - + methods for each interface element. - + Usage: .. code-block:: python diff --git a/cura/Arranging/ShapeArray.py b/cura/Arranging/ShapeArray.py index c704ae7ca2..840f9731c2 100644 --- a/cura/Arranging/ShapeArray.py +++ b/cura/Arranging/ShapeArray.py @@ -23,7 +23,7 @@ class ShapeArray: @classmethod def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray": """Instantiate from a bunch of vertices - + :param vertices: :param scale: scale the coordinates :return: a shape array instantiated from a bunch of vertices @@ -52,7 +52,7 @@ class ShapeArray: @classmethod def fromNode(cls, node: "SceneNode", min_offset: float, scale: float = 0.5, include_children: bool = False) -> Tuple[Optional["ShapeArray"], Optional["ShapeArray"]]: """Instantiate an offset and hull ShapeArray from a scene node. - + :param node: source node where the convex hull must be present :param min_offset: offset for the offset ShapeArray :param scale: scale the coordinates @@ -100,7 +100,7 @@ class ShapeArray: @classmethod def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array: """Create :py:class:`numpy.ndarray` with dimensions defined by shape - + Fills polygon defined by vertices with ones, all other values zero Only works correctly for convex hull vertices Originally from: `Stackoverflow - generating a filled polygon inside a numpy array `_ @@ -128,7 +128,7 @@ class ShapeArray: @classmethod def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]: """Return indices that mark one side of the line, used by arrayFromPolygon - + Uses the line defined by p1 and p2 to check array of input indices against interpolated value Returns boolean array, with True inside and False outside of shape diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py index 6d1906e1dc..44e1feef30 100644 --- a/cura/Backups/Backup.py +++ b/cura/Backups/Backup.py @@ -20,7 +20,7 @@ if TYPE_CHECKING: class Backup: """The back-up class holds all data about a back-up. - + It is also responsible for reading and writing the zip file to the user data folder. """ @@ -68,7 +68,7 @@ class Backup: material_count = len([s for s in files if "materials/" in s]) - 1 profile_count = len([s for s in files if "quality_changes/" in s]) - 1 plugin_count = len([s for s in files if "plugin.json" in s]) - + # Store the archive and metadata so the BackupManager can fetch them when needed. self.zip_file = buffer.getvalue() self.meta_data = { @@ -81,7 +81,7 @@ class Backup: def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]: """Make a full archive from the given root path with the given name. - + :param root_path: The root directory to archive recursively. :return: The archive as bytes. """ @@ -111,7 +111,7 @@ class Backup: def restore(self) -> bool: """Restore this back-up. - + :return: Whether we had success or not. """ @@ -151,7 +151,7 @@ class Backup: @staticmethod def _extractArchive(archive: "ZipFile", target_path: str) -> bool: """Extract the whole archive to the given target path. - + :param archive: The archive as ZipFile. :param target_path: The target path. :return: Whether we had success or not. diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index cf0e18ad6a..315eca9fe5 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -166,7 +166,7 @@ class BuildVolume(SceneNode): def _updateNodeListeners(self, node: SceneNode): """Updates the listeners that listen for changes in per-mesh stacks. - + :param node: The node for which the decorators changed. """ @@ -587,7 +587,7 @@ class BuildVolume(SceneNode): def _calculateExtraZClearance(self, extruders: List["ContainerStack"]) -> float: if not self._global_container_stack: return 0 - + extra_z = 0.0 for extruder in extruders: if extruder.getProperty("retraction_hop_enabled", "value"): @@ -728,7 +728,7 @@ class BuildVolume(SceneNode): def _updateDisallowedAreasAndRebuild(self): """Calls :py:meth:`cura.BuildVolume._updateDisallowedAreas` and makes sure the changes appear in the scene. - + This is required for a signal to trigger the update in one go. The :py:meth:`cura.BuildVolume._updateDisallowedAreas` method itself shouldn't call :py:meth:`cura.BuildVolume.rebuild`, since there may be other changes before it needs to be rebuilt, @@ -798,9 +798,9 @@ class BuildVolume(SceneNode): def _computeDisallowedAreasPrinted(self, used_extruders): """Computes the disallowed areas for objects that are printed with print features. - + This means that the brim, travel avoidance and such will be applied to these features. - + :return: A dictionary with for each used extruder ID the disallowed areas where that extruder may not print. """ @@ -843,10 +843,10 @@ class BuildVolume(SceneNode): def _computeDisallowedAreasPrimeBlob(self, border_size: float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]: """Computes the disallowed areas for the prime blobs. - + These are special because they are not subject to things like brim or travel avoidance. They do get a dilute with the border size though because they may not intersect with brims and such of other objects. - + :param border_size: The size with which to offset the disallowed areas due to skirt, brim, travel avoid distance , etc. :param used_extruders: The extruder stacks to generate disallowed areas for. @@ -882,7 +882,7 @@ class BuildVolume(SceneNode): def _computeDisallowedAreasStatic(self, border_size:float, used_extruders: List["ExtruderStack"]) -> Dict[str, List[Polygon]]: """Computes the disallowed areas that are statically placed in the machine. - + It computes different disallowed areas depending on the offset of the extruder. The resulting dictionary will therefore have an entry for each extruder that is used. @@ -1024,9 +1024,9 @@ class BuildVolume(SceneNode): def _getSettingFromAllExtruders(self, setting_key: str) -> List[Any]: """Private convenience function to get a setting from every extruder. - + For single extrusion machines, this gets the setting from the global stack. - + :return: A sequence of setting values, one for each extruder. """ @@ -1116,7 +1116,7 @@ class BuildVolume(SceneNode): def getEdgeDisallowedSize(self): """Calculate the disallowed radius around the edge. - + This disallowed radius is to allow for space around the models that is not part of the collision radius, such as bed adhesion (skirt/brim/raft) and travel avoid distance. """ diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index 75fdd99798..052f963fd4 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -237,7 +237,7 @@ class CrashHandler: scope.set_tag("locale_os", self.data["locale_os"]) scope.set_tag("locale_cura", self.cura_locale) scope.set_tag("is_enterprise", ApplicationMetadata.IsEnterpriseVersion) - + scope.set_user({"id": str(uuid.getnode())}) return group diff --git a/cura/CuraActions.py b/cura/CuraActions.py index 3b37c69adc..4f3e842379 100644 --- a/cura/CuraActions.py +++ b/cura/CuraActions.py @@ -78,7 +78,7 @@ class CuraActions(QObject): @pyqtSlot(int) def multiplySelection(self, count: int) -> None: """Multiply all objects in the selection - + :param count: The number of times to multiply the selection. """ @@ -114,7 +114,7 @@ class CuraActions(QObject): @pyqtSlot(str) def setExtruderForSelection(self, extruder_id: str) -> None: """Set the extruder that should be used to print the selection. - + :param extruder_id: The ID of the extruder stack to use for the selected objects. """ diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 4cd48e6bb3..9d53fa2c1f 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -730,7 +730,7 @@ class CuraApplication(QtApplication): def _loadPlugins(self) -> None: """Handle loading of all plugin types (and the backend explicitly) - + :py:class:`Uranium.UM.PluginRegistry` """ @@ -759,7 +759,7 @@ class CuraApplication(QtApplication): def _setLoadingHint(self, hint: str): """Set a short, user-friendly hint about current loading status. - + The way this message is displayed depends on application state """ @@ -1010,7 +1010,7 @@ class CuraApplication(QtApplication): def getMachineActionManager(self, *args: Any) -> MachineActionManager.MachineActionManager: """Get the machine action manager - + We ignore any *args given to this, as we also register the machine manager as qml singleton. It wants to give this function an engine and script engine, but we don't care about that. """ @@ -1068,7 +1068,7 @@ class CuraApplication(QtApplication): def registerObjects(self, engine): """Registers objects for the QML engine to use. - + :param engine: The QML engine. """ @@ -1391,7 +1391,7 @@ class CuraApplication(QtApplication): def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None: """Arrange a set of nodes given a set of fixed nodes - + :param nodes: nodes that we have to place :param fixed_nodes: nodes that are placed in the arranger before finding spots for nodes """ @@ -1685,7 +1685,7 @@ class CuraApplication(QtApplication): @pyqtSlot(str, "QVariant") def addAdditionalComponent(self, area_id: str, component): """Add a component to a list of components to be reparented to another area in the GUI. - + The actual reparenting is done by the area itself. :param area_id: dentifying name of the area to which the component should be reparented :param (QQuickComponent) component: The component that should be reparented @@ -1707,7 +1707,7 @@ class CuraApplication(QtApplication): @pyqtSlot(QUrl) def readLocalFile(self, file: QUrl, project_mode: Optional[str] = None): """Open a local file - + :param project_mode: How to handle project files. Either None(default): Follow user preference, "open_as_model" or "open_as_project". This parameter is only considered if the file is a project file. """ diff --git a/cura/Layer.py b/cura/Layer.py index 933d4436c9..af42488e2a 100644 --- a/cura/Layer.py +++ b/cura/Layer.py @@ -76,7 +76,7 @@ class Layer: def createMeshOrJumps(self, make_mesh: bool) -> MeshData: builder = MeshBuilder() - + line_count = 0 if make_mesh: for polygon in self._polygons: @@ -87,7 +87,7 @@ class Layer: # Reserve the necessary space for the data upfront builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count) - + for polygon in self._polygons: # Filter out the types of lines we are not interested in depending on whether we are drawing the mesh or the jumps. index_mask = numpy.logical_not(polygon.jumpMask) if make_mesh else polygon.jumpMask @@ -96,7 +96,7 @@ class Layer: points = numpy.concatenate((polygon.data[:-1], polygon.data[1:]), 1)[index_mask.ravel()] # Line types of the points we want to draw line_types = polygon.types[index_mask] - + # Shift the z-axis according to previous implementation. if make_mesh: points[polygon.isInfillOrSkinType(line_types), 1::3] -= 0.01 @@ -118,5 +118,5 @@ class Layer: f_colors = numpy.repeat(polygon.mapLineTypeToColor(line_types), 4, 0) builder.addFacesWithColor(f_points, f_indices, f_colors) - + return builder.build() \ No newline at end of file diff --git a/cura/LayerData.py b/cura/LayerData.py index 1b3f2a978e..e58fda597a 100644 --- a/cura/LayerData.py +++ b/cura/LayerData.py @@ -5,7 +5,7 @@ from UM.Mesh.MeshData import MeshData class LayerData(MeshData): """Class to holds the layer mesh and information about the layers. - + Immutable, use :py:class:`cura.LayerDataBuilder.LayerDataBuilder` to create one of these. """ diff --git a/cura/LayerDataBuilder.py b/cura/LayerDataBuilder.py index e104273e37..d8801c9e7b 100755 --- a/cura/LayerDataBuilder.py +++ b/cura/LayerDataBuilder.py @@ -45,7 +45,7 @@ class LayerDataBuilder(MeshBuilder): def build(self, material_color_map, line_type_brightness = 1.0): """Return the layer data as :py:class:`cura.LayerData.LayerData`. - + :param material_color_map: [r, g, b, a] for each extruder row. :param line_type_brightness: compatibility layer view uses line type brightness of 0.5 """ diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py index 2cfff612b9..6e518e984a 100644 --- a/cura/LayerPolygon.py +++ b/cura/LayerPolygon.py @@ -62,21 +62,21 @@ class LayerPolygon: # re-used and can save alot of memory usage. self._color_map = LayerPolygon.getColorMap() self._colors = self._color_map[self._types] # type: numpy.ndarray - + # When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType # Should be generated in better way, not hardcoded. self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = numpy.bool) - + self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray] self._build_cache_needed_points = None # type: Optional[numpy.ndarray] - + def buildCache(self) -> None: # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out. self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype = bool) mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask) self._index_begin = 0 self._index_end = mesh_line_count - + self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = numpy.bool) # Only if the type of line segment changes do we need to add an extra vertex to change colors self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1] @@ -88,9 +88,9 @@ class LayerPolygon: def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray, colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray, extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None: """Set all the arrays provided by the function caller, representing the LayerPolygon - + The arrays are either by vertex or by indices. - + :param vertex_offset: determines where to start and end filling the arrays :param index_offset: determines where to start and end filling the arrays :param vertices: vertex numpy array to be filled @@ -111,16 +111,16 @@ class LayerPolygon: line_mesh_mask = self._build_cache_line_mesh_mask needed_points_list = self._build_cache_needed_points - + # Index to the points we need to represent the line mesh. This is constructed by generating simple # start and end points for each line. For line segment n these are points n and n+1. Row n reads [n n+1] # Then then the indices for the points we don't need are thrown away based on the pre-calculated list. index_list = ( numpy.arange(len(self._types)).reshape((-1, 1)) + numpy.array([[0, 1]]) ).reshape((-1, 1))[needed_points_list.reshape((-1, 1))] - + # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. self._vertex_begin += vertex_offset self._vertex_end += vertex_offset - + # Points are picked based on the index list to get the vertices needed. vertices[self._vertex_begin:self._vertex_end, :] = self._data[index_list, :] @@ -142,14 +142,14 @@ class LayerPolygon: # The relative values of begin and end indices have already been set in buildCache, so we only need to offset them to the parents offset. self._index_begin += index_offset self._index_end += index_offset - + indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype = numpy.int32).reshape((-1, 1)) # When the line type changes the index needs to be increased by 2. indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1)) # Each line segment goes from it's starting point p to p+1, offset by the vertex index. # The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above. indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin]) - + self._build_cache_line_mesh_mask = None self._build_cache_needed_points = None @@ -195,7 +195,7 @@ class LayerPolygon: @property def lineFeedrates(self): return self._line_feedrates - + @property def jumpMask(self): return self._jump_mask diff --git a/cura/MachineAction.py b/cura/MachineAction.py index e18c37ccb4..74b742ef4d 100644 --- a/cura/MachineAction.py +++ b/cura/MachineAction.py @@ -62,7 +62,7 @@ class MachineAction(QObject, PluginObject): @pyqtSlot() def reset(self) -> None: """Reset the action to it's default state. - + This should not be re-implemented by child classes, instead re-implement _reset. :py:meth:`cura.MachineAction.MachineAction._reset` @@ -73,7 +73,7 @@ class MachineAction(QObject, PluginObject): def _reset(self) -> None: """Protected implementation of reset. - + See also :py:meth:`cura.MachineAction.MachineAction.reset` """ diff --git a/cura/Machines/ContainerNode.py b/cura/Machines/ContainerNode.py index 1fb02b099f..5341edd0c6 100644 --- a/cura/Machines/ContainerNode.py +++ b/cura/Machines/ContainerNode.py @@ -11,14 +11,14 @@ from UM.Settings.InstanceContainer import InstanceContainer class ContainerNode: """A node in the container tree. It represents one container. - + The container it represents is referenced by its container_id. During normal use of the tree, this container is not constructed. Only when parts of the tree need to get loaded in the container stack should it get constructed. """ def __init__(self, container_id: str) -> None: """Creates a new node for the container tree. - + :param container_id: The ID of the container that this node should represent. """ @@ -28,7 +28,7 @@ class ContainerNode: def getMetadata(self) -> Dict[str, Any]: """Gets the metadata of the container that this node represents. - + Getting the metadata from the container directly is about 10x as fast. :return: The metadata of the container in this node. @@ -38,7 +38,7 @@ class ContainerNode: def getMetaDataEntry(self, entry: str, default: Any = None) -> Any: """Get an entry from the metadata of the container that this node contains. - + This is just a convenience function. :param entry: The metadata entry key to return. @@ -56,7 +56,7 @@ class ContainerNode: @property def container(self) -> Optional[InstanceContainer]: """The container that this node's container ID refers to. - + This can be used to finally instantiate the container in order to put it in the container stack. :return: A container. diff --git a/cura/Machines/ContainerTree.py b/cura/Machines/ContainerTree.py index 7902f8163c..7f1e7900d0 100644 --- a/cura/Machines/ContainerTree.py +++ b/cura/Machines/ContainerTree.py @@ -21,9 +21,9 @@ if TYPE_CHECKING: class ContainerTree: """This class contains a look-up tree for which containers are available at which stages of configuration. - + The tree starts at the machine definitions. For every distinct definition there will be one machine node here. - + All of the fallbacks for material choices, quality choices, etc. should be encoded in this tree. There must always be at least one child node (for nodes that have children) but that child node may be a node representing the empty instance container. @@ -44,7 +44,7 @@ class ContainerTree: def getCurrentQualityGroups(self) -> Dict[str, "QualityGroup"]: """Get the quality groups available for the currently activated printer. - + This contains all quality groups, enabled or disabled. To check whether the quality group can be activated, test for the ``QualityGroup.is_available`` property. @@ -61,7 +61,7 @@ class ContainerTree: def getCurrentQualityChangesGroups(self) -> List["QualityChangesGroup"]: """Get the quality changes groups available for the currently activated printer. - + This contains all quality changes groups, enabled or disabled. To check whether the quality changes group can be activated, test for the ``QualityChangesGroup.is_available`` property. @@ -84,7 +84,7 @@ class ContainerTree: class _MachineNodeMap: """Dictionary-like object that contains the machines. - + This handles the lazy loading of MachineNodes. """ @@ -105,7 +105,7 @@ class ContainerTree: def __getitem__(self, definition_id: str) -> MachineNode: """Returns a machine node for the specified definition ID. - + If the machine node wasn't loaded yet, this will load it lazily. :param definition_id: The definition to look for. @@ -122,7 +122,7 @@ class ContainerTree: def get(self, definition_id: str, default: Optional[MachineNode] = None) -> Optional[MachineNode]: """Gets a machine node for the specified definition ID, with default. - + The default is returned if there is no definition with the specified ID. If the machine node wasn't loaded yet, this will load it lazily. @@ -140,7 +140,7 @@ class ContainerTree: def is_loaded(self, definition_id: str) -> bool: """Returns whether we've already cached this definition's node. - + :param definition_id: The definition that we may have cached. :return: ``True`` if it's cached. @@ -155,7 +155,7 @@ class ContainerTree: def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]) -> None: """Creates a new background task. - + :param tree_root: The container tree instance. This cannot be obtained through the singleton static function since the instance may not yet be constructed completely. :param container_stacks: All of the stacks to pre-load the container trees for. This needs to be provided @@ -168,7 +168,7 @@ class ContainerTree: def run(self) -> None: """Starts the background task. - + The ``JobQueue`` will schedule this on a different thread. """ diff --git a/cura/Machines/IntentNode.py b/cura/Machines/IntentNode.py index f25a171d55..949a5d3a2b 100644 --- a/cura/Machines/IntentNode.py +++ b/cura/Machines/IntentNode.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: class IntentNode(ContainerNode): """This class represents an intent profile in the container tree. - + This class has no more subnodes. """ diff --git a/cura/Machines/MachineNode.py b/cura/Machines/MachineNode.py index c546f4d68d..d487ea57f2 100644 --- a/cura/Machines/MachineNode.py +++ b/cura/Machines/MachineNode.py @@ -19,7 +19,7 @@ import UM.FlameProfiler class MachineNode(ContainerNode): """This class represents a machine in the container tree. - + The subnodes of these nodes are variants. """ @@ -51,7 +51,7 @@ class MachineNode(ContainerNode): def getQualityGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> Dict[str, QualityGroup]: """Get the available quality groups for this machine. - + This returns all quality groups, regardless of whether they are available to the combination of extruders or not. On the resulting quality groups, the is_available property is set to indicate whether the quality group can be selected according to the combination of extruders in the parameters. @@ -103,13 +103,13 @@ class MachineNode(ContainerNode): def getQualityChangesGroups(self, variant_names: List[str], material_bases: List[str], extruder_enabled: List[bool]) -> List[QualityChangesGroup]: """Returns all of the quality changes groups available to this printer. - + The quality changes groups store which quality type and intent category they were made for, but not which material and nozzle. Instead for the quality type and intent category, the quality changes will always be available but change the quality type and intent category when activated. - + The quality changes group does depend on the printer: Which quality definition is used. - + The quality changes groups that are available do depend on the quality types that are available, so it must still be known which extruders are enabled and which materials and variants are loaded in them. This allows setting the correct is_available flag. @@ -150,7 +150,7 @@ class MachineNode(ContainerNode): def preferredGlobalQuality(self) -> "QualityNode": """Gets the preferred global quality node, going by the preferred quality type. - + If the preferred global quality is not in there, an arbitrary global quality is taken. If there are no global qualities, an empty quality is returned. """ diff --git a/cura/Machines/MaterialGroup.py b/cura/Machines/MaterialGroup.py index bce7b410c3..2ff4b99c80 100644 --- a/cura/Machines/MaterialGroup.py +++ b/cura/Machines/MaterialGroup.py @@ -9,17 +9,17 @@ if TYPE_CHECKING: class MaterialGroup: """A MaterialGroup represents a group of material InstanceContainers that are derived from a single material profile. - + The main InstanceContainer which has the ID of the material profile file name is called the "root_material". For example: "generic_abs" is the root material (ID) of "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4", and "generic_abs_ultimaker3" and "generic_abs_ultimaker3_AA_0.4" are derived materials of "generic_abs". - + Using "generic_abs" as an example, the MaterialGroup for "generic_abs" will contain the following information: - name: "generic_abs", root_material_id - root_material_node: MaterialNode of "generic_abs" - derived_material_node_list: A list of MaterialNodes that are derived from "generic_abs", so "generic_abs_ultimaker3", "generic_abs_ultimaker3_AA_0.4", etc. - + """ __slots__ = ("name", "is_read_only", "root_material_node", "derived_material_node_list") diff --git a/cura/Machines/MaterialNode.py b/cura/Machines/MaterialNode.py index 333452cc01..c78c6aff03 100644 --- a/cura/Machines/MaterialNode.py +++ b/cura/Machines/MaterialNode.py @@ -17,7 +17,7 @@ if TYPE_CHECKING: class MaterialNode(ContainerNode): """Represents a material in the container tree. - + Its subcontainers are quality profiles. """ @@ -38,7 +38,7 @@ class MaterialNode(ContainerNode): def preferredQuality(self) -> QualityNode: """Finds the preferred quality for this printer with this material and this variant loaded. - + If the preferred quality is not available, an arbitrary quality is returned. If there is a configuration mistake (like a typo in the preferred quality) this returns a random available quality. If there are no available qualities, this will return the empty quality node. diff --git a/cura/Machines/Models/BaseMaterialsModel.py b/cura/Machines/Models/BaseMaterialsModel.py index bc903da698..97f1f4b646 100644 --- a/cura/Machines/Models/BaseMaterialsModel.py +++ b/cura/Machines/Models/BaseMaterialsModel.py @@ -15,7 +15,7 @@ from cura.Settings.CuraContainerRegistry import CuraContainerRegistry class BaseMaterialsModel(ListModel): """This is the base model class for GenericMaterialsModel and MaterialBrandsModel. - + Those 2 models are used by the material drop down menu to show generic materials and branded materials separately. The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu @@ -125,7 +125,7 @@ class BaseMaterialsModel(ListModel): def _materialsListChanged(self, material: MaterialNode) -> None: """Triggered when a list of materials changed somewhere in the container - + tree. This change may trigger an _update() call when the materials changed for the configuration that this model is looking for. """ diff --git a/cura/Machines/Models/DiscoveredPrintersModel.py b/cura/Machines/Models/DiscoveredPrintersModel.py index 4042d4ae17..459ec4d795 100644 --- a/cura/Machines/Models/DiscoveredPrintersModel.py +++ b/cura/Machines/Models/DiscoveredPrintersModel.py @@ -212,7 +212,7 @@ class DiscoveredPrintersModel(QObject): @pyqtProperty("QVariantMap", notify = discoveredPrintersChanged) def discoveredPrintersByAddress(self) -> Dict[str, DiscoveredPrinter]: return self._discovered_printer_by_ip_dict - + @pyqtProperty("QVariantList", notify = discoveredPrintersChanged) def discoveredPrinters(self) -> List["DiscoveredPrinter"]: item_list = list( diff --git a/cura/Machines/Models/ExtrudersModel.py b/cura/Machines/Models/ExtrudersModel.py index 942799cf43..98865ed37e 100644 --- a/cura/Machines/Models/ExtrudersModel.py +++ b/cura/Machines/Models/ExtrudersModel.py @@ -17,7 +17,7 @@ catalog = i18nCatalog("cura") class ExtrudersModel(ListModel): """Model that holds extruders. - + This model is designed for use by any list of extruders, but specifically intended for drop-down lists of the current machine's extruders in place of settings. """ @@ -33,7 +33,7 @@ class ExtrudersModel(ListModel): IndexRole = Qt.UserRole + 4 """Index of the extruder, which is also the value of the setting itself. - + An index of 0 indicates the first extruder, an index of 1 the second one, and so on. This is the value that will be saved in instance containers. """ @@ -58,7 +58,7 @@ class ExtrudersModel(ListModel): def __init__(self, parent = None): """Initialises the extruders model, defining the roles and listening for changes in the data. - + :param parent: Parent QtObject of this list. """ @@ -104,7 +104,7 @@ class ExtrudersModel(ListModel): def _extrudersChanged(self, machine_id = None): """Links to the stack-changed signal of the new extruders when an extruder is swapped out or added in the current machine. - + :param machine_id: The machine for which the extruders changed. This is filled by the ExtruderManager.extrudersChanged signal when coming from that signal. Application.globalContainerStackChanged doesn't fill this signal; it's assumed to be the current printer in that case. @@ -150,7 +150,7 @@ class ExtrudersModel(ListModel): @UM.FlameProfiler.profile def __updateExtruders(self): """Update the list of extruders. - + This should be called whenever the list of extruders changes. """ diff --git a/cura/Machines/Models/IntentCategoryModel.py b/cura/Machines/Models/IntentCategoryModel.py index d8502a7574..09a71b8ed6 100644 --- a/cura/Machines/Models/IntentCategoryModel.py +++ b/cura/Machines/Models/IntentCategoryModel.py @@ -57,7 +57,7 @@ class IntentCategoryModel(ListModel): def __init__(self, intent_category: str) -> None: """Creates a new model for a certain intent category. - + :param intent_category: category to list the intent profiles for. """ diff --git a/cura/Machines/Models/MaterialManagementModel.py b/cura/Machines/Models/MaterialManagementModel.py index d18b040936..4a696ec974 100644 --- a/cura/Machines/Models/MaterialManagementModel.py +++ b/cura/Machines/Models/MaterialManagementModel.py @@ -21,20 +21,20 @@ catalog = i18nCatalog("cura") class MaterialManagementModel(QObject): """Proxy class to the materials page in the preferences. - + This class handles the actions in that page, such as creating new materials, renaming them, etc. """ favoritesChanged = pyqtSignal(str) """Triggered when a favorite is added or removed. - + :param The base file of the material is provided as parameter when this emits """ @pyqtSlot("QVariant", result = bool) def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool: """Can a certain material be deleted, or is it still in use in one of the container stacks anywhere? - + We forbid the user from deleting a material if it's in use in any stack. Deleting it while it's in use can lead to corrupted stacks. In the future we might enable this functionality again (deleting the material from those stacks) but for now it is easier to prevent the user from doing this. @@ -54,7 +54,7 @@ class MaterialManagementModel(QObject): @pyqtSlot("QVariant", str) def setMaterialName(self, material_node: "MaterialNode", name: str) -> None: """Change the user-visible name of a material. - + :param material_node: The ContainerTree node of the material to rename. :param name: The new name for the material. """ @@ -69,7 +69,7 @@ class MaterialManagementModel(QObject): @pyqtSlot("QVariant") def removeMaterial(self, material_node: "MaterialNode") -> None: """Deletes a material from Cura. - + This function does not do any safety checking any more. Please call this function only if: - The material is not read-only. - The material is not used in any stacks. @@ -200,7 +200,7 @@ class MaterialManagementModel(QObject): def createMaterial(self) -> str: """Create a new material by cloning the preferred material for the current material diameter and generate a new GUID. - + The material type is explicitly left to be the one from the preferred material, since this allows the user to still have SOME profiles to work with. @@ -234,7 +234,7 @@ class MaterialManagementModel(QObject): @pyqtSlot(str) def addFavorite(self, material_base_file: str) -> None: """Adds a certain material to the favorite materials. - + :param material_base_file: The base file of the material to add. """ @@ -249,7 +249,7 @@ class MaterialManagementModel(QObject): @pyqtSlot(str) def removeFavorite(self, material_base_file: str) -> None: """Removes a certain material from the favorite materials. - + If the material was not in the favorite materials, nothing happens. """ diff --git a/cura/Machines/Models/QualityManagementModel.py b/cura/Machines/Models/QualityManagementModel.py index 6dfed60f7b..039f45181f 100644 --- a/cura/Machines/Models/QualityManagementModel.py +++ b/cura/Machines/Models/QualityManagementModel.py @@ -76,7 +76,7 @@ class QualityManagementModel(ListModel): @pyqtSlot(QObject) def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None: """Deletes a custom profile. It will be gone forever. - + :param quality_changes_group: The quality changes group representing the profile to delete. """ @@ -99,7 +99,7 @@ class QualityManagementModel(ListModel): @pyqtSlot(QObject, str, result = str) def renameQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", new_name: str) -> str: """Rename a custom profile. - + Because the names must be unique, the new name may not actually become the name that was given. The actual name is returned by this function. @@ -145,7 +145,7 @@ class QualityManagementModel(ListModel): @pyqtSlot(str, "QVariantMap") def duplicateQualityChanges(self, new_name: str, quality_model_item: Dict[str, Any]) -> None: """Duplicates a given quality profile OR quality changes profile. - + :param new_name: The desired name of the new profile. This will be made unique, so it might end up with a different name. :param quality_model_item: The item of this model to duplicate, as dictionary. See the descriptions of the @@ -180,7 +180,7 @@ class QualityManagementModel(ListModel): @pyqtSlot(str) def createQualityChanges(self, base_name: str) -> None: """Create quality changes containers from the user containers in the active stacks. - + This will go through the global and extruder stacks and create quality_changes containers from the user containers in each stack. These then replace the quality_changes containers in the stack and clear the user settings. @@ -229,7 +229,7 @@ class QualityManagementModel(ListModel): def _createQualityChanges(self, quality_type: str, intent_category: Optional[str], new_name: str, machine: "GlobalStack", extruder_stack: Optional["ExtruderStack"]) -> "InstanceContainer": """Create a quality changes container with the given set-up. - + :param quality_type: The quality type of the new container. :param intent_category: The intent category of the new container. :param new_name: The name of the container. This name must be unique. @@ -264,7 +264,7 @@ class QualityManagementModel(ListModel): def _qualityChangesListChanged(self, container: "ContainerInterface") -> None: """Triggered when any container changed. - + This filters the updates to the container manager: When it applies to the list of quality changes, we need to update our list. """ @@ -383,7 +383,7 @@ class QualityManagementModel(ListModel): The convenience meta-filters "All Supported Types" and "All Files" are added when listing readers, but not when listing writers. - + :param io_type: name of the needed IO type :return: A list of strings indicating file name filters for a file dialog. @@ -408,7 +408,7 @@ class QualityManagementModel(ListModel): def _getIOPlugins(self, io_type): """Gets a list of profile reader or writer plugins - + :return: List of tuples of (plugin_id, meta_data). """ from UM.PluginRegistry import PluginRegistry diff --git a/cura/Machines/QualityChangesGroup.py b/cura/Machines/QualityChangesGroup.py index ecaaa67b7a..668fff785a 100644 --- a/cura/Machines/QualityChangesGroup.py +++ b/cura/Machines/QualityChangesGroup.py @@ -8,7 +8,7 @@ from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal class QualityChangesGroup(QObject): """Data struct to group several quality changes instance containers together. - + Each group represents one "custom profile" as the user sees it, which contains an instance container for the global stack and one instance container per extruder. """ diff --git a/cura/Machines/QualityGroup.py b/cura/Machines/QualityGroup.py index 8e470e0102..2e5e8db905 100644 --- a/cura/Machines/QualityGroup.py +++ b/cura/Machines/QualityGroup.py @@ -12,7 +12,7 @@ from cura.Machines.ContainerNode import ContainerNode class QualityGroup: """A QualityGroup represents a group of quality containers that must be applied to each ContainerStack when it's used. - + A concrete example: When there are two extruders and the user selects the quality type "normal", this quality type must be applied to all stacks in a machine, although each stack can have different containers. So one global profile gets put on the global stack and one extruder profile gets put on each extruder stack. This quality group @@ -24,7 +24,7 @@ class QualityGroup: - um3_global_normal - um3_aa04_pla_normal - um3_aa04_abs_normal - + The purpose of these quality groups is to group the containers that can be applied to a configuration, so that when a quality level is selected, the container can directly be applied to each stack instead of looking them up again. @@ -32,7 +32,7 @@ class QualityGroup: def __init__(self, name: str, quality_type: str) -> None: """Constructs a new group. - + :param name: The user-visible name for the group. :param quality_type: The quality level that each profile in this group has. """ diff --git a/cura/Machines/QualityNode.py b/cura/Machines/QualityNode.py index 85e55c7a2d..dcbe486952 100644 --- a/cura/Machines/QualityNode.py +++ b/cura/Machines/QualityNode.py @@ -15,9 +15,9 @@ if TYPE_CHECKING: class QualityNode(ContainerNode): """Represents a quality profile in the container tree. - + This may either be a normal quality profile or a global quality profile. - + Its subcontainers are intent profiles. """ diff --git a/cura/Machines/VariantNode.py b/cura/Machines/VariantNode.py index 72372b5a45..39664946a3 100644 --- a/cura/Machines/VariantNode.py +++ b/cura/Machines/VariantNode.py @@ -19,9 +19,9 @@ if TYPE_CHECKING: class VariantNode(ContainerNode): """This class represents an extruder variant in the container tree. - + The subnodes of these nodes are materials. - + This node contains materials with ALL filament diameters underneath it. The tree of this variant is not specific to one global stack, so because the list of materials can be different per stack depending on the compatible material diameter setting, we cannot filter them here. Filtering must be done in the model. @@ -72,7 +72,7 @@ class VariantNode(ContainerNode): def preferredMaterial(self, approximate_diameter: int) -> MaterialNode: """Finds the preferred material for this printer with this nozzle in one of the extruders. - + If the preferred material is not available, an arbitrary material is returned. If there is a configuration mistake (like a typo in the preferred material) this returns a random available material. If there are no available materials, this will return the empty material node. @@ -85,14 +85,14 @@ class VariantNode(ContainerNode): for base_material, material_node in self.materials.items(): if self.machine.preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")): return material_node - + # First fallback: Check if we should be checking for the 175 variant. if approximate_diameter == 2: preferred_material = self.machine.preferred_material + "_175" for base_material, material_node in self.materials.items(): if preferred_material == base_material and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")): return material_node - + # Second fallback: Choose any material with matching diameter. for material_node in self.materials.values(): if material_node.getMetaDataEntry("approximate_diameter") and approximate_diameter == int(material_node.getMetaDataEntry("approximate_diameter")): diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py index 612c00de2f..e825afd2a9 100644 --- a/cura/OAuth2/AuthorizationHelpers.py +++ b/cura/OAuth2/AuthorizationHelpers.py @@ -31,12 +31,12 @@ class AuthorizationHelpers: def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse": """Request the access token from the authorization server. - + :param authorization_code: The authorization code from the 1st step. :param verification_code: The verification code needed for the PKCE extension. :return: An AuthenticationResponse object. """ - + data = { "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "", @@ -52,11 +52,11 @@ class AuthorizationHelpers: def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse": """Request the access token from the authorization server using a refresh token. - + :param refresh_token: :return: An AuthenticationResponse object. """ - + Logger.log("d", "Refreshing the access token.") data = { "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", @@ -73,11 +73,11 @@ class AuthorizationHelpers: @staticmethod def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse": """Parse the token response from the authorization server into an AuthenticationResponse object. - + :param token_response: The JSON string data response from the authorization server. :return: An AuthenticationResponse object. """ - + token_data = None try: @@ -101,11 +101,11 @@ class AuthorizationHelpers: def parseJWT(self, access_token: str) -> Optional["UserProfile"]: """Calls the authentication API endpoint to get the token data. - + :param access_token: The encoded JWT token. :return: Dict containing some profile data. """ - + try: token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = { "Authorization": "Bearer {}".format(access_token) @@ -130,20 +130,20 @@ class AuthorizationHelpers: @staticmethod def generateVerificationCode(code_length: int = 32) -> str: """Generate a verification code of arbitrary length. - + :param code_length:: How long should the code be? This should never be lower than 16, but it's probably better to leave it at 32 """ - + return "".join(random.choice("0123456789ABCDEF") for i in range(code_length)) @staticmethod def generateVerificationCodeChallenge(verification_code: str) -> str: """Generates a base64 encoded sha512 encrypted version of a given string. - + :param verification_code: :return: The encrypted code in base64 format. """ - + encoded = sha512(verification_code.encode()).digest() return b64encode(encoded, altchars = b"_-").decode() diff --git a/cura/OAuth2/AuthorizationRequestHandler.py b/cura/OAuth2/AuthorizationRequestHandler.py index 9bcfbfc805..c7ce9b6faf 100644 --- a/cura/OAuth2/AuthorizationRequestHandler.py +++ b/cura/OAuth2/AuthorizationRequestHandler.py @@ -16,10 +16,10 @@ catalog = i18nCatalog("cura") class AuthorizationRequestHandler(BaseHTTPRequestHandler): """This handler handles all HTTP requests on the local web server. - + It also requests the access token for the 2nd stage of the OAuth flow. """ - + def __init__(self, request, client_address, server) -> None: super().__init__(request, client_address, server) @@ -60,11 +60,11 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler): def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]: """Handler for the callback URL redirect. - + :param query: Dict containing the HTTP query parameters. :return: HTTP ResponseData containing a success page to show to the user. """ - + code = self._queryGet(query, "code") state = self._queryGet(query, "state") if state != self.state: diff --git a/cura/OAuth2/AuthorizationRequestServer.py b/cura/OAuth2/AuthorizationRequestServer.py index e2f9dddc32..74b0b5f012 100644 --- a/cura/OAuth2/AuthorizationRequestServer.py +++ b/cura/OAuth2/AuthorizationRequestServer.py @@ -11,11 +11,11 @@ if TYPE_CHECKING: class AuthorizationRequestServer(HTTPServer): """The authorization request callback handler server. - + This subclass is needed to be able to pass some data to the request handler. This cannot be done on the request handler directly as the HTTPServer creates an instance of the handler after init. """ - + def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None: """Set the authorization helpers instance on the request handler.""" diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index 9b0fcba1ba..9a5c81ae55 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -251,7 +251,7 @@ class AuthorizationService: if self._preferences is None: Logger.log("e", "Unable to save authentication data, since no preference has been set!") return - + self._auth_data = auth_data if auth_data: self._user_profile = self.getUserProfile() diff --git a/cura/OAuth2/LocalAuthorizationServer.py b/cura/OAuth2/LocalAuthorizationServer.py index 942967301a..f819042ae0 100644 --- a/cura/OAuth2/LocalAuthorizationServer.py +++ b/cura/OAuth2/LocalAuthorizationServer.py @@ -24,19 +24,19 @@ class LocalAuthorizationServer: auth_state_changed_callback: Callable[["AuthenticationResponse"], Any], daemon: bool) -> None: """The local LocalAuthorizationServer takes care of the oauth2 callbacks. - + Once the flow is completed, this server should be closed down again by calling :py:meth:`cura.OAuth2.LocalAuthorizationServer.LocalAuthorizationServer.stop()` - + :param auth_helpers: An instance of the authorization helpers class. :param auth_state_changed_callback: A callback function to be called when the authorization state changes. :param daemon: Whether the server thread should be run in daemon mode. - + .. note:: - + Daemon threads are abruptly stopped at shutdown. Their resources (e.g. open files) may never be released. """ - + self._web_server = None # type: Optional[AuthorizationRequestServer] self._web_server_thread = None # type: Optional[threading.Thread] self._web_server_port = auth_helpers.settings.CALLBACK_PORT @@ -46,11 +46,11 @@ class LocalAuthorizationServer: def start(self, verification_code: str, state: str) -> None: """Starts the local web server to handle the authorization callback. - + :param verification_code: The verification code part of the OAuth2 client identification. :param state: The unique state code (to ensure that the request we get back is really from the server. """ - + if self._web_server: # If the server is already running (because of a previously aborted auth flow), we don't have to start it. # We still inject the new verification code though. diff --git a/cura/OneAtATimeIterator.py b/cura/OneAtATimeIterator.py index 4853cffcc1..def0dac4fe 100644 --- a/cura/OneAtATimeIterator.py +++ b/cura/OneAtATimeIterator.py @@ -9,7 +9,7 @@ from functools import cmp_to_key class OneAtATimeIterator(Iterator.Iterator): """Iterator that returns a list of nodes in the order that they need to be printed - + If there is no solution an empty list is returned. Take note that the list of nodes can have children (that may or may not contain mesh data) """ @@ -80,7 +80,7 @@ class OneAtATimeIterator(Iterator.Iterator): def _checkBlockMultiple(self, node: SceneNode, other_nodes: List[SceneNode]) -> bool: """Check for a node whether it hits any of the other nodes. - + :param node: The node to check whether it collides with the other nodes. :param other_nodes: The nodes to check for collisions. :return: returns collision between nodes @@ -140,7 +140,7 @@ class _ObjectOrder: def __init__(self, order: List[SceneNode], todo: List[SceneNode]) -> None: """Creates the _ObjectOrder instance. - + :param order: List of indices in which to print objects, ordered by printing order. :param todo: List of indices which are not yet inserted into the order list. """ diff --git a/cura/Operations/SetParentOperation.py b/cura/Operations/SetParentOperation.py index 52bef4aabc..a8fab49395 100644 --- a/cura/Operations/SetParentOperation.py +++ b/cura/Operations/SetParentOperation.py @@ -11,7 +11,7 @@ class SetParentOperation(Operation.Operation): def __init__(self, node: SceneNode, parent_node: Optional[SceneNode]) -> None: """Initialises this SetParentOperation. - + :param node: The node which will be reparented. :param parent_node: The node which will be the parent. """ @@ -33,7 +33,7 @@ class SetParentOperation(Operation.Operation): def _set_parent(self, new_parent: Optional[SceneNode]) -> None: """Sets the parent of the node while applying transformations to the world-transform of the node stays the same. - + :param new_parent: The new parent. Note: this argument can be None, which would hide the node from the scene. """ @@ -64,7 +64,7 @@ class SetParentOperation(Operation.Operation): def __repr__(self) -> str: """Returns a programmer-readable representation of this operation. - + :return: A programmer-readable representation of this operation. """ diff --git a/cura/PickingPass.py b/cura/PickingPass.py index 8fd411807d..13a115f64b 100644 --- a/cura/PickingPass.py +++ b/cura/PickingPass.py @@ -21,9 +21,9 @@ if TYPE_CHECKING: class PickingPass(RenderPass): """A :py:class:`Uranium.UM.View.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 """ diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 015795e506..5fd2e70a1c 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -95,15 +95,15 @@ class PlatformPhysics: # Ignore root, ourselves and anything that is not a normal SceneNode. if other_node is root or not issubclass(type(other_node), SceneNode) or other_node is node or other_node.callDecoration("getBuildPlateNumber") != node.callDecoration("getBuildPlateNumber"): continue - + # Ignore collisions of a group with it's own children if other_node in node.getAllChildren() or node in other_node.getAllChildren(): continue - + # Ignore collisions within a group if other_node.getParent() and node.getParent() and (other_node.getParent().callDecoration("isGroup") is not None or node.getParent().callDecoration("isGroup") is not None): continue - + # Ignore nodes that do not have the right properties set. if not other_node.callDecoration("getConvexHull") or not other_node.getBoundingBox(): continue diff --git a/cura/PreviewPass.py b/cura/PreviewPass.py index 9e7c2c49fc..9cae5ad9bd 100644 --- a/cura/PreviewPass.py +++ b/cura/PreviewPass.py @@ -39,9 +39,9 @@ def prettier_color(color_list: List[float]) -> List[float]: class PreviewPass(RenderPass): """A :py:class:`Uranium.UM.View.RenderPass` subclass that renders slicable objects with default parameters. - + It uses the active camera by default, but it can be overridden to use a different camera. - + This is useful to get a preview image of a scene taken from a different location as the active camera. """ diff --git a/cura/PrinterOutput/FirmwareUpdater.py b/cura/PrinterOutput/FirmwareUpdater.py index 2794bf5c65..8688c5a623 100644 --- a/cura/PrinterOutput/FirmwareUpdater.py +++ b/cura/PrinterOutput/FirmwareUpdater.py @@ -36,7 +36,7 @@ class FirmwareUpdater(QObject): if self._firmware_file == "": self._setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error) return - + self._setFirmwareUpdateState(FirmwareUpdateState.updating) self._update_firmware_thread.start() diff --git a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py index b80a652163..ecc855ab8f 100644 --- a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py +++ b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py @@ -49,7 +49,7 @@ class ExtruderConfigurationModel(QObject): def isValid(self) -> bool: """This method is intended to indicate whether the configuration is valid or not. - + The method checks if the mandatory fields are or not set At this moment is always valid since we allow to have empty material and variants. """ diff --git a/cura/PrinterOutput/Models/ExtruderOutputModel.py b/cura/PrinterOutput/Models/ExtruderOutputModel.py index 9e74e9520c..9da3a7117d 100644 --- a/cura/PrinterOutput/Models/ExtruderOutputModel.py +++ b/cura/PrinterOutput/Models/ExtruderOutputModel.py @@ -106,7 +106,7 @@ class ExtruderOutputModel(QObject): @pyqtSlot(float, float) def preheatHotend(self, temperature: float, duration: float) -> None: """Pre-heats the extruder before printer. - + :param temperature: The temperature to heat the extruder to, in degrees Celsius. :param duration: How long the bed should stay warm, in seconds. diff --git a/cura/PrinterOutput/Models/PrinterConfigurationModel.py b/cura/PrinterOutput/Models/PrinterConfigurationModel.py index c5aa949ff3..54f52134b2 100644 --- a/cura/PrinterOutput/Models/PrinterConfigurationModel.py +++ b/cura/PrinterOutput/Models/PrinterConfigurationModel.py @@ -50,7 +50,7 @@ class PrinterConfigurationModel(QObject): def isValid(self) -> bool: """This method is intended to indicate whether the configuration is valid or not. - + The method checks if the mandatory fields are or not set """ if not self._extruder_configurations: @@ -101,7 +101,7 @@ class PrinterConfigurationModel(QObject): def __hash__(self): """The hash function is used to compare and create unique sets. The configuration is unique if the configuration - + of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same. """ extruder_hash = hash(0) diff --git a/cura/PrinterOutput/Models/PrinterOutputModel.py b/cura/PrinterOutput/Models/PrinterOutputModel.py index 8b716a1958..37464b0b7d 100644 --- a/cura/PrinterOutput/Models/PrinterOutputModel.py +++ b/cura/PrinterOutput/Models/PrinterOutputModel.py @@ -166,7 +166,7 @@ class PrinterOutputModel(QObject): @pyqtSlot(float, float) def preheatBed(self, temperature: float, duration: float) -> None: """Pre-heats the heated bed of the printer. - + :param temperature: The temperature to heat the bed to, in degrees Celsius. :param duration: How long the bed should stay warm, in seconds. diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index e57e461dde..2690c2651f 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -147,7 +147,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: """This method was only available privately before, but it was actually called from SendMaterialJob.py. - + We now have a public equivalent as well. We did not remove the private one as plugins might be using that. """ return self._createFormPart(content_header, data, content_type) @@ -194,7 +194,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): on_finished: Optional[Callable[[QNetworkReply], None]] = None, on_progress: Optional[Callable[[int, int], None]] = None) -> None: """Sends a put request to the given path. - + :param url: The path after the API prefix. :param data: The data to be sent in the body :param content_type: The content type of the body data. @@ -219,7 +219,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def delete(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: """Sends a delete request to the given path. - + :param url: The path after the API prefix. :param on_finished: The function to be call when the response is received. """ @@ -237,7 +237,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def get(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: """Sends a get request to the given path. - + :param url: The path after the API prefix. :param on_finished: The function to be call when the response is received. """ @@ -258,7 +258,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): on_progress: Optional[Callable[[int, int], None]] = None) -> None: """Sends a post request to the given path. - + :param url: The path after the API prefix. :param data: The data to be sent in the body :param on_finished: The function to call when the response is received. @@ -333,7 +333,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): def _checkCorrectGroupName(self, device_id: str, group_name: str) -> None: """This method checks if the name of the group stored in the definition container is correct. - + After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group then all the container stacks are updated, both the current and the hidden ones. """ @@ -385,7 +385,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): @pyqtProperty(str, constant = True) def key(self) -> str: """Get the unique key of this machine - + :return: key String containing the key of the machine. """ return self._id diff --git a/cura/PrinterOutput/Peripheral.py b/cura/PrinterOutput/Peripheral.py index e9a283ba2b..27d127832b 100644 --- a/cura/PrinterOutput/Peripheral.py +++ b/cura/PrinterOutput/Peripheral.py @@ -4,7 +4,7 @@ class Peripheral: """Data class that represents a peripheral for a printer. - + Output device plug-ins may specify that the printer has a certain set of peripherals. This set is then possibly shown in the interface of the monitor stage. @@ -12,7 +12,7 @@ class Peripheral: def __init__(self, peripheral_type: str, name: str) -> None: """Constructs the peripheral. - + :param peripheral_type: A unique ID for the type of peripheral. :param name: A human-readable name for the peripheral. """ diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index 9b10e6abec..526d713748 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -44,13 +44,13 @@ class ConnectionType(IntEnum): @signalemitter class PrinterOutputDevice(QObject, OutputDevice): """Printer output device adds extra interface options on top of output device. - + The assumption is made the printer is a FDM printer. - + Note that a number of settings are marked as "final". This is because decorators are not inherited by children. To fix this we use the private counter part of those functions to actually have the implementation. - + For all other uses it should be used in the same way as a "regular" OutputDevice. """ @@ -250,7 +250,7 @@ class PrinterOutputDevice(QObject, OutputDevice): def _setFirmwareName(self, name: str) -> None: """Set the device firmware name - + :param name: The name of the firmware. """ @@ -258,7 +258,7 @@ class PrinterOutputDevice(QObject, OutputDevice): def getFirmwareName(self) -> Optional[str]: """Get the name of device firmware - + This name can be used to define device type """ diff --git a/cura/ReaderWriters/ProfileReader.py b/cura/ReaderWriters/ProfileReader.py index 3d80411713..0d53bdebac 100644 --- a/cura/ReaderWriters/ProfileReader.py +++ b/cura/ReaderWriters/ProfileReader.py @@ -12,7 +12,7 @@ class NoProfileException(Exception): class ProfileReader(PluginObject): """A type of plug-ins that reads profiles from a file. - + The profile is then stored as instance container of the type user profile. """ @@ -21,7 +21,7 @@ class ProfileReader(PluginObject): def read(self, file_name): """Read profile data from a file and return a filled profile. - + :return: :type{Profile|Profile[]} The profile that was obtained from the file or a list of Profiles. """ diff --git a/cura/ReaderWriters/ProfileWriter.py b/cura/ReaderWriters/ProfileWriter.py index 0dd787335e..987924ccbf 100644 --- a/cura/ReaderWriters/ProfileWriter.py +++ b/cura/ReaderWriters/ProfileWriter.py @@ -6,13 +6,13 @@ from UM.PluginObject import PluginObject class ProfileWriter(PluginObject): """Base class for profile writer plugins. - + This class defines a write() function to write profiles to files with. """ def __init__(self): """Initialises the profile writer. - + This currently doesn't do anything since the writer is basically static. """ @@ -20,9 +20,9 @@ class ProfileWriter(PluginObject): def write(self, path, profiles): """Writes a profile to the specified file path. - + The profile writer may write its own file format to the specified file. - + :param path: :type{string} The file to output to. :param profiles: :type{Profile} or :type{List} The profile(s) to write to the file. :return: True if the writing was successful, or False if it wasn't. diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index df496aa401..46caa5b9e0 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: class ConvexHullDecorator(SceneNodeDecorator): """The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. - + If a scene node has a convex hull decorator, it will have a shadow in which other objects can not be printed. """ @@ -84,7 +84,7 @@ class ConvexHullDecorator(SceneNodeDecorator): def getAdhesionArea(self) -> Optional[Polygon]: """The polygon representing the 2D adhesion area. - + If no adhesion is used, the regular convex hull is returned """ if self._node is None: @@ -98,7 +98,7 @@ class ConvexHullDecorator(SceneNodeDecorator): def getConvexHull(self) -> Optional[Polygon]: """Get the unmodified 2D projected convex hull of the node (if any) - + In case of one-at-a-time, this includes adhesion and head+fans clearance """ if self._node is None: @@ -118,7 +118,7 @@ class ConvexHullDecorator(SceneNodeDecorator): def getConvexHullHeadFull(self) -> Optional[Polygon]: """For one at the time this is the convex hull of the node with the full head size - + In case of printing all at once this is None. """ if self._node is None: @@ -138,7 +138,7 @@ class ConvexHullDecorator(SceneNodeDecorator): def getConvexHullHead(self) -> Optional[Polygon]: """Get convex hull of the object + head size - + In case of printing all at once this is None. For one at the time this is area with intersection of mirrored head """ @@ -156,7 +156,7 @@ class ConvexHullDecorator(SceneNodeDecorator): def getConvexHullBoundary(self) -> Optional[Polygon]: """Get convex hull of the node - + In case of printing all at once this None?? For one at the time this is the area without the head. """ @@ -173,7 +173,7 @@ class ConvexHullDecorator(SceneNodeDecorator): def getPrintingArea(self) -> Optional[Polygon]: """Get the buildplate polygon where will be printed - + In case of printing all at once this is the same as convex hull (no individual adhesion) For one at the time this includes the adhesion area """ @@ -344,7 +344,7 @@ class ConvexHullDecorator(SceneNodeDecorator): def _add2DAdhesionMargin(self, poly: Polygon) -> Polygon: """Compensate given 2D polygon with adhesion margin - + :return: 2D polygon with added margin """ if not self._global_stack: @@ -379,7 +379,7 @@ class ConvexHullDecorator(SceneNodeDecorator): def _offsetHull(self, convex_hull: Polygon) -> Polygon: """Offset the convex hull with settings that influence the collision area. - + :param convex_hull: Polygon of the original convex hull. :return: New Polygon instance that is offset with everything that influences the collision area. @@ -476,6 +476,6 @@ class ConvexHullDecorator(SceneNodeDecorator): _influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh"} """Settings that change the convex hull. - + If these settings change, the convex hull should be recalculated. """ diff --git a/cura/Scene/ConvexHullNode.py b/cura/Scene/ConvexHullNode.py index cd0951cba6..765dae26a2 100644 --- a/cura/Scene/ConvexHullNode.py +++ b/cura/Scene/ConvexHullNode.py @@ -20,7 +20,7 @@ class ConvexHullNode(SceneNode): def __init__(self, node: SceneNode, hull: Optional[Polygon], thickness: float, parent: Optional[SceneNode] = None) -> None: """Convex hull node is a special type of scene node that is used to display an area, to indicate the - + location an object uses on the buildplate. This area (or area's in case of one at a time printing) is then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded to represent the raft as well. diff --git a/cura/Scene/CuraSceneNode.py b/cura/Scene/CuraSceneNode.py index b9f2279414..e7858a9633 100644 --- a/cura/Scene/CuraSceneNode.py +++ b/cura/Scene/CuraSceneNode.py @@ -17,7 +17,7 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # F class CuraSceneNode(SceneNode): """Scene nodes that are models are only seen when selecting the corresponding build plate - + Note that many other nodes can just be UM SceneNode objects. """ def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None: @@ -40,7 +40,7 @@ class CuraSceneNode(SceneNode): def getPrintingExtruder(self) -> Optional[ExtruderStack]: """Get the extruder used to print this node. If there is no active node, then the extruder in position zero is returned - + TODO The best way to do it is by adding the setActiveExtruder decorator to every node when is loaded """ global_container_stack = Application.getInstance().getGlobalContainerStack() diff --git a/cura/Scene/SliceableObjectDecorator.py b/cura/Scene/SliceableObjectDecorator.py index 982a38d667..ad51f7d755 100644 --- a/cura/Scene/SliceableObjectDecorator.py +++ b/cura/Scene/SliceableObjectDecorator.py @@ -4,7 +4,7 @@ from UM.Scene.SceneNodeDecorator import SceneNodeDecorator class SliceableObjectDecorator(SceneNodeDecorator): def __init__(self) -> None: super().__init__() - + def isSliceable(self) -> bool: return True diff --git a/cura/Settings/ContainerManager.py b/cura/Settings/ContainerManager.py index 29be16dcce..e70e307079 100644 --- a/cura/Settings/ContainerManager.py +++ b/cura/Settings/ContainerManager.py @@ -35,7 +35,7 @@ catalog = i18nCatalog("cura") class ContainerManager(QObject): """Manager class that contains common actions to deal with containers in Cura. - + This is primarily intended as a class to be able to perform certain actions from within QML. We want to be able to trigger things like removing a container when a certain action happens. This can be done through this class. @@ -72,16 +72,16 @@ class ContainerManager(QObject): @pyqtSlot("QVariant", str, str) def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool: """Set a metadata entry of the specified container. - + This will set the specified entry of the container's metadata to the specified value. Note that entries containing dictionaries can have their entries changed by using "/" as a separator. For example, to change an entry "foo" in a dictionary entry "bar", you can specify "bar/foo" as entry name. - + :param container_node: :type{ContainerNode} :param entry_name: :type{str} The name of the metadata entry to change. :param entry_value: The new value of the entry. - + TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this. Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want? """ @@ -131,14 +131,14 @@ class ContainerManager(QObject): @pyqtSlot(str, result = "QStringList") def getContainerNameFilters(self, type_name: str) -> List[str]: """Get a list of string that can be used as name filters for a Qt File Dialog - + This will go through the list of available container types and generate a list of strings out of that. The strings are formatted as "description (*.extension)" and can be directly passed to a nameFilters property of a Qt File Dialog. - + :param type_name: Which types of containers to list. These types correspond to the "type" key of the plugin metadata. - + :return: A string list with name filters. """ @@ -156,11 +156,11 @@ class ContainerManager(QObject): @pyqtSlot(str, str, QUrl, result = "QVariantMap") def exportContainer(self, container_id: str, file_type: str, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: """Export a container to a file - + :param container_id: The ID of the container to export :param file_type: The type of file to save as. Should be in the form of "description (*.extension, *.ext)" :param file_url_or_string: The URL where to save the file. - + :return: A dictionary containing a key "status" with a status code and a key "message" with a message explaining the status. The status code can be one of "error", "cancelled", "success" """ @@ -224,9 +224,9 @@ class ContainerManager(QObject): @pyqtSlot(QUrl, result = "QVariantMap") def importMaterialContainer(self, file_url_or_string: Union[QUrl, str]) -> Dict[str, str]: """Imports a profile from a file - + :param file_url: A URL that points to the file to import. - + :return: :type{Dict} dict with a 'status' key containing the string 'success' or 'error', and a 'message' key containing a message for the user """ @@ -278,10 +278,10 @@ class ContainerManager(QObject): @pyqtSlot(result = bool) def updateQualityChanges(self) -> bool: """Update the current active quality changes container with the settings from the user container. - + This will go through the active global stack and all active extruder stacks and merge the changes from the user container into the quality_changes container. After that, the user container is cleared. - + :return: :type{bool} True if successful, False if not. """ @@ -350,7 +350,7 @@ class ContainerManager(QObject): @pyqtSlot("QVariant", bool, result = "QStringList") def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False) -> List[str]: """Get a list of materials that have the same GUID as the reference material - + :param material_node: The node representing the material for which to get the same GUID. :param exclude_self: Whether to include the name of the material you provided. @@ -366,7 +366,7 @@ class ContainerManager(QObject): @pyqtSlot("QVariant") def unlinkMaterial(self, material_node: "MaterialNode") -> None: """Unlink a material from all other materials by creating a new GUID - + :param material_id: :type{str} the id of the material to create a new GUID for. """ # Get the material group diff --git a/cura/Settings/CuraContainerStack.py b/cura/Settings/CuraContainerStack.py index 1551e46ef2..36548ed5de 100755 --- a/cura/Settings/CuraContainerStack.py +++ b/cura/Settings/CuraContainerStack.py @@ -20,11 +20,11 @@ from . import Exceptions class CuraContainerStack(ContainerStack): """Base class for Cura related stacks that want to enforce certain containers are available. - + This class makes sure that the stack has the following containers set: user changes, quality changes, quality, material, variant, definition changes and finally definition. Initially, these will be equal to the empty instance container. - + The container types are determined based on the following criteria: - user: An InstanceContainer with the metadata entry "type" set to "user". - quality changes: An InstanceContainer with the metadata entry "type" set to "quality_changes". @@ -33,7 +33,7 @@ class CuraContainerStack(ContainerStack): - variant: An InstanceContainer with the metadata entry "type" set to "variant". - definition changes: An InstanceContainer with the metadata entry "type" set to "definition_changes". - definition: A DefinitionContainer. - + Internally, this class ensures the mentioned containers are always there and kept in a specific order. This also means that operations on the stack that modifies the container ordering is prohibited and will raise an exception. @@ -65,7 +65,7 @@ class CuraContainerStack(ContainerStack): def setUserChanges(self, new_user_changes: InstanceContainer) -> None: """Set the user changes container. - + :param new_user_changes: The new user changes container. It is expected to have a "type" metadata entry with the value "user". """ @@ -74,7 +74,7 @@ class CuraContainerStack(ContainerStack): @pyqtProperty(InstanceContainer, fset = setUserChanges, notify = pyqtContainersChanged) def userChanges(self) -> InstanceContainer: """Get the user changes container. - + :return: The user changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. """ @@ -82,7 +82,7 @@ class CuraContainerStack(ContainerStack): def setQualityChanges(self, new_quality_changes: InstanceContainer, postpone_emit = False) -> None: """Set the quality changes container. - + :param new_quality_changes: The new quality changes container. It is expected to have a "type" metadata entry with the value "quality_changes". """ @@ -91,7 +91,7 @@ class CuraContainerStack(ContainerStack): @pyqtProperty(InstanceContainer, fset = setQualityChanges, notify = pyqtContainersChanged) def qualityChanges(self) -> InstanceContainer: """Get the quality changes container. - + :return: The quality changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. """ @@ -99,7 +99,7 @@ class CuraContainerStack(ContainerStack): def setIntent(self, new_intent: InstanceContainer, postpone_emit: bool = False) -> None: """Set the intent container. - + :param new_intent: The new intent container. It is expected to have a "type" metadata entry with the value "intent". """ @@ -108,7 +108,7 @@ class CuraContainerStack(ContainerStack): @pyqtProperty(InstanceContainer, fset = setIntent, notify = pyqtContainersChanged) def intent(self) -> InstanceContainer: """Get the quality container. - + :return: The intent container. Should always be a valid container, but can be equal to the empty InstanceContainer. """ @@ -116,7 +116,7 @@ class CuraContainerStack(ContainerStack): def setQuality(self, new_quality: InstanceContainer, postpone_emit: bool = False) -> None: """Set the quality container. - + :param new_quality: The new quality container. It is expected to have a "type" metadata entry with the value "quality". """ @@ -125,7 +125,7 @@ class CuraContainerStack(ContainerStack): @pyqtProperty(InstanceContainer, fset = setQuality, notify = pyqtContainersChanged) def quality(self) -> InstanceContainer: """Get the quality container. - + :return: The quality container. Should always be a valid container, but can be equal to the empty InstanceContainer. """ @@ -133,7 +133,7 @@ class CuraContainerStack(ContainerStack): def setMaterial(self, new_material: InstanceContainer, postpone_emit: bool = False) -> None: """Set the material container. - + :param new_material: The new material container. It is expected to have a "type" metadata entry with the value "material". """ @@ -142,7 +142,7 @@ class CuraContainerStack(ContainerStack): @pyqtProperty(InstanceContainer, fset = setMaterial, notify = pyqtContainersChanged) def material(self) -> InstanceContainer: """Get the material container. - + :return: The material container. Should always be a valid container, but can be equal to the empty InstanceContainer. """ @@ -150,7 +150,7 @@ class CuraContainerStack(ContainerStack): def setVariant(self, new_variant: InstanceContainer) -> None: """Set the variant container. - + :param new_variant: The new variant container. It is expected to have a "type" metadata entry with the value "variant". """ @@ -159,7 +159,7 @@ class CuraContainerStack(ContainerStack): @pyqtProperty(InstanceContainer, fset = setVariant, notify = pyqtContainersChanged) def variant(self) -> InstanceContainer: """Get the variant container. - + :return: The variant container. Should always be a valid container, but can be equal to the empty InstanceContainer. """ @@ -167,7 +167,7 @@ class CuraContainerStack(ContainerStack): def setDefinitionChanges(self, new_definition_changes: InstanceContainer) -> None: """Set the definition changes container. - + :param new_definition_changes: The new definition changes container. It is expected to have a "type" metadata entry with the value "definition_changes". """ @@ -176,7 +176,7 @@ class CuraContainerStack(ContainerStack): @pyqtProperty(InstanceContainer, fset = setDefinitionChanges, notify = pyqtContainersChanged) def definitionChanges(self) -> InstanceContainer: """Get the definition changes container. - + :return: The definition changes container. Should always be a valid container, but can be equal to the empty InstanceContainer. """ @@ -184,7 +184,7 @@ class CuraContainerStack(ContainerStack): def setDefinition(self, new_definition: DefinitionContainerInterface) -> None: """Set the definition container. - + :param new_definition: The new definition container. It is expected to have a "type" metadata entry with the value "definition". """ @@ -206,10 +206,10 @@ class CuraContainerStack(ContainerStack): @pyqtSlot(str, result = bool) def hasUserValue(self, key: str) -> bool: """Check whether the specified setting has a 'user' value. - + A user value here is defined as the setting having a value in either the UserChanges or QualityChanges container. - + :return: True if the setting has a user value, False if not. """ @@ -223,11 +223,11 @@ class CuraContainerStack(ContainerStack): def setProperty(self, key: str, property_name: str, property_value: Any, container: "ContainerInterface" = None, set_from_cache: bool = False) -> None: """Set a property of a setting. - + This will set a property of a specified setting. Since the container stack does not contain any settings itself, it is required to specify a container to set the property on. The target container is matched by container type. - + :param key: The key of the setting to set. :param property_name: The name of the property to set. :param new_value: The new value to set the property to. @@ -239,7 +239,7 @@ class CuraContainerStack(ContainerStack): @override(ContainerStack) def addContainer(self, container: ContainerInterface) -> None: """Overridden from ContainerStack - + Since we have a fixed order of containers in the stack and this method would modify the container ordering, we disallow this operation. """ @@ -249,7 +249,7 @@ class CuraContainerStack(ContainerStack): @override(ContainerStack) def insertContainer(self, index: int, container: ContainerInterface) -> None: """Overridden from ContainerStack - + Since we have a fixed order of containers in the stack and this method would modify the container ordering, we disallow this operation. """ @@ -259,7 +259,7 @@ class CuraContainerStack(ContainerStack): @override(ContainerStack) def removeContainer(self, index: int = 0) -> None: """Overridden from ContainerStack - + Since we have a fixed order of containers in the stack and this method would modify the container ordering, we disallow this operation. """ @@ -269,10 +269,10 @@ class CuraContainerStack(ContainerStack): @override(ContainerStack) def replaceContainer(self, index: int, container: ContainerInterface, postpone_emit: bool = False) -> None: """Overridden from ContainerStack - + Replaces the container at the specified index with another container. This version performs checks to make sure the new container has the expected metadata and type. - + :throws Exception.InvalidContainerError Raised when trying to replace a container with a container that has an incorrect type. """ @@ -292,12 +292,12 @@ class CuraContainerStack(ContainerStack): @override(ContainerStack) def deserialize(self, serialized: str, file_name: Optional[str] = None) -> str: """Overridden from ContainerStack - + This deserialize will make sure the internal list of containers matches with what we expect. It will first check to see if the container at a certain index already matches with what we expect. If it does not, it will search for a matching container with the correct type. Should no container with the correct type be found, it will use the empty container. - + :raise InvalidContainerStackError: Raised when no definition can be found for the stack. """ @@ -357,12 +357,12 @@ class CuraContainerStack(ContainerStack): @classmethod def _findInstanceContainerDefinitionId(cls, machine_definition: DefinitionContainerInterface) -> str: """Find the ID that should be used when searching for instance containers for a specified definition. - + This handles the situation where the definition specifies we should use a different definition when searching for instance containers. - + :param machine_definition: The definition to find the "quality definition" for. - + :return: The ID of the definition container to use when searching for instance containers. """ diff --git a/cura/Settings/CuraStackBuilder.py b/cura/Settings/CuraStackBuilder.py index 37469f95f7..2b71db8034 100644 --- a/cura/Settings/CuraStackBuilder.py +++ b/cura/Settings/CuraStackBuilder.py @@ -20,10 +20,10 @@ class CuraStackBuilder: @classmethod def createMachine(cls, name: str, definition_id: str) -> Optional[GlobalStack]: """Create a new instance of a machine. - + :param name: The name of the new machine. :param definition_id: The ID of the machine definition to use. - + :return: The new global stack or None if an error occurred. """ @@ -77,7 +77,7 @@ class CuraStackBuilder: @classmethod def createExtruderStackWithDefaultSetup(cls, global_stack: "GlobalStack", extruder_position: int) -> None: """Create a default Extruder Stack - + :param global_stack: The global stack this extruder refers to. :param extruder_position: The position of the current extruder. """ @@ -134,7 +134,7 @@ class CuraStackBuilder: quality_container: "InstanceContainer") -> ExtruderStack: """Create a new Extruder stack - + :param new_stack_id: The ID of the new stack. :param extruder_definition: The definition to base the new stack on. :param machine_definition_id: The ID of the machine definition to use for the user container. @@ -142,7 +142,7 @@ class CuraStackBuilder: :param variant_container: The variant selected for the current extruder. :param material_container: The material selected for the current extruder. :param quality_container: The quality selected for the current extruder. - + :return: A new Extruder stack instance with the specified parameters. """ @@ -181,13 +181,13 @@ class CuraStackBuilder: quality_container: "InstanceContainer") -> GlobalStack: """Create a new Global stack - + :param new_stack_id: The ID of the new stack. :param definition: The definition to base the new stack on. :param variant_container: The variant selected for the current stack. :param material_container: The material selected for the current stack. :param quality_container: The quality selected for the current stack. - + :return: A new Global stack instance with the specified parameters. """ diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py index 52dfffd3a5..7520d436e9 100644 --- a/cura/Settings/ExtruderStack.py +++ b/cura/Settings/ExtruderStack.py @@ -37,7 +37,7 @@ class ExtruderStack(CuraContainerStack): @override(ContainerStack) def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None: """Overridden from ContainerStack - + This will set the next stack and ensure that we register this stack as an extruder. """ @@ -71,7 +71,7 @@ class ExtruderStack(CuraContainerStack): def getCompatibleMaterialDiameter(self) -> float: """Return the filament diameter that the machine requires. - + If the machine has no requirement for the diameter, -1 is returned. :return: The filament diameter for the printer """ @@ -99,12 +99,12 @@ class ExtruderStack(CuraContainerStack): def getApproximateMaterialDiameter(self) -> float: """Return the approximate filament diameter that the machine requires. - + The approximate material diameter is the material diameter rounded to the nearest millimetre. - + If the machine has no requirement for the diameter, -1 is returned. - + :return: The approximate filament diameter for the printer """ @@ -116,12 +116,12 @@ class ExtruderStack(CuraContainerStack): @override(ContainerStack) def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: """Overridden from ContainerStack - + It will perform a few extra checks when trying to get properties. - + The two extra checks it currently does is to ensure a next stack is set and to bypass the extruder when the property is not settable per extruder. - + :throws Exceptions.NoGlobalStackError Raised when trying to get a property from an extruder without having a next stack set. """ diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index e020221187..929c567921 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -62,7 +62,7 @@ class GlobalStack(CuraContainerStack): @deprecated("Please use extruderList instead.", "4.4") def extruders(self) -> Dict[str, "ExtruderStack"]: """Get the list of extruders of this stack. - + :return: The extruders registered with this stack. """ @@ -91,11 +91,11 @@ class GlobalStack(CuraContainerStack): @pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged) def configuredConnectionTypes(self) -> List[int]: """The configured connection types can be used to find out if the global - + stack is configured to be connected with a printer, without having to know all the details as to how this is exactly done (and without actually setting the stack to be active). - + This data can then in turn also be used when the global stack is active; If we can't get a network connection, but it is configured to have one, we can display a different icon to indicate the difference. @@ -171,9 +171,9 @@ class GlobalStack(CuraContainerStack): def addExtruder(self, extruder: ContainerStack) -> None: """Add an extruder to the list of extruders of this stack. - + :param extruder: The extruder to add. - + :raise Exceptions.TooManyExtrudersError: Raised when trying to add an extruder while we already have the maximum number of extruders. """ @@ -194,15 +194,15 @@ class GlobalStack(CuraContainerStack): @override(ContainerStack) def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any: """Overridden from ContainerStack - + This will return the value of the specified property for the specified setting, unless the property is "value" and that setting has a "resolve" function set. When a resolve is set, it will instead try and execute the resolve first and then fall back to the normal "value" property. - + :param key: The setting key to get the property of. :param property_name: The property to get the value of. - + :return: The value of the property for the specified setting, or None if not found. """ @@ -248,7 +248,7 @@ class GlobalStack(CuraContainerStack): @override(ContainerStack) def setNextStack(self, stack: CuraContainerStack, connect_signals: bool = True) -> None: """Overridden from ContainerStack - + This will simply raise an exception since the Global stack cannot have a next stack. """ @@ -281,7 +281,7 @@ class GlobalStack(CuraContainerStack): def isValid(self) -> bool: """Perform some sanity checks on the global stack - + Sanity check for extruders; they must have positions 0 and up to machine_extruder_count - 1 """ container_registry = ContainerRegistry.getInstance() diff --git a/cura/Settings/IntentManager.py b/cura/Settings/IntentManager.py index 9f636c9cc1..a556a86dd8 100644 --- a/cura/Settings/IntentManager.py +++ b/cura/Settings/IntentManager.py @@ -32,9 +32,9 @@ class IntentManager(QObject): def intentMetadatas(self, definition_id: str, nozzle_name: str, material_base_file: str) -> List[Dict[str, Any]]: """Gets the metadata dictionaries of all intent profiles for a given - + configuration. - + :param definition_id: ID of the printer. :param nozzle_name: Name of the nozzle. :param material_base_file: The base_file of the material. @@ -58,9 +58,9 @@ class IntentManager(QObject): def intentCategories(self, definition_id: str, nozzle_id: str, material_id: str) -> List[str]: """Collects and returns all intent categories available for the given - + parameters. Note that the 'default' category is always available. - + :param definition_id: ID of the printer. :param nozzle_name: Name of the nozzle. :param material_id: ID of the material. @@ -74,10 +74,10 @@ class IntentManager(QObject): def getCurrentAvailableIntents(self) -> List[Tuple[str, str]]: """List of intents to be displayed in the interface. - + For the interface this will have to be broken up into the different intent categories. That is up to the model there. - + :return: A list of tuples of intent_category and quality_type. The actual instance may vary per extruder. """ @@ -109,7 +109,7 @@ class IntentManager(QObject): def currentAvailableIntentCategories(self) -> List[str]: """List of intent categories available in either of the extruders. - + This is purposefully inconsistent with the way that the quality types are listed. The quality types will show all quality types available in the printer using any configuration. This will only list the intent @@ -134,7 +134,7 @@ class IntentManager(QObject): def getDefaultIntent(self) -> "InstanceContainer": """The intent that gets selected by default when no intent is available for - + the configuration, an extruder can't match the intent that the user selects, or just when creating a new printer. """ diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 045307c673..e5286e7032 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -342,7 +342,7 @@ class MachineManager(QObject): @staticmethod def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]: """Given a definition id, return the machine with this id. - + Optional: add a list of keys and values to filter the list of machines with the given definition id :param definition_id: :type{str} definition id that needs to look for :param metadata_filter: :type{dict} list of metadata keys and values used for filtering @@ -430,7 +430,7 @@ class MachineManager(QObject): @pyqtSlot(str) def clearUserSettingAllCurrentStacks(self, key: str) -> None: """Delete a user setting from the global stack and all extruder stacks. - + :param key: :type{str} the name of the key to delete """ Logger.log("i", "Clearing the setting [%s] from all stacks", key) @@ -464,7 +464,7 @@ class MachineManager(QObject): @pyqtProperty(bool, notify = stacksValidationChanged) def stacksHaveErrors(self) -> bool: """Check if none of the stacks contain error states - + Note that the _stacks_have_errors is cached due to performance issues Calling _checkStack(s)ForErrors on every change is simply too expensive """ @@ -503,7 +503,7 @@ class MachineManager(QObject): @pyqtProperty(bool, notify = printerConnectedStatusChanged) def activeMachineHasCloudRegistration(self) -> bool: return self.activeMachine is not None and ConnectionType.CloudConnection in self.activeMachine.configuredConnectionTypes - + @pyqtProperty(bool, notify = printerConnectedStatusChanged) def activeMachineIsUsingCloudConnection(self) -> bool: return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection @@ -544,9 +544,9 @@ class MachineManager(QObject): @pyqtProperty(float, notify = activeQualityGroupChanged) def activeQualityLayerHeight(self) -> float: """Gets the layer height of the currently active quality profile. - + This is indicated together with the name of the active quality profile. - + :return: The layer height of the currently active quality profile. If there is no quality profile, this returns the default layer height. """ @@ -623,7 +623,7 @@ class MachineManager(QObject): @pyqtProperty(bool, notify = activeQualityChanged) def isCurrentSetupSupported(self) -> bool: """Returns whether there is anything unsupported in the current set-up. - + The current set-up signifies the global stack and all extruder stacks, so this indicates whether there is any container in any of the container stacks that is not marked as supported. @@ -670,7 +670,7 @@ class MachineManager(QObject): @pyqtProperty(str, notify = globalContainerChanged) def activeQualityDefinitionId(self) -> str: """Get the Definition ID to use to select quality profiles for the currently active machine - + :returns: DefinitionID (string) if found, empty string otherwise """ global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() @@ -681,7 +681,7 @@ class MachineManager(QObject): @pyqtProperty(str, notify = globalContainerChanged) def activeDefinitionVariantsName(self) -> str: """Gets how the active definition calls variants - + Caveat: per-definition-variant-title is currently not translated (though the fallback is) """ fallback_title = catalog.i18nc("@label", "Nozzle") @@ -754,7 +754,7 @@ class MachineManager(QObject): @pyqtProperty(bool, notify = activeMaterialChanged) def variantBuildplateUsable(self) -> bool: """The selected buildplate is usable if it is usable for all materials OR it is compatible for one but not compatible - + for the other material but the buildplate is still usable """ if not self._global_container_stack: @@ -780,7 +780,7 @@ class MachineManager(QObject): @pyqtSlot(str, result = str) def getDefinitionByMachineId(self, machine_id: str) -> Optional[str]: """Get the Definition ID of a machine (specified by ID) - + :param machine_id: string machine id to get the definition ID of :returns: DefinitionID if found, None otherwise """ @@ -834,7 +834,7 @@ class MachineManager(QObject): def setActiveMachineExtruderCount(self, extruder_count: int) -> None: """Set the amount of extruders on the active machine (global stack) - + :param extruder_count: int the number of extruders to set """ if self._global_container_stack is None: @@ -979,7 +979,7 @@ class MachineManager(QObject): def _getContainerChangedSignals(self) -> List[Signal]: """Get the signals that signal that the containers changed for all stacks. - + This includes the global stack and all extruder stacks. So if any container changed anywhere. """ @@ -999,7 +999,7 @@ class MachineManager(QObject): @pyqtSlot(str) def resetSettingForAllExtruders(self, setting_name: str) -> None: """Reset all setting properties of a setting for all extruders. - + :param setting_name: The ID of the setting to reset. """ if self._global_container_stack is None: @@ -1247,7 +1247,7 @@ class MachineManager(QObject): @pyqtSlot() def updateMaterialWithVariant(self, position: Optional[str] = None) -> None: """Update the material profile in the current stacks when the variant is - + changed. :param position: The extruder stack to update. If provided with None, all extruder stacks will be updated. @@ -1289,7 +1289,7 @@ class MachineManager(QObject): @pyqtSlot(str) def switchPrinterType(self, machine_name: str) -> None: """Given a printer definition name, select the right machine instance. In case it doesn't exist, create a new - + instance with the same network key. """ # Don't switch if the user tries to change to the same type of printer @@ -1311,7 +1311,7 @@ class MachineManager(QObject): if not new_machine: Logger.log("e", "Failed to create new machine when switching configuration.") return - + for metadata_key in self._global_container_stack.getMetaData(): if metadata_key in new_machine.getMetaData(): continue # Don't copy the already preset stuff. @@ -1444,7 +1444,7 @@ class MachineManager(QObject): @pyqtSlot(str, "QVariant") def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None: """Global_stack: if you want to provide your own global_stack instead of the current active one - + if you update an active machine, special measures have to be taken. """ if global_stack is not None and global_stack != self._global_container_stack: @@ -1495,7 +1495,7 @@ class MachineManager(QObject): @pyqtSlot(QObject) def setQualityGroup(self, quality_group: "QualityGroup", no_dialog: bool = False, global_stack: Optional["GlobalStack"] = None) -> None: """Optionally provide global_stack if you want to use your own - + The active global_stack is treated differently. """ if global_stack is not None and global_stack != self._global_container_stack: @@ -1562,7 +1562,7 @@ class MachineManager(QObject): @pyqtSlot(str) def setIntentByCategory(self, intent_category: str) -> None: """Change the intent category of the current printer. - + All extruders can change their profiles. If an intent profile is available with the desired intent category, that one will get chosen. Otherwise the intent profile will be left to the empty profile, which @@ -1603,7 +1603,7 @@ class MachineManager(QObject): def activeQualityGroup(self) -> Optional["QualityGroup"]: """Get the currently activated quality group. - + If no printer is added yet or the printer doesn't have quality profiles, this returns ``None``. :return: The currently active quality group. @@ -1617,7 +1617,7 @@ class MachineManager(QObject): @pyqtProperty(str, notify = activeQualityGroupChanged) def activeQualityGroupName(self) -> str: """Get the name of the active quality group. - + :return: The name of the active quality group. """ quality_group = self.activeQualityGroup() diff --git a/cura/Settings/MachineNameValidator.py b/cura/Settings/MachineNameValidator.py index 2bb614f093..8ab8907355 100644 --- a/cura/Settings/MachineNameValidator.py +++ b/cura/Settings/MachineNameValidator.py @@ -13,7 +13,7 @@ from UM.Settings.InstanceContainer import InstanceContainer class MachineNameValidator(QObject): """Are machine names valid? - + Performs checks based on the length of the name. """ @@ -37,7 +37,7 @@ class MachineNameValidator(QObject): def validate(self, name): """Check if a specified machine name is allowed. - + :param name: The machine name to check. :return: ``QValidator.Invalid`` if it's disallowed, or ``QValidator.Acceptable`` if it's allowed. """ diff --git a/cura/Settings/SettingOverrideDecorator.py b/cura/Settings/SettingOverrideDecorator.py index d48bff042f..1b5fb84f4a 100644 --- a/cura/Settings/SettingOverrideDecorator.py +++ b/cura/Settings/SettingOverrideDecorator.py @@ -18,7 +18,7 @@ from cura.Settings.ExtruderManager import ExtruderManager @signalemitter class SettingOverrideDecorator(SceneNodeDecorator): """A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding - + the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by this stack still resolve. """ @@ -27,7 +27,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): _non_printing_mesh_settings = {"anti_overhang_mesh", "infill_mesh", "cutting_mesh"} """Non-printing meshes - + If these settings are True for any mesh, the mesh does not need a convex hull, and is sent to the slicer regardless of whether it fits inside the build volume. Note that Support Mesh is not in here because it actually generates @@ -83,7 +83,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): def getActiveExtruder(self): """Gets the currently active extruder to print this object with. - + :return: An extruder's container stack. """ @@ -91,7 +91,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): def getActiveExtruderChangedSignal(self): """Gets the signal that emits if the active extruder changed. - + This can then be accessed via a decorator. """ @@ -99,7 +99,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): def getActiveExtruderPosition(self): """Gets the currently active extruders position - + :return: An extruder's position, or None if no position info is available. """ @@ -137,7 +137,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): def _updateNextStack(self): """Makes sure that the stack upon which the container stack is placed is - + kept up to date. """ if self._extruder_stack: @@ -160,7 +160,7 @@ class SettingOverrideDecorator(SceneNodeDecorator): def setActiveExtruder(self, extruder_stack_id): """Changes the extruder with which to print this node. - + :param extruder_stack_id: The new extruder stack to print with. """ diff --git a/cura/UI/MachineActionManager.py b/cura/UI/MachineActionManager.py index 25234fd43f..5e31de32c2 100644 --- a/cura/UI/MachineActionManager.py +++ b/cura/UI/MachineActionManager.py @@ -75,7 +75,7 @@ class MachineActionManager(QObject): def addRequiredAction(self, definition_id: str, action_key: str) -> None: """Add a required action to a machine - + Raises an exception when the action is not recognised. """ if action_key in self._machine_actions: @@ -112,7 +112,7 @@ class MachineActionManager(QObject): def addMachineAction(self, action: "MachineAction") -> None: """Add a (unique) MachineAction - + if the Key of the action is not unique, an exception is raised. """ if action.getKey() not in self._machine_actions: @@ -123,7 +123,7 @@ class MachineActionManager(QObject): @pyqtSlot(str, result = "QVariantList") def getSupportedActions(self, definition_id: str) -> List["MachineAction"]: """Get all actions supported by given machine - + :param definition_id: The ID of the definition you want the supported actions of :returns: set of supported actions. """ @@ -134,7 +134,7 @@ class MachineActionManager(QObject): def getRequiredActions(self, definition_id: str) -> List["MachineAction"]: """Get all actions required by given machine - + :param definition_id: The ID of the definition you want the required actions of :returns: set of required actions. """ @@ -146,7 +146,7 @@ class MachineActionManager(QObject): @pyqtSlot(str, result = "QVariantList") def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]: """Get all actions that need to be performed upon first start of a given machine. - + Note that contrary to required / supported actions a list is returned (as it could be required to run the same action multiple times). :param definition_id: The ID of the definition that you want to get the "on added" actions for. @@ -159,7 +159,7 @@ class MachineActionManager(QObject): def removeMachineAction(self, action: "MachineAction") -> None: """Remove Machine action from manager - + :param action: to remove """ try: @@ -169,7 +169,7 @@ class MachineActionManager(QObject): def getMachineAction(self, key: str) -> Optional["MachineAction"]: """Get MachineAction by key - + :param key: String of key to select :return: Machine action if found, None otherwise """ diff --git a/cura/UI/PrintInformation.py b/cura/UI/PrintInformation.py index cbe0900ec0..ae4aab0407 100644 --- a/cura/UI/PrintInformation.py +++ b/cura/UI/PrintInformation.py @@ -23,7 +23,7 @@ catalog = i18nCatalog("cura") class PrintInformation(QObject): """A class for processing and the print times per build plate as well as managing the job name - + This class also mangles the current machine name and the filename of the first loaded mesh into a job name. This job name is requested by the JobSpecs qml file. """ diff --git a/cura/Utils/Decorators.py b/cura/Utils/Decorators.py index 7be718c51c..b478f172bc 100644 --- a/cura/Utils/Decorators.py +++ b/cura/Utils/Decorators.py @@ -13,9 +13,9 @@ SEMANTIC_VERSION_REGEX = re.compile(r"^[0-9]+\.[0-9]+(\.[0-9]+)?$") def api(since_version: str) -> Callable: """Decorator for functions that belong to a set of APIs. For now, this should only be used for officially supported - + APIs, meaning that those APIs should be versioned and maintained. - + :param since_version: The earliest version since when this API becomes supported. This means that since this version, this API function is supposed to behave the same. This parameter is not used. It's just a documentation. diff --git a/plugins/3MFReader/ThreeMFReader.py b/plugins/3MFReader/ThreeMFReader.py index 32e12cb91a..546c6fc27b 100755 --- a/plugins/3MFReader/ThreeMFReader.py +++ b/plugins/3MFReader/ThreeMFReader.py @@ -59,11 +59,11 @@ class ThreeMFReader(MeshReader): temp_mat = Matrix() """Transformation is saved as: M00 M01 M02 0.0 - + M10 M11 M12 0.0 - + M20 M21 M22 0.0 - + M30 M31 M32 1.0 We switch the row & cols as that is how everyone else uses matrices! """ @@ -87,7 +87,7 @@ class ThreeMFReader(MeshReader): def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]: """Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node. - + :returns: Scene node. """ try: @@ -252,7 +252,7 @@ class ThreeMFReader(MeshReader): def _getScaleFromUnit(self, unit: Optional[str]) -> Vector: """Create a scale vector based on a unit string. - + .. The core spec defines the following: * micron * millimeter (default) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 88187cd732..a744d29449 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -133,7 +133,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def getNewId(self, old_id: str): """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. """ if old_id not in self._id_mapping: @@ -142,7 +142,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def _determineGlobalAndExtruderStackFiles(self, project_file_name: str, file_list: List[str]) -> Tuple[str, List[str]]: """Separates the given file list into a list of GlobalStack files and a list of ExtruderStack files. - + In old versions, extruder stack files have the same suffix as container stack files ".stack.cfg". """ @@ -187,7 +187,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def preRead(self, file_name, show_dialog=True, *args, **kwargs): """Read some info so we can make decisions - + :param file_name: :param show_dialog: In case we use preRead() to check if a file is a valid project file, we don't want to show a dialog. @@ -588,12 +588,12 @@ class ThreeMFWorkspaceReader(WorkspaceReader): @call_on_qt_thread def read(self, file_name): """Read the project file - + Add all the definitions / materials / quality changes that do not exist yet. Then it loads all the stacks into the container registry. In some cases it will reuse the container for the global stack. It handles old style project files containing .stack.cfg as well as new style project files containing global.cfg / extruder.cfg - + :param file_name: """ application = CuraApplication.getInstance() @@ -879,7 +879,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def _createNewQualityChanges(self, quality_type: str, intent_category: Optional[str], name: str, global_stack: GlobalStack, extruder_stack: Optional[ExtruderStack]) -> InstanceContainer: """Helper class to create a new quality changes profile. - + This will then later be filled with the appropriate data. :param quality_type: The quality type of the new profile. diff --git a/plugins/3MFReader/__init__.py b/plugins/3MFReader/__init__.py index d68338c35f..5e2b68fce0 100644 --- a/plugins/3MFReader/__init__.py +++ b/plugins/3MFReader/__init__.py @@ -33,7 +33,7 @@ def getMetaData() -> Dict: "description": catalog.i18nc("@item:inlistbox", "3MF File") } ] - + return metaData diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index a8d42ab2ee..a7ee47dcf1 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -110,7 +110,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): @staticmethod def _writeContainerToArchive(container, archive): """Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive. - + :param container: That follows the :type{ContainerInterface} to archive. :param archive: The archive to write to. """ diff --git a/plugins/3MFWriter/ThreeMFWriter.py b/plugins/3MFWriter/ThreeMFWriter.py index 343b94a08b..6c02935080 100644 --- a/plugins/3MFWriter/ThreeMFWriter.py +++ b/plugins/3MFWriter/ThreeMFWriter.py @@ -62,7 +62,7 @@ class ThreeMFWriter(MeshWriter): def setStoreArchive(self, store_archive): """Should we store the archive - + Note that if this is true, the archive will not be closed. The object that set this parameter is then responsible for closing it correctly! """ @@ -70,7 +70,7 @@ class ThreeMFWriter(MeshWriter): def _convertUMNodeToSavitarNode(self, um_node, transformation = Matrix()): """Convenience function that converts an Uranium SceneNode object to a SavitarSceneNode - + :returns: Uranium Scene node. """ if not isinstance(um_node, SceneNode): diff --git a/plugins/AMFReader/AMFReader.py b/plugins/AMFReader/AMFReader.py index 5196119d6c..ef785f2f53 100644 --- a/plugins/AMFReader/AMFReader.py +++ b/plugins/AMFReader/AMFReader.py @@ -149,7 +149,7 @@ class AMFReader(MeshReader): def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData: """Converts a Trimesh to Uranium's MeshData. - + :param tri_node: A Trimesh containing the contents of a file that was just read. :param file_name: The full original filename used to watch for changes :return: Mesh data from the Trimesh in a way that Uranium can understand it. diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index 1c40052521..519d302618 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -207,7 +207,7 @@ class CuraEngineBackend(QObject, Backend): printDurationMessage = Signal() """Emitted when we get a message containing print duration and material amount. - + This also implies the slicing has finished. :param time: The amount of time the print will take. :param material_amount: The amount of material the print will use. diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py index 9cd48cdc94..e0a20177b5 100644 --- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py +++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py @@ -30,7 +30,7 @@ catalog = i18nCatalog("cura") def colorCodeToRGBA(color_code): """Return a 4-tuple with floats 0-1 representing the html color code - + :param color_code: html color code, i.e. "#FF0000" -> red """ @@ -55,7 +55,7 @@ class ProcessSlicedLayersJob(Job): def abort(self): """Aborts the processing of layers. - + This abort is made on a best-effort basis, meaning that the actual job thread will check once in a while to see whether an abort is requested and then stop processing by itself. There is no guarantee diff --git a/plugins/CuraProfileReader/CuraProfileReader.py b/plugins/CuraProfileReader/CuraProfileReader.py index fa65033691..8822e9bb17 100644 --- a/plugins/CuraProfileReader/CuraProfileReader.py +++ b/plugins/CuraProfileReader/CuraProfileReader.py @@ -16,21 +16,21 @@ import zipfile class CuraProfileReader(ProfileReader): """A plugin that reads profile data from Cura profile files. - + It reads a profile from a .curaprofile file, and returns it as a profile instance. """ def __init__(self) -> None: """Initialises the cura profile reader. - + This does nothing since the only other function is basically stateless. """ super().__init__() def read(self, file_name: str) -> List[Optional[InstanceContainer]]: """Reads a cura profile from a file and returns it. - + :param file_name: The file to read the cura profile from. :return: The cura profiles that were in the file, if any. If the file could not be read or didn't contain a valid profile, ``None`` is @@ -59,7 +59,7 @@ class CuraProfileReader(ProfileReader): def _upgradeProfile(self, serialized: str, profile_id: str) -> List[Tuple[str, str]]: """Convert a profile from an old Cura to this Cura if needed. - + :param serialized: The profile data to convert in the serialized on-disk format. :param profile_id: The name of the profile. :return: List of serialized profile strings and matching profile names. @@ -85,7 +85,7 @@ class CuraProfileReader(ProfileReader): def _loadProfile(self, serialized: str, profile_id: str) -> Optional[InstanceContainer]: """Load a profile from a serialized string. - + :param serialized: The profile data to read. :param profile_id: The name of the profile. :return: The profile that was stored in the string. @@ -114,7 +114,7 @@ class CuraProfileReader(ProfileReader): def _upgradeProfileVersion(self, serialized: str, profile_id: str, main_version: int, setting_version: int) -> List[Tuple[str, str]]: """Upgrade a serialized profile to the current profile format. - + :param serialized: The profile data to convert. :param profile_id: The name of the profile. :param source_version: The profile version of 'serialized'. diff --git a/plugins/CuraProfileWriter/CuraProfileWriter.py b/plugins/CuraProfileWriter/CuraProfileWriter.py index 6643bd8d1f..56f4d07a74 100644 --- a/plugins/CuraProfileWriter/CuraProfileWriter.py +++ b/plugins/CuraProfileWriter/CuraProfileWriter.py @@ -11,7 +11,7 @@ class CuraProfileWriter(ProfileWriter): def write(self, path, profiles): """Writes a profile to the specified file path. - + :param path: :type{string} The file to output to. :param profiles: :type{Profile} :type{List} The profile(s) to write to that file. :return: True if the writing was successful, or diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py index 92678cb546..8d0670c844 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateChecker.py @@ -20,7 +20,7 @@ i18n_catalog = i18nCatalog("cura") class FirmwareUpdateChecker(Extension): """This Extension checks for new versions of the firmware based on the latest checked version number. - + The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy to change it to work for other applications. """ @@ -62,10 +62,10 @@ class FirmwareUpdateChecker(Extension): def checkFirmwareVersion(self, container = None, silent = False): """Connect with software.ultimaker.com, load latest.version and check version info. - + If the version info is different from the current version, spawn a message to allow the user to download it. - + :param silent: type(boolean) Suppresses messages other than "new version found" messages. This is used when checking for a new firmware version at startup. """ diff --git a/plugins/GCodeGzReader/GCodeGzReader.py b/plugins/GCodeGzReader/GCodeGzReader.py index b4d9e85b3e..85a5b01107 100644 --- a/plugins/GCodeGzReader/GCodeGzReader.py +++ b/plugins/GCodeGzReader/GCodeGzReader.py @@ -10,7 +10,7 @@ from UM.PluginRegistry import PluginRegistry class GCodeGzReader(MeshReader): """A file reader that reads gzipped g-code. - + If you're zipping g-code, you might as well use gzip! """ diff --git a/plugins/GCodeGzWriter/GCodeGzWriter.py b/plugins/GCodeGzWriter/GCodeGzWriter.py index e9dcbad76b..2bbaaeb0a3 100644 --- a/plugins/GCodeGzWriter/GCodeGzWriter.py +++ b/plugins/GCodeGzWriter/GCodeGzWriter.py @@ -16,7 +16,7 @@ catalog = i18nCatalog("cura") class GCodeGzWriter(MeshWriter): """A file writer that writes gzipped g-code. - + If you're zipping g-code, you might as well use gzip! """ @@ -26,11 +26,11 @@ class GCodeGzWriter(MeshWriter): def write(self, stream: BufferedIOBase, nodes: List[SceneNode], mode = MeshWriter.OutputMode.BinaryMode) -> bool: """Writes the gzipped g-code to a stream. - + Note that even though the function accepts a collection of nodes, the entire scene is always written to the file since it is not possible to separate the g-code for just specific nodes. - + :param stream: The stream to write the gzipped g-code to. :param nodes: This is ignored. :param mode: Additional information on what type of stream to use. This diff --git a/plugins/GCodeProfileReader/GCodeProfileReader.py b/plugins/GCodeProfileReader/GCodeProfileReader.py index 93bba1db4d..047497e611 100644 --- a/plugins/GCodeProfileReader/GCodeProfileReader.py +++ b/plugins/GCodeProfileReader/GCodeProfileReader.py @@ -15,14 +15,14 @@ from cura.ReaderWriters.ProfileReader import ProfileReader, NoProfileException class GCodeProfileReader(ProfileReader): """A class that reads profile data from g-code files. - + It reads the profile data from g-code files and stores it in a new profile. This class currently does not process the rest of the g-code in any way. """ version = 3 """The file format version of the serialized g-code. - + It can only read settings with the same version as the version it was written with. If the file format is changed in a way that breaks reverse compatibility, increment this version number! @@ -48,7 +48,7 @@ class GCodeProfileReader(ProfileReader): def read(self, file_name): """Reads a g-code file, loading the profile from it. - + :param file_name: The name of the file to read the profile from. :return: The profile that was in the specified file, if any. If the specified file was no g-code or contained no parsable profile, @@ -106,7 +106,7 @@ class GCodeProfileReader(ProfileReader): def unescapeGcodeComment(string: str) -> str: """Unescape a string which has been escaped for use in a gcode comment. - + :param string: The string to unescape. :return: The unescaped string. """ @@ -120,7 +120,7 @@ def unescapeGcodeComment(string: str) -> str: def readQualityProfileFromString(profile_string) -> Optional[InstanceContainer]: """Read in a profile from a serialized string. - + :param profile_string: The profile data in serialized form. :return: The resulting Profile object or None if it could not be read. """ diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py index bcb7efc816..2d0bd7b7c0 100644 --- a/plugins/GCodeReader/FlavorParser.py +++ b/plugins/GCodeReader/FlavorParser.py @@ -237,7 +237,7 @@ class FlavorParser: def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position: """Reset the current position to the values specified. - + For example: G92 X10 will set the X to 10 without any physical motion. """ diff --git a/plugins/GCodeReader/RepRapFlavorParser.py b/plugins/GCodeReader/RepRapFlavorParser.py index 05f86beab0..10b7b78587 100644 --- a/plugins/GCodeReader/RepRapFlavorParser.py +++ b/plugins/GCodeReader/RepRapFlavorParser.py @@ -21,7 +21,7 @@ class RepRapFlavorParser(FlavorParser.FlavorParser): def _gCode90(self, position, params, path): """Set the absolute positioning - + RepRapFlavor code G90 sets position of X, Y, Z to absolute For absolute E, M82 is used """ @@ -30,7 +30,7 @@ class RepRapFlavorParser(FlavorParser.FlavorParser): def _gCode91(self, position, params, path): """Set the relative positioning - + RepRapFlavor code G91 sets position of X, Y, Z to relative For relative E, M83 is used """ diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index 93ff0f51ff..160857443b 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -17,12 +17,12 @@ catalog = i18nCatalog("cura") class GCodeWriter(MeshWriter): """Writes g-code to a file. - + While this poses as a mesh writer, what this really does is take the g-code in the entire scene and write it to an output device. Since the g-code of a single mesh isn't separable from the rest what with rafts and travel moves and all, it doesn't make sense to write just a single mesh. - + So this plug-in takes the g-code that is stored in the root of the scene node tree, adds a bit of extra information about the profiles and writes that to the output device. @@ -30,7 +30,7 @@ class GCodeWriter(MeshWriter): version = 3 """The file format version of the serialised g-code. - + It can only read settings with the same version as the version it was written with. If the file format is changed in a way that breaks reverse compatibility, increment this version number! @@ -42,9 +42,9 @@ class GCodeWriter(MeshWriter): re.escape("\r"): "\\r" # Carriage return. Windows users may need this for visualisation in their editors. } """Dictionary that defines how characters are escaped when embedded in - + g-code. - + Note that the keys of this dictionary are regex strings. The values are not. """ @@ -58,11 +58,11 @@ class GCodeWriter(MeshWriter): def write(self, stream, nodes, mode = MeshWriter.OutputMode.TextMode): """Writes the g-code for the entire scene to a stream. - + Note that even though the function accepts a collection of nodes, the entire scene is always written to the file since it is not possible to separate the g-code for just specific nodes. - + :param stream: The stream to write the g-code to. :param nodes: This is ignored. :param mode: Additional information on how to format the g-code in the @@ -117,10 +117,10 @@ class GCodeWriter(MeshWriter): def _serialiseSettings(self, stack): """Serialises a container stack to prepare it for writing at the end of the g-code. - + The settings are serialised, and special characters (including newline) are escaped. - + :param stack: A container stack to serialise. :return: A serialised string of the settings. """ diff --git a/plugins/LegacyProfileReader/LegacyProfileReader.py b/plugins/LegacyProfileReader/LegacyProfileReader.py index c67136ce9b..0bc82ad287 100644 --- a/plugins/LegacyProfileReader/LegacyProfileReader.py +++ b/plugins/LegacyProfileReader/LegacyProfileReader.py @@ -18,14 +18,14 @@ from cura.ReaderWriters.ProfileReader import ProfileReader # The plug-in type t class LegacyProfileReader(ProfileReader): """A plugin that reads profile data from legacy Cura versions. - + It reads a profile from an .ini file, and performs some translations on it. Not all translations are correct, mind you, but it is a best effort. """ def __init__(self): """Initialises the legacy profile reader. - + This does nothing since the only other function is basically stateless. """ @@ -33,9 +33,9 @@ class LegacyProfileReader(ProfileReader): def prepareDefaults(self, json: Dict[str, Dict[str, str]]) -> Dict[str, str]: """Prepares the default values of all legacy settings. - + These are loaded from the Dictionary of Doom. - + :param json: The JSON file to load the default setting values from. This should not be a URL but a pre-loaded JSON handle. :return: A dictionary of the default values of the legacy Cura version. @@ -49,13 +49,13 @@ class LegacyProfileReader(ProfileReader): def prepareLocals(self, config_parser, config_section, defaults): """Prepares the local variables that can be used in evaluation of computing - + new setting values from the old ones. - + This fills a dictionary with all settings from the legacy Cura version and their values, so that they can be used in evaluating the new setting values as Python code. - + :param config_parser: The ConfigParser that finds the settings in the legacy profile. :param config_section: The section in the profile where the settings @@ -71,7 +71,7 @@ class LegacyProfileReader(ProfileReader): def read(self, file_name): """Reads a legacy Cura profile from a file and returns it. - + :param file_name: The file to read the legacy Cura profile from. :return: The legacy Cura profile that was in the file, if any. If the file could not be read or didn't contain a valid profile, None is returned. diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.py b/plugins/MachineSettingsAction/MachineSettingsAction.py index c82ef8f351..f3359a1c56 100755 --- a/plugins/MachineSettingsAction/MachineSettingsAction.py +++ b/plugins/MachineSettingsAction/MachineSettingsAction.py @@ -25,7 +25,7 @@ catalog = UM.i18n.i18nCatalog("cura") class MachineSettingsAction(MachineAction): """This action allows for certain settings that are "machine only") to be modified. - + It automatically detects machine definitions that it knows how to change and attaches itself to those. """ def __init__(self, parent: Optional["QObject"] = None) -> None: @@ -60,7 +60,7 @@ class MachineSettingsAction(MachineAction): def _updateHasMaterialsInContainerTree(self) -> None: """Triggered when the global container stack changes or when the g-code - + flavour setting is changed. """ global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() diff --git a/plugins/ModelChecker/ModelChecker.py b/plugins/ModelChecker/ModelChecker.py index 138d84cc86..b482667976 100644 --- a/plugins/ModelChecker/ModelChecker.py +++ b/plugins/ModelChecker/ModelChecker.py @@ -49,7 +49,7 @@ class ModelChecker(QObject, Extension): def _pluginsInitialized(self): """Called when plug-ins are initialized. - + This makes sure that we listen to changes of the material and that the button is created that indicates warnings with the current set-up. """ diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py index c44e4a4df3..445f7ff676 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingVisibilityHandler.py @@ -17,7 +17,7 @@ from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHandler.SettingVisibilityHandler): """The per object setting visibility handler ensures that only setting - + definitions that have a matching instance Container are returned as visible. """ def __init__(self, parent = None, *args, **kwargs): diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py index d23096c0a3..77f1c33a5f 100644 --- a/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py +++ b/plugins/PerObjectSettingsTool/PerObjectSettingsTool.py @@ -14,7 +14,7 @@ from UM.Event import Event class PerObjectSettingsTool(Tool): """This tool allows the user to add & change settings per node in the scene. - + The settings per object are kept in a ContainerStack, which is linked to a node by decorator. """ def __init__(self): @@ -52,7 +52,7 @@ class PerObjectSettingsTool(Tool): def getSelectedActiveExtruder(self): """Gets the active extruder of the currently selected object. - + :return: The active extruder of the currently selected object. """ @@ -61,7 +61,7 @@ class PerObjectSettingsTool(Tool): def setSelectedActiveExtruder(self, extruder_stack_id): """Changes the active extruder of the currently selected object. - + :param extruder_stack_id: The ID of the extruder to print the currently selected object with. """ diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index c1135640dd..90f3d26cd6 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -132,7 +132,7 @@ class PostProcessingPlugin(QObject, Extension): def loadAllScripts(self) -> None: """Load all scripts from all paths where scripts can be found. - + This should probably only be done on init. """ @@ -157,7 +157,7 @@ class PostProcessingPlugin(QObject, Extension): def loadScripts(self, path: str) -> None: """Load all scripts from provided path. - + This should probably only be done on init. :param path: Path to check for scripts. """ @@ -360,7 +360,7 @@ class PostProcessingPlugin(QObject, Extension): def _propertyChanged(self) -> None: """Property changed: trigger re-slice - + To do this we use the global container stack propertyChanged. Re-slicing is necessary for setting changes in this plugin, because the changes are applied only once per "fresh" gcode diff --git a/plugins/PostProcessingPlugin/Script.py b/plugins/PostProcessingPlugin/Script.py index d15b209478..3228870dca 100644 --- a/plugins/PostProcessingPlugin/Script.py +++ b/plugins/PostProcessingPlugin/Script.py @@ -81,7 +81,7 @@ class Script: def getSettingData(self) -> Dict[str, Any]: """Needs to return a dict that can be used to construct a settingcategory file. - + See the example script for an example. It follows the same style / guides as the Uranium settings. Scripts can either override getSettingData directly, or use getSettingDataString @@ -116,7 +116,7 @@ class Script: def getValue(self, line: str, key: str, default = None) -> Any: """Convenience function that finds the value in a line of g-code. - + When requesting key = x from line "G1 X100" the value 100 is returned. """ if not key in line or (';' in line and line.find(key) > line.find(';')): @@ -135,7 +135,7 @@ class Script: def putValue(self, line: str = "", **kwargs) -> str: """Convenience function to produce a line of g-code. - + You can put in an original g-code line and it'll re-use all the values in that line. All other keyword parameters are put in the result in g-code's format. @@ -190,7 +190,7 @@ class Script: def execute(self, data: List[str]) -> List[str]: """This is called when the script is executed. - + It gets a list of g-code strings and needs to return a (modified) list. """ raise NotImplementedError() diff --git a/plugins/PostProcessingPlugin/scripts/ColorMix.py b/plugins/PostProcessingPlugin/scripts/ColorMix.py index 45b2a0ad70..dacb40e905 100644 --- a/plugins/PostProcessingPlugin/scripts/ColorMix.py +++ b/plugins/PostProcessingPlugin/scripts/ColorMix.py @@ -20,7 +20,7 @@ # Uses - # M163 - Set Mix Factor # M164 - Save Mix - saves to T2 as a unique mix - + import re #To perform the search and replace. from ..Script import Script @@ -127,7 +127,7 @@ class ColorMix(Script): firstMix = self.getSettingValueByKey("mix_start") secondMix = self.getSettingValueByKey("mix_finish") modelOfInterest = self.getSettingValueByKey("object_number") - + #get layer height layerHeight = 0 for active_layer in data: @@ -138,11 +138,11 @@ class ColorMix(Script): break if layerHeight != 0: break - + #default layerHeight if not found if layerHeight == 0: layerHeight = .2 - + #get layers to use startLayer = 0 endLayer = 0 diff --git a/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py b/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py index cbd131f17e..d589e63fb3 100644 --- a/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py +++ b/plugins/PostProcessingPlugin/scripts/DisplayFilenameAndLayerOnLCD.py @@ -56,7 +56,7 @@ class DisplayFilenameAndLayerOnLCD(Script): } } }""" - + def execute(self, data): max_layer = 0 if self.getSettingValueByKey("name") != "": @@ -96,5 +96,5 @@ class DisplayFilenameAndLayerOnLCD(Script): i += 1 final_lines = "\n".join(lines) data[layer_index] = final_lines - + return data diff --git a/plugins/PostProcessingPlugin/scripts/FilamentChange.py b/plugins/PostProcessingPlugin/scripts/FilamentChange.py index 351b985d49..74b9687f8c 100644 --- a/plugins/PostProcessingPlugin/scripts/FilamentChange.py +++ b/plugins/PostProcessingPlugin/scripts/FilamentChange.py @@ -65,7 +65,7 @@ class FilamentChange(Script): def execute(self, data: List[str]): """Inserts the filament change g-code at specific layer numbers. - + :param data: A list of layers of g-code. :return: A similar list, with filament change commands inserted. """ diff --git a/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py index b601a037da..a0c3648304 100644 --- a/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py +++ b/plugins/PostProcessingPlugin/scripts/SearchAndReplace.py @@ -8,7 +8,7 @@ from ..Script import Script class SearchAndReplace(Script): """Performs a search-and-replace on all g-code. - + Due to technical limitations, the search can't cross the border between layers. """ diff --git a/plugins/PostProcessingPlugin/scripts/UsePreviousProbeMeasurements.py b/plugins/PostProcessingPlugin/scripts/UsePreviousProbeMeasurements.py index 271cb57100..62989f6c7e 100644 --- a/plugins/PostProcessingPlugin/scripts/UsePreviousProbeMeasurements.py +++ b/plugins/PostProcessingPlugin/scripts/UsePreviousProbeMeasurements.py @@ -30,7 +30,7 @@ class UsePreviousProbeMeasurements(Script): } } }""" - + def execute(self, data): text = "M501 ;load bed level data\nM420 S1 ;enable bed leveling" if self.getSettingValueByKey("use_previous_measurements"): diff --git a/plugins/PreviewStage/PreviewStage.py b/plugins/PreviewStage/PreviewStage.py index deec8b4197..88f432ef9b 100644 --- a/plugins/PreviewStage/PreviewStage.py +++ b/plugins/PreviewStage/PreviewStage.py @@ -14,7 +14,7 @@ if TYPE_CHECKING: class PreviewStage(CuraStage): """Displays a preview of what you're about to print. - + The Python component of this stage just loads PreviewMain.qml for display when the stage is selected, and makes sure that it reverts to the previous view when the previous stage is activated. @@ -28,14 +28,14 @@ class PreviewStage(CuraStage): def onStageSelected(self) -> None: """When selecting the stage, remember which was the previous view so that - + we can revert to that view when we go out of the stage later. """ self._previously_active_view = self._application.getController().getActiveView() def onStageDeselected(self) -> None: """Called when going to a different stage (away from the Preview Stage). - + When going to a different stage, the view should be reverted to what it was before. Normally, that just reverts it to solid view. """ @@ -46,7 +46,7 @@ class PreviewStage(CuraStage): def _engineCreated(self) -> None: """Delayed load of the QML files. - + We need to make sure that the QML engine is running before we can load these. """ diff --git a/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py b/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py index 3661bfa63c..7b3363308d 100644 --- a/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py +++ b/plugins/RemovableDriveOutputDevice/LinuxRemovableDrivePlugin.py @@ -13,7 +13,7 @@ import subprocess class LinuxRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin): """Support for removable devices on Linux. - + TODO: This code uses the most basic interfaces for handling this. We should instead use UDisks2 to handle mount/unmount and hotplugging events. """ diff --git a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py index a4fe7309f7..46f38500ee 100644 --- a/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py +++ b/plugins/RemovableDriveOutputDevice/RemovableDriveOutputDevice.py @@ -30,7 +30,7 @@ class RemovableDriveOutputDevice(OutputDevice): def requestWrite(self, nodes, file_name = None, filter_by_machine = False, file_handler = None, **kwargs): """Request the specified nodes to be written to the removable drive. - + :param nodes: A collection of scene nodes that should be written to the removable drive. :param file_name: :type{string} A suggestion for the file name to write to. @@ -38,7 +38,7 @@ class RemovableDriveOutputDevice(OutputDevice): meshes. :param limit_mimetypes: Should we limit the available MIME types to the MIME types available to the currently active machine? - + """ filter_by_machine = True # This plugin is intended to be used by machine (regardless of what it was told to do) @@ -110,10 +110,10 @@ class RemovableDriveOutputDevice(OutputDevice): def _automaticFileName(self, nodes): """Generate a file name automatically for the specified nodes to be saved in. - + The name generated will be the name of one of the nodes. Which node that is can not be guaranteed. - + :param nodes: A collection of nodes for which to generate a file name. """ for root in nodes: diff --git a/plugins/SentryLogger/SentryLogger.py b/plugins/SentryLogger/SentryLogger.py index 55bcc7e806..29230abb1f 100644 --- a/plugins/SentryLogger/SentryLogger.py +++ b/plugins/SentryLogger/SentryLogger.py @@ -20,7 +20,7 @@ class SentryLogger(LogOutput): # processed and ready for sending. # Note that this only prepares them for sending. It only sends them when the user actually agrees to sending the # information. - + _levels = { "w": "warning", "i": "info", @@ -32,10 +32,10 @@ class SentryLogger(LogOutput): def __init__(self) -> None: super().__init__() self._show_once = set() # type: Set[str] - + def log(self, log_type: str, message: str) -> None: """Log the message to the sentry hub as a breadcrumb - + :param log_type: "e" (error), "i"(info), "d"(debug), "w"(warning) or "c"(critical) (can postfix with "_once") :param message: String containing message to be logged """ diff --git a/plugins/SliceInfoPlugin/SliceInfo.py b/plugins/SliceInfoPlugin/SliceInfo.py index a4b17236dd..630c0abb1b 100755 --- a/plugins/SliceInfoPlugin/SliceInfo.py +++ b/plugins/SliceInfoPlugin/SliceInfo.py @@ -28,7 +28,7 @@ catalog = i18nCatalog("cura") class SliceInfo(QObject, Extension): """This Extension runs in the background and sends several bits of information to the Ultimaker servers. - + The data is only sent when the user in question gave permission to do so. All data is anonymous and no model files are being sent (Just a SHA256 hash of the model). """ @@ -59,7 +59,7 @@ class SliceInfo(QObject, Extension): def messageActionTriggered(self, message_id, action_id): """Perform action based on user input. - + Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it. """ self._application.getPreferences().setValue("info/asked_send_slice_info", True) diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py index 4d1c812981..9a8e7f5dfe 100644 --- a/plugins/Toolbox/src/AuthorsModel.py +++ b/plugins/Toolbox/src/AuthorsModel.py @@ -73,7 +73,7 @@ class AuthorsModel(ListModel): def setFilter(self, filter_dict: Dict[str, str]) -> None: """Set the filter of this model based on a string. - + :param filter_dict: Dictionary to do the filtering by. """ if filter_dict != self._filter: diff --git a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py index a168381efa..cee2f6318a 100644 --- a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py +++ b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py @@ -10,7 +10,7 @@ from .SubscribedPackagesModel import SubscribedPackagesModel class DiscrepanciesPresenter(QObject): """Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's - + choices are emitted on the `packageMutations` Signal. """ diff --git a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py index 11c1dffcb4..635cd89af2 100644 --- a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py +++ b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py @@ -18,7 +18,7 @@ from .SubscribedPackagesModel import SubscribedPackagesModel class DownloadPresenter: """Downloads a set of packages from the Ultimaker Cloud Marketplace - + use download() exactly once: should not be used for multiple sets of downloads since this class contains state """ diff --git a/plugins/Toolbox/src/CloudSync/LicensePresenter.py b/plugins/Toolbox/src/CloudSync/LicensePresenter.py index 8701120cc5..9a68c93d71 100644 --- a/plugins/Toolbox/src/CloudSync/LicensePresenter.py +++ b/plugins/Toolbox/src/CloudSync/LicensePresenter.py @@ -45,7 +45,7 @@ class LicensePresenter(QObject): def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None: """Show a license dialog for multiple packages where users can read a license and accept or decline them - + :param plugin_path: Root directory of the Toolbox plugin :param packages: Dict[package id, file path] """ diff --git a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py b/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py index 1242bd3b49..d0222029fd 100644 --- a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py +++ b/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py @@ -5,7 +5,7 @@ from cura.CuraApplication import CuraApplication class RestartApplicationPresenter: """Presents a dialog telling the user that a restart is required to apply changes - + Since we cannot restart Cura, the app is closed instead when the button is clicked """ def __init__(self, app: CuraApplication) -> None: diff --git a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py index 89f20815bf..5693b82ded 100644 --- a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py +++ b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py @@ -18,7 +18,7 @@ from .SubscribedPackagesModel import SubscribedPackagesModel class SyncOrchestrator(Extension): """Orchestrates the synchronizing of packages from the user account to the installed packages - + Example flow: - CloudPackageChecker compares a list of packages the user `subscribed` to in their account @@ -68,7 +68,7 @@ class SyncOrchestrator(Extension): def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None: """Called when a set of packages have finished downloading - + :param success_items:: Dict[package_id, Dict[str, str]] :param error_items:: List[package_id] """ diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py index 4cbabde07e..85811a9eb4 100644 --- a/plugins/Toolbox/src/PackagesModel.py +++ b/plugins/Toolbox/src/PackagesModel.py @@ -137,7 +137,7 @@ class PackagesModel(ListModel): def setFilter(self, filter_dict: Dict[str, str]) -> None: """Set the filter of this model based on a string. - + :param filter_dict: Dictionary to do the filtering by. """ if filter_dict != self._filter: diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 632af456b5..0ad9f7c89c 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -315,7 +315,7 @@ class Toolbox(QObject, Extension): @pyqtSlot(str) def checkPackageUsageAndUninstall(self, package_id: str) -> None: """Check package usage and uninstall - + If the package is in use, you'll get a confirmation dialog to set everything to default """ @@ -359,7 +359,7 @@ class Toolbox(QObject, Extension): @pyqtSlot() def resetMaterialsQualitiesAndUninstall(self) -> None: """Uses "uninstall variables" to reset qualities and materials, then uninstall - + It's used as an action on Confirm reset on Uninstall """ diff --git a/plugins/TrimeshReader/TrimeshReader.py b/plugins/TrimeshReader/TrimeshReader.py index f746c55cc5..cbec2e2482 100644 --- a/plugins/TrimeshReader/TrimeshReader.py +++ b/plugins/TrimeshReader/TrimeshReader.py @@ -83,7 +83,7 @@ class TrimeshReader(MeshReader): def _read(self, file_name: str) -> Union["SceneNode", List["SceneNode"]]: """Reads a file using Trimesh. - + :param file_name: The file path. This is assumed to be one of the file types that Trimesh can read. It will not be checked again. :return: A scene node that contains the file's contents. @@ -136,7 +136,7 @@ class TrimeshReader(MeshReader): def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData: """Converts a Trimesh to Uranium's MeshData. - + :param tri_node: A Trimesh containing the contents of a file that was just read. :param file_name: The full original filename used to watch for changes :return: Mesh data from the Trimesh in a way that Uranium can understand it. diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 939b9cce23..b8c5a30524 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -37,7 +37,7 @@ I18N_CATALOG = i18nCatalog("cura") class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): """The cloud output device is a network output device that works remotely but has limited functionality. - + Currently it only supports viewing the printer and print job status and adding a new job to the queue. As such, those methods have been implemented here. Note that this device represents a single remote cluster, not a list of multiple clusters. @@ -60,7 +60,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None: """Creates a new cloud output device - + :param api_client: The client that will run the API calls :param cluster: The device response received from the cloud API. :param parent: The optional parent of this output device. @@ -165,7 +165,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): def _onStatusCallFinished(self, status: CloudClusterStatus) -> None: """Method called when HTTP request to status endpoint is finished. - + Contains both printers and print jobs statuses in a single response. """ self._responseReceived() @@ -202,7 +202,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): def _onPrintJobCreated(self, job: ExportFileJob) -> None: """Handler for when the print job was created locally. - + It can now be sent over the cloud. """ output = job.getOutput() @@ -217,7 +217,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): def _uploadPrintJob(self, job_response: CloudPrintJobResponse) -> None: """Uploads the mesh when the print job was registered with the cloud API. - + :param job_response: The response received from the cloud API. """ if not self._tool_path: @@ -236,7 +236,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None: """Shows a message when the upload has succeeded - + :param response: The response from the cloud API. """ self._progress.hide() @@ -245,7 +245,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): def _onUploadError(self, message: str = None) -> None: """Displays the given message if uploading the mesh has failed - + :param message: The message to display. """ self._progress.hide() diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 84698ff371..fa1271ab66 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -22,7 +22,7 @@ from ..Models.Http.CloudClusterResponse import CloudClusterResponse class CloudOutputDeviceManager: """The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters. - + Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code. API spec is available on https://api.ultimaker.com/docs/connect/spec/. """ diff --git a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py index 0287d72eb6..7fc1b4a7d3 100644 --- a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py +++ b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py @@ -32,7 +32,7 @@ class MeshFormatHandler: @property def preferred_format(self) -> Dict[str, Union[str, int, bool]]: """Chooses the preferred file format. - + :return: A dict with the file format details, with the following keys: {id: str, extension: str, description: str, mime_type: str, mode: int, hide_in_file_dialog: bool} """ @@ -41,7 +41,7 @@ class MeshFormatHandler: @property def writer(self) -> Optional[FileWriter]: """Gets the file writer for the given file handler and mime type. - + :return: A file writer. """ return self._writer @@ -84,7 +84,7 @@ class MeshFormatHandler: def _getPreferredFormat(self, firmware_version: str) -> Dict[str, Union[str, int, bool]]: """Chooses the preferred file format for the given file handler. - + :param firmware_version: The version of the firmware. :return: A dict with the file format details. """ @@ -121,7 +121,7 @@ class MeshFormatHandler: def _getWriter(self, mime_type: str) -> Optional[FileWriter]: """Gets the file writer for the given file handler and mime type. - + :param mime_type: The mine type. :return: A file writer. """ diff --git a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py index b7ddf7f550..63fa037890 100644 --- a/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py +++ b/plugins/UM3NetworkPrinting/src/Messages/PrintJobUploadProgressMessage.py @@ -28,7 +28,7 @@ class PrintJobUploadProgressMessage(Message): def update(self, percentage: int) -> None: """Updates the percentage of the uploaded. - + :param percentage: The percentage amount (0-100). """ if not self._visible: diff --git a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py index f1385a0270..92d7246489 100644 --- a/plugins/UM3NetworkPrinting/src/Models/BaseModel.py +++ b/plugins/UM3NetworkPrinting/src/Models/BaseModel.py @@ -20,7 +20,7 @@ class BaseModel: def __eq__(self, other): """Checks whether the two models are equal. - + :param other: The other model. :return: True if they are equal, False if they are different. """ @@ -28,7 +28,7 @@ class BaseModel: def __ne__(self, other) -> bool: """Checks whether the two models are different. - + :param other: The other model. :return: True if they are different, False if they are the same. """ @@ -42,7 +42,7 @@ class BaseModel: @staticmethod def parseModel(model_class: Type[T], values: Union[T, Dict[str, Any]]) -> T: """Parses a single model. - + :param model_class: The model class. :param values: The value of the model, which is usually a dictionary, but may also be already parsed. :return: An instance of the model_class given. @@ -54,7 +54,7 @@ class BaseModel: @classmethod def parseModels(cls, model_class: Type[T], values: List[Union[T, Dict[str, Any]]]) -> List[T]: """Parses a list of models. - + :param model_class: The model class. :param values: The value of the list. Each value is usually a dictionary, but may also be already parsed. :return: A list of instances of the model_class given. @@ -64,7 +64,7 @@ class BaseModel: @staticmethod def parseDate(date: Union[str, datetime]) -> datetime: """Parses the given date string. - + :param date: The date to parse. :return: The parsed date. """ diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py index a108b8dc87..94f20b65c6 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterResponse.py @@ -13,7 +13,7 @@ class CloudClusterResponse(BaseModel): host_internal_ip: Optional[str] = None, host_version: Optional[str] = None, friendly_name: Optional[str] = None, printer_type: str = "ultimaker3", **kwargs) -> None: """Creates a new cluster response object. - + :param cluster_id: The secret unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. :param host_guid: The unique identifier of the print cluster host, e.g. 'e90ae0ac-1257-4403-91ee-a44c9b7e8050'. :param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users. diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py index 10f7b0ce6b..5cd151d8ef 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudClusterStatus.py @@ -16,7 +16,7 @@ class CloudClusterStatus(BaseModel): generated_time: Union[str, datetime], **kwargs) -> None: """Creates a new cluster status model object. - + :param printers: The latest status of each printer in the cluster. :param print_jobs: The latest status of each print job in the cluster. :param generated_time: The datetime when the object was generated on the server-side. diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py index 97e7862ff6..05f303e5c9 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudError.py @@ -11,7 +11,7 @@ class CloudError(BaseModel): def __init__(self, id: str, code: str, title: str, http_status: str, detail: Optional[str] = None, meta: Optional[Dict[str, Any]] = None, **kwargs) -> None: """Creates a new error object. - + :param id: Unique identifier for this particular occurrence of the problem. :param title: A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization. diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py index ccc9ffb2fc..83cbb5a030 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobResponse.py @@ -12,7 +12,7 @@ class CloudPrintJobResponse(BaseModel): upload_url: Optional[str] = None, content_type: Optional[str] = None, status_description: Optional[str] = None, slicing_details: Optional[dict] = None, **kwargs) -> None: """Creates a new print job response model. - + :param job_id: The job unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='. :param status: The status of the print job. :param status_description: Contains more details about the status, e.g. the cause of failures. diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py index efa1efb7e4..7e108027d9 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintJobUploadRequest.py @@ -8,7 +8,7 @@ class CloudPrintJobUploadRequest(BaseModel): def __init__(self, job_name: str, file_size: int, content_type: str, **kwargs) -> None: """Creates a new print job upload request. - + :param job_name: The name of the print job. :param file_size: The size of the file in bytes. :param content_type: The content type of the print job (e.g. text/plain or application/gzip) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py index ee2b8a307f..f8b3210210 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/CloudPrintResponse.py @@ -12,7 +12,7 @@ class CloudPrintResponse(BaseModel): def __init__(self, job_id: str, status: str, generated_time: Union[str, datetime], cluster_job_id: Optional[str] = None, **kwargs) -> None: """Creates a new print response object. - + :param job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect. :param status: The status of the print request (queued or failed). :param generated_time: The datetime when the object was generated on the server-side. diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py index c81e0a372c..771389e102 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterBuildPlate.py @@ -8,7 +8,7 @@ class ClusterBuildPlate(BaseModel): def __init__(self, type: str = "glass", **kwargs) -> None: """Create a new build plate - + :param type: The type of build plate glass or aluminium """ self.type = type diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py index 75ce234e23..529f3928fd 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py @@ -11,14 +11,14 @@ from ..BaseModel import BaseModel class ClusterPrintCoreConfiguration(BaseModel): """Class representing a cloud cluster printer configuration - + Also used for representing slots in a Material Station (as from Cura's perspective these are the same). """ def __init__(self, extruder_index: int, material: Union[None, Dict[str, Any], ClusterPrinterConfigurationMaterial] = None, print_core_id: Optional[str] = None, **kwargs) -> None: """Creates a new cloud cluster printer configuration object - + :param extruder_index: The position of the extruder on the machine as list index. Numbered from left to right. :param material: The material of a configuration object in a cluster printer. May be in a dict or an object. :param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'. @@ -32,7 +32,7 @@ class ClusterPrintCoreConfiguration(BaseModel): def updateOutputModel(self, model: ExtruderOutputModel) -> None: """Updates the given output model. - + :param model: The output model to update. """ diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py index cdfa633170..0c83cd1b31 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConfigurationChange.py @@ -12,7 +12,7 @@ class ClusterPrintJobConfigurationChange(BaseModel): def __init__(self, type_of_change: str, target_id: str, origin_id: str, index: Optional[int] = None, target_name: Optional[str] = None, origin_name: Optional[str] = None, **kwargs) -> None: """Creates a new print job constraint. - + :param type_of_change: The type of configuration change, one of: "material", "print_core_change" :param index: The hotend slot or extruder index to change :param target_id: Target material guid or hotend id diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py index 258d940a03..5271130dd6 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobConstraint.py @@ -10,7 +10,7 @@ class ClusterPrintJobConstraints(BaseModel): def __init__(self, require_printer_name: Optional[str] = None, **kwargs) -> None: """Creates a new print job constraint. - + :param require_printer_name: Unique name of the printer that this job should be printed on. Should be one of the unique_name field values in the cluster, e.g. 'ultimakersystem-ccbdd30044ec' """ diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py index 7beaf6f61f..3c9e03223a 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobImpediment.py @@ -8,7 +8,7 @@ class ClusterPrintJobImpediment(BaseModel): def __init__(self, translation_key: str, severity: int, **kwargs) -> None: """Creates a new print job constraint. - + :param translation_key: A string indicating a reason the print cannot be printed, such as 'does_not_fit_in_build_volume' :param severity: A number indicating the severity of the problem, with higher being more severe diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py index 9fb94ab12f..8c8b187288 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py @@ -35,7 +35,7 @@ class ClusterPrintJobStatus(BaseModel): **kwargs) -> None: """Creates a new cloud print job status model. - + :param assigned_to: The name of the printer this job is assigned to while being queued. :param configuration: The required print core configurations of this print job. :param constraints: Print job constraints object. @@ -96,7 +96,7 @@ class ClusterPrintJobStatus(BaseModel): def createOutputModel(self, controller: ClusterOutputController) -> UM3PrintJobOutputModel: """Creates an UM3 print job output model based on this cloud cluster print job. - + :param printer: The output model of the printer """ @@ -115,7 +115,7 @@ class ClusterPrintJobStatus(BaseModel): def updateOutputModel(self, model: UM3PrintJobOutputModel) -> None: """Updates an UM3 print job output model based on this cloud cluster print job. - + :param model: The model to update. """ diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py index 62f99293d1..5d55f2f16b 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterConfigurationMaterial.py @@ -16,7 +16,7 @@ class ClusterPrinterConfigurationMaterial(BaseModel): material: Optional[str] = None, **kwargs) -> None: """Creates a new material configuration model. - + :param brand: The brand of material in this print core, e.g. 'Ultimaker'. :param color: The color of material in this print core, e.g. 'Blue'. :param guid: he GUID of the material in this print core, e.g. '506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9'. @@ -31,7 +31,7 @@ class ClusterPrinterConfigurationMaterial(BaseModel): def createOutputModel(self) -> MaterialOutputModel: """Creates a material output model based on this cloud printer material. - + A material is chosen that matches the current GUID. If multiple such materials are available, read-only materials are preferred and the material with the earliest alphabetical name will be selected. diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py index 1929c2a388..b03a2291c4 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py @@ -13,7 +13,7 @@ class ClusterPrinterMaterialStation(BaseModel): material_slots: List[Union[ClusterPrinterMaterialStationSlot, Dict[str, Any]]] = None, **kwargs) -> None: """Creates a new Material Station status. - + :param status: The status of the material station. :param: supported: Whether the material station is supported on this machine or not. :param material_slots: The active slots configurations of this material station. diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py index d41d6c14fc..11e2736ded 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py @@ -11,7 +11,7 @@ class ClusterPrinterMaterialStationSlot(ClusterPrintCoreConfiguration): def __init__(self, slot_index: int, compatible: bool, material_remaining: float, material_empty: Optional[bool] = False, **kwargs) -> None: """Create a new material station slot object. - + :param slot_index: The index of the slot in the material station (ranging 0 to 5). :param compatible: Whether the configuration is compatible with the print core. :param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data). diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 3d342a519d..5b4d7fb161 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -29,7 +29,7 @@ class ClusterPrinterStatus(BaseModel): build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None: """Creates a new cluster printer status - + :param enabled: A printer can be disabled if it should not receive new jobs. By default every printer is enabled. :param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster. :param friendly_name: Human readable name of the printer. Can be used for identification purposes. @@ -68,7 +68,7 @@ class ClusterPrinterStatus(BaseModel): def createOutputModel(self, controller: PrinterOutputController) -> PrinterOutputModel: """Creates a new output model. - + :param controller: - The controller of the model. """ @@ -82,7 +82,7 @@ class ClusterPrinterStatus(BaseModel): def updateOutputModel(self, model: PrinterOutputModel) -> None: """Updates the given output model. - + :param model: - The output model to update. """ @@ -121,7 +121,7 @@ class ClusterPrinterStatus(BaseModel): def _getSlotsForExtruder(self, extruder_index: int) -> List[ClusterPrinterMaterialStationSlot]: """Create a list of Material Station slots for the given extruder index. - + Returns a list with a single empty material slot if none are found to ensure we don't miss configurations. """ @@ -136,7 +136,7 @@ class ClusterPrinterStatus(BaseModel): @staticmethod def _isSupportedConfiguration(slot: ClusterPrinterMaterialStationSlot, extruder_index: int) -> bool: """Check if a configuration is supported in order to make it selectable by the user. - + We filter out any slot that is not supported by the extruder index, print core type or if the material is empty. """ diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index 7d6b260b90..d1840bf90c 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -32,7 +32,7 @@ class ClusterApiClient: def __init__(self, address: str, on_error: Callable) -> None: """Initializes a new cluster API client. - + :param address: The network address of the cluster to call. :param on_error: The callback to be called whenever we receive errors from the server. """ @@ -43,7 +43,7 @@ class ClusterApiClient: def getSystem(self, on_finished: Callable) -> None: """Get printer system information. - + :param on_finished: The callback in case the response is successful. """ url = "{}/system".format(self.PRINTER_API_PREFIX) @@ -52,7 +52,7 @@ class ClusterApiClient: def getMaterials(self, on_finished: Callable[[List[ClusterMaterial]], Any]) -> None: """Get the installed materials on the printer. - + :param on_finished: The callback in case the response is successful. """ url = "{}/materials".format(self.CLUSTER_API_PREFIX) @@ -61,7 +61,7 @@ class ClusterApiClient: def getPrinters(self, on_finished: Callable[[List[ClusterPrinterStatus]], Any]) -> None: """Get the printers in the cluster. - + :param on_finished: The callback in case the response is successful. """ url = "{}/printers".format(self.CLUSTER_API_PREFIX) @@ -70,7 +70,7 @@ class ClusterApiClient: def getPrintJobs(self, on_finished: Callable[[List[ClusterPrintJobStatus]], Any]) -> None: """Get the print jobs in the cluster. - + :param on_finished: The callback in case the response is successful. """ url = "{}/print_jobs".format(self.CLUSTER_API_PREFIX) @@ -112,7 +112,7 @@ class ClusterApiClient: def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest: """We override _createEmptyRequest in order to add the user credentials. - + :param url: The URL to request :param content_type: The type of the body contents. """ @@ -126,7 +126,7 @@ class ClusterApiClient: @staticmethod def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]: """Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well. - + :param reply: The reply from the server. :return: A tuple with a status code and a dictionary. """ @@ -141,7 +141,7 @@ class ClusterApiClient: def _parseModels(self, response: Dict[str, Any], on_finished: Union[Callable[[ClusterApiClientModel], Any], Callable[[List[ClusterApiClientModel]], Any]], model_class: Type[ClusterApiClientModel]) -> None: """Parses the given models and calls the correct callback depending on the result. - + :param response: The response from the server, after being converted to a dict. :param on_finished: The callback in case the response is successful. :param model_class: The type of the model to convert the response to. It may either be a single record or a list. @@ -163,7 +163,7 @@ class ClusterApiClient: Callable[[List[ClusterApiClientModel]], Any]], model: Type[ClusterApiClientModel] = None, ) -> None: """Creates a callback function so that it includes the parsing of the response into the correct model. - + The callback is added to the 'finished' signal of the reply. :param reply: The reply that should be listened to. :param on_finished: The callback in case the response is successful. diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index ea120f8978..48e552241a 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -98,7 +98,7 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): def setJobState(self, print_job_uuid: str, action: str) -> None: """Set the remote print job state. - + :param print_job_uuid: The UUID of the print job to set the state for. :param action: The action to undertake ('pause', 'resume', 'abort'). """ @@ -118,7 +118,7 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): def sendMaterialProfiles(self) -> None: """Sync the material profiles in Cura with the printer. - + This gets called when connecting to a printer as well as when sending a print. """ job = SendMaterialJob(device = self) @@ -143,14 +143,14 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): @pyqtSlot(str, name="selectTargetPrinter") def selectTargetPrinter(self, unique_name: str = "") -> None: """Allows the user to choose a printer to print with from the printer selection dialogue. - + :param unique_name: The unique name of the printer to target. """ self._startPrintJobUpload(unique_name if unique_name != "" else None) def _onPrintJobCreated(self, job: ExportFileJob) -> None: """Handler for when the print job was created locally. - + It can now be sent over the network. """ @@ -208,7 +208,7 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): def _onUploadError(self, message: str = None) -> None: """Displays the given message if uploading the mesh has failed - + :param message: The message to display. """ diff --git a/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py index 90aa68eedb..2740f86605 100644 --- a/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py +++ b/plugins/UM3NetworkPrinting/src/Network/SendMaterialJob.py @@ -18,7 +18,7 @@ if TYPE_CHECKING: class SendMaterialJob(Job): """Asynchronous job to send material profiles to the printer. - + This way it won't freeze up the interface while sending those materials. """ @@ -40,7 +40,7 @@ class SendMaterialJob(Job): def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None: """Determine which materials should be updated and send them to the printer. - + :param remote_materials_by_guid: The remote materials by GUID. """ local_materials_by_guid = self._getLocalMaterials() @@ -57,7 +57,7 @@ class SendMaterialJob(Job): def _determineMaterialsToSend(local_materials: Dict[str, LocalMaterial], remote_materials: Dict[str, ClusterMaterial]) -> Set[str]: """From the local and remote materials, determine which ones should be synchronized. - + Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that are newer in Cura. :param local_materials: The local materials by GUID. @@ -72,7 +72,7 @@ class SendMaterialJob(Job): def _sendMaterials(self, materials_to_send: Set[str]) -> None: """Send the materials to the printer. - + The given materials will be loaded from disk en sent to to printer. The given id's will be matched with filenames of the locally stored materials. :param materials_to_send: A set with id's of materials that must be sent. @@ -97,7 +97,7 @@ class SendMaterialJob(Job): def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None: """Send a single material file to the printer. - + Also add the material signature file if that is available. :param file_path: The path of the material file. :param file_name: The name of the material file. @@ -143,7 +143,7 @@ class SendMaterialJob(Job): @staticmethod def _getLocalMaterials() -> Dict[str, LocalMaterial]: """Retrieves a list of local materials - + Only the new newest version of the local materials is returned :return: a dictionary of LocalMaterial objects by GUID """ diff --git a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py index b41cd7d151..d59f2f2893 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ZeroConfClient.py @@ -14,7 +14,7 @@ from cura.CuraApplication import CuraApplication class ZeroConfClient: """The ZeroConfClient handles all network discovery logic. - + It emits signals when new network services were found or disappeared. """ @@ -34,7 +34,7 @@ class ZeroConfClient: def start(self) -> None: """The ZeroConf service changed requests are handled in a separate thread so we don't block the UI. - + We can also re-schedule the requests when they fail to get detailed service info. Any new or re-reschedule requests will be appended to the request queue and the thread will process them. """ @@ -108,7 +108,7 @@ class ZeroConfClient: def _onServiceChanged(self, zero_conf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange) -> bool: """Handler for zeroConf detection. - + Return True or False indicating if the process succeeded. Note that this function can take over 3 seconds to complete. Be careful calling it from the main thread. """ diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 72b2da33cc..44dbfbb5f5 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -69,7 +69,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): """Remove a manually connected networked printer.""" self._network_output_device_manager.removeManualDevice(key, address) - + def getDiscoveredDevices(self) -> Dict[str, LocalClusterOutputDevice]: """Get the discovered devices from the local network.""" diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py index dd3f3939ed..772a9d1973 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py @@ -17,7 +17,7 @@ I18N_CATALOG = i18nCatalog("cura") class UltimakerNetworkedPrinterAction(MachineAction): """Machine action that allows to connect the active machine to a networked devices. - + TODO: in the future this should be part of the new discovery workflow baked into Cura. """ diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 8090177b83..13aa0d7063 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -24,7 +24,7 @@ from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): """Output device class that forms the basis of Ultimaker networked printer output devices. - + Currently used for local networking and cloud printing using Ultimaker Connect. This base class primarily contains all the Qt properties and slots needed for the monitor page to work. """ @@ -218,7 +218,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): def _checkStillConnected(self) -> None: """Check if we're still connected by comparing the last timestamps for network response and the current time. - + This implementation is similar to the base NetworkedPrinterOutputDevice, but is tweaked slightly. Re-connecting is handled automatically by the output device managers in this plugin. TODO: it would be nice to have this logic in the managers, but connecting those with signals causes crashes. @@ -233,7 +233,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): def _reconnectForActiveMachine(self) -> None: """Reconnect for the active output device. - + Does nothing if the device is not meant for the active machine. """ active_machine = CuraApplication.getInstance().getGlobalContainerStack() @@ -301,7 +301,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): def _updatePrintJobs(self, remote_jobs: List[ClusterPrintJobStatus]) -> None: """Updates the local list of print jobs with the list received from the cluster. - + :param remote_jobs: The print jobs received from the cluster. """ self._responseReceived() @@ -336,7 +336,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): def _createPrintJobModel(self, remote_job: ClusterPrintJobStatus) -> UM3PrintJobOutputModel: """Create a new print job model based on the remote status of the job. - + :param remote_job: The remote print job data. """ model = remote_job.createOutputModel(ClusterOutputController(self)) diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 2e52a89713..bfc5385a81 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -118,7 +118,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None: """Request the current scene to be sent to a USB-connected printer. - + :param nodes: A collection of scene nodes to send. This is ignored. :param file_name: A suggestion for a file name to write. :param filter_by_machine: Whether to filter MIME types by machine. This @@ -148,7 +148,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): def _printGCode(self, gcode: str): """Start a print based on a g-code. - + :param gcode: The g-code to print. """ self._gcode.clear() diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index 3ee43e071e..c5a017db7f 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -110,7 +110,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): def getSerialPortList(self, only_list_usb = False): """Create a list of serial ports on the system. - + :param only_list_usb: If true, only usb ports are listed """ base_list = [] diff --git a/plugins/USBPrinting/avr_isp/intelHex.py b/plugins/USBPrinting/avr_isp/intelHex.py index 671f1788f7..fc410882dd 100644 --- a/plugins/USBPrinting/avr_isp/intelHex.py +++ b/plugins/USBPrinting/avr_isp/intelHex.py @@ -31,7 +31,7 @@ def readHex(filename): check_sum &= 0xFF if check_sum != 0: raise Exception("Checksum error in hex file: " + line) - + if rec_type == 0:#Data record while len(data) < addr + rec_len: data.append(0) diff --git a/plugins/USBPrinting/avr_isp/ispBase.py b/plugins/USBPrinting/avr_isp/ispBase.py index 077cfc36e6..9ba3a65b47 100644 --- a/plugins/USBPrinting/avr_isp/ispBase.py +++ b/plugins/USBPrinting/avr_isp/ispBase.py @@ -22,7 +22,7 @@ class IspBase(): if not self.chip: raise IspError("Chip with signature: " + str(self.getSignature()) + "not found") self.chipErase() - + Logger.log("d", "Flashing %i bytes", len(flash_data)) self.writeFlash(flash_data) Logger.log("d", "Verifying %i bytes", len(flash_data)) diff --git a/plugins/USBPrinting/avr_isp/stk500v2.py b/plugins/USBPrinting/avr_isp/stk500v2.py index dbfc8dc756..2b1a86155c 100644 --- a/plugins/USBPrinting/avr_isp/stk500v2.py +++ b/plugins/USBPrinting/avr_isp/stk500v2.py @@ -56,7 +56,7 @@ class Stk500v2(ispBase.IspBase): self.close() raise self.serial.timeout = 5 - + def close(self): if self.serial is not None: self.serial.close() diff --git a/plugins/UltimakerMachineActions/BedLevelMachineAction.py b/plugins/UltimakerMachineActions/BedLevelMachineAction.py index 7d2eb74f84..f76e0c6746 100644 --- a/plugins/UltimakerMachineActions/BedLevelMachineAction.py +++ b/plugins/UltimakerMachineActions/BedLevelMachineAction.py @@ -16,7 +16,7 @@ catalog = i18nCatalog("cura") class BedLevelMachineAction(MachineAction): """A simple action to handle manual bed leveling procedure for printers that don't have it on the firmware. - + This is currently only used by the Ultimaker Original+ """ diff --git a/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py b/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py index 39d02c078c..0741312011 100644 --- a/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py +++ b/plugins/VersionUpgrade/VersionUpgrade41to42/VersionUpgrade41to42.py @@ -216,13 +216,13 @@ _creality_limited_quality_type = { class VersionUpgrade41to42(VersionUpgrade): """Upgrades configurations from the state they were in at version 4.1 to the - + state they should be in at version 4.2. """ def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: """Upgrades instance containers to have the new version number. - + This renames the renamed settings in the containers. """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) @@ -262,7 +262,7 @@ class VersionUpgrade41to42(VersionUpgrade): def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: """Upgrades Preferences to have the new version number. - + This renames the renamed settings in the list of visible settings. """ diff --git a/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py b/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py index 7359e88179..73d6578c9b 100644 --- a/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py +++ b/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py @@ -59,7 +59,7 @@ _renamed_settings = { class VersionUpgrade42to43(VersionUpgrade): """Upgrades configurations from the state they were in at version 4.2 to the - + state they should be in at version 4.3. """ def upgradePreferences(self, serialized: str, filename: str): @@ -79,14 +79,14 @@ class VersionUpgrade42to43(VersionUpgrade): parser["general"]["visible_settings"] = ";".join(all_setting_keys) parser["metadata"]["setting_version"] = "9" - + result = io.StringIO() parser.write(result) return [filename], [result.getvalue()] def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: """Upgrades instance containers to have the new version number. - + This renames the renamed settings in the containers. """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) diff --git a/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py b/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py index 249f55a42c..ec0a767105 100644 --- a/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py +++ b/plugins/VersionUpgrade/VersionUpgrade43to44/VersionUpgrade43to44.py @@ -29,7 +29,7 @@ class VersionUpgrade43to44(VersionUpgrade): def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: """Upgrades Preferences to have the new version number. - + This renames the renamed settings in the list of visible settings. """ @@ -45,7 +45,7 @@ class VersionUpgrade43to44(VersionUpgrade): def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: """Upgrades instance containers to have the new version number. - + This renames the renamed settings in the containers. """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) diff --git a/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py b/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py index df4f95d74d..00faa216eb 100644 --- a/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py +++ b/plugins/VersionUpgrade/VersionUpgrade44to45/VersionUpgrade44to45.py @@ -124,7 +124,7 @@ class VersionUpgrade44to45(VersionUpgrade): def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: """Upgrades Preferences to have the new version number. - + This renames the renamed settings in the list of visible settings. """ @@ -140,7 +140,7 @@ class VersionUpgrade44to45(VersionUpgrade): def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]: """Upgrades instance containers to have the new version number. - + This renames the renamed settings in the containers. """ parser = configparser.ConfigParser(interpolation = None, comment_prefixes = ()) diff --git a/plugins/XmlMaterialProfile/XmlMaterialProfile.py b/plugins/XmlMaterialProfile/XmlMaterialProfile.py index 0b21b57fef..7978c0cdba 100644 --- a/plugins/XmlMaterialProfile/XmlMaterialProfile.py +++ b/plugins/XmlMaterialProfile/XmlMaterialProfile.py @@ -38,11 +38,11 @@ class XmlMaterialProfile(InstanceContainer): @staticmethod def xmlVersionToSettingVersion(xml_version: str) -> int: """Translates the version number in the XML files to the setting_version metadata entry. - + Since the two may increment independently we need a way to say which versions of the XML specification are compatible with our setting data version numbers. - + :param xml_version: The version number found in an XML file. :return: The corresponding setting_version. """ @@ -56,7 +56,7 @@ class XmlMaterialProfile(InstanceContainer): def setMetaDataEntry(self, key, value, apply_to_all = True): """set the meta data for all machine / variant combinations - + The "apply_to_all" flag indicates whether this piece of metadata should be applied to all material containers or just this specific container. For example, when you change the material name, you want to apply it to all its derived containers, but for @@ -95,7 +95,7 @@ class XmlMaterialProfile(InstanceContainer): def setName(self, new_name): """Overridden from InstanceContainer, similar to setMetaDataEntry. - + without this function the setName would only set the name of the specific nozzle / material / machine combination container The function is a bit tricky. It will not set the name of all containers if it has the correct name itself. """ @@ -130,7 +130,7 @@ class XmlMaterialProfile(InstanceContainer): def serialize(self, ignored_metadata_keys: Optional[Set[str]] = None): """Overridden from InstanceContainer - + base file: common settings + supported machines machine / variant combination: only changes for itself. """ @@ -1075,7 +1075,7 @@ class XmlMaterialProfile(InstanceContainer): @classmethod def getProductIdMap(cls) -> Dict[str, List[str]]: """Gets a mapping from product names in the XML files to their definition IDs. - + This loads the mapping from a file. """ diff --git a/scripts/check_gcode_buffer.py b/scripts/check_gcode_buffer.py index f35dd20c24..321b2439c0 100755 --- a/scripts/check_gcode_buffer.py +++ b/scripts/check_gcode_buffer.py @@ -89,7 +89,7 @@ def calc_acceleration_time_from_distance(initial_feedrate: float, distance: floa def calc_intersection_distance(initial_feedrate: float, final_feedrate: float, acceleration: float, distance: float) -> float: """Calculates the point at which you must start braking. - + This gives the distance from the start of a line at which you must start decelerating (at a rate of `-acceleration`) if you started at speed `initial_feedrate` and accelerated until this point and want to end at the @@ -145,7 +145,7 @@ class Command: def calculate_trapezoid(self, entry_factor, exit_factor): """Calculate the velocity-time trapezoid function for this move. - + Each move has a three-part function mapping time to velocity. """ diff --git a/scripts/lionbridge_import.py b/scripts/lionbridge_import.py index 0bd6c5a8af..0a7b63e9ac 100644 --- a/scripts/lionbridge_import.py +++ b/scripts/lionbridge_import.py @@ -11,7 +11,7 @@ uranium_files = {"uranium"} def lionbridge_import(source: str) -> None: """Imports translation files from Lionbridge. - + Lionbridge has a bit of a weird export feature. It exports it to the same file type as what we imported, so that's a .pot file. However this .pot file only contains the translations, so the header is completely empty. We need @@ -48,7 +48,7 @@ def lionbridge_import(source: str) -> None: def destination_cura() -> str: """Gets the destination path to copy the translations for Cura to. - + :return: Destination path for Cura. """ return os.path.abspath(os.path.join(__file__, "..", "..", "resources", "i18n")) @@ -56,7 +56,7 @@ def destination_cura() -> str: def destination_uranium() -> str: """Gets the destination path to copy the translations for Uranium to. - + :return: Destination path for Uranium. """ try: @@ -75,7 +75,7 @@ def destination_uranium() -> str: def merge(source: str, destination: str) -> str: """Merges translations from the source file into the destination file if they - + were missing in the destination file. :param source: The contents of the source .po file. :param destination: The contents of the destination .po file. @@ -133,7 +133,7 @@ def merge(source: str, destination: str) -> str: def find_translation(source: str, msgctxt: str, msgid: str) -> str: """Finds a translation in the source file. - + :param source: The contents of the source .po file. :param msgctxt: The ctxt of the translation to find. :param msgid: The id of the translation to find. diff --git a/tests/Machines/TestMachineNode.py b/tests/Machines/TestMachineNode.py index 3b0822770b..cb1d4005ec 100644 --- a/tests/Machines/TestMachineNode.py +++ b/tests/Machines/TestMachineNode.py @@ -29,7 +29,7 @@ def container_registry(): @pytest.fixture def empty_machine_node(): """Creates a machine node without anything underneath it. No sub-nodes. - + For testing stuff with machine nodes without testing _loadAll(). You'll need to add subnodes manually in your test. """ @@ -82,7 +82,7 @@ def test_metadataProperties(container_registry): def test_getQualityGroupsBothExtrudersAvailable(empty_machine_node): """Test getting quality groups when there are quality profiles available for - + the requested configurations on two extruders. """ @@ -130,7 +130,7 @@ def test_getQualityGroupsBothExtrudersAvailable(empty_machine_node): def test_getQualityGroupsAvailability(empty_machine_node): """Test the "is_available" flag on quality groups. - + If a profile is available for a quality type on an extruder but not on all extruders, there should be a quality group for it but it should not be made available. diff --git a/tests/Machines/TestVariantNode.py b/tests/Machines/TestVariantNode.py index 96084001c1..7f06ad468c 100644 --- a/tests/Machines/TestVariantNode.py +++ b/tests/Machines/TestVariantNode.py @@ -52,7 +52,7 @@ def machine_node(): @pytest.fixture def empty_variant_node(machine_node): """Constructs a variant node without any subnodes. - + This is useful for performing tests on VariantNode without being dependent on how _loadAll works. """ @@ -137,7 +137,7 @@ def test_materialAdded_update(container_registry, machine_node, metadata, change def test_preferredMaterialExactMatch(empty_variant_node): """Tests the preferred material when the exact base file is available in the - + materials list for this node. """ empty_variant_node.materials = { @@ -151,7 +151,7 @@ def test_preferredMaterialExactMatch(empty_variant_node): def test_preferredMaterialSubmaterial(empty_variant_node): """Tests the preferred material when a submaterial is available in the - + materials list for this node. """ empty_variant_node.materials = { diff --git a/tests/Settings/MockContainer.py b/tests/Settings/MockContainer.py index 8bb570bae1..bb99710ef6 100644 --- a/tests/Settings/MockContainer.py +++ b/tests/Settings/MockContainer.py @@ -7,7 +7,7 @@ from UM.Signal import Signal class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): """Fake container class to add to the container registry. - + This allows us to test the container registry without testing the container class. If something is wrong in the container class it won't influence this test. @@ -15,7 +15,7 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): def __init__(self, metadata = None): """Initialise a new definition container. - + The container will have the specified ID and all metadata in the provided dictionary. """ @@ -29,7 +29,7 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): def getId(self): """Gets the ID that was provided at initialisation. - + :return: The ID of the container. """ @@ -37,10 +37,10 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): def getMetaData(self): """Gets all metadata of this container. - + This returns the metadata dictionary that was provided in the constructor of this mock container. - + :return: The metadata for this container. """ @@ -48,7 +48,7 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): def getMetaDataEntry(self, entry, default = None): """Gets a metadata entry from the metadata dictionary. - + :param key: The key of the metadata entry. :return: The value of the metadata entry, or None if there is no such entry. @@ -60,7 +60,7 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): def getName(self): """Gets a human-readable name for this container. - + :return: The name from the metadata, or "MockContainer" if there was no name provided. """ @@ -69,14 +69,14 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): @property def isEnabled(self): """Get whether a container stack is enabled or not. - + :return: Always returns True. """ return True def isReadOnly(self): """Get whether the container item is stored on a read only location in the filesystem. - + :return: Always returns False """ @@ -110,9 +110,9 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): def getValue(self, key): """Get the value of a container item. - + Since this mock container cannot contain any items, it always returns None. - + :return: Always returns None. """ @@ -120,7 +120,7 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): def hasProperty(self, key, property_name): """Get whether the container item has a specific property. - + This method is not implemented in the mock container. """ @@ -128,7 +128,7 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): def serialize(self, ignored_metadata_keys = None): """Serializes the container to a string representation. - + This method is not implemented in the mock container. """ @@ -136,7 +136,7 @@ class MockContainer(ContainerInterface, UM.PluginObject.PluginObject): def deserialize(self, serialized, file_name: Optional[str] = None): """Deserializes the container from a string representation. - + This method is not implemented in the mock container. """ diff --git a/tests/Settings/TestDefinitionContainer.py b/tests/Settings/TestDefinitionContainer.py index f2a9c0d245..8622db26ee 100644 --- a/tests/Settings/TestDefinitionContainer.py +++ b/tests/Settings/TestDefinitionContainer.py @@ -89,7 +89,7 @@ def assertIsDefinitionValid(definition_container, file_path): @pytest.mark.parametrize("file_path", definition_filepaths) def test_validateOverridingDefaultValue(file_path: str): """Tests whether setting values are not being hidden by parent containers. - + When a definition container defines a "default_value" but inherits from a definition that defines a "value", the "default_value" is ineffective. This test fails on those things. @@ -135,7 +135,7 @@ def getInheritedSettings(definition_id: str) -> Dict[str, Any]: def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]: """Put all settings in the main dictionary rather than in children dicts. - + :param settings: Nested settings. The keys are the setting IDs. The values are dictionaries of properties per setting, including the "children" property. :return: A dictionary of settings by key. Each setting is a dictionary of properties. @@ -152,7 +152,7 @@ def flattenSettings(settings: Dict[str, Any]) -> Dict[str, Any]: def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, Any]: """Make one dictionary override the other. Nested dictionaries override each - + other in the same way. :param base: A dictionary of settings that will get overridden by the other. :param overrides: A dictionary of settings that will override the other. @@ -176,7 +176,7 @@ def merge_dicts(base: Dict[str, Any], overrides: Dict[str, Any]) -> Dict[str, An @pytest.mark.parametrize("file_path", definition_filepaths) def test_noId(file_path: str): """Verifies that definition contains don't have an ID field. - + ID fields are legacy. They should not be used any more. This is legacy that people don't seem to be able to get used to. """ diff --git a/tests/Settings/TestExtruderStack.py b/tests/Settings/TestExtruderStack.py index 0af0f1dcf3..c5ecdea5c4 100644 --- a/tests/Settings/TestExtruderStack.py +++ b/tests/Settings/TestExtruderStack.py @@ -16,7 +16,7 @@ from cura.Settings.cura_empty_instance_containers import empty_container def getInstanceContainer(container_type) -> InstanceContainer: """Gets an instance container with a specified container type. - + :param container_type: The type metadata for the instance container. :return: An instance container instance. """ diff --git a/tests/Settings/TestGlobalStack.py b/tests/Settings/TestGlobalStack.py index 391013233d..3c509bf8c8 100755 --- a/tests/Settings/TestGlobalStack.py +++ b/tests/Settings/TestGlobalStack.py @@ -18,7 +18,7 @@ from cura.Settings.cura_empty_instance_containers import empty_container def getInstanceContainer(container_type) -> InstanceContainer: """Gets an instance container with a specified container type. - + :param container_type: The type metadata for the instance container. :return: An instance container instance. """ @@ -219,7 +219,7 @@ def test_constrainDefinitionValid(container, global_stack): def test_deserializeCompletesEmptyContainers(global_stack): """Tests whether deserialising completes the missing containers with empty ones. The initial containers are just the - + definition and the definition_changes (that cannot be empty after CURA-5281) """