def find_julia_version(self): julia_path = self._toolbox.qsettings().value("appSettings/juliaPath", defaultValue="") if julia_path != "": self.julia_exe = julia_path else: self.julia_exe = JULIA_EXECUTABLE self.julia_project_path = self._toolbox.qsettings().value( "appSettings/juliaProjectPath", defaultValue="") if self.julia_project_path == "": self.julia_project_path = "@." args = list() args.append("-e") args.append("println(VERSION)") exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, silent=True) exec_mngr.start_execution() if exec_mngr.wait_for_process_finished(msecs=5000): self._julia_version = exec_mngr.process_output args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append("println(Base.active_project())") exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, silent=True) exec_mngr.start_execution() if exec_mngr.wait_for_process_finished(msecs=5000): self._julia_active_project = exec_mngr.process_output
def test_execute_python_interpreter(self): program = sys.executable logger = MagicMock() manager = QProcessExecutionManager(logger, program, args=["--version"]) manager.start_execution() self.assertTrue(manager.wait_for_process_finished()) self.assertFalse(manager.process_failed_to_start) self.assertFalse(manager.process_failed)
class GAMSToolInstance(ToolInstance): """Class for GAMS Tool instances.""" def prepare(self, optional_input_files, input_database_urls, output_database_urls, tool_args): """See base class.""" gams_path = self._settings.value("appSettings/gamsPath", defaultValue="") if gams_path != '': gams_exe = gams_path else: gams_exe = GAMS_EXECUTABLE self.program = gams_exe self.args.append(self.tool_specification.main_prgm) self.args.append("curDir=") self.args.append(self.basedir) self.args.append( "logoption=3") # TODO: This should be an option in Settings self.append_cmdline_args(optional_input_files, input_database_urls, output_database_urls, tool_args) def execute(self, **kwargs): """Executes a prepared instance.""" self.exec_mngr = QProcessExecutionManager(self._logger, self.program, self.args, **kwargs) self.exec_mngr.execution_finished.connect( self.handle_execution_finished) # TODO: Check if this sets the curDir argument. Is the curDir arg now useless? self.exec_mngr.start_execution(workdir=self.basedir) @Slot(int) def handle_execution_finished(self, ret): """Handles execution finished. Args: ret (int) """ self.exec_mngr.execution_finished.disconnect( self.handle_execution_finished) if self.exec_mngr.process_failed: # process_failed should be True if ret != 0 if self.exec_mngr.process_failed_to_start: self._logger.msg_error.emit( f"\t<b>{self.exec_mngr.program()}</b> failed to start. Make sure that " "GAMS is installed properly on your computer " "and GAMS directory is given in Settings (F1).") else: try: return_msg = self.tool_specification.return_codes[ret] self._logger.msg_error.emit( f"\t<b>{return_msg}</b> [exit code:{ret}]") except KeyError: self._logger.msg_error.emit( f"\tUnknown return code ({ret})") else: # Return code 0: success self._logger.msg.emit("\tTool specification execution finished") self.exec_mngr.deleteLater() self.exec_mngr = None self.instance_finished.emit(ret)
class ExecutableToolInstance(ToolInstance): """Class for Executable Tool instances.""" def prepare(self, optional_input_files, input_database_urls, output_database_urls, tool_args): """See base class.""" batch_path = os.path.join(self.basedir, self.tool_specification.main_prgm) if sys.platform != "win32": self.program = "sh" self.args.append(batch_path) else: self.program = batch_path self.append_cmdline_args(optional_input_files, input_database_urls, output_database_urls, tool_args) def execute(self, **kwargs): """Executes a prepared instance.""" self.exec_mngr = QProcessExecutionManager(self._logger, self.program, self.args, **kwargs) self.exec_mngr.execution_finished.connect( self.handle_execution_finished) self.exec_mngr.start_execution(workdir=self.basedir) @Slot(int) def handle_execution_finished(self, ret): """Handles execution finished. Args: ret (int): Tool specification process return value """ self.exec_mngr.execution_finished.disconnect( self.handle_execution_finished) if self.exec_mngr.process_failed: # process_failed should be True if ret != 0 if self.exec_mngr.process_failed_to_start: self._logger.msg_error.emit( f"\t<b>{self.exec_mngr.program()}</b> failed to start.") else: try: return_msg = self.tool_specification.return_codes[ret] self._logger.msg_error.emit( f"\t<b>{return_msg}</b> [exit code:{ret}]") except KeyError: self._logger.msg_error.emit( f"\tUnknown return code ({ret})") else: # Return code 0: success self._logger.msg.emit("\tTool specification execution finished") self.exec_mngr.deleteLater() self.exec_mngr = None self.instance_finished.emit(ret)
def find_julia_version(self): args = list() args.append("-e") args.append("println(VERSION)") exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, silent=True) exec_mngr.start_execution() if exec_mngr.wait_for_process_finished(msecs=5000): self._julia_version = exec_mngr.process_output args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append("println(Base.active_project())") exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, silent=True) exec_mngr.start_execution() if exec_mngr.wait_for_process_finished(msecs=5000): self._julia_active_project = exec_mngr.process_output
def python_exists(program, logger): """Checks that Python is set up correctly in Settings. This executes 'python -V' in a QProcess and if the process finishes successfully, the python is ready to be used. Args: program (str): Python executable to check logger (LoggerInterface) Returns: bool: True if Python is found, False otherwise """ args = ["-V"] python_check_process = QProcessExecutionManager(logger, program, args, silent=True) python_check_process.start_execution() if not python_check_process.wait_for_process_finished(msecs=3000): logger.msg_error.emit( "Couldn't execute Python. Please check the <b>Python interpreter</b> option in Settings." ) return False return True
class SpineOptConfigurationAssistant(StateMachineWidget): _required_julia_version = "1.1.0" py_call_program_check_needed = Signal() spine_opt_process_failed = Signal() py_call_installation_needed = Signal() py_call_reconfiguration_needed = Signal() py_call_process_failed = Signal() spine_opt_ready = Signal() def __init__(self, toolbox): super().__init__("SpineOpt.jl configuration assistant", toolbox) self._toolbox = toolbox self.exec_mngr = None self.julia_exe = None self.julia_project_path = None self._julia_version = None self._julia_active_project = None self._py_call_program = None self.julia_exe, self.julia_project_path = self.resolve_julia() self.python = self.resolve_python() self._welcome_text = ( "<html><p>Welcome! This assistant will help you configure Spine Toolbox for using SpineOpt.jl<br><br>" "Your setup<br>" "Julia: <b>{0}</b><br>" "Julia Project: <b>{1}</b><br>" "Python for PyCall: <b>{2}</b>" "</p></html>".format(self.julia_exe, self.julia_project_path, self.python)) self.button_left.clicked.connect(self.close) def resolve_julia(self): """Returns Julia executable and project according to user's Settings.""" use_embedded_julia = int(self._toolbox.qsettings().value( "appSettings/useEmbeddedJulia", defaultValue="2")) if use_embedded_julia == 2: # If user has enabled the embedded Julia Console, we need to take julia exe and project from kernel specs # Read Julia kernel name from app settings kernel_name = self._toolbox.qsettings().value( "appSettings/juliaKernel", defaultValue="") if not self.check_kernel_is_ok(kernel_name, "Julia"): return None, None julia_kernels = find_julia_kernels() kernel_deats = KernelEditor.get_kernel_deats( julia_kernels[kernel_name]) julia_exe = kernel_deats["exe"] julia_project_path = kernel_deats["project"] else: julia_path = self._toolbox.qsettings().value( "appSettings/juliaPath", defaultValue="") if julia_path != "": julia_exe = julia_path else: julia_exe = resolve_julia_executable_from_path() if julia_exe == "": self._toolbox.msg_error.emit( "Julia not found in PATH. Please select a valid Julia executable in Settings->Tools." ) # Julia was not found in PATH return None, None julia_project_path = self._toolbox.qsettings().value( "appSettings/juliaProjectPath", defaultValue="") if julia_project_path == "": julia_project_path = "@." return julia_exe, julia_project_path def resolve_python(self): """Returns the full path to Python interpreter according to user's Settings.""" use_embedded_python = int(self._toolbox.qsettings().value( "appSettings/useEmbeddedPython", defaultValue="2")) if use_embedded_python == 2: # Get Python interpreter from selected Python kernel python_kernel_name = self._toolbox.qsettings().value( "appSettings/pythonKernel", defaultValue="") if not self.check_kernel_is_ok(python_kernel_name, "Python"): return None kernel_deats = KernelEditor.get_kernel_deats( find_python_kernels()[python_kernel_name]) python_path = kernel_deats["exe"] return python_path else: return python_interpreter(self._toolbox.qsettings()) def check_kernel_is_ok(self, kernel_name, prgm): """Checks that kernel spec is valid for the configuration assistant to continue. Args: kernel_name (str): Kernel name prgm (str): Either "Python" or "Julia", determines which kernel type to check Returns: bool: True if kernel is ok, False otherwise """ if kernel_name == "": self._toolbox.msg_error.emit( f"No kernel selected. Go to Settings->Tools to select a {prgm} kernel" ) return False if prgm == "Python": kernels = find_python_kernels() else: kernels = find_julia_kernels() try: kernel_path = kernels[kernel_name] except KeyError: self._toolbox.msg_error.emit( f"Kernel <b>{kernel_name}</b> not found. Go to Settings->Tools and select another {prgm} kernel." ) return False kernel_json = os.path.join(kernel_path, "kernel.json") if not os.path.exists(kernel_json): self._toolbox.msg_error.emit( f"Path <b>{kernel_json}</b> does not exist") return False if os.stat(kernel_json).st_size == 0: self._toolbox.msg_error.emit(f"File <b>{kernel_json}</b> is empty") return False with open(kernel_json, "r") as fh: try: kernel_dict = json.load(fh) except json.decoder.JSONDecodeError: self._toolbox.msg_error.emit( f"Error reading file <b>{kernel_json}</b> file. Invalid JSON." ) return False return True @busy_effect def find_julia_version(self): args = list() args.append("-e") args.append("println(VERSION)") exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, silent=True) exec_mngr.start_execution() if exec_mngr.wait_for_process_finished(msecs=5000): self._julia_version = exec_mngr.process_output args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append("println(Base.active_project())") exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, silent=True) exec_mngr.start_execution() if exec_mngr.wait_for_process_finished(msecs=5000): self._julia_active_project = exec_mngr.process_output def _make_processing_state(self, name, text): s = self._make_state(name) s.assignProperty(self.label_msg, "text", text) s.assignProperty(self.button_right, "visible", False) s.assignProperty(self.label_loader, "visible", True) s.assignProperty(self.button_left, "visible", False) return s def _make_report_state(self, name, text): s = self._make_state(name) s.assignProperty(self.label_msg, "text", text) s.assignProperty(self.button_right, "visible", False) s.assignProperty(self.label_loader, "visible", False) s.assignProperty(self.button_left, "visible", True) s.assignProperty(self.button_left, "text", "Close") return s def _make_prompt_state(self, name, text): s = self._make_state(name) s.assignProperty(self.label_msg, "text", text) s.assignProperty(self.button_right, "visible", True) s.assignProperty(self.button_right, "text", "Allow") s.assignProperty(self.label_loader, "visible", False) s.assignProperty(self.button_left, "visible", True) s.assignProperty(self.button_left, "text", "Cancel") return s def _make_report_julia_not_found(self): return self._make_report_state( "report_julia_not_found", "<html><p>Unable to find Julia. Make sure that Julia is installed correctly and try again.</p></html>", ) def _make_report_bad_julia_version(self): return self._make_report_state( "report_bad_julia_version", f"<html><p>SpineOpt.jl requires Julia version >= {self._required_julia_version}, " f"whereas current version is {self._julia_version}. " "Upgrade Julia and try again.</p></html>", ) def _make_welcome(self): self.find_julia_version() # TODO: Report and abort if Julia or Python is None if self._julia_version is None: return self._make_report_julia_not_found() if self._julia_version < self._required_julia_version: return self._make_report_bad_julia_version() return super()._make_welcome() def _make_updating_spine_opt(self): return self._make_processing_state( "updating_spine_opt", "<html><p>Updating SpineOpt.jl to the latest version...</p></html>" ) def _make_prompt_to_install_latest_spine_opt(self): return self._make_prompt_state( "prompt_to_install_latest_spine_opt", "<html><p>Spine Toolbox needs to do the following modifications to the Julia project " f"at <b>{self._julia_active_project}</b>:</p><p>Install the SpineOpt.jl package</p>", ) def _make_installing_latest_spine_opt(self): return self._make_processing_state( "installing_latest_spine_opt", "<html><p>Installing latest SpineOpt.jl...</p></html>") def _make_report_spine_opt_installation_failed(self): return self._make_report_state( "report_spine_opt_installation_failed", "<html><p>SpineOpt.jl installation failed. See Process log for error messages.</p></html>", ) def _make_checking_py_call_program(self): return self._make_processing_state( "checking_py_call_program", "<html><p>Checking PyCall.jl's configuration...</p></html>") def _make_prompt_to_reconfigure_py_call(self): return self._make_prompt_state( "prompt_to_reconfigure_py_call", "<html><p>Spine Toolbox needs to do the following modifications to the Julia project " f"at <b>{self._julia_active_project}</b>:</p>" f"<p>Change the Python program used by PyCall.jl to {self.python}</p>", ) def _make_prompt_to_install_py_call(self): return self._make_prompt_state( "prompt_to_install_py_call", "<html><p>Spine Toolbox needs to do the following modifications to the Julia project " f"at <b>{self._julia_active_project}</b>:</p>" "<p>Install the PyCall.jl package.</p>", ) def _make_report_spine_opt_ready(self): return self._make_report_state( "report_spine_opt_ready", "<html><p>SpineOpt.jl has been successfully configured.</p></html>" ) def _make_reconfiguring_py_call(self): return self._make_processing_state( "reconfiguring_py_call", "<html><p>Reconfiguring PyCall.jl...</p></html>") def _make_installing_py_call(self): return self._make_processing_state( "installing_py_call", "<html><p>Installing PyCall.jl...</p></html>") def _make_report_py_call_process_failed(self): return self._make_report_state( "report_py_call_process_failed", "<html><p>PyCall.jl installation/reconfiguration failed. See Process log for error messages.</p></html>", ) @Slot() def update_spine_opt(self): args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append("using Pkg; Pkg.update(ARGS[1]);") args.append("SpineOpt") self.exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, semisilent=True) self.exec_mngr.execution_finished.connect( self._handle_spine_opt_process_finished) self.exec_mngr.start_execution() @Slot() def install_spine_opt(self): args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append( "using Pkg; Pkg.Registry.add(RegistrySpec(url=ARGS[1])); Pkg.add(ARGS[2]);" ) args.append("https://github.com/Spine-project/SpineJuliaRegistry.git") args.append("SpineOpt") self.exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, semisilent=True) self.exec_mngr.execution_finished.connect( self._handle_spine_opt_process_finished) self.exec_mngr.start_execution() @Slot(int) def _handle_spine_opt_process_finished(self, ret): self.exec_mngr.execution_finished.disconnect( self._handle_spine_opt_process_finished) if ret == 0: self.py_call_program_check_needed.emit() else: self.spine_opt_process_failed.emit() @Slot() def check_py_call_program(self): args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append("using PyCall; println(PyCall.pyprogramname);") self.exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, silent=True) self.exec_mngr.execution_finished.connect( self._handle_check_py_call_program_finished) self.exec_mngr.start_execution() @Slot(int) def _handle_check_py_call_program_finished(self, ret): self.exec_mngr.execution_finished.disconnect( self._handle_check_py_call_program_finished) if ret == 0: self._py_call_program = self.exec_mngr.process_output if os.path.normcase(self._py_call_program) == os.path.normcase( self.python): self.spine_opt_ready.emit() else: self.py_call_reconfiguration_needed.emit() else: self.py_call_installation_needed.emit() @Slot() def reconfigure_py_call(self): """Starts process that reconfigures PyCall to use selected Python interpreter.""" args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append( "using PyCall; ENV[ARGS[1]] = ARGS[2]; using Pkg; Pkg.build(ARGS[3]);" ) args.append("PYTHON") args.append(self.python) args.append("PyCall") self.exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, semisilent=True) self.exec_mngr.execution_finished.connect( self._handle_reconfigure_py_call_finished) self.exec_mngr.start_execution() @Slot(int) def _handle_reconfigure_py_call_finished(self, ret): self.exec_mngr.execution_finished.disconnect( self._handle_reconfigure_py_call_finished) if ret == 0: self.spine_opt_ready.emit() else: self.py_call_process_failed.emit() @Slot() def install_py_call(self): """Starts process that installs PyCall in current julia version.""" args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append("ENV[ARGS[1]] = ARGS[2]; using Pkg; Pkg.add(ARGS[3]);") args.append("PYTHON") args.append(self.python) args.append("PyCall") self.exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, semisilent=True) self.exec_mngr.execution_finished.connect( self._handle_install_py_call_finished) self.exec_mngr.start_execution() @Slot(int) def _handle_install_py_call_finished(self, ret): self.exec_mngr.execution_finished.disconnect( self._handle_install_py_call_finished) if ret == 0: self.py_call_program_check_needed.emit() else: self.py_call_process_failed.emit() def set_up_machine(self): super().set_up_machine() # States updating_spine_opt = self._make_updating_spine_opt() prompt_to_install_latest_spine_opt = self._make_prompt_to_install_latest_spine_opt( ) installing_latest_spine_opt = self._make_installing_latest_spine_opt() report_spine_opt_installation_failed = self._make_report_spine_opt_installation_failed( ) checking_py_call_program = self._make_checking_py_call_program() prompt_to_reconfigure_py_call = self._make_prompt_to_reconfigure_py_call( ) prompt_to_install_py_call = self._make_prompt_to_install_py_call() report_spine_opt_ready = self._make_report_spine_opt_ready() reconfiguring_py_call = self._make_reconfiguring_py_call() installing_py_call = self._make_installing_py_call() report_py_call_process_failed = self._make_report_py_call_process_failed( ) # Transitions self.welcome.addTransition(self.welcome.finished, updating_spine_opt) updating_spine_opt.addTransition(self.spine_opt_process_failed, prompt_to_install_latest_spine_opt) updating_spine_opt.addTransition(self.py_call_program_check_needed, checking_py_call_program) prompt_to_install_latest_spine_opt.addTransition( self.button_right.clicked, installing_latest_spine_opt) installing_latest_spine_opt.addTransition( self.spine_opt_process_failed, report_spine_opt_installation_failed) installing_latest_spine_opt.addTransition( self.py_call_program_check_needed, checking_py_call_program) checking_py_call_program.addTransition( self.py_call_reconfiguration_needed, prompt_to_reconfigure_py_call) checking_py_call_program.addTransition( self.py_call_installation_needed, prompt_to_install_py_call) checking_py_call_program.addTransition(self.py_call_process_failed, report_py_call_process_failed) checking_py_call_program.addTransition(self.spine_opt_ready, report_spine_opt_ready) prompt_to_reconfigure_py_call.addTransition(self.button_right.clicked, reconfiguring_py_call) prompt_to_install_py_call.addTransition(self.button_right.clicked, installing_py_call) reconfiguring_py_call.addTransition(self.py_call_process_failed, report_py_call_process_failed) reconfiguring_py_call.addTransition(self.spine_opt_ready, report_spine_opt_ready) installing_py_call.addTransition(self.py_call_process_failed, report_py_call_process_failed) installing_py_call.addTransition(self.py_call_program_check_needed, checking_py_call_program) # Entered updating_spine_opt.entered.connect(self.update_spine_opt) checking_py_call_program.entered.connect(self.check_py_call_program) installing_latest_spine_opt.entered.connect(self.install_spine_opt) reconfiguring_py_call.entered.connect(self.reconfigure_py_call) installing_py_call.entered.connect(self.install_py_call)
class PythonToolInstance(ToolInstance): """Class for Python Tool instances.""" def __init__(self, tool_specification, basedir, settings, embedded_python_console, logger): """ Args: tool_specification (ToolSpecification): the tool specification for this instance basedir (str): the path to the directory where this instance should run settings (QSettings): Toolbox settings embedded_python_console (PythonReplWidget): a Python console widget for execution in embedded console logger (LoggerInterface): A logger instance """ super().__init__(tool_specification, basedir, settings, logger) self._embedded_console = embedded_python_console self.ipython_command_list = list() def prepare(self, optional_input_files, input_database_urls, output_database_urls, tool_args): """See base class.""" work_dir = self.basedir use_embedded_python = self._settings.value( "appSettings/useEmbeddedPython", defaultValue="0") if use_embedded_python == "2" and self._embedded_console is not None: # Prepare a command list (FIFO queue) with two commands for Python Console # 1st cmd: Change current work directory # 2nd cmd: Run script with given args # Cast args in list to strings and combine them to a single string tool_spec_args = self.tool_specification.get_cmdline_args( optional_input_files, input_database_urls, output_database_urls) all_args = tool_spec_args + tool_args cd_work_dir_cmd = f"%cd -q {work_dir}" # -q: quiet run_script_cmd = f'%run "{self.tool_specification.main_prgm}"' if all_args: run_script_cmd = run_script_cmd + " " + '"' + '" "'.join( all_args) + '"' # Populate FIFO command queue self.ipython_command_list.append(cd_work_dir_cmd) self.ipython_command_list.append(run_script_cmd) else: # Prepare command "python script.py script_arguments" python_path = self._settings.value("appSettings/pythonPath", defaultValue="") if python_path != "": python_cmd = python_path else: python_cmd = PYTHON_EXECUTABLE script_path = os.path.join(work_dir, self.tool_specification.main_prgm) self.program = python_cmd self.args.append( script_path ) # First argument for the Python interpreter is path to the tool script self.append_cmdline_args(optional_input_files, input_database_urls, output_database_urls, tool_args) def execute(self, **kwargs): """Executes a prepared instance.""" if (self._settings.value("appSettings/useEmbeddedPython", defaultValue="0") == "2" and self._embedded_console is not None): self.exec_mngr = ConsoleExecutionManager(self._embedded_console, self.ipython_command_list, self._logger) self.exec_mngr.execution_finished.connect( self.handle_console_execution_finished) self.exec_mngr.start_execution() else: self.exec_mngr = QProcessExecutionManager(self._logger, self.program, self.args, **kwargs) self.exec_mngr.execution_finished.connect( self.handle_execution_finished) self.exec_mngr.start_execution(workdir=self.basedir) @Slot(int) def handle_console_execution_finished(self, ret): """Handles console-execution finished. Args: ret (int): Tool specification process return value """ self.exec_mngr.execution_finished.disconnect( self.handle_console_execution_finished) if ret != 0: try: return_msg = self.tool_specification.return_codes[ret] self._logger.msg_error.emit( f"\t<b>{return_msg}</b> [exit code: {ret}]") except KeyError: self._logger.msg_error.emit(f"\tUnknown return code ({ret})") else: self._logger.msg.emit("\tTool specification execution finished") self.exec_mngr.deleteLater() self.exec_mngr = None self.instance_finished.emit(ret) @Slot(int) def handle_execution_finished(self, ret): """Handles execution finished. Args: ret (int): Tool specification process return value """ self.exec_mngr.execution_finished.disconnect( self.handle_execution_finished) if self.exec_mngr.process_failed: # process_failed should be True if ret != 0 if self.exec_mngr.process_failed_to_start: self._logger.msg_error.emit( f"\t<b>{self.exec_mngr.program()}</b> failed to start. Make sure that " "Python is installed properly on your computer.") else: try: return_msg = self.tool_specification.return_codes[ret] self._logger.msg_error.emit( f"\t<b>{return_msg}</b> [exit code:{ret}]") except KeyError: self._logger.msg_error.emit( f"\tUnknown return code ({ret})") else: # Return code 0: success self._logger.msg.emit("\tTool specification execution finished") self.exec_mngr.deleteLater() self.exec_mngr = None self.instance_finished.emit(ret)
class JuliaToolInstance(ToolInstance): """Class for Julia Tool instances.""" def __init__(self, tool_specification, basedir, settings, embedded_julia_console, logger): """ Args: tool_specification (ToolSpecification): the tool specification for this instance basedir (str): the path to the directory where this instance should run settings (QSettings): Toolbox settings embedded_julia_console (JuliaREPLWidget): a Julia console for execution in the embedded console logger (LoggerInterface): a logger instance """ super().__init__(tool_specification, basedir, settings, logger) self._embedded_console = embedded_julia_console self.ijulia_command_list = list() def prepare(self, optional_input_files, input_database_urls, output_database_urls, tool_args): """See base class.""" work_dir = self.basedir use_embedded_julia = self._settings.value( "appSettings/useEmbeddedJulia", defaultValue="2") if use_embedded_julia == "2" and self._embedded_console is not None: # Prepare Julia REPL command mod_work_dir = repr(work_dir).strip("'") cmdline_args = self.tool_specification.get_cmdline_args( optional_input_files, input_database_urls, output_database_urls) cmdline_args += tool_args args = '["' + repr('", "'.join(cmdline_args)).strip("'") + '"]' self.ijulia_command_list += [ f'cd("{mod_work_dir}");', 'empty!(ARGS);', f'append!(ARGS, {args});', f'include("{self.tool_specification.main_prgm}")', ] else: # Prepare command "julia --project={PROJECT_DIR} script.jl" julia_path = self._settings.value("appSettings/juliaPath", defaultValue="") if julia_path != "": julia_exe = julia_path else: julia_exe = JULIA_EXECUTABLE julia_project_path = self._settings.value( "appSettings/juliaProjectPath", defaultValue="") script_path = os.path.join(work_dir, self.tool_specification.main_prgm) self.program = julia_exe self.args.append(f"--project={julia_project_path}") self.args.append(script_path) self.append_cmdline_args(optional_input_files, input_database_urls, output_database_urls, tool_args) def execute(self, **kwargs): """Executes a prepared instance.""" if (self._settings.value("appSettings/useEmbeddedJulia", defaultValue="2") == "2" and self._embedded_console is not None): self.exec_mngr = ConsoleExecutionManager(self._embedded_console, self.ijulia_command_list, self._logger) self.exec_mngr.execution_finished.connect( self.handle_repl_execution_finished) self.exec_mngr.start_execution() else: self.exec_mngr = QProcessExecutionManager(self._logger, self.program, self.args, **kwargs) self.exec_mngr.execution_finished.connect( self.handle_execution_finished) # On Julia the Qprocess workdir must be set to the path where the main script is # Otherwise it doesn't find input files in subdirectories self.exec_mngr.start_execution(workdir=self.basedir) @Slot(int) def handle_repl_execution_finished(self, ret): """Handles repl-execution finished. Args: ret (int): Tool specification process return value """ self.exec_mngr.execution_finished.disconnect( self.handle_repl_execution_finished) if ret != 0: try: return_msg = self.tool_specification.return_codes[ret] self._logger.msg_error.emit( f"\t<b>{return_msg}</b> [exit code: {ret}]") except KeyError: self._logger.msg_error.emit(f"\tUnknown return code ({ret})") else: self._logger.msg.emit("\tTool specification execution finished") self.exec_mngr.deleteLater() self.exec_mngr = None self.instance_finished.emit(ret) @Slot(int) def handle_execution_finished(self, ret): """Handles execution finished. Args: ret (int): Tool specification process return value """ self.exec_mngr.execution_finished.disconnect( self.handle_execution_finished) if self.exec_mngr.process_failed: # process_failed should be True if ret != 0 if self.exec_mngr.process_failed_to_start: self._logger.msg_error.emit( f"\t<b>{self.exec_mngr.program()}</b> failed to start. Make sure that " "Julia is installed properly on your computer.") else: try: return_msg = self.tool_specification.return_codes[ret] self._logger.msg_error.emit( f"\t<b>{return_msg}</b> [exit code:{ret}]") except KeyError: self._logger.msg_error.emit( f"\tUnknown return code ({ret})") else: # Return code 0: success self._logger.msg.emit("\tTool specification execution finished") self.exec_mngr.deleteLater() self.exec_mngr = None self.instance_finished.emit(ret)
class SpineModelConfigurationAssistant(StateMachineWidget): _required_julia_version = "1.1.0" py_call_program_check_needed = Signal() spine_model_process_failed = Signal() py_call_installation_needed = Signal() py_call_reconfiguration_needed = Signal() py_call_process_failed = Signal() spine_model_ready = Signal() def __init__(self, toolbox): super().__init__("SpineModel.jl configuration assistant", toolbox) self._toolbox = toolbox self.exec_mngr = None self._welcome_text = ( "<html><p>Welcome! This assistant will help you configure Spine Toolbox for using SpineModel.jl</p></html>" ) self.julia_exe = None self.julia_project_path = None self._julia_version = None self._julia_active_project = None self._py_call_program = None self.button_left.clicked.connect(self.close) @busy_effect def find_julia_version(self): julia_path = self._toolbox.qsettings().value("appSettings/juliaPath", defaultValue="") if julia_path != "": self.julia_exe = julia_path else: self.julia_exe = JULIA_EXECUTABLE self.julia_project_path = self._toolbox.qsettings().value( "appSettings/juliaProjectPath", defaultValue="") if self.julia_project_path == "": self.julia_project_path = "@." args = list() args.append("-e") args.append("println(VERSION)") exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, silent=True) exec_mngr.start_execution() if exec_mngr.wait_for_process_finished(msecs=5000): self._julia_version = exec_mngr.process_output args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append("println(Base.active_project())") exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, silent=True) exec_mngr.start_execution() if exec_mngr.wait_for_process_finished(msecs=5000): self._julia_active_project = exec_mngr.process_output def _make_processing_state(self, name, text): s = self._make_state(name) s.assignProperty(self.label_msg, "text", text) s.assignProperty(self.button_right, "visible", False) s.assignProperty(self.label_loader, "visible", True) s.assignProperty(self.button_left, "visible", False) return s def _make_report_state(self, name, text): s = self._make_state(name) s.assignProperty(self.label_msg, "text", text) s.assignProperty(self.button_right, "visible", False) s.assignProperty(self.label_loader, "visible", False) s.assignProperty(self.button_left, "visible", True) s.assignProperty(self.button_left, "text", "Close") return s def _make_prompt_state(self, name, text): s = self._make_state(name) s.assignProperty(self.label_msg, "text", text) s.assignProperty(self.button_right, "visible", True) s.assignProperty(self.button_right, "text", "Allow") s.assignProperty(self.label_loader, "visible", False) s.assignProperty(self.button_left, "visible", True) s.assignProperty(self.button_left, "text", "Cancel") return s def _make_report_julia_not_found(self): return self._make_report_state( "report_julia_not_found", "<html><p>Unable to find Julia. Make sure that Julia is installed correctly and try again.</p></html>", ) def _make_report_bad_julia_version(self): return self._make_report_state( "report_bad_julia_version", f"<html><p>SpineModel.jl requires Julia version >= {self._required_julia_version}, whereas current version is {self._julia_version}. " "Upgrade Julia and try again.</p></html>", ) def _make_welcome(self): self.find_julia_version() if self._julia_version is None: return self._make_report_julia_not_found() if self._julia_version < self._required_julia_version: return self._make_report_bad_julia_version() return super()._make_welcome() def _make_updating_spine_model(self): return self._make_processing_state( "updating_spine_model", "<html><p>Updating SpineModel.jl to the latest version...</p></html>" ) def _make_prompt_to_install_latest_spine_model(self): return self._make_prompt_state( "prompt_to_install_latest_spine_model", "<html><p>Spine Toolbox needs to do the following modifications to the Julia project " f"at <b>{self._julia_active_project}</b>:</p><p>Install the SpineModel.jl package</p>", ) def _make_installing_latest_spine_model(self): return self._make_processing_state( "installing_latest_spine_model", "<html><p>Installing latest SpineModel.jl...</p></html>") def _make_report_spine_model_installation_failed(self): return self._make_report_state( "report_spine_model_installation_failed", "<html><p>SpineModel.jl installation failed. See Process log for error messages.</p></html>", ) def _make_checking_py_call_program(self): return self._make_processing_state( "checking_py_call_program", "<html><p>Checking PyCall.jl's configuration...</p></html>") def _make_prompt_to_reconfigure_py_call(self): return self._make_prompt_state( "prompt_to_reconfigure_py_call", "<html><p>Spine Toolbox needs to do the following modifications to the Julia project " f"at <b>{self._julia_active_project}</b>:</p>" f"<p>Change the Python program used by PyCall.jl to {sys.executable}</p>", ) def _make_prompt_to_install_py_call(self): return self._make_prompt_state( "prompt_to_install_py_call", "<html><p>Spine Toolbox needs to do the following modifications to the Julia project " f"at <b>{self._julia_active_project}</b>:</p>" "<p>Install the PyCall.jl package.</p>", ) def _make_report_spine_model_ready(self): return self._make_report_state( "report_spine_model_ready", "<html><p>SpineModel.jl has been successfully configured.</p></html>" ) def _make_reconfiguring_py_call(self): return self._make_processing_state( "reconfiguring_py_call", "<html><p>Reconfiguring PyCall.jl...</p></html>") def _make_installing_py_call(self): return self._make_processing_state( "installing_py_call", "<html><p>Installing PyCall.jl...</p></html>") def _make_report_py_call_process_failed(self): return self._make_report_state( "report_py_call_process_failed", "<html><p>PyCall.jl installation/reconfiguration failed. See Process log for error messages.</p></html>", ) @Slot() def update_spine_model(self): args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append("using Pkg; Pkg.update(ARGS[1]);") args.append("SpineModel") self.exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, semisilent=True) self.exec_mngr.execution_finished.connect( self._handle_spine_model_process_finished) self.exec_mngr.start_execution() @Slot() def install_spine_model(self): args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append( "using Pkg; Pkg.add(PackageSpec(url=ARGS[1])); Pkg.add(PackageSpec(url=ARGS[2]));" ) args.append("https://github.com/Spine-project/SpineInterface.jl.git") args.append("https://github.com/Spine-project/Spine-Model.git") self.exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, semisilent=True) self.exec_mngr.execution_finished.connect( self._handle_spine_model_process_finished) self.exec_mngr.start_execution() @Slot(int) def _handle_spine_model_process_finished(self, ret): self.exec_mngr.execution_finished.disconnect( self._handle_spine_model_process_finished) if ret == 0: self.py_call_program_check_needed.emit() else: self.spine_model_process_failed.emit() @Slot() def check_py_call_program(self): args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append("using PyCall; println(PyCall.pyprogramname);") self.exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, silent=True) self.exec_mngr.execution_finished.connect( self._handle_check_py_call_program_finished) self.exec_mngr.start_execution() @Slot(int) def _handle_check_py_call_program_finished(self, ret): self.exec_mngr.execution_finished.disconnect( self._handle_check_py_call_program_finished) if ret == 0: self._py_call_program = self.exec_mngr.process_output if self._py_call_program == sys.executable: self.spine_model_ready.emit() else: self.py_call_reconfiguration_needed.emit() else: self.py_call_installation_needed.emit() @Slot() def reconfigure_py_call(self): """Starts process that reconfigures PyCall to use given python program. """ args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append( "using PyCall; ENV[ARGS[1]] = ARGS[2]; using Pkg; Pkg.build(ARGS[3]);" ) args.append("PYTHON") args.append(sys.executable) args.append("PyCall") self.exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, semisilent=True) self.exec_mngr.execution_finished.connect( self._handle_reconfigure_py_call_finished) self.exec_mngr.start_execution() @Slot(int) def _handle_reconfigure_py_call_finished(self, ret): self.exec_mngr.execution_finished.disconnect( self._handle_reconfigure_py_call_finished) if ret == 0: self.spine_model_ready.emit() else: self.py_call_process_failed.emit() @Slot() def install_py_call(self): """Starts process that installs PyCall in current julia version. """ args = list() args.append(f"--project={self.julia_project_path}") args.append("-e") args.append("using Pkg; Pkg.add(ARGS[1]);") args.append("PyCall") self.exec_mngr = QProcessExecutionManager(self._toolbox, self.julia_exe, args, semisilent=True) self.exec_mngr.execution_finished.connect( self._handle_install_py_call_finished) self.exec_mngr.start_execution() @Slot(int) def _handle_install_py_call_finished(self, ret): self.exec_mngr.execution_finished.disconnect( self._handle_install_py_call_finished) if ret == 0: self.py_call_program_check_needed.emit() else: self.py_call_process_failed.emit() def set_up_machine(self): super().set_up_machine() # States updating_spine_model = self._make_updating_spine_model() prompt_to_install_latest_spine_model = self._make_prompt_to_install_latest_spine_model( ) installing_latest_spine_model = self._make_installing_latest_spine_model( ) report_spine_model_installation_failed = self._make_report_spine_model_installation_failed( ) checking_py_call_program = self._make_checking_py_call_program() prompt_to_reconfigure_py_call = self._make_prompt_to_reconfigure_py_call( ) prompt_to_install_py_call = self._make_prompt_to_install_py_call() report_spine_model_ready = self._make_report_spine_model_ready() reconfiguring_py_call = self._make_reconfiguring_py_call() installing_py_call = self._make_installing_py_call() report_py_call_process_failed = self._make_report_py_call_process_failed( ) # Transitions self.welcome.addTransition(self.welcome.finished, updating_spine_model) updating_spine_model.addTransition( self.spine_model_process_failed, prompt_to_install_latest_spine_model) updating_spine_model.addTransition(self.py_call_program_check_needed, checking_py_call_program) prompt_to_install_latest_spine_model.addTransition( self.button_right.clicked, installing_latest_spine_model) installing_latest_spine_model.addTransition( self.spine_model_process_failed, report_spine_model_installation_failed) installing_latest_spine_model.addTransition( self.py_call_program_check_needed, checking_py_call_program) checking_py_call_program.addTransition( self.py_call_reconfiguration_needed, prompt_to_reconfigure_py_call) checking_py_call_program.addTransition( self.py_call_installation_needed, prompt_to_install_py_call) checking_py_call_program.addTransition(self.py_call_process_failed, report_py_call_process_failed) checking_py_call_program.addTransition(self.spine_model_ready, report_spine_model_ready) prompt_to_reconfigure_py_call.addTransition(self.button_right.clicked, reconfiguring_py_call) prompt_to_install_py_call.addTransition(self.button_right.clicked, installing_py_call) reconfiguring_py_call.addTransition(self.py_call_process_failed, report_py_call_process_failed) reconfiguring_py_call.addTransition(self.spine_model_ready, report_spine_model_ready) installing_py_call.addTransition(self.py_call_process_failed, report_py_call_process_failed) installing_py_call.addTransition(self.py_call_program_check_needed, checking_py_call_program) # Entered updating_spine_model.entered.connect(self.update_spine_model) checking_py_call_program.entered.connect(self.check_py_call_program) installing_latest_spine_model.entered.connect(self.install_spine_model) reconfiguring_py_call.entered.connect(self.reconfigure_py_call) installing_py_call.entered.connect(self.install_py_call)
def test_execute_nothing(self): logger = MagicMock() manager = QProcessExecutionManager(logger) manager.start_execution() self.assertFalse(manager.wait_for_process_finished()) self.assertTrue(manager.process_failed_to_start)