class CondaProcess(QObject): """Conda API modified to work with QProcess instead of popen.""" # Signals sig_finished = Signal(str, object, str) sig_partial = Signal(str, object, str) sig_started = Signal() ENCODING = "ascii" ROOT_PREFIX = None def __init__(self, parent): QObject.__init__(self, parent) self._parent = parent self._output = None self._partial = None self._stdout = None self._error = None self._parse = False self._function_called = "" self._name = None self._process = QProcess() self.set_root_prefix() # Signals self._process.finished.connect(self._call_conda_ready) self._process.readyReadStandardOutput.connect(self._call_conda_partial) # --- Helpers # ------------------------------------------------------------------------- def _is_running(self): return self._process.state() != QProcess.NotRunning def _is_not_running(self): return self._process.state() == QProcess.NotRunning def _call_conda_partial(self): """ """ stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(stdout, CondaProcess.ENCODING) stderr = self._process.readAllStandardError() stderr = handle_qbytearray(stderr, CondaProcess.ENCODING) if self._parse: try: self._output = json.loads(stdout) except Exception: # Result is a partial json. Can only be parsed when finished self._output = stdout else: self._output = stdout self._partial = self._output self._stdout = self._output self._error = stderr self.sig_partial.emit(self._function_called, self._partial, self._error) def _call_conda_ready(self): """Function called when QProcess in _call_conda finishes task.""" function = self._function_called if self._stdout is None: stdout = to_text_string(self._process.readAllStandardOutput(), encoding=CondaProcess.ENCODING) else: stdout = self._stdout if self._error is None: stderr = to_text_string(self._process.readAllStandardError(), encoding=CondaProcess.ENCODING) else: stderr = self._error if function == "get_conda_version": pat = re.compile(r"conda:?\s+(\d+\.\d\S+|unknown)") m = pat.match(stderr.strip()) if m is None: m = pat.match(stdout.strip()) if m is None: raise Exception("output did not match: {0}".format(stderr)) self._output = m.group(1) elif function == "config_path": result = self._output self._output = result["rc_path"] elif function == "config_get": result = self._output self._output = result["get"] elif ( function == "config_delete" or function == "config_add" or function == "config_set" or function == "config_remove" ): result = self._output self._output = result.get("warnings", []) elif function == "pip": result = [] lines = self._output.split("\n") for line in lines: if "<pip>" in line: temp = line.split()[:-1] + ["pip"] result.append("-".join(temp)) self._output = result if stderr.strip(): self._error = stderr self._parse = False self.sig_finished.emit(self._function_called, self._output, self._error) def _get_prefix_envname_helper(self, name, envs): """ """ if name == "root": return CondaProcess.ROOT_PREFIX for prefix in envs: if basename(prefix) == name: return prefix return None def _abspath(self, abspath): """ """ if abspath: if sys.platform == "win32": python = join(CondaProcess.ROOT_PREFIX, "python.exe") conda = join(CondaProcess.ROOT_PREFIX, "Scripts", "conda-script.py") else: python = join(CondaProcess.ROOT_PREFIX, "bin/python") conda = join(CondaProcess.ROOT_PREFIX, "bin/conda") cmd_list = [python, conda] else: # Just use whatever conda is on the path cmd_list = ["conda"] return cmd_list def _call_conda(self, extra_args, abspath=True): """ """ if self._is_not_running(): cmd_list = self._abspath(abspath) cmd_list.extend(extra_args) self._error, self._output = None, None self._process.start(cmd_list[0], cmd_list[1:]) self.sig_started.emit() def _call_and_parse(self, extra_args, abspath=True): """ """ self._parse = True self._call_conda(extra_args, abspath=abspath) def _setup_install_commands_from_kwargs(self, kwargs, keys=tuple()): """ """ cmd_list = [] if kwargs.get("override_channels", False) and "channel" not in kwargs: raise TypeError("conda search: override_channels requires channel") if "env" in kwargs: cmd_list.extend(["--name", kwargs.pop("env")]) if "prefix" in kwargs: cmd_list.extend(["--prefix", kwargs.pop("prefix")]) if "channel" in kwargs: channel = kwargs.pop("channel") if isinstance(channel, str): cmd_list.extend(["--channel", channel]) else: cmd_list.append("--channel") cmd_list.extend(channel) for key in keys: if key in kwargs and kwargs[key]: cmd_list.append("--" + key.replace("_", "-")) return cmd_list # --- Public api # ------------------------------------------------------------------------ def linked(self, prefix): """ Return the (set of canonical names) of linked packages in `prefix`. """ if not isdir(prefix): raise Exception("no such directory: {0}".format(prefix)) meta_dir = join(prefix, "conda-meta") if not isdir(meta_dir): # we might have nothing in linked (and no conda-meta directory) result = set() result = set(fn[:-5] for fn in os.listdir(meta_dir) if fn.endswith(".json")) self.sig_finished.emit("linked", result, "") return result def split_canonical_name(self, cname): """ Split a canonical package name into (name, version, build) strings. """ result = tuple(cname.rsplit("-", 2)) self.sig_finished.emit("split_canonical_name", result, "") return result def set_root_prefix(self, prefix=None): """ Set the prefix to the root environment (default is /opt/anaconda). """ if prefix: CondaProcess.ROOT_PREFIX = prefix else: # Find conda instance, and then use info() to get 'root_prefix' if CondaProcess.ROOT_PREFIX is None: info = self.info(abspath=False) CondaProcess.ROOT_PREFIX = info["root_prefix"] def get_conda_version(self): """ Return the version of conda being used (invoked) as a string. """ if self._is_not_running(): self._function_called = "get_conda_version" self._call_conda(["--version"]) def get_envs(self): """ Return all of the (named) environment (this does not include the root environment), as a list of absolute path to their prefixes. """ if self._is_not_running(): info = self.info() result = info["envs"] self.sig_finished.emit("get_envs", result, "") return result def get_prefix_envname(self, name): """ Given the name of an environment return its full prefix path, or None if it cannot be found. """ if self._is_not_running(): if name == "root": pref = CondaProcess.ROOT_PREFIX for prefix in self.get_envs(): if basename(prefix) == name: pref = prefix break self.sig_finished.emit("get_prefix_envname", pref, "") return pref def info(self, abspath=True): """ Return a dictionary with configuration information. No guarantee is made about which keys exist. Therefore this function should only be used for testing and debugging. """ if self._is_not_running(): qprocess = QProcess() cmd_list = self._abspath(abspath) cmd_list.extend(["info", "--json"]) qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) info = json.loads(output) self.sig_finished.emit("info", str(info), "") return info def package_info(self, package, abspath=True): """ Return a dictionary with package information. Structure is { 'package_name': [{ 'depends': list, 'version': str, 'name': str, 'license': str, 'fn': ..., ... }] } """ if self._is_not_running(): self._function_called = "package_info" self._call_and_parse(["info", package, "--json"], abspath=abspath) def search(self, regex=None, spec=None, **kwargs): """ Search for packages. """ cmd_list = ["search", "--json"] if regex and spec: raise TypeError("conda search: only one of regex or spec allowed") if regex: cmd_list.append(regex) if spec: cmd_list.extend(["--spec", spec]) if "platform" in kwargs: platform = kwargs.pop("platform") platforms = ("win-32", "win-64", "osx-64", "linux-32", "linux-64") if platform not in platforms: raise TypeError("conda search: platform must be one of " + ", ".join(platforms)) cmd_list.extend(["--platform", platform]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ("canonical", "unknown", "use_index_cache", "outdated", "override_channels") ) ) if self._is_not_running(): self._function_called = "search" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def share(self, prefix): """ Create a "share package" of the environment located in `prefix`, and return a dictionary with (at least) the following keys: - 'path': the full path to the created package - 'warnings': a list of warnings This file is created in a temp directory, and it is the callers responsibility to remove this directory (after the file has been handled in some way). """ if self._is_not_running: self._function_called = "share" self._call_and_parse(["share", "--json", "--prefix", prefix]) def create(self, name=None, path=None, pkgs=None): """ Create an environment either by name or path with a specified set of packages """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError("must specify a list of one or more packages to " "install into new environment") cmd_list = ["create", "--yes", "--quiet"] if name: ref = name search = [os.path.join(d, name) for d in self.info()["envs_dirs"]] cmd_list = ["create", "--yes", "--quiet", "--name", name] elif path: ref = path search = [path] cmd_list = ["create", "--yes", "--quiet", "--prefix", path] else: raise TypeError("must specify either an environment name or a path" " for new environment") if any(os.path.exists(path) for path in search): raise CondaEnvExistsError("Conda environment [%s] already exists" % ref) cmd_list.extend(pkgs) if self._is_not_running: self._function_called = "create" self._call_conda(cmd_list) def install(self, name=None, path=None, pkgs=None, dep=True): """ Install packages into an environment either by name or path with a specified set of packages """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError("must specify a list of one or more packages to " "install into existing environment") cmd_list = ["install", "--yes", "--json", "--force-pscheck"] if name: cmd_list.extend(["--name", name]) elif path: cmd_list.extend(["--prefix", path]) else: # just install into the current environment, whatever that is pass cmd_list.extend(pkgs) if not dep: cmd_list.extend(["--no-deps"]) if self._is_not_running: self._function_called = "install" self._call_conda(cmd_list) def update(self, *pkgs, **kwargs): """ Update package(s) (in an environment) by name. """ cmd_list = ["update", "--json", "--quiet", "--yes"] if not pkgs and not kwargs.get("all"): raise TypeError( "Must specify at least one package to update, \ or all=True." ) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ( "dry_run", "no_deps", "override_channels", "no_pin", "force", "all", "use_index_cache", "use_local", "alt_hint", ), ) ) cmd_list.extend(pkgs) if self._is_not_running: self._function_called = "update" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def remove(self, *pkgs, **kwargs): """ Remove a package (from an environment) by name. Returns { success: bool, (this is always true), (other information) } """ cmd_list = ["remove", "--json", "--quiet", "--yes", "--force-pscheck"] if not pkgs and not kwargs.get("all"): raise TypeError( "Must specify at least one package to remove, \ or all=True." ) if kwargs.get("name") and kwargs.get("path"): raise TypeError("conda remove: At most one of name, path allowed") if kwargs.get("name"): cmd_list.extend(["--name", kwargs.pop("name")]) if kwargs.get("path"): cmd_list.extend(["--prefix", kwargs.pop("path")]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ("dry_run", "features", "override_channels", "no_pin", "force", "all") ) ) cmd_list.extend(pkgs) if self._is_not_running: self._function_called = "remove" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def remove_environment(self, name=None, path=None, **kwargs): """ Remove an environment entirely. See ``remove``. """ if self._is_not_running: self._function_called = "remove_environment" self.remove(name=name, path=path, all=True, **kwargs) def clone_environment(self, clone, name=None, path=None, **kwargs): """ Clone the environment ``clone`` into ``name`` or ``path``. """ cmd_list = ["create", "--json", "--quiet"] if (name and path) or not (name or path): raise TypeError( "conda clone_environment: exactly one of name or \ path required" ) if name: cmd_list.extend(["--name", name]) if path: cmd_list.extend(["--prefix", path]) cmd_list.extend(["--clone", clone]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ( "dry_run", "unknown", "use_index_cache", "use_local", "no_pin", "force", "all", "channel", "override_channels", "no_default_packages", ), ) ) if self._is_not_running(): self._function_called = "clone_environment" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def clone(self, path, prefix): """ Clone a "share package" (located at `path`) by creating a new environment at `prefix`, and return a dictionary with (at least) the following keys: - 'warnings': a list of warnings The directory `path` is located in, should be some temp directory or some other directory OUTSIDE /opt/anaconda. After calling this function, the original file (at `path`) may be removed (by the caller of this function). The return object is a list of warnings. """ if self._process.state() == QProcess.NotRunning: self._function_called = "clone" self._call_and_parse(["clone", "--json", "--prefix", prefix, path]) def _setup_config_from_kwargs(kwargs): cmd_list = ["--json", "--force"] if "file" in kwargs: cmd_list.extend(["--file", kwargs["file"]]) if "system" in kwargs: cmd_list.append("--system") return cmd_list def config_path(self, **kwargs): """ Get the path to the config file. """ cmd_list = ["config", "--get"] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_path" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def config_get(self, *keys, **kwargs): """ Get the values of configuration keys. Returns a dictionary of values. Note, the key may not be in the dictionary if the key wasn't set in the configuration file. """ cmd_list = ["config", "--get"] cmd_list.extend(keys) cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_get" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def config_set(self, key, value, **kwargs): """ Set a key to a (bool) value. Returns a list of warnings Conda may have emitted. """ cmd_list = ["config", "--set", key, str(value)] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_set" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def config_add(self, key, value, **kwargs): """ Add a value to a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ["config", "--add", key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_add" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def config_remove(self, key, value, **kwargs): """ Remove a value from a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ["config", "--remove", key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_remove" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def config_delete(self, key, **kwargs): """ Remove a key entirely. Returns a list of warnings Conda may have emitted. """ cmd_list = ["config", "--remove-key", key] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = "config_delete" self._call_and_parse(cmd_list, abspath=kwargs.get("abspath", True)) def run(self, command, abspath=True): """ Launch the specified app by name or full package name. Returns a dictionary containing the key "fn", whose value is the full package (ending in ``.tar.bz2``) of the app. """ cmd_list = ["run", "--json", command] if self._is_not_running: self._function_called = "run" self._call_and_parse(cmd_list, abspath=abspath) # --- Additional methods not in conda-api # ------------------------------------------------------------------------ def pip(self, name): """Get list of pip installed packages.""" cmd_list = ["list", "-n", name] if self._is_not_running: self._function_called = "pip" self._call_conda(cmd_list) def dependencies(self, name=None, path=None, pkgs=None, dep=True): """ Install packages into an environment either by name or path with a specified set of packages. """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError("must specify a list of one or more packages to " "install into existing environment") cmd_list = ["install", "--dry-run", "--json"] cmd_list = ["install", "--dry-run", "--json", "--force-pscheck"] if not dep: cmd_list.extend(["--no-deps"]) if name: cmd_list.extend(["--name", name]) elif path: cmd_list.extend(["--prefix", path]) else: pass cmd_list.extend(pkgs) if self._is_not_running: self._function_called = "install_dry" self._call_and_parse(cmd_list)
class ProfilerWidget(QWidget): """ Profiler widget """ DATAPATH = get_conf_path('profiler.results') VERSION = '0.0.1' redirect_stdio = Signal(bool) def __init__(self, parent, max_entries=100): QWidget.__init__(self, parent) self.setWindowTitle("Profiler") self.output = None self.error_output = None self._last_wdir = None self._last_args = None self._last_pythonpath = None self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton(self, icon=ima.icon('run'), text=_("Profile"), tip=_("Run profiler"), triggered=lambda : self.start(), text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current profiling"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) #self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data) # FIXME: The combobox emits this signal on almost any event # triggering show_data() too early, too often. browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python script'), triggered=self.select_file) self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Show program's output"), triggered=self.show_log) self.datatree = ProfilerDataTree(self) self.collapse_button = create_toolbutton(self, icon=ima.icon('collapse'), triggered=lambda dD: self.datatree.change_view(-1), tip=_('Collapse one level up')) self.expand_button = create_toolbutton(self, icon=ima.icon('expand'), triggered=lambda dD: self.datatree.change_view(1), tip=_('Expand one level down')) self.save_button = create_toolbutton(self, text_beside_icon=True, text=_("Save data"), icon=ima.icon('filesave'), triggered=self.save_data, tip=_('Save profiling data')) self.load_button = create_toolbutton(self, text_beside_icon=True, text=_("Load data"), icon=ima.icon('fileimport'), triggered=self.compare, tip=_('Load profiling data for comparison')) self.clear_button = create_toolbutton(self, text_beside_icon=True, text=_("Clear comparison"), icon=ima.icon('editdelete'), triggered=self.clear) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.collapse_button) hlayout2.addWidget(self.expand_button) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) hlayout2.addWidget(self.save_button) hlayout2.addWidget(self.load_button) hlayout2.addWidget(self.clear_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.datatree) self.setLayout(layout) self.process = None self.set_running_state(False) self.start_button.setEnabled(False) self.clear_button.setEnabled(False) if not is_profiler_installed(): # This should happen only on certain GNU/Linux distributions # or when this a home-made Python build because the Python # profilers are included in the Python standard library for widget in (self.datatree, self.filecombo, self.start_button, self.stop_button): widget.setDisabled(True) url = 'http://docs.python.org/library/profile.html' text = '%s <a href=%s>%s</a>' % (_('Please install'), url, _("the Python profiler modules")) self.datelabel.setText(text) else: pass # self.show_data() def save_data(self): """Save data""" title = _( "Save profiler result") filename, _selfilter = getsavefilename(self, title, getcwd(), _("Profiler result")+" (*.Result)") if filename: self.datatree.save_data(filename) def compare(self): filename, _selfilter = getopenfilename(self, _("Select script to compare"), getcwd(), _("Profiler result")+" (*.Result)") if filename: self.datatree.compare(filename) self.show_data() self.clear_button.setEnabled(True) def clear(self): self.datatree.compare(None) self.datatree.hide_diff_cols(True) self.show_data() self.clear_button.setEnabled(False) def analyze(self, filename, wdir=None, args=None, pythonpath=None): if not is_profiler_installed(): return self.kill_if_running() #index, _data = self.get_data(filename) index = None # FIXME: storing data is not implemented yet if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count()-1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): if wdir is None: wdir = osp.dirname(filename) self.start(wdir, args, pythonpath) def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename(self, _("Select Python script"), getcwd(), _("Python scripts")+" (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def show_log(self): if self.output: TextEditor(self.output, title=_("Profiler output"), readonly=True, size=(700, 500)).exec_() def show_errorlog(self): if self.error_output: TextEditor(self.error_output, title=_("Profiler output"), readonly=True, size=(700, 500)).exec_() def start(self, wdir=None, args=None, pythonpath=None): filename = to_text_string(self.filecombo.currentText()) if wdir is None: wdir = self._last_wdir if wdir is None: wdir = osp.basename(filename) if args is None: args = self._last_args if args is None: args = [] if pythonpath is None: pythonpath = self._last_pythonpath self._last_wdir = wdir self._last_args = args self._last_pythonpath = pythonpath self.datelabel.setText(_('Profiling, please wait...')) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(wdir) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect(lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.process.kill) if pythonpath is not None: env = [to_text_string(_pth) for _pth in self.process.systemEnvironment()] baseshell.add_pathlist_to_PYTHONPATH(env, pythonpath) processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) self.output = '' self.error_output = '' p_args = ['-m', 'cProfile', '-o', self.DATAPATH] if os.name == 'nt': # On Windows, one has to replace backslashes by slashes to avoid # confusion with escape characters (otherwise, for example, '\t' # will be interpreted as a tabulation): p_args.append(osp.normpath(filename).replace(os.sep, '/')) else: p_args.append(filename) if args: p_args.extend(shell_split(args)) executable = sys.executable if executable.endswith("spyder.exe"): # py2exe distribution executable = "python.exe" self.process.start(executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string( locale_codec.toUnicode(qba.data()) ) if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) self.show_errorlog() # If errors occurred, show them. self.output = self.error_output + self.output # FIXME: figure out if show_data should be called here or # as a signal from the combobox self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None \ and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return self.datelabel.setText(_('Sorting data, please wait...')) QApplication.processEvents() self.datatree.load_data(self.DATAPATH) self.datatree.show_tree() text_style = "<span style=\'color: #444444\'><b>%s </b></span>" date_text = text_style % time.strftime("%d %b %Y %H:%M", time.localtime()) self.datelabel.setText(date_text)
class ProcessWorker(QObject): """ """ sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs={}): super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs self._timer = QTimer() self._process = QProcess() self._timer.setInterval(50) self._timer.timeout.connect(self._communicate) self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) json_stdout = stdout.replace('\n\x00', '') try: json_stdout = json.loads(json_stdout) except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """ """ if not self._communicate_first: if self._process.state() == QProcess.NotRunning: self.communicate() elif self._fired: self._timer.stop() def communicate(self): """ """ self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda cloud api site' not in stderr.lower(): if stderr.strip() and self._conda: raise Exception('{0}:\n' 'STDERR:\n{1}\nEND' ''.format(' '.join(self._cmd_list), stderr)) # elif stderr.strip() and self._pip: # raise PipError(self._cmd_list) else: result[-1] = '' if self._parse and stdout: try: result = json.loads(stdout), result[-1] except ValueError as error: result = stdout, error if 'error' in result[0]: error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['error']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: logger.error(str(('error', result[-1]))) self._fired = True return result def close(self): """ """ self._process.close() def is_finished(self): """ """ return self._process.state() == QProcess.NotRunning and self._fired def start(self): """ """ logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class CondaProcess(QObject): """Conda API modified to work with QProcess instead of popen.""" # Signals sig_finished = Signal(str, object, str) sig_partial = Signal(str, object, str) sig_started = Signal() ENCODING = 'ascii' ROOT_PREFIX = None def __init__(self, parent): QObject.__init__(self, parent) self._parent = parent self._output = None self._partial = None self._stdout = None self._error = None self._parse = False self._function_called = '' self._name = None self._process = QProcess() self.set_root_prefix() # Signals self._process.finished.connect(self._call_conda_ready) self._process.readyReadStandardOutput.connect(self._call_conda_partial) # --- Helpers # ------------------------------------------------------------------------- def _is_running(self): return self._process.state() != QProcess.NotRunning def _is_not_running(self): return self._process.state() == QProcess.NotRunning def _call_conda_partial(self): """ """ stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(stdout, CondaProcess.ENCODING) stderr = self._process.readAllStandardError() stderr = handle_qbytearray(stderr, CondaProcess.ENCODING) if self._parse: try: self._output = json.loads(stdout) except Exception: # Result is a partial json. Can only be parsed when finished self._output = stdout else: self._output = stdout self._partial = self._output self._stdout = self._output self._error = stderr self.sig_partial.emit(self._function_called, self._partial, self._error) def _call_conda_ready(self): """Function called when QProcess in _call_conda finishes task.""" function = self._function_called if self._stdout is None: stdout = to_text_string(self._process.readAllStandardOutput(), encoding=CondaProcess.ENCODING) else: stdout = self._stdout if self._error is None: stderr = to_text_string(self._process.readAllStandardError(), encoding=CondaProcess.ENCODING) else: stderr = self._error if function == 'get_conda_version': pat = re.compile(r'conda:?\s+(\d+\.\d\S+|unknown)') m = pat.match(stderr.strip()) if m is None: m = pat.match(stdout.strip()) if m is None: raise Exception('output did not match: {0}'.format(stderr)) self._output = m.group(1) elif function == 'config_path': result = self._output self._output = result['rc_path'] elif function == 'config_get': result = self._output self._output = result['get'] elif (function == 'config_delete' or function == 'config_add' or function == 'config_set' or function == 'config_remove'): result = self._output self._output = result.get('warnings', []) elif function == 'pip': result = [] lines = self._output.split('\n') for line in lines: if '<pip>' in line: temp = line.split()[:-1] + ['pip'] result.append('-'.join(temp)) self._output = result if stderr.strip(): self._error = stderr self._parse = False self.sig_finished.emit(self._function_called, self._output, self._error) def _abspath(self, abspath): """ """ if abspath: if sys.platform == 'win32': python = join(CondaProcess.ROOT_PREFIX, 'python.exe') conda = join(CondaProcess.ROOT_PREFIX, 'Scripts', 'conda-script.py') else: python = join(CondaProcess.ROOT_PREFIX, 'bin/python') conda = join(CondaProcess.ROOT_PREFIX, 'bin/conda') cmd_list = [python, conda] else: # Just use whatever conda/pip is on the path cmd_list = ['conda'] return cmd_list def _call_conda(self, extra_args, abspath=True): """ """ if self._is_not_running(): cmd_list = self._abspath(abspath) cmd_list.extend(extra_args) self._error, self._output = None, None self._process.start(cmd_list[0], cmd_list[1:]) self.sig_started.emit() def _call_pip(self, name=None, prefix=None, extra_args=None): """ """ if self._is_not_running(): cmd_list = self._pip_cmd(name=name, prefix=prefix) cmd_list.extend(extra_args) self._error, self._output = None, None self._parse = False self._process.start(cmd_list[0], cmd_list[1:]) self.sig_started.emit() def _call_and_parse(self, extra_args, abspath=True): """ """ self._parse = True self._call_conda(extra_args, abspath=abspath) def _setup_install_commands_from_kwargs(self, kwargs, keys=tuple()): """ """ cmd_list = [] if kwargs.get('override_channels', False) and 'channel' not in kwargs: raise TypeError('conda search: override_channels requires channel') if 'env' in kwargs: cmd_list.extend(['--name', kwargs.pop('env')]) if 'prefix' in kwargs: cmd_list.extend(['--prefix', kwargs.pop('prefix')]) if 'channel' in kwargs: channel = kwargs.pop('channel') if isinstance(channel, str): cmd_list.extend(['--channel', channel]) else: cmd_list.append('--channel') cmd_list.extend(channel) for key in keys: if key in kwargs and kwargs[key]: cmd_list.append('--' + key.replace('_', '-')) return cmd_list # --- Public api # ------------------------------------------------------------------------ @staticmethod def linked(prefix, as_spec=False): """ Return the (set of canonical names) of linked packages in `prefix`. """ if not isdir(prefix): raise Exception('no such directory: {0}'.format(prefix)) meta_dir = join(prefix, 'conda-meta') if not isdir(meta_dir): # We might have nothing in linked (and no conda-meta directory) result = set() result = set(fn[:-5] for fn in os.listdir(meta_dir) if fn.endswith('.json')) new_result = [] if as_spec: for r in result: n, v, b = CondaProcess.split_canonical_name(r) new_result.append("{0}={1}".format(n, v)) result = "\n".join(new_result) return result @staticmethod def split_canonical_name(cname): """ Split a canonical package name into (name, version, build) strings. """ result = tuple(cname.rsplit('-', 2)) return result def set_root_prefix(self, prefix=None): """ Set the prefix to the root environment (default is /opt/anaconda). """ if prefix: CondaProcess.ROOT_PREFIX = prefix else: # Find conda instance, and then use info() to get 'root_prefix' if CondaProcess.ROOT_PREFIX is None: info = self.info(abspath=False) CondaProcess.ROOT_PREFIX = info['root_prefix'] def get_conda_version(self): """ Return the version of conda being used (invoked) as a string. """ if self._is_not_running(): self._function_called = 'get_conda_version' self._call_conda(['--version']) def get_envs(self, emit=False): """ Return all of the (named) environments (this does not include the root environment), as a list of absolute paths to their prefixes. """ if self._is_not_running(): info = self.info() result = info['envs'] if emit: self.sig_finished.emit('get_envs', result, "") return result def get_prefix_envname(self, name, emit=False): """ Given the name of an environment return its full prefix path, or None if it cannot be found. """ if self._is_not_running(): if name == 'root': prefix = CondaProcess.ROOT_PREFIX for env_prefix in self.get_envs(): if basename(env_prefix) == name: prefix = env_prefix break if emit: self.sig_finished.emit('get_prefix_envname', prefix, "") return prefix def info(self, abspath=True, emit=False): """ Return a dictionary with configuration information. No guarantee is made about which keys exist. Therefore this function should only be used for testing and debugging. """ if self._is_not_running(): qprocess = QProcess() cmd_list = self._abspath(abspath) cmd_list.extend(['info', '--json']) qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) info = json.loads(output) if emit: self.sig_finished.emit("info", str(info), "") return info def package_info(self, package, abspath=True): """ Return a dictionary with package information. Structure is { 'package_name': [{ 'depends': list, 'version': str, 'name': str, 'license': str, 'fn': ..., ... }] } """ if self._is_not_running(): self._function_called = 'package_info' self._call_and_parse(['info', package, '--json'], abspath=abspath) def search(self, regex=None, spec=None, **kwargs): """ Search for packages. """ cmd_list = ['search', '--json'] if regex and spec: raise TypeError('conda search: only one of regex or spec allowed') if regex: cmd_list.append(regex) if spec: cmd_list.extend(['--spec', spec]) if 'platform' in kwargs: platform = kwargs.pop('platform') platforms = ('win-32', 'win-64', 'osx-64', 'linux-32', 'linux-64') if platform not in platforms: raise TypeError('conda search: platform must be one of ' + ', '.join(platforms)) cmd_list.extend(['--platform', platform]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('canonical', 'unknown', 'use_index_cache', 'outdated', 'override_channels'))) if self._is_not_running(): self._function_called = 'search' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def share(self, prefix): """ Create a "share package" of the environment located in `prefix`, and return a dictionary with (at least) the following keys: - 'prefix': the full path to the created package - 'warnings': a list of warnings This file is created in a temp directory, and it is the callers responsibility to remove this directory (after the file has been handled in some way). """ if self._is_not_running: self._function_called = 'share' self._call_and_parse(['share', '--json', '--prefix', prefix]) def create(self, name=None, prefix=None, pkgs=None): """ Create an environment either by 'name' or 'prefix' with a specified set of packages. """ # TODO: Fix temporal hack if not pkgs or not isinstance(pkgs, (list, tuple, str)): raise TypeError('must specify a list of one or more packages to ' 'install into new environment') cmd_list = ['create', '--yes', '--quiet', '--json', '--mkdir'] if name: ref = name search = [os.path.join(d, name) for d in self.info()['envs_dirs']] cmd_list.extend(['--name', name]) elif prefix: ref = prefix search = [prefix] cmd_list.extend(['--prefix', prefix]) else: raise TypeError("Must specify either an environment 'name' or a " "'prefix' for new environment.") if any(os.path.exists(path) for path in search): raise CondaEnvExistsError('Conda environment [%s] already exists' % ref) # TODO: Fix temporal hack if isinstance(pkgs, (list, tuple)): cmd_list.extend(pkgs) elif isinstance(pkgs, str): cmd_list.extend(['--file', pkgs]) if self._is_not_running: self._function_called = 'create' self._call_conda(cmd_list) def install(self, name=None, prefix=None, pkgs=None, dep=True): """ Install packages into an environment either by 'name' or 'prefix' with a specified set of packages """ # TODO: Fix temporal hack if not pkgs or not isinstance(pkgs, (list, tuple, str)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--yes', '--json', '--force-pscheck'] if name: cmd_list.extend(['--name', name]) elif prefix: cmd_list.extend(['--prefix', prefix]) else: # just install into the current environment, whatever that is pass # TODO: Fix temporal hack if isinstance(pkgs, (list, tuple)): cmd_list.extend(pkgs) elif isinstance(pkgs, str): cmd_list.extend(['--file', pkgs]) if not dep: cmd_list.extend(['--no-deps']) if self._is_not_running: self._function_called = 'install' self._call_conda(cmd_list) def update(self, *pkgs, **kwargs): """ Update package(s) (in an environment) by name. """ cmd_list = ['update', '--json', '--quiet', '--yes'] if not pkgs and not kwargs.get('all'): raise TypeError("Must specify at least one package to update, \ or all=True.") cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'no_deps', 'override_channels', 'no_pin', 'force', 'all', 'use_index_cache', 'use_local', 'alt_hint'))) cmd_list.extend(pkgs) if self._is_not_running: self._function_called = 'update' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def remove(self, *pkgs, **kwargs): """ Remove a package (from an environment) by name. Returns { success: bool, (this is always true), (other information) } """ cmd_list = ['remove', '--json', '--quiet', '--yes', '--force-pscheck'] if not pkgs and not kwargs.get('all'): raise TypeError("Must specify at least one package to remove, \ or all=True.") if kwargs.get('name') and kwargs.get('prefix'): raise TypeError("conda remove: At most one of 'name', 'prefix' " "allowed") if kwargs.get('name'): cmd_list.extend(['--name', kwargs.pop('name')]) if kwargs.get('prefix'): cmd_list.extend(['--prefix', kwargs.pop('prefix')]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'features', 'override_channels', 'no_pin', 'force', 'all'))) cmd_list.extend(pkgs) if self._is_not_running: self._function_called = 'remove' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def remove_environment(self, name=None, prefix=None, **kwargs): """ Remove an environment entirely. See ``remove``. """ if self._is_not_running: self._function_called = 'remove_environment' self.remove(name=name, prefix=prefix, all=True, **kwargs) def clone_environment(self, clone, name=None, prefix=None, **kwargs): """ Clone the environment ``clone`` into ``name`` or ``prefix``. """ cmd_list = ['create', '--json', '--quiet'] if (name and prefix) or not (name or prefix): raise TypeError("conda clone_environment: exactly one of 'name' " "or 'prefix' required.") if name: cmd_list.extend(['--name', name]) if prefix: cmd_list.extend(['--prefix', prefix]) cmd_list.extend(['--clone', clone]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'unknown', 'use_index_cache', 'use_local', 'no_pin', 'force', 'all', 'channel', 'override_channels', 'no_default_packages'))) if self._is_not_running(): self._function_called = 'clone_environment' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def clone(self, path, prefix): """ Clone a "share package" (located at `path`) by creating a new environment at `prefix`, and return a dictionary with (at least) the following keys: - 'warnings': a list of warnings The directory `path` is located in, should be some temp directory or some other directory OUTSIDE /opt/anaconda. After calling this function, the original file (at `path`) may be removed (by the caller of this function). The return object is a list of warnings. """ if self._process.state() == QProcess.NotRunning: self._function_called = 'clone' self._call_and_parse(['clone', '--json', '--prefix', prefix, path]) def _setup_config_from_kwargs(kwargs): cmd_list = ['--json', '--force'] if 'file' in kwargs: cmd_list.extend(['--file', kwargs['file']]) if 'system' in kwargs: cmd_list.append('--system') return cmd_list def config_path(self, **kwargs): """ Get the path to the config file. """ cmd_list = ['config', '--get'] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_path' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_get(self, *keys, **kwargs): """ Get the values of configuration keys. Returns a dictionary of values. Note, the key may not be in the dictionary if the key wasn't set in the configuration file. """ cmd_list = ['config', '--get'] cmd_list.extend(keys) cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_get' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_set(self, key, value, **kwargs): """ Set a key to a (bool) value. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--set', key, str(value)] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_set' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_add(self, key, value, **kwargs): """ Add a value to a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--add', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_add' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_remove(self, key, value, **kwargs): """ Remove a value from a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_remove' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_delete(self, key, **kwargs): """ Remove a key entirely. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove-key', key] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) if self._is_not_running: self._function_called = 'config_delete' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def run(self, command, abspath=True): """ Launch the specified app by name or full package name. Returns a dictionary containing the key "fn", whose value is the full package (ending in ``.tar.bz2``) of the app. """ cmd_list = ['run', '--json', command] if self._is_not_running: self._function_called = 'run' self._call_and_parse(cmd_list, abspath=abspath) # --- Additional methods not in conda-api # ------------------------------------------------------------------------ def dependencies(self, name=None, prefix=None, pkgs=None, dep=True): """ Get dependenciy list for packages to be installed into an environment defined either by 'name' or 'prefix'. """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--dry-run', '--json'] cmd_list = ['install', '--dry-run', '--json', '--force-pscheck'] if not dep: cmd_list.extend(['--no-deps']) if name: cmd_list.extend(['--name', name]) elif prefix: cmd_list.extend(['--prefix', prefix]) else: pass cmd_list.extend(pkgs) if self._is_not_running: self._function_called = 'install_dry' self._call_and_parse(cmd_list) def environment_exists(self, name=None, prefix=None, abspath=True, emit=False): """ Check if an environment exists by 'name' or by 'prefix'. If query is by 'name' only the default conda environments directory is searched. """ if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") qprocess = QProcess() cmd_list = self._abspath(abspath) cmd_list.extend(['list', '--json']) if name: cmd_list.extend(['--name', name]) else: cmd_list.extend(['--prefix', prefix]) qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) info = json.loads(output) if emit: self.sig_finished.emit("info", unicode(info), "") return 'error' not in info def export(self, filename, name=None, prefix=None, emit=False): """ Export environment by 'prefix' or 'name' as yaml 'filename'. """ if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") if self._is_not_running: if name: temporal_envname = name if prefix: temporal_envname = 'tempenv' + int(random.random()*10000000) envs_dir = self.info()['envs_dirs'][0] os.symlink(prefix, os.sep.join([envs_dir, temporal_envname])) cmd = self._abspath(True) cmd.extend(['env', 'export', '--name', temporal_envname, '--file', os.path.abspath(filename)]) qprocess = QProcess() qprocess.start(cmd[0], cmd[1:]) qprocess.waitForFinished() if prefix: os.unlink(os.sep.join([envs_dir, temporal_envname])) def update_environment(self, filename, name=None, prefix=None, emit=False): """ Set environment at 'prefix' or 'name' to match 'filename' spec as yaml. """ if name and prefix: raise TypeError("Exactly one of 'name' or 'prefix' is required.") if self._is_not_running: if name: temporal_envname = name if prefix: temporal_envname = 'tempenv' + int(random.random()*10000000) envs_dir = self.info()['envs_dirs'][0] os.symlink(prefix, os.sep.join([envs_dir, temporal_envname])) cmd = self._abspath(True) cmd.extend(['env', 'update', '--name', temporal_envname, '--file', os.path.abspath(filename)]) qprocess = QProcess() qprocess.start(cmd[0], cmd[1:]) qprocess.waitForFinished() if prefix: os.unlink(os.sep.join([envs_dir, 'tempenv'])) @property def error(self): return self._error @property def output(self): return self._output # --- Pip commands # ------------------------------------------------------------------------- def _pip_cmd(self, name=None, prefix=None): """ Get pip location based on environment `name` or `prefix`. """ if (name and prefix) or not (name or prefix): raise TypeError("conda pip: exactly one of 'name' ""or 'prefix' " "required.") if name and self.environment_exists(name=name): prefix = self.get_prefix_envname(name) if sys.platform == 'win32': python = join(prefix, 'python.exe') # FIXME: pip = join(prefix, 'pip.exe') # FIXME: else: python = join(prefix, 'bin/python') pip = join(prefix, 'bin/pip') cmd_list = [python, pip] return cmd_list def pip_list(self, name=None, prefix=None, abspath=True, emit=False): """ Get list of pip installed packages. """ if (name and prefix) or not (name or prefix): raise TypeError("conda pip: exactly one of 'name' ""or 'prefix' " "required.") if self._is_not_running: cmd_list = self._abspath(abspath) if name: cmd_list.extend(['list', '--name', name]) if prefix: cmd_list.extend(['list', '--prefix', prefix]) qprocess = QProcess() qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) result = [] lines = output.split('\n') for line in lines: if '<pip>' in line: temp = line.split()[:-1] + ['pip'] result.append('-'.join(temp)) if emit: self.sig_finished.emit("pip", str(result), "") return result def pip_remove(self, name=None, prefix=None, pkgs=None): """ Remove a pip pacakge in given environment by 'name' or 'prefix'. """ if isinstance(pkgs, list) or isinstance(pkgs, tuple): pkg = ' '.join(pkgs) else: pkg = pkgs extra_args = ['uninstall', '--yes', pkg] if self._is_not_running(): self._function_called = 'pip_remove' self._call_pip(name=name, prefix=prefix, extra_args=extra_args)
class ProcessWorker(QObject): """Conda worker based on a QProcess for non blocking UI.""" sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs=None): """Conda worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. parse : bool (optional) Parse json from output. pip : bool (optional) Define as a pip command. callback : func (optional) If the process has a callback to process output from comd_list. extra_kwargs : dict Arguments for the callback. """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs if extra_kwargs else {} self._timer = QTimer() self._process = QProcess() self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) # self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) json_stdout = stdout.replace('\n\x00', '') try: json_stdout = json.loads(json_stdout) except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda' not in stderr.lower(): if stderr.strip() and self._conda: logger.error('{0}:\nSTDERR:\n{1}\nEND'.format( ' '.join(self._cmd_list), stderr)) elif stderr.strip() and self._pip: logger.error("pip error: {}".format(self._cmd_list)) result[-1] = '' if self._parse and stdout: try: result = json.loads(stdout), result[-1] except Exception as error: result = stdout, str(error) if 'error' in result[0]: if not isinstance(result[0], dict): result = {'error': str(result[0])}, None error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['error']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: logger.error(str(('error', result[-1]))) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def start(self): """Start process.""" logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class AsyncClient(QObject): """ A class which handles a connection to a client through a QProcess. """ # Emitted when the client has initialized. initialized = Signal() # Emitted when the client errors. errored = Signal() # Emitted when a request response is received. received = Signal(object) def __init__(self, target, executable=None, name=None, extra_args=None, libs=None, cwd=None, env=None): super(AsyncClient, self).__init__() self.executable = executable or sys.executable self.extra_args = extra_args self.target = target self.name = name or self self.libs = libs self.cwd = cwd self.env = env self.is_initialized = False self.closing = False self.context = zmq.Context() QApplication.instance().aboutToQuit.connect(self.close) # Set up the heartbeat timer. self.timer = QTimer(self) self.timer.timeout.connect(self._heartbeat) def run(self): """Handle the connection with the server. """ # Set up the zmq port. self.socket = self.context.socket(zmq.PAIR) self.port = self.socket.bind_to_random_port('tcp://*') # Set up the process. self.process = QProcess(self) if self.cwd: self.process.setWorkingDirectory(self.cwd) p_args = ['-u', self.target, str(self.port)] if self.extra_args is not None: p_args += self.extra_args # Set up environment variables. processEnvironment = QProcessEnvironment() env = self.process.systemEnvironment() if (self.env and 'PYTHONPATH' not in self.env) or DEV: python_path = osp.dirname(get_module_path('spyderlib')) # Add the libs to the python path. for lib in self.libs: try: path = osp.dirname(imp.find_module(lib)[1]) python_path = osp.pathsep.join([python_path, path]) except ImportError: pass env.append("PYTHONPATH=%s" % python_path) if self.env: env.update(self.env) for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) # Start the process and wait for started. self.process.start(self.executable, p_args) self.process.finished.connect(self._on_finished) running = self.process.waitForStarted() if not running: raise IOError('Could not start %s' % self) # Set up the socket notifer. fid = self.socket.getsockopt(zmq.FD) self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self) self.notifier.activated.connect(self._on_msg_received) def request(self, func_name, *args, **kwargs): """Send a request to the server. The response will be a dictionary the 'request_id' and the 'func_name' as well as a 'result' field with the object returned by the function call or or an 'error' field with a traceback. """ if not self.is_initialized: return request_id = uuid.uuid4().hex request = dict(func_name=func_name, args=args, kwargs=kwargs, request_id=request_id) self._send(request) return request_id def close(self): """Cleanly close the connection to the server. """ self.closing = True self.is_initialized = False self.timer.stop() self.notifier.activated.disconnect(self._on_msg_received) self.notifier.setEnabled(False) del self.notifier self.request('server_quit') self.process.waitForFinished(1000) self.process.close() self.context.destroy() def _on_finished(self): """Handle a finished signal from the process. """ if self.closing: return if self.is_initialized: debug_print('Restarting %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.is_initialized = False self.notifier.setEnabled(False) self.run() else: debug_print('Errored %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.errored.emit() def _on_msg_received(self): """Handle a message trigger from the socket. """ self.notifier.setEnabled(False) while 1: try: resp = self.socket.recv_pyobj(flags=zmq.NOBLOCK) except zmq.ZMQError: self.notifier.setEnabled(True) return if not self.is_initialized: self.is_initialized = True debug_print('Initialized %s' % self.name) self.initialized.emit() self.timer.start(HEARTBEAT) continue resp['name'] = self.name self.received.emit(resp) def _heartbeat(self): """Send a heartbeat to keep the server alive. """ self._send(dict(func_name='server_heartbeat')) def _send(self, obj): """Send an object to the server. """ try: self.socket.send_pyobj(obj) except Exception as e: debug_print(e) self.is_initialized = False self._on_finished()
class AsyncClient(QObject): """ A class which handles a connection to a client through a QProcess. """ # Emitted when the client has initialized. initialized = Signal() # Emitted when the client errors. errored = Signal() # Emitted when a request response is received. received = Signal(object) def __init__(self, target, executable=None, name=None, extra_args=None, libs=None, cwd=None, env=None): super(AsyncClient, self).__init__() self.executable = executable or sys.executable self.extra_args = extra_args self.target = target self.name = name or self self.libs = libs self.cwd = cwd self.env = env self.is_initialized = False self.closing = False self.context = zmq.Context() QApplication.instance().aboutToQuit.connect(self.close) # Set up the heartbeat timer. self.timer = QTimer(self) self.timer.timeout.connect(self._heartbeat) def run(self): """Handle the connection with the server. """ # Set up the zmq port. self.socket = self.context.socket(zmq.PAIR) self.port = self.socket.bind_to_random_port('tcp://*') # Set up the process. self.process = QProcess(self) if self.cwd: self.process.setWorkingDirectory(self.cwd) p_args = ['-u', self.target, str(self.port)] if self.extra_args is not None: p_args += self.extra_args # Set up environment variables. processEnvironment = QProcessEnvironment() env = self.process.systemEnvironment() if (self.env and 'PYTHONPATH' not in self.env) or DEV: python_path = osp.dirname(get_module_path('spyderlib')) # Add the libs to the python path. for lib in self.libs: try: path = osp.dirname(imp.find_module(lib)[1]) python_path = osp.pathsep.join([python_path, path]) except ImportError: pass env.append("PYTHONPATH=%s" % python_path) if self.env: env.update(self.env) for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) # Start the process and wait for started. self.process.start(self.executable, p_args) self.process.finished.connect(self._on_finished) running = self.process.waitForStarted() if not running: raise IOError('Could not start %s' % self) # Set up the socket notifer. fid = self.socket.getsockopt(zmq.FD) self.notifier = QSocketNotifier(fid, QSocketNotifier.Read, self) self.notifier.activated.connect(self._on_msg_received) def request(self, func_name, *args, **kwargs): """Send a request to the server. The response will be a dictionary the 'request_id' and the 'func_name' as well as a 'result' field with the object returned by the function call or or an 'error' field with a traceback. """ if not self.is_initialized: return request_id = uuid.uuid4().hex request = dict(func_name=func_name, args=args, kwargs=kwargs, request_id=request_id) self._send(request) return request_id def close(self): """Cleanly close the connection to the server. """ self.closing = True self.is_initialized = False self.timer.stop() self.notifier.activated.disconnect(self._on_msg_received) self.notifier.setEnabled(False) del self.notifier self.request('server_quit') self.process.waitForFinished(1000) self.process.close() self.context.destroy() def _on_finished(self): """Handle a finished signal from the process. """ if self.closing: return if self.is_initialized: debug_print('Restarting %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.is_initialized = False self.notifier.setEnabled(False) self.run() else: debug_print('Errored %s' % self.name) debug_print(self.process.readAllStandardOutput()) debug_print(self.process.readAllStandardError()) self.errored.emit() def _on_msg_received(self): """Handle a message trigger from the socket. """ self.notifier.setEnabled(False) while 1: try: resp = self.socket.recv_pyobj(flags=zmq.NOBLOCK) except zmq.ZMQError: self.notifier.setEnabled(True) return if not self.is_initialized: self.is_initialized = True debug_print('Initialized %s' % self.name) self.initialized.emit() self.timer.start(HEARTBEAT) continue resp['name'] = self.name self.received.emit(resp) def _heartbeat(self): """Send a heartbeat to keep the server alive. """ self._send(dict(func_name='server_heartbeat')) def _send(self, obj): """Send an object to the server. """ try: self.socket.send_pyobj(obj, zmq.NOBLOCK) except Exception as e: debug_print(e) self.is_initialized = False self._on_finished()
class ProcessWorker(QObject): """Conda worker based on a QProcess for non blocking UI.""" sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs=None): """Conda worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. parse : bool (optional) Parse json from output. pip : bool (optional) Define as a pip command. callback : func (optional) If the process has a callback to process output from comd_list. extra_kwargs : dict Arguments for the callback. """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs if extra_kwargs else {} self._timer = QTimer() self._process = QProcess() self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) # self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) json_stdout = stdout.replace('\n\x00', '') try: json_stdout = json.loads(json_stdout) except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda' not in stderr.lower(): if stderr.strip() and self._conda: logger.error('{0}:\nSTDERR:\n{1}\nEND'.format( ' '.join(self._cmd_list), stderr)) elif stderr.strip() and self._pip: logger.error("pip error: {}".format(self._cmd_list)) result[-1] = '' if self._parse and stdout: try: result = json.loads(stdout), result[-1] except Exception as error: result = stdout, str(error) if 'error' in result[0]: if not isinstance(result[0], dict): result = {'error': str(result[0])}, None error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['error']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: logger.error(str(('error', result[-1]))) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def start(self): """Start process.""" logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class ProcessWorker(QObject): """ """ sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, parse=False, pip=False, callback=None, extra_kwargs={}): super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._parse = parse self._pip = pip self._conda = not pip self._callback = callback self._fired = False self._communicate_first = False self._partial_stdout = None self._extra_kwargs = extra_kwargs self._timer = QTimer() self._process = QProcess() self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) # self._process.finished.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _partial(self): raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) json_stdout = stdout.replace('\n\x00', '') try: json_stdout = json.loads(json_stdout) except Exception: json_stdout = stdout if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, json_stdout, None) def _communicate(self): """ """ if not self._communicate_first: if self._process.state() == QProcess.NotRunning: self.communicate() elif self._fired: self._timer.stop() def communicate(self): """ """ self._communicate_first = True self._process.waitForFinished() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8) result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)] # FIXME: Why does anaconda client print to stderr??? if PY2: stderr = stderr.decode() if 'using anaconda cloud api site' not in stderr.lower(): if stderr.strip() and self._conda: raise Exception('{0}:\n' 'STDERR:\n{1}\nEND' ''.format(' '.join(self._cmd_list), stderr)) # elif stderr.strip() and self._pip: # raise PipError(self._cmd_list) else: result[-1] = '' if self._parse and stdout: try: result = json.loads(stdout), result[-1] except ValueError as error: result = stdout, error if 'error' in result[0]: error = '{0}: {1}'.format(" ".join(self._cmd_list), result[0]['error']) result = result[0], error if self._callback: result = self._callback(result[0], result[-1], **self._extra_kwargs), result[-1] self._result = result self.sig_finished.emit(self, result[0], result[-1]) if result[-1]: logger.error(str(('error', result[-1]))) self._fired = True return result def close(self): """ """ self._process.close() def is_finished(self): """ """ return self._process.state() == QProcess.NotRunning and self._fired def start(self): """ """ logger.debug(str(' '.join(self._cmd_list))) if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() else: raise CondaProcessWorker('A Conda ProcessWorker can only run once ' 'per method call.')
class ProfilerWidget(QWidget): """ Profiler widget """ DATAPATH = get_conf_path('profiler.results') VERSION = '0.0.1' redirect_stdio = Signal(bool) def __init__(self, parent, max_entries=100, options_button=None, text_color=None): QWidget.__init__(self, parent) self.setWindowTitle("Profiler") self.output = None self.error_output = None self.text_color = text_color self._last_wdir = None self._last_args = None self._last_pythonpath = None self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton(self, icon=ima.icon('run'), text=_("Profile"), tip=_("Run profiler"), triggered=lambda: self.start(), text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current profiling"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) #self.connect(self.filecombo, SIGNAL('valid(bool)'), self.show_data) # FIXME: The combobox emits this signal on almost any event # triggering show_data() too early, too often. browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python script'), triggered=self.select_file) self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Show program's output"), triggered=self.show_log) self.datatree = ProfilerDataTree(self) self.collapse_button = create_toolbutton( self, icon=ima.icon('collapse'), triggered=lambda dD: self.datatree.change_view(-1), tip=_('Collapse one level up')) self.expand_button = create_toolbutton( self, icon=ima.icon('expand'), triggered=lambda dD: self.datatree.change_view(1), tip=_('Expand one level down')) self.save_button = create_toolbutton(self, text_beside_icon=True, text=_("Save data"), icon=ima.icon('filesave'), triggered=self.save_data, tip=_('Save profiling data')) self.load_button = create_toolbutton( self, text_beside_icon=True, text=_("Load data"), icon=ima.icon('fileimport'), triggered=self.compare, tip=_('Load profiling data for comparison')) self.clear_button = create_toolbutton(self, text_beside_icon=True, text=_("Clear comparison"), icon=ima.icon('editdelete'), triggered=self.clear) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) if options_button: hlayout1.addWidget(options_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.collapse_button) hlayout2.addWidget(self.expand_button) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) hlayout2.addWidget(self.save_button) hlayout2.addWidget(self.load_button) hlayout2.addWidget(self.clear_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.datatree) self.setLayout(layout) self.process = None self.set_running_state(False) self.start_button.setEnabled(False) self.clear_button.setEnabled(False) if not is_profiler_installed(): # This should happen only on certain GNU/Linux distributions # or when this a home-made Python build because the Python # profilers are included in the Python standard library for widget in (self.datatree, self.filecombo, self.start_button, self.stop_button): widget.setDisabled(True) url = 'https://docs.python.org/3/library/profile.html' text = '%s <a href=%s>%s</a>' % (_('Please install'), url, _("the Python profiler modules")) self.datelabel.setText(text) else: pass # self.show_data() def save_data(self): """Save data""" title = _("Save profiler result") filename, _selfilter = getsavefilename( self, title, getcwd_or_home(), _("Profiler result") + " (*.Result)") if filename: self.datatree.save_data(filename) def compare(self): filename, _selfilter = getopenfilename( self, _("Select script to compare"), getcwd_or_home(), _("Profiler result") + " (*.Result)") if filename: self.datatree.compare(filename) self.show_data() self.clear_button.setEnabled(True) def clear(self): self.datatree.compare(None) self.datatree.hide_diff_cols(True) self.show_data() self.clear_button.setEnabled(False) def analyze(self, filename, wdir=None, args=None, pythonpath=None): if not is_profiler_installed(): return self.kill_if_running() #index, _data = self.get_data(filename) index = None # FIXME: storing data is not implemented yet if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count() - 1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): if wdir is None: wdir = osp.dirname(filename) self.start(wdir, args, pythonpath) def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python script"), getcwd_or_home(), _("Python scripts") + " (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def show_log(self): if self.output: TextEditor(self.output, title=_("Profiler output"), readonly=True, size=(700, 500), parent=self).exec_() def show_errorlog(self): if self.error_output: TextEditor(self.error_output, title=_("Profiler output"), readonly=True, size=(700, 500), parent=self).exec_() def start(self, wdir=None, args=None, pythonpath=None): filename = to_text_string(self.filecombo.currentText()) if wdir is None: wdir = self._last_wdir if wdir is None: wdir = osp.basename(filename) if args is None: args = self._last_args if args is None: args = [] if pythonpath is None: pythonpath = self._last_pythonpath self._last_wdir = wdir self._last_args = args self._last_pythonpath = pythonpath self.datelabel.setText(_('Profiling, please wait...')) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(wdir) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect( lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.kill) if pythonpath is not None: env = [ to_text_string(_pth) for _pth in self.process.systemEnvironment() ] add_pathlist_to_PYTHONPATH(env, pythonpath) processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) self.output = '' self.error_output = '' self.stopped = False p_args = ['-m', 'cProfile', '-o', self.DATAPATH] if os.name == 'nt': # On Windows, one has to replace backslashes by slashes to avoid # confusion with escape characters (otherwise, for example, '\t' # will be interpreted as a tabulation): p_args.append(osp.normpath(filename).replace(os.sep, '/')) else: p_args.append(filename) if args: p_args.extend(shell_split(args)) executable = sys.executable if executable.endswith("spyder.exe"): # py2exe distribution executable = "python.exe" self.process.start(executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def kill(self): """Stop button pressed.""" self.process.kill() self.stopped = True def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string(qba.data(), encoding='utf-8') if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) self.show_errorlog() # If errors occurred, show them. self.output = self.error_output + self.output # FIXME: figure out if show_data should be called here or # as a signal from the combobox self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return if self.stopped: self.datelabel.setText(_('Run stopped by user.')) self.datatree.initialize_view() return self.datelabel.setText(_('Sorting data, please wait...')) QApplication.processEvents() self.datatree.load_data(self.DATAPATH) self.datatree.show_tree() text_style = "<span style=\'color: %s\'><b>%s </b></span>" date_text = text_style % (self.text_color, time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())) self.datelabel.setText(date_text)
class PylintWidget(QWidget): """ Pylint widget """ DATAPATH = get_conf_path('pylint.results') VERSION = '1.1.0' redirect_stdio = Signal(bool) start_analysis = Signal() def __init__(self, parent, max_entries=100, options_button=None, text_color=None, prevrate_color=None, top_max_entries=100): super().__init__(parent) self.setWindowTitle("Pylint") self.parent = parent self.output = None self.error_output = None self.filename = None self.text_color = text_color self.prevrate_color = prevrate_color self.max_entries = max_entries self.top_max_entries = top_max_entries self.rdata = [] if osp.isfile(self.DATAPATH): try: data = pickle.loads(open(self.DATAPATH, 'rb').read()) if data[0] == self.VERSION: self.rdata = data[1:] except (EOFError, ImportError): pass self.filecombo = PythonModulesComboBox(self) self.filecombo.setInsertPolicy(self.filecombo.InsertAtTop) self.start_button = create_toolbutton( self, icon=ima.icon('run'), text=_("Analyze"), tip=_("Run analysis"), triggered=self.analyze_button_handler, text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current analysis"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) self.filecombo.valid.connect(self.check_new_file) browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python file'), triggered=self.select_file) self.ratelabel = QLabel() self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Complete output"), triggered=self.show_log) self.treewidget = ResultsTree(self) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) if options_button: hlayout1.addWidget(options_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.ratelabel) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.treewidget) self.setLayout(layout) self.process = None self.set_running_state(False) self.show_data() if self.rdata: self.remove_obsolete_items() self.filecombo.insertItems(0, self.get_filenames()) self.start_button.setEnabled(self.filecombo.is_valid()) else: self.start_button.setEnabled(False) if self.parent: self.curr_filenames = self.parent.get_option( 'history_filenames', []) else: self.curr_filenames = [] for f in self.curr_filenames[::-1]: self.set_filename(f) def check_new_file(self): fname = self.get_filename() if fname != self.filename: self.filename = fname self.show_data() def get_filename(self): """Get current filename in combobox.""" return self.filecombo.currentText() @Slot(str) def set_filename(self, filename): """Set filename without performing code analysis.""" filename = str(filename) # filename is a QString instance self.kill_if_running() index, _data = self.get_data(filename) is_parent = self.parent is not None if filename not in self.curr_filenames: self.filecombo.insertItem(0, filename) self.curr_filenames.insert(0, filename) self.filecombo.setCurrentIndex(0) else: index = self.filecombo.findText(filename) self.filecombo.removeItem(index) self.curr_filenames.pop(index) self.filecombo.insertItem(0, filename) self.curr_filenames.insert(0, filename) self.filecombo.setCurrentIndex(0) num_elements = self.filecombo.count() if is_parent: if num_elements > self.parent.get_option('max_entries'): self.filecombo.removeItem(num_elements - 1) self.filecombo.selected() def change_history_limit(self, new_limit): """Change the number of files listed in the history combobox.""" if self.filecombo.count() > new_limit: num_elements = self.filecombo.count() diff = num_elements - new_limit for __ in range(diff): num_elements = self.filecombo.count() self.filecombo.removeItem(num_elements - 1) self.filecombo.selected() else: num_elements = self.filecombo.count() diff = new_limit - num_elements for i in range(num_elements, num_elements + diff): if i >= len(self.curr_filenames): break act_filename = self.curr_filenames[i] self.filecombo.insertItem(i, act_filename) def save_history(self): """Save the current history filenames.""" if self.parent: list_save_files = [] for f in self.curr_filenames: if _('untitled') not in f: list_save_files.append(f) self.curr_filenames = list_save_files[:self.top_max_entries] self.parent.set_option('history_filenames', self.curr_filenames) else: self.curr_filenames = [] def analyze(self, filename=None): """ Perform code analysis for given `filename`. If `filename` is None default to current filename in combobox. """ if filename is not None: self.set_filename(filename) if self.filecombo.is_valid(): self.start() @Slot() def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python file"), getcwd_or_home(), _("Python files") + " (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def remove_obsolete_items(self): """Removing obsolete items""" self.rdata = [(filename, data) for filename, data in self.rdata if is_module_or_package(filename)] def get_filenames(self): return [filename for filename, _data in self.rdata] def get_data(self, filename): filename = osp.abspath(filename) for index, (fname, data) in enumerate(self.rdata): if fname == filename: return index, data else: return None, None def set_data(self, filename, data): filename = osp.abspath(filename) index, _data = self.get_data(filename) if index is not None: self.rdata.pop(index) self.rdata.insert(0, (filename, data)) self.save() def save(self): while len(self.rdata) > self.max_entries: self.rdata.pop(-1) pickle.dump([self.VERSION] + self.rdata, open(self.DATAPATH, 'wb'), 2) @Slot() def show_log(self): if self.output: output_dialog = TextEditor(self.output, title=_("Pylint output"), parent=self, readonly=True) output_dialog.resize(700, 500) output_dialog.exec_() @Slot() def analyze_button_handler(self): """Try to start code analysis when Analyze button pressed.""" self.start_analysis.emit() def get_pylintrc_path(self, filename): """Get the path to the most proximate pylintrc config to the file.""" parent = self.parentWidget() if parent is not None: project_dir = parent.main.projects.get_active_project_path() else: project_dir = None search_paths = [ osp.dirname(filename), # File's directory getcwd_or_home(), # Working directory project_dir, # Project directory osp.expanduser("~"), # Home directory ] return get_pylintrc_path(search_paths=search_paths) @Slot() def start(self): """Start the code analysis.""" filename = str(self.filecombo.currentText()) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(getcwd_or_home()) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect( lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.process.kill) self.output = '' self.error_output = '' if PYLINT_VER is not None: pylint_args = [ '-m', 'pylint', '--output-format=text', '--msg-template=' "'{msg_id}:{symbol}:{line:3d},{column}: {msg}'" ] pylintrc_path = self.get_pylintrc_path(filename=filename) if pylintrc_path is not None: pylint_args += ['--rcfile={}'.format(pylintrc_path)] pylint_args.append(filename) processEnvironment = QProcessEnvironment() processEnvironment.insert("PYTHONIOENCODING", "utf8") self.process.setProcessEnvironment(processEnvironment) self.process.start(sys.executable, pylint_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = str(qba.data(), 'utf-8') if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) if not self.output: if self.error_output: QMessageBox.critical(self, _("Error"), self.error_output) print("pylint error:\n\n" + self.error_output, file=sys.stderr) return # Convention, Refactor, Warning, Error results = {'C:': [], 'R:': [], 'W:': [], 'E:': []} txt_module = '************* Module ' module = '' # Should not be needed - just in case something goes wrong for line in self.output.splitlines(): if line.startswith(txt_module): # New module module = line[len(txt_module):] continue # Supporting option include-ids: ('R3873:' instead of 'R:') if not re.match(r'^[CRWE]+([0-9]{4})?:', line): continue items = {} idx_0 = 0 idx_1 = 0 key_names = ["msg_id", "message_name", "line_nb", "message"] for key_idx, key_name in enumerate(key_names): if key_idx == len(key_names) - 1: idx_1 = len(line) else: idx_1 = line.find(":", idx_0) if idx_1 < 0: break item = line[(idx_0):idx_1] if not item: break if key_name == "line_nb": item = int(item.split(',')[0]) items[key_name] = item idx_0 = idx_1 + 1 else: pylint_item = (module, items["line_nb"], items["message"], items["msg_id"], items["message_name"]) results[line[0] + ':'].append(pylint_item) # Rate rate = None txt_rate = 'Your code has been rated at ' i_rate = self.output.find(txt_rate) if i_rate > 0: i_rate_end = self.output.find('/10', i_rate) if i_rate_end > 0: rate = self.output[i_rate + len(txt_rate):i_rate_end] # Previous run previous = '' if rate is not None: txt_prun = 'previous run: ' i_prun = self.output.find(txt_prun, i_rate_end) if i_prun > 0: i_prun_end = self.output.find('/10', i_prun) previous = self.output[i_prun + len(txt_prun):i_prun_end] filename = str(self.filecombo.currentText()) self.set_data(filename, (time.localtime(), rate, previous, results)) self.output = self.error_output + self.output self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None and len(self.output) > 0) self.kill_if_running() filename = str(self.filecombo.currentText()) if not filename: return _index, data = self.get_data(filename) if data is None: text = _('Source code has not been rated yet.') self.treewidget.clear_results() date_text = '' else: datetime, rate, previous_rate, results = data if rate is None: text = _('Analysis did not succeed ' '(see output for more details).') self.treewidget.clear_results() date_text = '' else: text_style = "<span style=\'color: %s\'><b>%s </b></span>" rate_style = "<span style=\'color: %s\'><b>%s</b></span>" prevrate_style = "<span style=\'color: %s\'>%s</span>" color = "#FF0000" if float(rate) > 5.: color = "#22AA22" elif float(rate) > 3.: color = "#EE5500" text = _('Global evaluation:') text = ((text_style % (self.text_color, text)) + (rate_style % (color, ('%s/10' % rate)))) if previous_rate: text_prun = _('previous run:') text_prun = ' (%s %s/10)' % (text_prun, previous_rate) text += prevrate_style % (self.prevrate_color, text_prun) self.treewidget.set_results(filename, results) date = time.strftime("%Y-%m-%d %H:%M:%S", datetime) date_text = text_style % (self.text_color, date) self.ratelabel.setText(text) self.datelabel.setText(date_text)
class ProcessWorker(QObject): """Process worker based on a QProcess for non blocking UI.""" sig_started = Signal(object) sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, environ=None): """ Process worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. environ : dict Process environment, """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._fired = False self._communicate_first = False self._partial_stdout = None self._started = False self._timer = QTimer() self._process = QProcess() self._set_environment(environ) self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _get_encoding(self): """Return the encoding/codepage to use.""" enco = 'utf-8' # Currently only cp1252 is allowed? if WIN: import ctypes codepage = to_text_string(ctypes.cdll.kernel32.GetACP()) # import locale # locale.getpreferredencoding() # Differences? enco = 'cp' + codepage return enco def _set_environment(self, environ): """Set the environment on the QProcess.""" if environ: q_environ = self._process.processEnvironment() for k, v in environ.items(): q_environ.insert(k, v) self._process.setProcessEnvironment(q_environ) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, self._get_encoding()) if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() enco = self._get_encoding() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, enco) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, enco) result = [stdout.encode(enco), stderr.encode(enco)] if PY2: stderr = stderr.decode() result[-1] = '' self._result = result if not self._fired: self.sig_finished.emit(self, result[0], result[-1]) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def _start(self): """Start process.""" if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() def terminate(self): """Terminate running processes.""" if self._process.state() == QProcess.Running: try: self._process.terminate() except Exception: pass self._fired = True def start(self): """Start worker.""" if not self._started: self.sig_started.emit(self) self._started = True
class PylintWidget(QWidget): """ Pylint widget """ DATAPATH = get_conf_path('pylint.results') VERSION = '1.1.0' redirect_stdio = Signal(bool) def __init__(self, parent, max_entries=100, options_button=None): QWidget.__init__(self, parent) self.setWindowTitle("Pylint") self.output = None self.error_output = None self.max_entries = max_entries self.rdata = [] if osp.isfile(self.DATAPATH): try: data = pickle.loads(open(self.DATAPATH, 'rb').read()) if data[0] == self.VERSION: self.rdata = data[1:] except (EOFError, ImportError): pass self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton(self, icon=ima.icon('run'), text=_("Analyze"), tip=_("Run analysis"), triggered=self.start, text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current analysis"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) self.filecombo.valid.connect(self.show_data) browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python file'), triggered=self.select_file) self.ratelabel = QLabel() self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Complete output"), triggered=self.show_log) self.treewidget = ResultsTree(self) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) if options_button: hlayout1.addWidget(options_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.ratelabel) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.treewidget) self.setLayout(layout) self.process = None self.set_running_state(False) self.show_data() if self.rdata: self.remove_obsolete_items() self.filecombo.addItems(self.get_filenames()) else: self.start_button.setEnabled(False) def analyze(self, filename): filename = to_text_string(filename) # filename is a QString instance self.kill_if_running() index, _data = self.get_data(filename) if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count()-1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): self.start() @Slot() def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python file"), getcwd_or_home(), _("Python files")+" (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def remove_obsolete_items(self): """Removing obsolete items""" self.rdata = [(filename, data) for filename, data in self.rdata if is_module_or_package(filename)] def get_filenames(self): return [filename for filename, _data in self.rdata] def get_data(self, filename): filename = osp.abspath(filename) for index, (fname, data) in enumerate(self.rdata): if fname == filename: return index, data else: return None, None def set_data(self, filename, data): filename = osp.abspath(filename) index, _data = self.get_data(filename) if index is not None: self.rdata.pop(index) self.rdata.insert(0, (filename, data)) self.save() def save(self): while len(self.rdata) > self.max_entries: self.rdata.pop(-1) pickle.dump([self.VERSION]+self.rdata, open(self.DATAPATH, 'wb'), 2) @Slot() def show_log(self): if self.output: TextEditor(self.output, title=_("Pylint output"), readonly=True, size=(700, 500)).exec_() @Slot() def start(self): filename = to_text_string(self.filecombo.currentText()) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(osp.dirname(filename)) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect(lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.process.kill) self.output = '' self.error_output = '' plver = PYLINT_VER if plver is not None: p_args = ['-m', 'pylint', '--output-format=text'] if plver.split('.')[0] == '0': p_args += ['-i', 'yes'] else: # Option '-i' (alias for '--include-ids') was removed in pylint # 1.0 p_args += ["--msg-template='{msg_id}:{line:3d},"\ "{column}: {obj}: {msg}"] p_args += [osp.basename(filename)] else: p_args = [osp.basename(filename)] self.process.start(sys.executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string( locale_codec.toUnicode(qba.data()) ) if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) if not self.output: if self.error_output: QMessageBox.critical(self, _("Error"), self.error_output) print("pylint error:\n\n" + self.error_output, file=sys.stderr) return # Convention, Refactor, Warning, Error results = {'C:': [], 'R:': [], 'W:': [], 'E:': []} txt_module = '************* Module ' module = '' # Should not be needed - just in case something goes wrong for line in self.output.splitlines(): if line.startswith(txt_module): # New module module = line[len(txt_module):] continue # Supporting option include-ids: ('R3873:' instead of 'R:') if not re.match(r'^[CRWE]+([0-9]{4})?:', line): continue i1 = line.find(':') if i1 == -1: continue msg_id = line[:i1] i2 = line.find(':', i1+1) if i2 == -1: continue line_nb = line[i1+1:i2].strip() if not line_nb: continue line_nb = int(line_nb.split(',')[0]) message = line[i2+1:] item = (module, line_nb, message, msg_id) results[line[0]+':'].append(item) # Rate rate = None txt_rate = 'Your code has been rated at ' i_rate = self.output.find(txt_rate) if i_rate > 0: i_rate_end = self.output.find('/10', i_rate) if i_rate_end > 0: rate = self.output[i_rate+len(txt_rate):i_rate_end] # Previous run previous = '' if rate is not None: txt_prun = 'previous run: ' i_prun = self.output.find(txt_prun, i_rate_end) if i_prun > 0: i_prun_end = self.output.find('/10', i_prun) previous = self.output[i_prun+len(txt_prun):i_prun_end] filename = to_text_string(self.filecombo.currentText()) self.set_data(filename, (time.localtime(), rate, previous, results)) self.output = self.error_output + self.output self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None \ and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return _index, data = self.get_data(filename) if data is None: text = _('Source code has not been rated yet.') self.treewidget.clear_results() date_text = '' else: datetime, rate, previous_rate, results = data if rate is None: text = _('Analysis did not succeed ' '(see output for more details).') self.treewidget.clear_results() date_text = '' else: text_style = "<span style=\'color: #444444\'><b>%s </b></span>" rate_style = "<span style=\'color: %s\'><b>%s</b></span>" prevrate_style = "<span style=\'color: #666666\'>%s</span>" color = "#FF0000" if float(rate) > 5.: color = "#22AA22" elif float(rate) > 3.: color = "#EE5500" text = _('Global evaluation:') text = (text_style % text)+(rate_style % (color, ('%s/10' % rate))) if previous_rate: text_prun = _('previous run:') text_prun = ' (%s %s/10)' % (text_prun, previous_rate) text += prevrate_style % text_prun self.treewidget.set_results(filename, results) date = to_text_string(time.strftime("%d %b %Y %H:%M", datetime), encoding='utf8') date_text = text_style % date self.ratelabel.setText(text) self.datelabel.setText(date_text)
class LineProfilerWidget(QWidget): """ Line profiler widget. """ DATAPATH = get_conf_path('lineprofiler.results') VERSION = '0.0.1' redirect_stdio = Signal(bool) def __init__(self, parent): QWidget.__init__(self, parent) self.setWindowTitle("Line profiler") self.output = None self.error_output = None self.use_colors = True self._last_wdir = None self._last_args = None self._last_pythonpath = None self.filecombo = PythonModulesComboBox(self) self.start_button = create_toolbutton( self, icon=get_icon('run.png'), text=_("Profile by line"), tip=_("Run line profiler"), triggered=self.start, text_beside_icon=True) self.stop_button = create_toolbutton( self, icon=get_icon('terminate.png'), text=_("Stop"), tip=_("Stop current profiling"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) #self.filecombo.valid.connect(self.show_data) # FIXME: The combobox emits this signal on almost any event # triggering show_data() too early, too often. browse_button = create_toolbutton( self, icon=get_icon('fileopen.png'), tip=_('Select Python script'), triggered=self.select_file) self.datelabel = QLabel() self.log_button = create_toolbutton( self, icon=get_icon('log.png'), text=_("Output"), text_beside_icon=True, tip=_("Show program's output"), triggered=self.show_log) self.datatree = LineProfilerDataTree(self) self.collapse_button = create_toolbutton( self, icon=get_icon('collapse.png'), triggered=lambda dD=-1: self.datatree.collapseAll(), tip=_('Collapse all')) self.expand_button = create_toolbutton( self, icon=get_icon('expand.png'), triggered=lambda dD=1: self.datatree.expandAll(), tip=_('Expand all')) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.collapse_button) hlayout2.addWidget(self.expand_button) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.datatree) self.setLayout(layout) self.process = None self.set_running_state(False) self.start_button.setEnabled(False) if not is_lineprofiler_installed(): for widget in (self.datatree, self.filecombo, self.log_button, self.start_button, self.stop_button, browse_button, self.collapse_button, self.expand_button): widget.setDisabled(True) text = _( '<b>Please install the <a href="%s">line_profiler module</a></b>' ) % WEBSITE_URL self.datelabel.setText(text) self.datelabel.setOpenExternalLinks(True) else: pass # self.show_data() def analyze(self, filename, wdir=None, args=None, pythonpath=None, use_colors=True): self.use_colors = use_colors if not is_lineprofiler_installed(): return self.kill_if_running() #index, _data = self.get_data(filename) index = None # FIXME: storing data is not implemented yet if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count()-1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): if wdir is None: wdir = osp.dirname(filename) self.start(wdir, args, pythonpath) def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename( self, _("Select Python script"), getcwd(), _("Python scripts")+" (*.py ; *.pyw)") self.redirect_stdio.emit(False) if filename: self.analyze(filename) def show_log(self): if self.output: TextEditor(self.output, title=_("Line profiler output"), readonly=True, size=(700, 500)).exec_() def show_errorlog(self): if self.error_output: TextEditor(self.error_output, title=_("Line profiler output"), readonly=True, size=(700, 500)).exec_() def start(self, wdir=None, args=None, pythonpath=None): filename = to_text_string(self.filecombo.currentText()) if wdir is None: wdir = self._last_wdir if wdir is None: wdir = osp.basename(filename) if args is None: args = self._last_args if args is None: args = [] if pythonpath is None: pythonpath = self._last_pythonpath self._last_wdir = wdir self._last_args = args self._last_pythonpath = pythonpath self.datelabel.setText(_('Profiling, please wait...')) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(wdir) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect(self.finished) self.stop_button.clicked.connect(self.process.kill) if pythonpath is not None: env = [to_text_string(_pth) for _pth in self.process.systemEnvironment()] add_pathlist_to_PYTHONPATH(env, pythonpath) processEnvironment = QProcessEnvironment() for envItem in env: envName, separator, envValue = envItem.partition('=') processEnvironment.insert(envName, envValue) self.process.setProcessEnvironment(processEnvironment) self.output = '' self.error_output = '' if os.name == 'nt': # On Windows, one has to replace backslashes by slashes to avoid # confusion with escape characters (otherwise, for example, '\t' # will be interpreted as a tabulation): filename = osp.normpath(filename).replace(os.sep, '/') p_args = ['-lvb', '-o', '"' + self.DATAPATH + '"', '"' + filename + '"'] if args: p_args.extend(programs.shell_split(args)) executable = '"' + programs.find_program('kernprof') + '"' executable += ' ' + ' '.join(p_args) executable = executable.replace(os.sep, '/') self.process.start(executable) else: p_args = ['-lvb', '-o', self.DATAPATH, filename] if args: p_args.extend(programs.shell_split(args)) executable = 'kernprof' self.process.start(executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string(locale_codec.toUnicode(qba.data())) if error: self.error_output += text else: self.output += text def finished(self): self.set_running_state(False) self.show_errorlog() # If errors occurred, show them. self.output = self.error_output + self.output # FIXME: figure out if show_data should be called here or # as a signal from the combobox self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled( self.output is not None and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return self.datatree.load_data(self.DATAPATH) self.datelabel.setText(_('Sorting data, please wait...')) QApplication.processEvents() self.datatree.show_tree() text_style = "<span style=\'color: #444444\'><b>%s </b></span>" date_text = text_style % time.strftime("%d %b %Y %H:%M", time.localtime()) self.datelabel.setText(date_text)
class CondaProcess(QObject): """conda-api modified to work with QProcess instead of popen""" ENCODING = 'ascii' def __init__(self, parent, on_finished=None, on_partial=None): QObject.__init__(self, parent) self._parent = parent self.output = None self.partial = None self.stdout = None self.error = None self._parse = False self._function_called = '' self._name = None self._process = QProcess() self._on_finished = on_finished self._process.finished.connect(self._call_conda_ready) self._process.readyReadStandardOutput.connect(self._call_conda_partial) if on_finished is not None: self._process.finished.connect(on_finished) if on_partial is not None: self._process.readyReadStandardOutput.connect(on_partial) self.set_root_prefix() def _call_conda_partial(self): """ """ stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(stdout, CondaProcess.ENCODING) stderr = self._process.readAllStandardError() stderr = handle_qbytearray(stderr, CondaProcess.ENCODING) if self._parse: self.output = json.loads(stdout) else: self.output = stdout self.partial = self.output self.stdout = self.output self.error = stderr # print(self.partial) # print(self.error) def _call_conda_ready(self): """function called when QProcess in _call_conda finishes task""" function = self._function_called if self.stdout is None: stdout = to_text_string(self._process.readAllStandardOutput(), encoding=CondaProcess.ENCODING) else: stdout = self.stdout if self.error is None: stderr = to_text_string(self._process.readAllStandardError(), encoding=CondaProcess.ENCODING) else: stderr = self.error if function == 'get_conda_version': pat = re.compile(r'conda:?\s+(\d+\.\d\S+|unknown)') m = pat.match(stderr.strip()) if m is None: m = pat.match(stdout.strip()) if m is None: raise Exception('output did not match: %r' % stderr) self.output = m.group(1) # elif function == 'get_envs': # info = self.output # self.output = info['envs'] # elif function == 'get_prefix_envname': # name = self._name # envs = self.output # self.output = self._get_prefix_envname_helper(name, envs) # self._name = None elif function == 'config_path': result = self.output self.output = result['rc_path'] elif function == 'config_get': result = self.output self.output = result['get'] elif (function == 'config_delete' or function == 'config_add' or function == 'config_set' or function == 'config_remove'): result = self.output self.output = result.get('warnings', []) elif function == 'pip': result = [] lines = self.output.split('\n') for line in lines: if '<pip>' in line: temp = line.split()[:-1] + ['pip'] result.append('-'.join(temp)) self.output = result if stderr.strip(): self.error = stderr # raise Exception('conda %r:\nSTDERR:\n%s\nEND' % (extra_args, # stderr.decode())) self._parse = False def _get_prefix_envname_helper(self, name, envs): """ """ global ROOTPREFIX if name == 'root': return ROOT_PREFIX for prefix in envs: if basename(prefix) == name: return prefix return None def _abspath(self, abspath): """ """ if abspath: if sys.platform == 'win32': python = join(ROOT_PREFIX, 'python.exe') conda = join(ROOT_PREFIX, 'Scripts', 'conda-script.py') else: python = join(ROOT_PREFIX, 'bin/python') conda = join(ROOT_PREFIX, 'bin/conda') cmd_list = [python, conda] else: # just use whatever conda is on the path cmd_list = ['conda'] return cmd_list def _call_conda(self, extra_args, abspath=True): """ """ # call conda with the list of extra arguments, and return the tuple # stdout, stderr global ROOT_PREFIX # if abspath: # if sys.platform == 'win32': # python = join(ROOT_PREFIX, 'python.exe') # conda = join(ROOT_PREFIX, # 'Scripts', 'conda-script.py') # else: # python = join(ROOT_PREFIX, 'bin/python') # conda = join(ROOT_PREFIX, 'bin/conda') # cmd_list = [python, conda] # else: # just use whatever conda is on the path # cmd_list = ['conda'] cmd_list = self._abspath(abspath) cmd_list.extend(extra_args) # try: # p = Popen(cmd_list, stdout=PIPE, stderr=PIPE) # except OSError: # raise Exception("could not invoke %r\n" % args) # return p.communicate() # adapted code # ------------ self.error, self.output = None, None self._process.start(cmd_list[0], cmd_list[1:]) def _call_and_parse(self, extra_args, abspath=True): """ """ # stdout, stderr = _call_conda(extra_args, abspath=abspath) # if stderr.decode().strip(): # raise Exception('conda %r:\nSTDERR:\n%s\nEND' % (extra_args, # stderr.decode())) # return json.loads(stdout.decode()) # adapted code # ------------ self._parse = True self._call_conda(extra_args, abspath=abspath) def _setup_install_commands_from_kwargs(self, kwargs, keys=tuple()): """ """ cmd_list = [] if kwargs.get('override_channels', False) and 'channel' not in kwargs: raise TypeError('conda search: override_channels requires channel') if 'env' in kwargs: cmd_list.extend(['--name', kwargs.pop('env')]) if 'prefix' in kwargs: cmd_list.extend(['--prefix', kwargs.pop('prefix')]) if 'channel' in kwargs: channel = kwargs.pop('channel') if isinstance(channel, str): cmd_list.extend(['--channel', channel]) else: cmd_list.append('--channel') cmd_list.extend(channel) for key in keys: if key in kwargs and kwargs[key]: cmd_list.append('--' + key.replace('_', '-')) return cmd_list def set_root_prefix(self, prefix=None): """ Set the prefix to the root environment (default is /opt/anaconda). This function should only be called once (right after importing conda_api). """ global ROOT_PREFIX if prefix: ROOT_PREFIX = prefix # find *some* conda instance, and then use info() to get 'root_prefix' else: pass # i = self.info(abspath=False) # self.ROOT_PREFIX = i['root_prefix'] ''' plat = 'posix' if sys.platform.lower().startswith('win'): listsep = ';' plat = 'win' else: listsep = ':' for p in os.environ['PATH'].split(listsep): if (os.path.exists(os.path.join(p, 'conda')) or os.path.exists(os.path.join(p, 'conda.exe')) or os.path.exists(os.path.join(p, 'conda.bat'))): # TEMPORARY: ROOT_PREFIX = os.path.dirname(p) # root prefix is 1 dir up i = info() # REAL: ROOT_PREFIX = i['root_prefix'] break else: # fall back to standard install location, which may be wrong if plat == 'win': ROOT_PREFIX = 'C:\Anaconda' else: ROOT_PREFIX = '/opt/anaconda' ''' # adapted code # ------------ if ROOT_PREFIX is None: # qprocess = QProcess() # cmd_list = ['conda', 'info', '--json'] # qprocess.start(cmd_list[0], cmd_list[1:]) # qprocess.waitForFinished() # output = qprocess.readAllStandardOutput() # output = handle_qbytearray(output, CondaProcess.ENCODING) # info = json.loads(output) # ROOT_PREFIX = info['root_prefix'] info = self.info(abspath=False) ROOT_PREFIX = info['root_prefix'] def get_conda_version(self): """ return the version of conda being used (invoked) as a string """ # pat = re.compile(r'conda:?\s+(\d+\.\d\S+|unknown)') # stdout, stderr = self._call_conda(['--version']) # # argparse outputs version to stderr in Python < 3.4. # # http://bugs.python.org/issue18920 # m = pat.match(stderr.decode().strip()) # if m is None: # m = pat.match(stdout.decode().strip()) # # if m is None: # raise Exception('output did not match: %r' % stderr) # return m.group(1) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'get_conda_version' self._call_conda(['--version']) def get_envs(self): """ Return all of the (named) environment (this does not include the root environment), as a list of absolute path to their prefixes. """ # info = self._call_and_parse(['info', '--json']) # return info['envs'] info = self.info() return info['envs'] # adapted code # ------------ # if self._process.state() == QProcess.NotRunning: # self._function_called = 'get_envs' # self._call_and_parse(['info', '--json']) def get_prefix_envname(self, name): """ Given the name of an environment return its full prefix path, or None if it cannot be found. """ if name == 'root': return ROOT_PREFIX for prefix in self.get_envs(): if basename(prefix) == name: return prefix return None # adapted code # ------------ # if self._process.state() == QProcess.NotRunning: # self._name = name # self._function_called = 'get_prefix_envname' # self._call_and_parse(['info', '--json']) def info(self, abspath=True): """ Return a dictionary with configuration information. No guarantee is made about which keys exist. Therefore this function should only be used for testing and debugging. """ # return self._call_and_parse(['info', '--json'], abspath=abspath) qprocess = QProcess() cmd_list = self._abspath(abspath) cmd_list.extend(['info', '--json']) qprocess.start(cmd_list[0], cmd_list[1:]) qprocess.waitForFinished() output = qprocess.readAllStandardOutput() output = handle_qbytearray(output, CondaProcess.ENCODING) info = json.loads(output) return info # adapted code # ------------ # if self._process.state() == QProcess.NotRunning: # self._function_called = 'info' # self._call_and_parse(['info', '--json'], abspath=abspath) def package_info(self, package, abspath=True): """ Return a dictionary with package information. Structure is { 'package_name': [{ 'depends': list, 'version': str, 'name': str, 'license': str, 'fn': ..., ... }] } """ # return self._call_and_parse(['info', package, '--json'], # abspath=abspath) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'package_info' self._call_and_parse(['info', package, '--json'], abspath=abspath) def search(self, regex=None, spec=None, **kwargs): """ Search for packages. """ cmd_list = ['search', '--json'] if regex and spec: raise TypeError('conda search: only one of regex or spec allowed') if regex: cmd_list.append(regex) if spec: cmd_list.extend(['--spec', spec]) if 'platform' in kwargs: platform = kwargs.pop('platform') platforms = ('win-32', 'win-64', 'osx-64', 'linux-32', 'linux-64') if platform not in platforms: raise TypeError('conda search: platform must be one of ' + ', '.join(platforms)) cmd_list.extend(['--platform', platform]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('canonical', 'unknown', 'use_index_cache', 'outdated', 'override_channels'))) # return self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'search' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def share(self, prefix): """ Create a "share package" of the environment located in `prefix`, and return a dictionary with (at least) the following keys: - 'path': the full path to the created package - 'warnings': a list of warnings This file is created in a temp directory, and it is the callers responsibility to remove this directory (after the file has been handled in some way). """ # return self._call_and_parse(['share', '--json', '--prefix', prefix]) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'share' self._call_and_parse(['share', '--json', '--prefix', prefix]) def create(self, name=None, path=None, pkgs=None): """ Create an environment either by name or path with a specified set of packages """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError('must specify a list of one or more packages to ' 'install into new environment') cmd_list = ['create', '--yes', '--quiet'] if name: ref = name search = [os.path.join(d, name) for d in self.info()['envs_dirs']] cmd_list = ['create', '--yes', '--quiet', '--name', name] elif path: ref = path search = [path] cmd_list = ['create', '--yes', '--quiet', '--prefix', path] else: raise TypeError('must specify either an environment name or a path' ' for new environment') if any(os.path.exists(path) for path in search): raise CondaEnvExistsError('Conda environment [%s] already exists' % ref) cmd_list.extend(pkgs) # (out, err) = self._call_conda(cmd_list) # if err.decode().strip(): # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # err.decode())) # return out # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'create' self._call_conda(cmd_list) def install(self, name=None, path=None, pkgs=None, dep=True): """ Install packages into an environment either by name or path with a specified set of packages """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--yes', '--json', '--force-pscheck'] # cmd_list = ['install', '--yes', '--quiet'] if name: cmd_list.extend(['--name', name]) elif path: cmd_list.extend(['--prefix', path]) else: # just install into the current environment, whatever that is pass cmd_list.extend(pkgs) # (out, err) = self._call_conda(cmd_list) # if err.decode().strip(): # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # err.decode())) # return out # adapted code # ------------ if not dep: cmd_list.extend(['--no-deps']) if self._process.state() == QProcess.NotRunning: self._function_called = 'install' self._call_conda(cmd_list) def update(self, *pkgs, **kwargs): """ Update package(s) (in an environment) by name. """ cmd_list = ['update', '--json', '--quiet', '--yes'] if not pkgs and not kwargs.get('all'): raise TypeError("Must specify at least one package to update, \ or all=True.") cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'no_deps', 'override_channels', 'no_pin', 'force', 'all', 'use_index_cache', 'use_local', 'alt_hint'))) cmd_list.extend(pkgs) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # # return result # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'update' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def remove(self, *pkgs, **kwargs): """ Remove a package (from an environment) by name. Returns { success: bool, (this is always true), (other information) } """ cmd_list = ['remove', '--json', '--quiet', '--yes', '--force-pscheck'] # cmd_list = ['remove', '--json', '--quiet', '--yes'] if not pkgs and not kwargs.get('all'): raise TypeError("Must specify at least one package to remove, \ or all=True.") if kwargs.get('name') and kwargs.get('path'): raise TypeError('conda remove: At most one of name, path allowed') if kwargs.get('name'): cmd_list.extend(['--name', kwargs.pop('name')]) if kwargs.get('path'): cmd_list.extend(['--prefix', kwargs.pop('path')]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'features', 'override_channels', 'no_pin', 'force', 'all'))) cmd_list.extend(pkgs) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # # return result # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'remove' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def remove_environment(self, name=None, path=None, **kwargs): """ Remove an environment entirely. See ``remove``. """ # return self.remove(name=name, path=path, all=True, **kwargs) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'remove_environment' self.remove(name=name, path=path, all=True, **kwargs) def clone_environment(self, clone, name=None, path=None, **kwargs): """ Clone the environment ``clone`` into ``name`` or ``path``. """ cmd_list = ['create', '--json', '--quiet'] if (name and path) or not (name or path): raise TypeError("conda clone_environment: exactly one of name or \ path required") if name: cmd_list.extend(['--name', name]) if path: cmd_list.extend(['--prefix', path]) cmd_list.extend(['--clone', clone]) cmd_list.extend( self._setup_install_commands_from_kwargs( kwargs, ('dry_run', 'unknown', 'use_index_cache', 'use_local', 'no_pin', 'force', 'all', 'channel', 'override_channels', 'no_default_packages'))) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # # return result # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'clone_environment' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) # def process(self, name=None, path=None, cmd=None, args=None, stdin=None, # stdout=None, stderr=None, timeout=None): # """ # Create a Popen process for cmd using the specified args but in the # conda environment specified by name or path. # # The returned object will need to be invoked with p.communicate() or # similar. # # :param name: name of conda environment # :param path: path to conda environment (if no name specified) # :param cmd: command to invoke # :param args: argument # :param stdin: stdin # :param stdout: stdout # :param stderr: stderr # :return: Popen object # """ # # if bool(name) == bool(path): # raise TypeError('exactly one of name or path must be specified') # # if not cmd: # raise TypeError('cmd to execute must be specified') # # if not args: # args = [] # # if name: # path = self.get_prefix_envname(name) # # plat = 'posix' # if sys.platform.lower().startswith('win'): # listsep = ';' # plat = 'win' # else: # listsep = ':' # # conda_env = dict(os.environ) # # if plat == 'posix': # conda_env['PATH'] = path + os.path.sep + 'bin' + listsep + \ # conda_env['PATH'] # else: # win # conda_env['PATH'] = path + os.path.sep + 'Scripts' + listsep + \ # conda_env['PATH'] # # conda_env['PATH'] = path + listsep + conda_env['PATH'] # # cmd_list = [cmd] # cmd_list.extend(args) # # try: # p = Popen(cmd_list, env=conda_env, stdin=stdin, stdout=stdout, # stderr=stderr) # except OSError: # raise Exception("could not invoke %r\n" % cmd_list) # return p def clone(self, path, prefix): """ Clone a "share package" (located at `path`) by creating a new environment at `prefix`, and return a dictionary with (at least) the following keys: - 'warnings': a list of warnings The directory `path` is located in, should be some temp directory or some other directory OUTSIDE /opt/anaconda. After calling this function, the original file (at `path`) may be removed (by the caller of this function). The return object is a list of warnings. """ # return self._call_and_parse(['clone', '--json', '--prefix', prefix, # path]) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'clone' self._call_and_parse(['clone', '--json', '--prefix', prefix, path]) def _setup_config_from_kwargs(kwargs): cmd_list = ['--json', '--force'] if 'file' in kwargs: cmd_list.extend(['--file', kwargs['file']]) if 'system' in kwargs: cmd_list.append('--system') return cmd_list def config_path(self, **kwargs): """ Get the path to the config file. """ cmd_list = ['config', '--get'] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result['rc_path'] # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_path' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_get(self, *keys, **kwargs): """ Get the values of configuration keys. Returns a dictionary of values. Note, the key may not be in the dictionary if the key wasn't set in the configuration file. """ cmd_list = ['config', '--get'] cmd_list.extend(keys) cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result['get'] # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_get' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_set(self, key, value, **kwargs): """ Set a key to a (bool) value. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--set', key, str(value)] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result.get('warnings', []) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_set' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_add(self, key, value, **kwargs): """ Add a value to a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--add', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result.get('warnings', []) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_add' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_remove(self, key, value, **kwargs): """ Remove a value from a key. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove', key, value] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result.get('warnings', []) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_remove' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def config_delete(self, key, **kwargs): """ Remove a key entirely. Returns a list of warnings Conda may have emitted. """ cmd_list = ['config', '--remove-key', key] cmd_list.extend(self._setup_config_from_kwargs(kwargs)) # result = self._call_and_parse(cmd_list, # abspath=kwargs.get('abspath', True)) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result.get('warnings', []) # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'config_delete' self._call_and_parse(cmd_list, abspath=kwargs.get('abspath', True)) def run(self, command, abspath=True): """ Launch the specified app by name or full package name. Returns a dictionary containing the key "fn", whose value is the full package (ending in ``.tar.bz2``) of the app. """ cmd_list = ['run', '--json', command] # result = self._call_and_parse(cmd_list, abspath=abspath) # # if 'error' in result: # raise CondaError('conda %s: %s' % (" ".join(cmd_list), # result['error'])) # return result # adapted code # ------------ if self._process.state() == QProcess.NotRunning: self._function_called = 'run' self._call_and_parse(cmd_list, abspath=abspath) # def test(): # """ # Self-test function, which prints useful debug information. # This function returns None on success, and will crash the interpreter # on failure. # """ # print('sys.version: %r' % sys.version) # print('sys.prefix : %r' % sys.prefix) # print('conda_api.__version__: %r' % __version__) # print('conda_api.ROOT_PREFIX: %r' % ROOT_PREFIX) # if isdir(ROOT_PREFIX): # conda_version = get_conda_version() # print('conda version: %r' % conda_version) # print('conda info:') # d = info() # for kv in d.items(): # print('\t%s=%r' % kv) # assert d['conda_version'] == conda_version # else: # print('Warning: no such directory: %r' % ROOT_PREFIX) # print('OK') # ---- Additional methods not in conda-api def pip(self, name): """Get list of pip installed packages.""" cmd_list = ['list', '-n', name] if self._process.state() == QProcess.NotRunning: self._function_called = 'pip' self._call_conda(cmd_list) def dependencies(self, name=None, path=None, pkgs=None, dep=True): """ Install packages into an environment either by name or path with a specified set of packages """ if not pkgs or not isinstance(pkgs, (list, tuple)): raise TypeError('must specify a list of one or more packages to ' 'install into existing environment') cmd_list = ['install', '--dry-run', '--json'] cmd_list = ['install', '--dry-run', '--json', '--force-pscheck'] if not dep: cmd_list.extend(['--no-deps']) if name: cmd_list.extend(['--name', name]) elif path: cmd_list.extend(['--prefix', path]) else: # just install into the current environment, whatever that is pass cmd_list.extend(pkgs) if self._process.state() == QProcess.NotRunning: self._function_called = 'install_dry' self._call_and_parse(cmd_list)
class PylintWidget(QWidget): """ Pylint widget """ DATAPATH = get_conf_path('pylint.results') VERSION = '1.1.0' redirect_stdio = Signal(bool) def __init__(self, parent, max_entries=100): QWidget.__init__(self, parent) self.setWindowTitle("Pylint") self.output = None self.error_output = None self.max_entries = max_entries self.rdata = [] if osp.isfile(self.DATAPATH): try: data = pickle.loads(open(self.DATAPATH, 'rb').read()) if data[0] == self.VERSION: self.rdata = data[1:] except (EOFError, ImportError): pass self.filecombo = PythonModulesComboBox(self) if self.rdata: self.remove_obsolete_items() self.filecombo.addItems(self.get_filenames()) self.start_button = create_toolbutton(self, icon=ima.icon('run'), text=_("Analyze"), tip=_("Run analysis"), triggered=self.start, text_beside_icon=True) self.stop_button = create_toolbutton(self, icon=ima.icon('stop'), text=_("Stop"), tip=_("Stop current analysis"), text_beside_icon=True) self.filecombo.valid.connect(self.start_button.setEnabled) self.filecombo.valid.connect(self.show_data) browse_button = create_toolbutton(self, icon=ima.icon('fileopen'), tip=_('Select Python file'), triggered=self.select_file) self.ratelabel = QLabel() self.datelabel = QLabel() self.log_button = create_toolbutton(self, icon=ima.icon('log'), text=_("Output"), text_beside_icon=True, tip=_("Complete output"), triggered=self.show_log) self.treewidget = ResultsTree(self) hlayout1 = QHBoxLayout() hlayout1.addWidget(self.filecombo) hlayout1.addWidget(browse_button) hlayout1.addWidget(self.start_button) hlayout1.addWidget(self.stop_button) hlayout2 = QHBoxLayout() hlayout2.addWidget(self.ratelabel) hlayout2.addStretch() hlayout2.addWidget(self.datelabel) hlayout2.addStretch() hlayout2.addWidget(self.log_button) layout = QVBoxLayout() layout.addLayout(hlayout1) layout.addLayout(hlayout2) layout.addWidget(self.treewidget) self.setLayout(layout) self.process = None self.set_running_state(False) self.show_data() def analyze(self, filename): filename = to_text_string(filename) # filename is a QString instance self.kill_if_running() index, _data = self.get_data(filename) if index is None: self.filecombo.addItem(filename) self.filecombo.setCurrentIndex(self.filecombo.count()-1) else: self.filecombo.setCurrentIndex(self.filecombo.findText(filename)) self.filecombo.selected() if self.filecombo.is_valid(): self.start() @Slot() def select_file(self): self.redirect_stdio.emit(False) filename, _selfilter = getopenfilename(self, _("Select Python file"), getcwd(), _("Python files")+" (*.py ; *.pyw)") self.redirect_stdio.emit(True) if filename: self.analyze(filename) def remove_obsolete_items(self): """Removing obsolete items""" self.rdata = [(filename, data) for filename, data in self.rdata if is_module_or_package(filename)] def get_filenames(self): return [filename for filename, _data in self.rdata] def get_data(self, filename): filename = osp.abspath(filename) for index, (fname, data) in enumerate(self.rdata): if fname == filename: return index, data else: return None, None def set_data(self, filename, data): filename = osp.abspath(filename) index, _data = self.get_data(filename) if index is not None: self.rdata.pop(index) self.rdata.insert(0, (filename, data)) self.save() def save(self): while len(self.rdata) > self.max_entries: self.rdata.pop(-1) pickle.dump([self.VERSION]+self.rdata, open(self.DATAPATH, 'wb'), 2) @Slot() def show_log(self): if self.output: TextEditor(self.output, title=_("Pylint output"), readonly=True, size=(700, 500)).exec_() @Slot() def start(self): filename = to_text_string(self.filecombo.currentText()) self.process = QProcess(self) self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setWorkingDirectory(osp.dirname(filename)) self.process.readyReadStandardOutput.connect(self.read_output) self.process.readyReadStandardError.connect( lambda: self.read_output(error=True)) self.process.finished.connect(lambda ec, es=QProcess.ExitStatus: self.finished(ec, es)) self.stop_button.clicked.connect(self.process.kill) self.output = '' self.error_output = '' plver = PYLINT_VER if plver is not None: p_args = ['-m', 'pylint', '--output-format=text'] if plver.split('.')[0] == '0': p_args += ['-i', 'yes'] else: # Option '-i' (alias for '--include-ids') was removed in pylint # 1.0 p_args += ["--msg-template='{msg_id}:{line:3d},"\ "{column}: {obj}: {msg}"] p_args += [osp.basename(filename)] else: p_args = [osp.basename(filename)] self.process.start(sys.executable, p_args) running = self.process.waitForStarted() self.set_running_state(running) if not running: QMessageBox.critical(self, _("Error"), _("Process failed to start")) def set_running_state(self, state=True): self.start_button.setEnabled(not state) self.stop_button.setEnabled(state) def read_output(self, error=False): if error: self.process.setReadChannel(QProcess.StandardError) else: self.process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.process.bytesAvailable(): if error: qba += self.process.readAllStandardError() else: qba += self.process.readAllStandardOutput() text = to_text_string( locale_codec.toUnicode(qba.data()) ) if error: self.error_output += text else: self.output += text def finished(self, exit_code, exit_status): self.set_running_state(False) if not self.output: if self.error_output: QMessageBox.critical(self, _("Error"), self.error_output) print("pylint error:\n\n" + self.error_output, file=sys.stderr) return # Convention, Refactor, Warning, Error results = {'C:': [], 'R:': [], 'W:': [], 'E:': []} txt_module = '************* Module ' module = '' # Should not be needed - just in case something goes wrong for line in self.output.splitlines(): if line.startswith(txt_module): # New module module = line[len(txt_module):] continue # Supporting option include-ids: ('R3873:' instead of 'R:') if not re.match('^[CRWE]+([0-9]{4})?:', line): continue i1 = line.find(':') if i1 == -1: continue msg_id = line[:i1] i2 = line.find(':', i1+1) if i2 == -1: continue line_nb = line[i1+1:i2].strip() if not line_nb: continue line_nb = int(line_nb.split(',')[0]) message = line[i2+1:] item = (module, line_nb, message, msg_id) results[line[0]+':'].append(item) # Rate rate = None txt_rate = 'Your code has been rated at ' i_rate = self.output.find(txt_rate) if i_rate > 0: i_rate_end = self.output.find('/10', i_rate) if i_rate_end > 0: rate = self.output[i_rate+len(txt_rate):i_rate_end] # Previous run previous = '' if rate is not None: txt_prun = 'previous run: ' i_prun = self.output.find(txt_prun, i_rate_end) if i_prun > 0: i_prun_end = self.output.find('/10', i_prun) previous = self.output[i_prun+len(txt_prun):i_prun_end] filename = to_text_string(self.filecombo.currentText()) self.set_data(filename, (time.localtime(), rate, previous, results)) self.output = self.error_output + self.output self.show_data(justanalyzed=True) def kill_if_running(self): if self.process is not None: if self.process.state() == QProcess.Running: self.process.kill() self.process.waitForFinished() def show_data(self, justanalyzed=False): if not justanalyzed: self.output = None self.log_button.setEnabled(self.output is not None \ and len(self.output) > 0) self.kill_if_running() filename = to_text_string(self.filecombo.currentText()) if not filename: return _index, data = self.get_data(filename) if data is None: text = _('Source code has not been rated yet.') self.treewidget.clear_results() date_text = '' else: datetime, rate, previous_rate, results = data if rate is None: text = _('Analysis did not succeed ' '(see output for more details).') self.treewidget.clear_results() date_text = '' else: text_style = "<span style=\'color: #444444\'><b>%s </b></span>" rate_style = "<span style=\'color: %s\'><b>%s</b></span>" prevrate_style = "<span style=\'color: #666666\'>%s</span>" color = "#FF0000" if float(rate) > 5.: color = "#22AA22" elif float(rate) > 3.: color = "#EE5500" text = _('Global evaluation:') text = (text_style % text)+(rate_style % (color, ('%s/10' % rate))) if previous_rate: text_prun = _('previous run:') text_prun = ' (%s %s/10)' % (text_prun, previous_rate) text += prevrate_style % text_prun self.treewidget.set_results(filename, results) date = to_text_string(time.strftime("%d %b %Y %H:%M", datetime), encoding='utf8') date_text = text_style % date self.ratelabel.setText(text) self.datelabel.setText(date_text)
class ProcessWorker(QObject): """Process worker based on a QProcess for non blocking UI.""" sig_started = Signal(object) sig_finished = Signal(object, object, object) sig_partial = Signal(object, object, object) def __init__(self, cmd_list, environ=None): """ Process worker based on a QProcess for non blocking UI. Parameters ---------- cmd_list : list of str Command line arguments to execute. environ : dict Process environment, """ super(ProcessWorker, self).__init__() self._result = None self._cmd_list = cmd_list self._fired = False self._communicate_first = False self._partial_stdout = None self._started = False self._timer = QTimer() self._process = QProcess() self._set_environment(environ) self._timer.setInterval(150) self._timer.timeout.connect(self._communicate) self._process.readyReadStandardOutput.connect(self._partial) def _get_encoding(self): """Return the encoding/codepage to use.""" enco = 'utf-8' # Currently only cp1252 is allowed? if WIN: import ctypes codepage = to_text_string(ctypes.cdll.kernel32.GetACP()) # import locale # locale.getpreferredencoding() # Differences? enco = 'cp' + codepage return enco def _set_environment(self, environ): """Set the environment on the QProcess.""" if environ: q_environ = self._process.processEnvironment() for k, v in environ.items(): q_environ.insert(k, v) self._process.setProcessEnvironment(q_environ) def _partial(self): """Callback for partial output.""" raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, self._get_encoding()) if self._partial_stdout is None: self._partial_stdout = stdout else: self._partial_stdout += stdout self.sig_partial.emit(self, stdout, None) def _communicate(self): """Callback for communicate.""" if (not self._communicate_first and self._process.state() == QProcess.NotRunning): self.communicate() elif self._fired: self._timer.stop() def communicate(self): """Retrieve information.""" self._communicate_first = True self._process.waitForFinished() enco = self._get_encoding() if self._partial_stdout is None: raw_stdout = self._process.readAllStandardOutput() stdout = handle_qbytearray(raw_stdout, enco) else: stdout = self._partial_stdout raw_stderr = self._process.readAllStandardError() stderr = handle_qbytearray(raw_stderr, enco) result = [stdout.encode(enco), stderr.encode(enco)] if PY2: stderr = stderr.decode() result[-1] = '' self._result = result if not self._fired: self.sig_finished.emit(self, result[0], result[-1]) self._fired = True return result def close(self): """Close the running process.""" self._process.close() def is_finished(self): """Return True if worker has finished processing.""" return self._process.state() == QProcess.NotRunning and self._fired def _start(self): """Start process.""" if not self._fired: self._partial_ouput = None self._process.start(self._cmd_list[0], self._cmd_list[1:]) self._timer.start() def terminate(self): """Terminate running processes.""" if self._process.state() == QProcess.Running: try: self._process.terminate() except Exception: pass self._fired = True def start(self): """Start worker.""" if not self._started: self.sig_started.emit(self) self._started = True