diff --git a/ilastik/shell/gui/ilastikShell.py b/ilastik/shell/gui/ilastikShell.py index fa32efbd8..eebdf5c23 100644 --- a/ilastik/shell/gui/ilastikShell.py +++ b/ilastik/shell/gui/ilastikShell.py @@ -29,6 +29,7 @@ import platform import threading import warnings +from typing import List, Type, Optional # SciPy import numpy @@ -74,7 +75,7 @@ # ilastik import ilastik.ilastik_logging.default_config -from ilastik.workflow import getAvailableWorkflows, getWorkflowFromName +from ilastik.workflow import getAvailableWorkflows, getWorkflowFromName, Workflow from ilastik.utility import bind, log_exception from ilastik.utility.gui import ThunkEventHandler, ThreadRouter, threadRouted from ilastik.exceptions import UserAbort @@ -508,8 +509,7 @@ def workflow(self): def loadWorkflow(self, workflow_class): self.onNewProjectActionTriggered(workflow_class) - def getWorkflow(self, w=None): - + def getWorkflow(self, w: Optional[str] = None) -> Type[Workflow]: listOfItems = [workflowDisplayName for _, __, workflowDisplayName in getAvailableWorkflows()] if w is not None and w in listOfItems: cur = listOfItems.index(w) @@ -579,7 +579,6 @@ def _createProjectMenu(self): return (menu, shellActions) def setupOpenFileButtons(self): - for b in self.openFileButtons: b.close() b.deleteLater() @@ -1644,13 +1643,13 @@ def _loadProject(self, hdf5File, projectFilePath, workflow_class, readOnly, impo try: assert self.projectManager is None, "Expected projectManager to be None." + QApplication.setOverrideCursor(Qt.WaitCursor) self.projectManager = ProjectManager( self, workflow_class, workflow_cmdline_args=self._workflow_cmdline_args, project_creation_args=project_creation_args, ) - except Exception as e: msg = "Could not load project file.\n" + str(e) log_exception(logger, msg) @@ -1658,67 +1657,69 @@ def _loadProject(self, hdf5File, projectFilePath, workflow_class, readOnly, impo # no project will be loaded, free the file resource hdf5File.close() - else: + return + finally: + QApplication.restoreOverrideCursor() + + try: + # Add all the applets from the workflow + for index, app in enumerate(self.projectManager.workflow.applets): + self.addApplet(index, app) + + start = time.perf_counter() + # load the project data from file + if importFromPath is None: + # FIXME: load the project asynchronously + self.projectManager.loadProject(hdf5File, projectFilePath, readOnly) + else: + assert not readOnly, "Can't import into a read-only file." + self.projectManager.importProject(importFromPath, hdf5File, projectFilePath) + except Exception as ex: + self.closeCurrentProject() + # loadProject failed, so we cannot expect it to clean up + # the hdf5 file (but it might have cleaned it up, so we catch + # the error) try: - # Add all the applets from the workflow - for index, app in enumerate(self.projectManager.workflow.applets): - self.addApplet(index, app) - - start = time.perf_counter() - # load the project data from file - if importFromPath is None: - # FIXME: load the project asynchronously - self.projectManager.loadProject(hdf5File, projectFilePath, readOnly) - else: - assert not readOnly, "Can't import into a read-only file." - self.projectManager.importProject(importFromPath, hdf5File, projectFilePath) - except Exception as ex: - self.closeCurrentProject() - - # loadProject failed, so we cannot expect it to clean up - # the hdf5 file (but it might have cleaned it up, so we catch - # the error) - try: - hdf5File.close() - except: - pass + hdf5File.close() + except: + pass - if not isinstance(ex, UserAbort): - log_exception(logger) - QMessageBox.warning(self, "Failed to Load", "Could not load project file.\n" + str(ex)) + if not isinstance(ex, UserAbort): + log_exception(logger) + QMessageBox.warning(self, "Failed to Load", "Could not load project file.\n" + str(ex)) + return - else: - stop = time.perf_counter() - logger.debug("Loading the project took {:.2f} sec.".format(stop - start)) + stop = time.perf_counter() + logger.debug("Loading the project took {:.2f} sec.".format(stop - start)) - workflowDisplayName = self.projectManager.workflow.workflowDisplayName - self._setRecentlyOpenedList(projectFilePath, workflowDisplayName) + workflowDisplayName = self.projectManager.workflow.workflowDisplayName + self._setRecentlyOpenedList(projectFilePath, workflowDisplayName) - workflowName = self.projectManager.workflow.workflowName - # be friendly to user: if this file has not specified a default workflow, do it now - if not "workflowName" in list(hdf5File.keys()) and not readOnly: - hdf5File.create_dataset("workflowName", data=workflowName.encode("utf-8")) + workflowName = self.projectManager.workflow.workflowName + # be friendly to user: if this file has not specified a default workflow, do it now + if not "workflowName" in list(hdf5File.keys()) and not readOnly: + hdf5File.create_dataset("workflowName", data=workflowName.encode("utf-8")) - # switch away from the startup screen to show the loaded project - self.mainStackedWidget.setCurrentIndex(1) - # By default, make the splitter control expose a reasonable width of the applet bar - self.mainSplitter.setSizes([300, 1]) + # switch away from the startup screen to show the loaded project + self.mainStackedWidget.setCurrentIndex(1) + # By default, make the splitter control expose a reasonable width of the applet bar + self.mainSplitter.setSizes([300, 1]) - self.progressDisplayManager.cleanUp() - self.progressDisplayManager.initializeForWorkflow(self.projectManager.workflow) + self.progressDisplayManager.cleanUp() + self.progressDisplayManager.initializeForWorkflow(self.projectManager.workflow) - self.setImageNameListSlot(self.projectManager.workflow.imageNameListSlot) - self.updateShellProjectDisplay() + self.setImageNameListSlot(self.projectManager.workflow.imageNameListSlot) + self.updateShellProjectDisplay() - # Enable all the applet controls - self.enableWorkflow = True + # Enable all the applet controls + self.enableWorkflow = True - if "currentApplet" in list(hdf5File.keys()): - appletName = hdf5File["currentApplet"][()] - self.setSelectedAppletDrawer(appletName) - else: - self.setSelectedAppletDrawer(self.projectManager.workflow.defaultAppletIndex) + if "currentApplet" in list(hdf5File.keys()): + appletName = hdf5File["currentApplet"][()] + self.setSelectedAppletDrawer(appletName) + else: + self.setSelectedAppletDrawer(self.projectManager.workflow.defaultAppletIndex) def _setRecentlyOpenedList(self, projectFilePath, workflowDisplayName): recentlyOpenedList = [(projectFilePath, workflowDisplayName)] + [ @@ -1735,7 +1736,6 @@ def closeCurrentProject(self): """ assert threading.current_thread().name == "MainThread" if self.projectManager is not None: - self.removeAllAppletWidgets() for f in self.cleanupFunctions: f() diff --git a/ilastik/shell/projectManager.py b/ilastik/shell/projectManager.py index d5eb24957..ff55ecd68 100644 --- a/ilastik/shell/projectManager.py +++ b/ilastik/shell/projectManager.py @@ -22,6 +22,8 @@ import gc import copy import platform +from typing import Optional, List, Type, Dict + import h5py import logging import time @@ -33,7 +35,7 @@ from ilastik import Project from ilastik import isVersionCompatible from ilastik.utility import log_exception -from ilastik.workflow import getWorkflowFromName +from ilastik.workflow import getWorkflowFromName, Workflow from lazyflow.utility.timer import Timer, timeLogged try: @@ -57,10 +59,6 @@ class ProjectManager(object): member for direct access to its applets and their top-level operators. """ - ######################### - ## Error types - ######################### - class ProjectVersionError(RuntimeError): """ Raised if an attempt is made to open a project file that was generated with an old version of ilastik. @@ -88,13 +86,49 @@ class SaveError(RuntimeError): pass - ######################### - ## Class methods - ######################### + @property + def _applets(self): + if self.workflow is not None: + return self.workflow.applets + else: + return [] + + def __init__( + self, + shell, + workflowClass: Type[Workflow], + headless: bool = False, + workflow_cmdline_args: Optional[List[str]] = None, + project_creation_args: Optional[List[str]] = None, + ): + """ + :param shell + :param workflowClass: The class of the workflow to be managed. + :param headless: Indicates whether the workflow should be opened in 'headless' mode (default is False). + :param workflow_cmdline_args: Optional list of strings from the command-line to configure the workflow. + :param project_creation_args: Optional list of strings for project creation arguments. + """ + # Init + self.closed = True + self._shell = shell + self.workflow = None + self.currentProjectFile = None + self.currentProjectPath = None + self.currentProjectIsReadOnly = False + + # Instantiate the workflow. + self._workflowClass = workflowClass + self._workflow_cmdline_args = workflow_cmdline_args or [] + self._project_creation_args = project_creation_args or [] + self._headless = headless - @classmethod + # the workflow class has to be specified at this point + assert workflowClass is not None + self.workflow = workflowClass(shell, headless, self._workflow_cmdline_args, self._project_creation_args) + + @staticmethod def createBlankProjectFile( - cls, projectFilePath, workflow_class=None, workflow_cmdline_args=None, h5_file_kwargs={} + projectFilePath, workflow_class=None, workflow_cmdline_args=None, h5_file_kwargs: Optional[Dict] = None ): """Create a new ilp file at the given path and initialize it with a project version. @@ -109,6 +143,7 @@ def createBlankProjectFile( :rtype: h5py.File """ + h5_file_kwargs = h5_file_kwargs or {} # Create the blank project file if "mode" in h5_file_kwargs: raise ValueError("ProjectManager.createBlankProjectFile(): 'mode' is not allowed as a h5py.File kwarg") @@ -123,12 +158,12 @@ def createBlankProjectFile( return h5File - @classmethod - def getWorkflowName(self, projectFile): + @staticmethod + def getWorkflowName(projectFile): return str(projectFile["workflowName"][()].decode("utf-8")) - @classmethod - def openProjectFile(cls, projectFilePath, forceReadOnly=False): + @staticmethod + def openProjectFile(projectFilePath, forceReadOnly=False): """ Class method. Attempt to open the given path to an existing project file. @@ -170,8 +205,8 @@ def openProjectFile(cls, projectFilePath, forceReadOnly=False): return (hdf5File, workflow_class, readOnly) - @classmethod - def downloadProjectFromDvid(cls, hostname, node_uuid, keyvalue_name, project_key=None, local_filepath=None): + @staticmethod + def downloadProjectFromDvid(hostname, node_uuid, keyvalue_name, project_key=None, local_filepath=None): """ Download a file from a dvid keyvalue data instance and store it to the given local_filepath. If no local_filepath is given, create a new temporary file. @@ -208,37 +243,6 @@ def downloadProjectFromDvid(cls, hostname, node_uuid, keyvalue_name, project_key return local_filepath - ######################### - ## Public methods - ######################### - - def __init__(self, shell, workflowClass, headless=False, workflow_cmdline_args=None, project_creation_args=None): - """ - Constructor. - - :param workflowClass: A subclass of ilastik.workflow.Workflow (the class, not an instance). - :param headless: A bool that is passed to the workflow constructor, - indicating whether or not the workflow should be opened in 'headless' mode. - :param workflow_cmdline_args: A list of strings from the command-line to configure the workflow. - """ - # Init - self.closed = True - self._shell = shell - self.workflow = None - self.currentProjectFile = None - self.currentProjectPath = None - self.currentProjectIsReadOnly = False - - # Instantiate the workflow. - self._workflowClass = workflowClass - self._workflow_cmdline_args = workflow_cmdline_args or [] - self._project_creation_args = project_creation_args or [] - self._headless = headless - - # the workflow class has to be specified at this point - assert workflowClass is not None - self.workflow = workflowClass(shell, headless, self._workflow_cmdline_args, self._project_creation_args) - def cleanUp(self): """ Should be called when the Projectmanager is canceled. Closes the project file. @@ -417,17 +421,6 @@ def saveProjectAs(self, newPath): # Save the current project state self.saveProject() - ######################### - ## Private methods - ######################### - - @property - def _applets(self): - if self.workflow is not None: - return self.workflow.applets - else: - return [] - @timeLogged(logger, logging.DEBUG) def loadProject(self, hdf5File, projectFilePath, readOnly): """ diff --git a/ilastik/workflow.py b/ilastik/workflow.py index 779362ca2..14ae04857 100644 --- a/ilastik/workflow.py +++ b/ilastik/workflow.py @@ -26,7 +26,7 @@ from ilastik.shell.shellAbc import ShellABC import logging -from typing import Tuple +from typing import Tuple, Type, Iterator, Optional logger = logging.getLogger(__name__) @@ -41,10 +41,6 @@ class Workflow(Operator): #: Should workflow be automatically added to start widget auto_register = True - ############################### - # Abstract methods/properties # - ############################### - @abstractproperty def applets(self): """ @@ -82,6 +78,10 @@ def workflowDescription(self): def defaultAppletIndex(self): return 0 + @property + def shell(self): + return self._shell + @abstractmethod def connectLane(self, laneIndex): """ @@ -138,10 +138,6 @@ def handleSendMessageToServer(self, name, data): def postprocessClusterSubResult(self, roi, result, blockwise_fileset): pass - ################## - # Public methods # - ################## - def __init__( self, shell, headless=False, workflow_cmdline_args=(), project_creation_args=(), parent=None, graph=None ): @@ -169,10 +165,6 @@ def __init__( self._shell = shell self._headless = headless - @property - def shell(self): - return self._shell - def cleanUp(self): """ The user closed the project, so this workflow is being destroyed. @@ -203,10 +195,6 @@ def getSubclass(cls, name): return subcls raise RuntimeError(f"No known workflow class has name {name}") - ################### - # Private methods # - ################### - def _after_init(self): """ Overridden from Operator. @@ -254,7 +242,7 @@ def all_subclasses(cls): return cls.__subclasses__() + [g for s in cls.__subclasses__() for g in all_subclasses(s)] -def getAvailableWorkflows() -> Tuple[Workflow, str, str]: +def getAvailableWorkflows() -> Iterator[Tuple[Type[Workflow], str, str]]: """ This function used to iterate over all workflows that have been imported so far, but now we rely on the explicit list in workflows/__init__.py, @@ -273,7 +261,7 @@ def getAvailableWorkflows() -> Tuple[Workflow, str, str]: from . import workflows - def _makeWorkflowTuple(workflow_cls): + def _makeWorkflowTuple(workflow_cls: Type[Workflow]): if isinstance(workflow_cls.workflowName, str): if workflow_cls.workflowDisplayName is None: workflow_cls.workflowDisplayName = workflow_cls.workflowName @@ -318,8 +306,9 @@ def _makeWorkflowTuple(workflow_cls): yield _makeWorkflowTuple(W) -def getWorkflowFromName(Name): +def getWorkflowFromName(name: str) -> Optional[Type[Workflow]]: """return workflow by naming its workflowName variable""" for w, _name, _displayName in getAvailableWorkflows(): - if _name == Name or w.__name__ == Name or _displayName == Name: + if _name == name or w.__name__ == name or _displayName == name: return w + return None