From adf8285d20d4fd7236e1ab8835f2c7bd2ef1ed34 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Wed, 3 Oct 2018 16:36:58 +0200 Subject: [PATCH] Typing fixes Since I was stupid enough to touch it, I was also forced to boyscout the code. --- .../PostProcessingPlugin.py | 141 ++++++++++-------- plugins/PostProcessingPlugin/Script.py | 54 ++++--- 2 files changed, 115 insertions(+), 80 deletions(-) diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py index 78aa690106..1a1ea92d10 100644 --- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py +++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py @@ -2,6 +2,7 @@ # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot +from typing import Dict, Type, TYPE_CHECKING, List, Optional, cast from UM.PluginRegistry import PluginRegistry from UM.Resources import Resources @@ -9,55 +10,62 @@ from UM.Application import Application from UM.Extension import Extension from UM.Logger import Logger -import configparser #The script lists are stored in metadata as serialised config files. -import io #To allow configparser to write to a string. +import configparser # The script lists are stored in metadata as serialised config files. +import io # To allow configparser to write to a string. import os.path import pkgutil import sys import importlib.util from UM.i18n import i18nCatalog +from cura.CuraApplication import CuraApplication + i18n_catalog = i18nCatalog("cura") +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): - def __init__(self, parent = None): - super().__init__(parent) + def __init__(self, parent = None) -> None: + QObject.__init__(self, parent) + Extension.__init__(self) self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup) self._view = None # Loaded scripts are all scripts that can be used - self._loaded_scripts = {} - self._script_labels = {} + self._loaded_scripts = {} # type: Dict[str, Type[Script]] + self._script_labels = {} # type: Dict[str, str] # Script list contains instances of scripts in loaded_scripts. # There can be duplicates, which will be executed in sequence. - self._script_list = [] + self._script_list = [] # type: List[Script] self._selected_script_index = -1 Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute) - Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) #When the current printer changes, update the list of scripts. - Application.getInstance().mainWindowChanged.connect(self._createView) #When the main window is created, create the view so that we can display the post-processing icon if necessary. + Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) # When the current printer changes, update the list of scripts. + CuraApplication.getInstance().mainWindowChanged.connect(self._createView) # When the main window is created, create the view so that we can display the post-processing icon if necessary. selectedIndexChanged = pyqtSignal() - @pyqtProperty("QVariant", notify = selectedIndexChanged) - def selectedScriptDefinitionId(self): + + @pyqtProperty(str, notify = selectedIndexChanged) + def selectedScriptDefinitionId(self) -> Optional[str]: try: return self._script_list[self._selected_script_index].getDefinitionId() except: return "" - @pyqtProperty("QVariant", notify=selectedIndexChanged) - def selectedScriptStackId(self): + @pyqtProperty(str, notify=selectedIndexChanged) + def selectedScriptStackId(self) -> Optional[str]: try: return self._script_list[self._selected_script_index].getStackId() except: return "" ## Execute all post-processing scripts on the gcode. - def execute(self, output_device): + def execute(self, output_device) -> None: scene = Application.getInstance().getController().getScene() # If the scene does not have a gcode, do nothing if not hasattr(scene, "gcode_dict"): @@ -67,7 +75,7 @@ class PostProcessingPlugin(QObject, Extension): return # get gcode list for the active build plate - active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate + active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate gcode_list = gcode_dict[active_build_plate_id] if not gcode_list: return @@ -86,16 +94,17 @@ class PostProcessingPlugin(QObject, Extension): Logger.log("e", "Already post processed") @pyqtSlot(int) - def setSelectedScriptIndex(self, index): - self._selected_script_index = index - self.selectedIndexChanged.emit() + def setSelectedScriptIndex(self, index: int) -> None: + if self._selected_script_index != index: + self._selected_script_index = index + self.selectedIndexChanged.emit() @pyqtProperty(int, notify = selectedIndexChanged) - def selectedScriptIndex(self): + def selectedScriptIndex(self) -> int: return self._selected_script_index @pyqtSlot(int, int) - def moveScript(self, index, new_index): + def moveScript(self, index: int, new_index: int) -> None: if new_index < 0 or new_index > len(self._script_list) - 1: return # nothing needs to be done else: @@ -107,7 +116,7 @@ class PostProcessingPlugin(QObject, Extension): ## Remove a script from the active script list by index. @pyqtSlot(int) - def removeScriptByIndex(self, index): + def removeScriptByIndex(self, index: int) -> None: self._script_list.pop(index) if len(self._script_list) - 1 < self._selected_script_index: self._selected_script_index = len(self._script_list) - 1 @@ -118,14 +127,16 @@ class PostProcessingPlugin(QObject, Extension): ## Load all scripts from all paths where scripts can be found. # # This should probably only be done on init. - def loadAllScripts(self): - if self._loaded_scripts: #Already loaded. + def loadAllScripts(self) -> None: + if self._loaded_scripts: # Already loaded. return - #The PostProcessingPlugin path is for built-in scripts. - #The Resources path is where the user should store custom scripts. - #The Preferences path is legacy, where the user may previously have stored scripts. + # The PostProcessingPlugin path is for built-in scripts. + # The Resources path is where the user should store custom scripts. + # The Preferences path is legacy, where the user may previously have stored scripts. for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Resources), Resources.getStoragePath(Resources.Preferences)]: + if root is None: + continue path = os.path.join(root, "scripts") if not os.path.isdir(path): try: @@ -139,7 +150,7 @@ class PostProcessingPlugin(QObject, Extension): ## 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): + def loadScripts(self, path: str) -> None: ## Load all scripts in the scripts folders scripts = pkgutil.iter_modules(path = [path]) for loader, script_name, ispkg in scripts: @@ -148,6 +159,8 @@ class PostProcessingPlugin(QObject, Extension): try: spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py")) loaded_script = importlib.util.module_from_spec(spec) + if spec.loader is None: + continue spec.loader.exec_module(loaded_script) sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name? @@ -172,21 +185,21 @@ class PostProcessingPlugin(QObject, Extension): loadedScriptListChanged = pyqtSignal() @pyqtProperty("QVariantList", notify = loadedScriptListChanged) - def loadedScriptList(self): + def loadedScriptList(self) -> List[str]: return sorted(list(self._loaded_scripts.keys())) @pyqtSlot(str, result = str) - def getScriptLabelByKey(self, key): - return self._script_labels[key] + def getScriptLabelByKey(self, key: str) -> Optional[str]: + return self._script_labels.get(key) scriptListChanged = pyqtSignal() - @pyqtProperty("QVariantList", notify = scriptListChanged) - def scriptList(self): + @pyqtProperty("QStringList", notify = scriptListChanged) + def scriptList(self) -> List[str]: script_list = [script.getSettingData()["key"] for script in self._script_list] return script_list @pyqtSlot(str) - def addScriptToList(self, key): + def addScriptToList(self, key: str) -> None: Logger.log("d", "Adding script %s to list.", key) new_script = self._loaded_scripts[key]() new_script.initialize() @@ -197,82 +210,89 @@ class PostProcessingPlugin(QObject, Extension): ## When the global container stack is changed, swap out the list of active # scripts. - def _onGlobalContainerStackChanged(self): + def _onGlobalContainerStackChanged(self) -> None: self.loadAllScripts() new_stack = Application.getInstance().getGlobalContainerStack() + if new_stack is None: + return self._script_list.clear() - if not new_stack.getMetaDataEntry("post_processing_scripts"): #Missing or empty. - self.scriptListChanged.emit() #Even emit this if it didn't change. We want it to write the empty list to the stack's metadata. + if not new_stack.getMetaDataEntry("post_processing_scripts"): # Missing or empty. + self.scriptListChanged.emit() # Even emit this if it didn't change. We want it to write the empty list to the stack's metadata. return self._script_list.clear() scripts_list_strs = new_stack.getMetaDataEntry("post_processing_scripts") - for script_str in scripts_list_strs.split("\n"): #Encoded config files should never contain three newlines in a row. At most 2, just before section headers. - if not script_str: #There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here). + for script_str in scripts_list_strs.split("\n"): # Encoded config files should never contain three newlines in a row. At most 2, just before section headers. + if not script_str: # There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here). continue - script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") #Unescape escape sequences. + script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") # Unescape escape sequences. script_parser = configparser.ConfigParser(interpolation = None) - script_parser.optionxform = str #Don't transform the setting keys as they are case-sensitive. + script_parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive. script_parser.read_string(script_str) - for script_name, settings in script_parser.items(): #There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script. - if script_name == "DEFAULT": #ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one. + for script_name, settings in script_parser.items(): # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script. + if script_name == "DEFAULT": # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one. continue - if script_name not in self._loaded_scripts: #Don't know this post-processing plug-in. + if script_name not in self._loaded_scripts: # Don't know this post-processing plug-in. Logger.log("e", "Unknown post-processing script {script_name} was encountered in this global stack.".format(script_name = script_name)) continue new_script = self._loaded_scripts[script_name]() new_script.initialize() - for setting_key, setting_value in settings.items(): #Put all setting values into the script. - new_script._instance.setProperty(setting_key, "value", setting_value) + for setting_key, setting_value in settings.items(): # Put all setting values into the script. + if new_script._instance is not None: + new_script._instance.setProperty(setting_key, "value", setting_value) self._script_list.append(new_script) self.setSelectedScriptIndex(0) self.scriptListChanged.emit() @pyqtSlot() - def writeScriptsToStack(self): - script_list_strs = [] + def writeScriptsToStack(self) -> None: + script_list_strs = [] # type: List[str] for script in self._script_list: - parser = configparser.ConfigParser(interpolation = None) #We'll encode the script as a config with one section. The section header is the key and its values are the settings. - parser.optionxform = str #Don't transform the setting keys as they are case-sensitive. + parser = configparser.ConfigParser(interpolation = None) # We'll encode the script as a config with one section. The section header is the key and its values are the settings. + parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive. script_name = script.getSettingData()["key"] parser.add_section(script_name) for key in script.getSettingData()["settings"]: value = script.getSettingValueByKey(key) parser[script_name][key] = str(value) - serialized = io.StringIO() #ConfigParser can only write to streams. Fine. + serialized = io.StringIO() # ConfigParser can only write to streams. Fine. parser.write(serialized) serialized.seek(0) script_str = serialized.read() - script_str = script_str.replace("\\\\", r"\\\\").replace("\n", r"\\\n") #Escape newlines because configparser sees those as section delimiters. + script_str = script_str.replace("\\\\", r"\\\\").replace("\n", r"\\\n") # Escape newlines because configparser sees those as section delimiters. script_list_strs.append(script_str) - script_list_strs = "\n".join(script_list_strs) #ConfigParser should never output three newlines in a row when serialised, so it's a safe delimiter. + script_list_string = "\n".join(script_list_strs) # ConfigParser should never output three newlines in a row when serialised, so it's a safe delimiter. global_stack = Application.getInstance().getGlobalContainerStack() + if global_stack is None: + return + if "post_processing_scripts" not in global_stack.getMetaData(): global_stack.setMetaDataEntry("post_processing_scripts", "") - Application.getInstance().getGlobalContainerStack().setMetaDataEntry("post_processing_scripts", script_list_strs) + + global_stack.setMetaDataEntry("post_processing_scripts", script_list_string) ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. - def _createView(self): + def _createView(self) -> None: Logger.log("d", "Creating post processing plugin view.") self.loadAllScripts() # Create the plugin dialog component - path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml") - self._view = Application.getInstance().createQmlComponent(path, {"manager": self}) + path = os.path.join(cast(str, PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin")), "PostProcessingPlugin.qml") + self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) if self._view is None: Logger.log("e", "Not creating PostProcessing button near save button because the QML component failed to be created.") return Logger.log("d", "Post processing view created.") # Create the save button component - Application.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton")) + CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton")) ## Show the (GUI) popup of the post processing plugin. - def showPopup(self): + def showPopup(self) -> None: if self._view is None: self._createView() if self._view is None: @@ -284,8 +304,9 @@ class PostProcessingPlugin(QObject, Extension): # 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): + def _propertyChanged(self) -> None: global_container_stack = Application.getInstance().getGlobalContainerStack() - global_container_stack.propertyChanged.emit("post_processing_plugin", "value") + 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 b5211401c1..e502f107f9 100644 --- a/plugins/PostProcessingPlugin/Script.py +++ b/plugins/PostProcessingPlugin/Script.py @@ -1,6 +1,8 @@ # Copyright (c) 2015 Jaime van Kessel # Copyright (c) 2018 Ultimaker B.V. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. +from typing import Optional, Any, Dict, TYPE_CHECKING, List + from UM.Signal import Signal, signalemitter from UM.i18n import i18nCatalog @@ -17,16 +19,20 @@ import json import collections i18n_catalog = i18nCatalog("cura") +if TYPE_CHECKING: + from UM.Settings.Interfaces import DefinitionContainerInterface + ## Base class for scripts. All scripts should inherit the script class. @signalemitter class Script: - def __init__(self): + def __init__(self) -> None: super().__init__() - self._settings = None - self._stack = None + self._stack = None # type: Optional[ContainerStack] + self._definition = None # type: Optional[DefinitionContainerInterface] + self._instance = None # type: Optional[InstanceContainer] - def initialize(self): + def initialize(self) -> None: setting_data = self.getSettingData() self._stack = ContainerStack(stack_id=str(id(self))) self._stack.setDirty(False) # This stack does not need to be saved. @@ -45,6 +51,8 @@ class Script: except ContainerFormatError: self._definition = None return + if self._definition is None: + return self._stack.addContainer(self._definition) self._instance = InstanceContainer(container_id="ScriptInstanceContainer") self._instance.setDefinition(self._definition.getId()) @@ -58,16 +66,17 @@ class Script: settingsLoaded = Signal() valueChanged = Signal() # Signal emitted whenever a value of a setting is changed - def _onPropertyChanged(self, key, property_name): + def _onPropertyChanged(self, key: str, property_name: str) -> None: if property_name == "value": self.valueChanged.emit() # Property changed: trigger reslice # To do this we use the global container stack propertyChanged. - # Reslicing is necessary for setting changes in this plugin, because the changes + # 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() - global_container_stack.propertyChanged.emit(key, property_name) + 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. @@ -75,30 +84,35 @@ class Script: # 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): - setting_data = self.getSettingDataString() - if type(setting_data) == str: - setting_data = json.loads(setting_data, object_pairs_hook = collections.OrderedDict) + def getSettingData(self) -> Dict[str, Any]: + setting_data_as_string = self.getSettingDataString() + setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict) return setting_data - def getSettingDataString(self): + def getSettingDataString(self) -> str: raise NotImplementedError() - def getDefinitionId(self): + def getDefinitionId(self) -> Optional[str]: if self._stack: - return self._stack.getBottom().getId() + bottom = self._stack.getBottom() + if bottom is not None: + return bottom.getId() + return None - def getStackId(self): + def getStackId(self) -> Optional[str]: if self._stack: return self._stack.getId() + return None ## Convenience function that retrieves value of a setting from the stack. - def getSettingValueByKey(self, key): - return self._stack.getProperty(key, "value") + def getSettingValueByKey(self, key: str) -> Any: + 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, key, default = None): + def getValue(self, line: str, key: str, default = None) -> Any: if not key in line or (';' in line and line.find(key) > line.find(';')): return default sub_part = line[line.find(key) + 1:] @@ -126,7 +140,7 @@ class Script: # \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 = "", **kwargs): + def putValue(self, line: str = "", **kwargs) -> str: #Strip the comment. comment = "" if ";" in line: @@ -167,5 +181,5 @@ class Script: ## 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): + def execute(self, data: List[str]) -> List[str]: raise NotImplementedError()