# -*- coding: utf-8 -*- # pylint: disable=protected-access # ----------------------------------------------------------------------------- # Copyright (c) 2016-2017 Anaconda, Inc. # # May be copied and distributed freely only as part of an Anaconda or # Miniconda installation. # ----------------------------------------------------------------------------- """Components for environment management.""" __all__ = ['ApplicationsComponent'] import contextlib import html import os import typing import webbrowser from distutils.version import LooseVersion from qtpy import QtCore from anaconda_navigator.api import external_apps from anaconda_navigator import config as anaconda_config from anaconda_navigator.utils import constants from anaconda_navigator.utils import launch from anaconda_navigator.widgets import dialogs from anaconda_navigator.widgets.dialogs import environment as environment_dialogs from . import common if typing.TYPE_CHECKING: import typing_extensions from anaconda_navigator.widgets import main_window class ApplicationsComponent(common.Component): """Component for launching third-party applications.""" __alias__ = 'applications' def __init__(self, parent: 'main_window.MainWindow') -> None: """Initialize new :class:`~EnvironmentsComponent` instance.""" super().__init__(parent=parent) self.__running_processes: 'typing_extensions.Final[typing.List[launch.RunningProcess]]' = [] self.__listener_timer: 'typing_extensions.Final[QtCore.QTimer]' = QtCore.QTimer() self.__listener_timer.timeout.connect(self.update_running_processes) self.__listener_timer.setInterval(5000) self.__listener_timer.start() self.__feedback_timer: typing.Optional[QtCore.QTimer] = None @property def running_processes(self) -> typing.List[launch.RunningProcess]: """Collection of currently launched processes.""" return self.__running_processes[:] def update_running_processes(self) -> None: """Update status of applications launched from Navigator.""" index: int for index in reversed(range(len(self.__running_processes))): running_application: launch.RunningProcess = self.__running_processes[index] if running_application.return_code is None: continue if (running_application.return_code != 0) and (running_application.age.total_seconds() < 15.0): self.show_application_launch_errors(running_application) del self.__running_processes[index] running_application.cleanup() def launch_application( # pylint: disable=too-many-arguments self, package_name: str, command: str, extra_arguments: typing.Iterable[typing.Any], leave_path_alone: bool, prefix: str, sender: str, # pylint: disable=unused-argument non_conda: bool, app_type: constants.AppType, ) -> None: """ Launch application from home screen. :param package_name: Name of the conda package, or alias of the external application. :param command: Exact command to launch application with. :param extra_arguments: Additional arguments to attach to command. :param prefix: Conda prefix, which should be active. :param app_type: Type of the application. """ self.main_window.update_status(action=f'Launching {package_name}', value=0, max_value=0) def next_step(*args: typing.Any) -> None: # pylint: disable=unused-argument self.__launch_application( package_name=package_name, command=command, extra_arguments=extra_arguments, leave_path_alone=leave_path_alone, prefix=prefix, non_conda=non_conda, ) if app_type == constants.AppType.CONDA: next_step() elif app_type == constants.AppType.INSTALLABLE: app = external_apps.apps[package_name]( config=self.main_window.api.config, process_api=self.main_window.api._process_api, ) app.update_config(prefix=self.main_window.current_prefix) # Install extensions first! worker = app.install_extensions() worker.sig_finished.connect(next_step) worker.start() elif app_type == constants.AppType.WEB: webbrowser.open_new_tab(command) def __launch_application( # pylint: disable=too-many-arguments self, package_name: str, command: str, extra_arguments: typing.Iterable[typing.Any], leave_path_alone: bool, prefix: str, non_conda: bool, ) -> None: """Second phase of the :meth:`~ApplicationComponent.launch_application`.""" environment: typing.Dict[str, str] = dict(os.environ) environment.pop('QT_API') if anaconda_config.MAC: # See https://github.com/ContinuumIO/anaconda-issues/issues/3287 os.environ['LANG'] = os.environ.get('LANG') or os.environ.get('LC_ALL') or 'en_US.UTF-8' os.environ['LC_ALL'] = os.environ.get('LC_ALL') or os.environ['LANG'] # See https://github.com/ContinuumIO/navigator/issues/1233 environment['EVENT_NOKQUEUE'] = '1' running_process: typing.Optional[launch.RunningProcess] = launch.launch( root_prefix=self.main_window.api.ROOT_PREFIX, prefix=prefix, command=command, extra_arguments=extra_arguments, package_name=package_name, environment=environment, leave_path_alone=leave_path_alone, non_conda=non_conda, ) if running_process is None: return self.__running_processes.append(running_process) # Set timer def timeout() -> None: # actual feedback is done in `update_running_applications` self.main_window.update_status() self.__feedback_timer = None if self.__feedback_timer is not None: self.__feedback_timer.stop() self.__feedback_timer.timeout.disconnect() self.__feedback_timer = QtCore.QTimer() self.__feedback_timer.setSingleShot(True) self.__feedback_timer.setInterval(5000) self.__feedback_timer.timeout.connect(timeout) self.__feedback_timer.start() def show_application_launch_errors(self, application: launch.RunningProcess) -> None: """Show a dialog with details on application launch error.""" self.main_window.update_status() if not self.main_window.config.get('main', 'show_application_launch_errors'): return content: typing.Optional[str] = application.stderr if content: content = content.strip() else: content = f'Exit code: {application.return_code}' self.main_window.tracker.track_page('/home/errors', pagetitle='Launch errors dialog.') dialogs.MessageBoxError( text=f'Application {application.package} launch may have produced errors.', title='Application launch error', error=content, report=False, learn_more=None, ).exec_() self.main_window.tracker.track_page('/home') def check_dependencies_before_install( # pylint: disable=too-many-statements self, worker, output, error, # pylint: disable=unused-argument ): """ Check if the package to be installed changes navigator dependencies. This check is made for Orange3 which is not qt5 compatible. """ if isinstance(output, dict): exception_type = str(output.get('exception_type', '')) actions = output.get('actions', {}) else: exception_type = '' actions = {} conflicts = False nav_deps_conflict = self.main_window.api.check_navigator_dependencies(actions, self.main_window.current_prefix) conflict_message = '' # Try to install in a new environment if 'UnsatisfiableError' in exception_type or nav_deps_conflict: conflicts = True # Try to set the default python to None to avoid issues that # prevent a package to be installed in a new environment due to # python pinning, fusion for 2.7, rstudio on win for 2.7 etc. self.main_window.api.conda_config_set('default_python', None) if conflicts: self.main_window.tracker.track_page( '/environments/create/conflict', pagetitle='Create new environment due to conflict import' ) dlg = environment_dialogs.ConflictDialog( parent=self.main_window, package=worker.pkgs[0], extra_message=conflict_message, current_prefix=self.main_window.current_prefix, ) self.main_window._dialog_environment_action = dlg worker_info = self.main_window.api.conda_data(prefix=self.main_window.current_prefix) worker_info.sig_chain_finished.connect(dlg.setup) if dlg.exec_(): env_prefix = dlg.prefix action_msg = f'Installing application {worker.pkgs[0]} on newenvironment {env_prefix}' if env_prefix not in dlg.environments: new_worker = self.main_window.api.create_environment( prefix=env_prefix, packages=worker.pkgs, no_default_python=True, ) # Save the old prefix in case of errors new_worker.old_prefix = worker.prefix new_worker.action_msg = action_msg new_worker.sig_finished.connect(self.main_window._conda_output_ready) new_worker.sig_partial.connect(self.main_window._conda_partial_output_ready) else: new_worker = self.main_window.api.install_packages( prefix=env_prefix, pkgs=worker.pkgs, no_default_python=True, ) # Save the old prefix in case of errors new_worker.old_prefix = worker.prefix new_worker.action = constants.ACTION_INSTALL new_worker.action_msg = action_msg new_worker.pkgs = worker.pkgs new_worker.sig_finished.connect(self.main_window._conda_output_ready) new_worker.sig_partial.connect(self.main_window._conda_partial_output_ready) self.main_window.update_status(action_msg, value=0, max_value=0) else: self.main_window.set_widgets_enabled(True) self.main_window.set_busy_status(conda=False) self.main_window.update_status() self.main_window._dialog_environment_action = None self.main_window.tracker.track_page('/environments') else: if worker.action == constants.APPLICATION_INSTALL: action_msg = f'Install application {worker.pkgs[0]} on {worker.prefix}' elif worker.action == constants.APPLICATION_UPDATE: action_msg = f'Updating application {worker.pkgs[0]} on {worker.prefix}' new_worker = self.main_window.api.install_packages( prefix=worker.prefix, pkgs=worker.pkgs, ) new_worker.action_msg = action_msg new_worker.action = worker.action new_worker.sender = worker.sender new_worker.non_conda = worker.non_conda new_worker.pkgs = worker.pkgs new_worker.sig_finished.connect(self.main_window._conda_output_ready) new_worker.sig_partial.connect(self.main_window._conda_partial_output_ready) self.main_window.update_status(action_msg, value=0, max_value=0) def check_license_requirements(self, worker, output, error): """Check if package requires licensing and try to get a trial.""" worker.output = output self.check_dependencies_before_install(worker, output, error) def conda_application_action( # pylint: disable=missing-function-docstring,too-many-arguments,too-many-statements self, action, package_name, version, sender, non_conda, app_type, ): if app_type == constants.AppType.INSTALLABLE and action == constants.APPLICATION_INSTALL: self.install_external_app(package_name) return self.main_window.tab_home.set_widgets_enabled(False) if 'environments' in self.main_window.components: self.main_window.components.environments.tab.set_widgets_enabled(False) self.main_window.set_busy_status(conda=True) current_version = self.main_window.api.conda_package_version( pkg=package_name, prefix=self.main_window.current_prefix, ) if version: pkgs = [f'{package_name}=={version}'] else: pkgs = [f'{package_name}'] if action == constants.APPLICATION_INSTALL: worker = self.main_window.api.install_packages( prefix=self.main_window.current_prefix, pkgs=pkgs, dry_run=True, ) text_action = 'Installing' if current_version: with contextlib.suppress(BaseException): cur_ver = LooseVersion(current_version) ver = LooseVersion(version) if cur_ver > ver: text_action = 'Downgrading' elif cur_ver < ver: text_action = 'Upgrading' action_msg = ( f'{html.escape(text_action)} application {html.escape(package_name)} on ' f'{html.escape(self.main_window.current_prefix)}' ) worker.prefix = self.main_window.current_prefix worker.action = action worker.action_msg = action_msg worker.sender = sender worker.pkgs = pkgs worker.non_conda = non_conda worker.sig_finished.connect(self.check_license_requirements) worker.sig_partial.connect(self.main_window._conda_partial_output_ready) elif action == constants.APPLICATION_UPDATE: worker = self.main_window.api.install_packages( prefix=self.main_window.current_prefix, pkgs=pkgs, dry_run=True, ) action_msg = ( f'Updating application {html.escape(package_name)} on ' f'{html.escape(self.main_window.current_prefix)}' ) worker.prefix = self.main_window.current_prefix worker.action = action worker.action_msg = action_msg worker.sender = sender worker.pkgs = pkgs worker.non_conda = non_conda worker.sig_finished.connect(self.check_license_requirements) worker.sig_partial.connect(self.main_window._conda_partial_output_ready) elif action == constants.APPLICATION_REMOVE: worker = self.main_window.api.remove_packages(prefix=self.main_window.current_prefix, pkgs=pkgs) action_msg = ( f'Removing application {html.escape(package_name)} from ' f'{html.escape(self.main_window.current_prefix)}' ) worker.action = action worker.action_msg = action_msg worker.sender = sender worker.pkgs = pkgs worker.non_conda = non_conda worker.sig_finished.connect(self.main_window._conda_output_ready) worker.sig_partial.connect(self.main_window._conda_partial_output_ready) self.main_window.update_status(action_msg, value=0, max_value=0) def install_external_app(self, app_name): """Installing external app (VSCode, pycharm).""" external_apps.apps[app_name]( config=self.main_window.config, process_api=self.main_window.api._process_api, ).install() def setup(self, worker: typing.Any, output: typing.Any, error: str, initial: bool) -> None: """Perform component configuration from `conda_data`.""" prefix: str for prefix in output['processed_info']['__environments']: launch.remove_package_logs(root_prefix=self.main_window.api.ROOT_PREFIX, prefix=prefix)