def run(session, text, *, log=True, downgrade_errors=False): """execute a textual command Parameters ---------- text : string The text of the command to execute. log : bool Print the command text to the reply log. downgrade_errors : bool True if errors in the command should be logged as informational. """ from chimerax.core.commands import Command from chimerax.core.errors import UserError command = Command(session) try: results = command.run(text, log=log) except UserError as err: if downgrade_errors: session.logger.info(str(err)) else: raise results = [] return results[0] if len(results) == 1 else results
def process_command(self, command_text): if self._command_runner is None: from chimerax.core.commands import Command from chimerax.core.commands.cli import RegisteredCommandInfo command_registry = RegisteredCommandInfo() self._register_commands(command_registry) self._command_runner = Command(self.alignment.session, registry=command_registry) self._command_runner.run(command_text, log=False)
def run(session, sv, text): from chimerax.core.commands import Command cmd = Command(session, registry=registry) global _sv _sv = sv try: cmd.run(text, log=False) finally: _sv = None
def execute(self): from contextlib import contextmanager @contextmanager def processing_command(line_edit, cmd_text, command_worked, select_failed): line_edit.blockSignals(True) self._processing_command = True # as per the docs for contextmanager, the yield needs # to be in a try/except if the exit code is to execute # after errors try: yield finally: line_edit.blockSignals(False) line_edit.setText(cmd_text) if command_worked[0] or select_failed: line_edit.selectAll() self._processing_command = False session = self.session logger = session.logger text = self.text.lineEdit().text() logger.status("") from chimerax.core import errors from chimerax.core.commands import Command from html import escape for cmd_text in text.split("\n"): if not cmd_text: continue # don't select the text if the command failed, so that # an accidental keypress won't erase the command, which # probably needs to be edited to work command_worked = [False] with processing_command(self.text.lineEdit(), cmd_text, command_worked, self.settings.select_failed): try: self._just_typed_command = cmd_text cmd = Command(session) cmd.run(cmd_text) command_worked[0] = True except SystemExit: # TODO: somehow quit application raise except errors.UserError as err: logger.status(str(err), color="crimson") from chimerax.core.logger import error_text_format logger.info(error_text_format % escape(str(err)), is_html=True) except BaseException: raise self.set_focus()
def log_equivalent_command(session, command_text): """ Log a command. This is typically used to show the equivalent command for some action (button press, mouse drag, ...) done with the grahical user interface. """ from chimerax.core.commands import Command from chimerax.core.errors import UserError command = Command(session) try: command.run(command_text, log_only=True) except UserError as err: session.logger.info(str(err))
def process_command(session, name, structure, substring): # all the commands use the trick that the structure arg is temporarily made available in the # global namespace as '_structure' global _structure, command_registry _structure = structure try: if command_registry is None: # register commands to private registry from chimerax.core.commands.cli import RegisteredCommandInfo command_registry = RegisteredCommandInfo() from chimerax.core.commands import Float3Arg, StringArg, BoolArg, \ Float2Arg, DynamicEnum # atom register("atom", CmdDesc( keyword=[("position", Float3Arg), ("res_name", StringArg), ("select", BoolArg)], synopsis="place helium atom" ), shim_place_atom, registry=command_registry) # peptide from chimerax.core.commands import Annotation class RepeatableFloat2Arg(Annotation): allow_repeat = True parse = Float2Arg.parse unparse = Float2Arg.unparse register("peptide", CmdDesc( required=[("sequence", StringArg), ("phi_psis", RepeatableFloat2Arg)], keyword=[("position", Float3Arg), ("chain_id", StringArg), ("rot_lib", DynamicEnum(session.rotamers.library_names))], synopsis="construct peptide from sequence" ), shim_place_peptide, registry=command_registry) cmd = Command(session, registry=command_registry) return cmd.run(name + ' ' + substring, log=False)[0] finally: _structure = None
def cmd_save(session, file_name, rest_of_line, *, log=True): tokens = [] remainder = rest_of_line while remainder: token, token_log, remainder = next_token(remainder) remainder = remainder.lstrip() tokens.append(token) format_name = None for i in range(len(tokens)-2, -1, -2): test_token = tokens[i].lower() if "format".startswith(test_token): format_name = tokens[i+1] provider_cmd_text = "save " + " ".join([FileNameArg.unparse(file_name)] + [StringArg.unparse(token) for token in tokens]) try: from .manager import NoSaverError mgr = session.save_command data_format= file_format(session, file_name, format_name) try: provider_args = mgr.save_args(data_format) except NoSaverError as e: raise LimitationError(str(e)) # register a private 'save' command that handles the provider's keywords registry = RegisteredCommandInfo() keywords = { 'format': DynamicEnum(lambda ses=session: format_names(ses)), } for keyword, annotation in provider_args.items(): if keyword in keywords: raise ValueError("Save-provider keyword '%s' conflicts with builtin arg" " of same name" % keyword) keywords[keyword] = annotation # for convenience, allow 'models' to be a second positional argument instead of a keyword if 'models' in keywords: optional = [('models', keywords['models'])] del keywords['models'] else: optional = [] desc = CmdDesc(required=[('file_name', SaveFileNameArg)], optional=optional, keyword=keywords.items(), hidden=mgr.hidden_args(data_format), synopsis="unnecessary") register("save", desc, provider_save, registry=registry) except BaseException as e: # want to log command even for keyboard interrupts log_command(session, "save", provider_cmd_text, url=_main_save_CmdDesc.url) raise Command(session, registry=registry).run(provider_cmd_text, log=log)
def run_expectfail(session, command): from chimerax.core.errors import NotABug, UserError from chimerax.core.commands import run, Command # first confirm that command is an actual command cmd = Command(session) cmd.current_text = command cmd._find_command_name(no_aliases=True) if not cmd._ci: raise UserError("Unknown commmand") try: cmd.run(command) except NotABug: session.logger.info("Failed as expected") else: raise UserError("Command failed to fail")
class HeaderSequence(list): # sort_val determines the default ordering of headers. # Built-in headers change their sort_val to a value in the range # [1.0, 2.0) so they normally appear before registered headers. # Identical sort_vals tie-break on sequence name. sort_val = 2.0 numbering_start = None fast_update = True # can header be updated quickly if only a few columns are changed? single_column_updateable = True # can a single column be updated, or only the entire header? ident = None # should be string, used to identify header in commands and as part of the # generated residue attribute name, so the string should be "attribute friendly" value_type = float value_none_okay = True ATTR_PREFIX = "seq_" def __init__(self, alignment, name=None, *, eval_while_hidden=False, session_restore=False): if name is None: if not hasattr(self, 'name'): self.name = "" else: self.name = name if not session_restore and self.ident is None: raise AssertionError("%s header class failed to define 'ident' attribute" % self.__class__.__name__) from weakref import proxy self.alignment = proxy(alignment) self.alignment.add_observer(self) self._notifications_suppressed = 0 self._shown = False self.eval_while_hidden = eval_while_hidden self._update_needed = True self._edit_bounds = None self._alignment_being_edited = False self._command_runner = None if not hasattr(self.__class__, 'settings'): self.__class__.settings = self.make_settings(alignment.session) if self.eval_while_hidden: self.reevaluate() def add_options(self, options_container, *, category=None, verbose_labels=True): pass def align_change(self, left, right): """alignment changed in positions from 'left' to 'right'""" if self._alignment_being_edited and not self.fast_update: if self._edit_bounds is None: self._edit_bounds = (left, right) else: self._edit_bounds = (min(left, self._edit_bounds[0]), max(right, self._edit_bounds[1])) return if single_column_updateable: self.reevaluate(left, right) else: self.reevaluate() def alignment_notification(self, note_name, note_data): if note_name == self.alignment.NOTE_EDIT_START: self._alignment_being_edited = True if note_name == self.alignment.NOTE_EDIT_END: self._alignment_being_edited = False @contextmanager def alignment_notifications_suppressed(self): self._notifications_suppressed += 1 try: yield finally: self._notifications_suppressed -= 1 def destroy(self): if not self.alignment.being_destroyed: self.alignment.remove_observer(self) def evaluate(self, pos): raise NotImplementedError("evaluate() method must be" " implemented by %s subclass" % self.__class__.__name__) def position_color(self, position): return 'black' def get_state(self): state = { 'name': self.name, 'shown': self._shown, 'eval_while_hidden': self.eval_while_hidden, 'contents': self[:] } return state def __hash__(self): return id(self) def hist_infinity(self, position): """Convenience function to map arbitrary number to 0-1 range Used as the 'depiction_val' method for some kinds of data """ raw = self[position] if raw is None: return 0.0 from math import exp if raw >= 0: return 1.0 - 0.5 * exp(-raw) return 0.5 * exp(raw) def __lt__(self, other): return self.sort_val < other.sort_val def make_settings(self, session): """For derived classes with their own settings, the settings_info() method must be overridden (which see)""" settings_name, settings_info = self.settings_info() settings_defaults = {} self.__class__._setting_cmd_annotations = cmd_annotations = {} for attr_name, info in settings_info.items(): annotation, default_value = info settings_defaults[attr_name] = default_value cmd_annotations[attr_name] = annotation from chimerax.core.settings import Settings class HeaderSettings(Settings): EXPLICIT_SAVE = settings_defaults return HeaderSettings(session, settings_name) def notify_alignment(self, note_name, *args): if not self._notifications_suppressed: if args: note_data = (self, *args) else: note_data = self self.alignment.notify(note_name, note_data) def num_options(self): return 0 def option_data(self): from chimerax.ui.options import BooleanOption return [ ("show initially", 'initially_shown', BooleanOption, {}, "Show this header when sequence/alignment initially shown") ] def option_sorting(self, option): for base_label, attr_name, opt_class, opt_kw, balloon in HeaderSequence.option_data(self): if option.attr_name == attr_name: return (0, option.name.casefold()) return (1, option.name.casefold()) def positive_hist_infinity(self, position): """Convenience function to map arbitrary positive number to 0-1 range Used as the 'depiction_val' method for some kinds of data """ raw = self[position] if raw is None: return 0.0 from math import exp return 1.0 - exp(-raw) def process_command(self, command_text): if self._command_runner is None: from chimerax.core.commands import Command from chimerax.core.commands.cli import RegisteredCommandInfo command_registry = RegisteredCommandInfo() self._register_commands(command_registry) self._command_runner = Command(self.alignment.session, registry=command_registry) self._command_runner.run(command_text, log=False) def reason_requires_update(self, reason): return False def reevaluate(self, pos1=0, pos2=None, *, evaluation_func=None): """sequences changed, possibly including length""" if not self._shown and not self.eval_while_hidden: self._update_needed = True return prev_vals = self[:] if pos2 is None: pos2 = len(self.alignment.seqs[0]) - 1 if evaluation_func is None: self[:] = [] for pos in range(pos1, pos2+1): self.append(self.evaluate(pos)) else: evaluation_func(pos1, pos2) self._update_needed = False self._edit_bounds = None if self._shown and not self._notifications_suppressed: cur_vals = self[:] if len(prev_vals) != len(cur_vals): bounds = None elif prev_vals == cur_vals: return elif prev_vals[0] != cur_vals[0] and prev_vals[-1] != cur_vals[-1]: bounds = None else: first_mismatch = last_mismatch = None for i, val in enumerate(prev_vals): if val != cur_vals[i]: last_mismatch = i if first_mismatch is None: first_mismatch = i bounds = (first_mismatch, last_mismatch) self.notify_alignment(self.alignment.NOTE_HDR_VALUES, bounds) @property def relevant(self): return True @property def residue_attr_name(self): return self.ATTR_PREFIX + self.ident @classmethod def session_restore(cls, session, alignment, state): inst = cls(alignment, session_restore=True) inst.set_state(state) return inst def set_state(self, state): self.name = state['name'] self._shown = state.get('shown', state.get('visible', None)) self.eval_while_hidden = state['eval_while_hidden'] self[:] = state['contents'] def settings_info(self): """This method needs to return a (name, dict) tuple where 'name' is used to distingush this group of settings from settings of other headers or tools (e.g. "consensus sequence header"), and 'dict' is a dictionary of (attr_name: (Annotation subclass, default_value)) key/value pairs. Annotation subclass will be used by the "seq header header_name setting" command to parse the text for the value into the proper value type. The dictionary must include the base class settings, so super().settings_info() must be called and the returned dictionary updated with the derived class's settings""" # the code relies on the fact that the returned settings dict is a different object every # time (it gets update()d), so don't make it a class variable! from chimerax.core.commands import BoolArg return "base header sequence", { 'initially_shown': (BoolArg, False) } @property def shown(self): return self._shown @shown.setter def shown(self, show): if show == self._shown: return self._shown = show # suppress the alignment notification with self.alignment_notifications_suppressed(): if show: if self._edit_bounds: self.reevaluate(*self._edit_bounds, suppress_callback=True) elif self._update_needed: self.reevaluate() self.notify_alignment(self.alignment.NOTE_HDR_SHOWN) def _add_options(self, options_container, category, verbose_labels, option_data): for base_label, attr_name, opt_class, opt_kw, balloon in option_data: option = opt_class(self._final_option_label(base_label, verbose_labels), None, self._setting_option_cb, balloon=balloon, attr_name=attr_name, settings=self.settings, auto_set_attr=False, **opt_kw) if category is not None: options_container.add_option(category, option) else: options_container.add_option(option) def _final_option_label(self, base_label, verbose_labels): if verbose_labels: return "%s: %s" % (getattr(self, "settings_name", self.name), base_label) return base_label[0].upper() + base_label[1:] def _process_setting_command(self, session, setting_arg_text): from chimerax.core.commands import EnumOf enum = EnumOf(list(self._setting_cmd_annotations.keys())) attr_name, arg_text, remainder = enum.parse(setting_arg_text, session) remainder = remainder.strip() if not remainder: from chimerax.core.errors import UserError raise UserError("No value provided for setting") val, val_text, remainder = self._setting_cmd_annotations[attr_name].parse(remainder, session) if remainder and not remainder.isspace(): from chimerax.core.errors import UserError raise UserError("Extraneous text after command") setattr(self.settings, attr_name, val) def _register_commands(self, registry): from chimerax.core.commands import register, CmdDesc, RestOfLine, SaveFileNameArg register("show", CmdDesc(synopsis='Show %s header' % self.ident), lambda session, hdr=self: setattr(hdr, "shown", True), registry=registry) register("hide", CmdDesc(synopsis='Hide %s header' % self.ident), lambda session, hdr=self: setattr(hdr, "shown", False), registry=registry) register("setting", CmdDesc(required=[('setting_arg_text', RestOfLine)], synopsis="change header setting"), self._process_setting_command, registry=registry) register("save", CmdDesc(required=[('file_name', SaveFileNameArg)], synopsis="save header values to file"), self._save, registry=registry) def _setting_option_cb(self, opt): from chimerax.core.commands import run, StringArg session = self.alignment.session align_arg = "%s " % StringArg.unparse(str(self.alignment)) \ if len(session.alignments.alignments) > 1 else "" run(session, "seq header %s%s setting %s %s" % (align_arg, self.ident, opt.attr_name, StringArg.unparse(str(opt.value)))) def _save(self, session, file_name): from chimerax.io import open_output with open_output(file_name, encoding="utf-8") as f: if hasattr(self, 'save_file_preamble'): print(self.save_file_preamble, file=f) print("%s header for %s" % (self.name, self.alignment), file=f) for i, val in enumerate(self): print("%d:" % (i+1), val, file=f)
def run(session, cmd_text): from chimerax.core.commands import Command cmd = Command(session) cmd.run(cmd_text)
def cmd_open(session, file_names, rest_of_line, *, log=True): tokens = [] remainder = rest_of_line while remainder: token, token_log, remainder = next_token(remainder) remainder = remainder.lstrip() tokens.append(token) provider_cmd_text = "open " + " ".join([FileNameArg.unparse(fn) for fn in file_names] + [StringArg.unparse(token) for token in tokens]) try: database_name = format_name = None for i in range(len(tokens)-2, -1, -2): test_token = tokens[i].lower() if "format".startswith(test_token): format_name = tokens[i+1] elif "fromdatabase".startswith(test_token): database_name = tokens[i+1] from .manager import NoOpenerError mgr = session.open_command fetches, files = fetches_vs_files(mgr, file_names, format_name, database_name) if fetches: try: provider_args = mgr.fetch_args(fetches[0][1], format_name=fetches[0][2]) except NoOpenerError as e: raise LimitationError(str(e)) else: data_format = file_format(session, files[0], format_name) if data_format is None: # let provider_open raise the error, which will show the command provider_args = {} else: try: provider_args = mgr.open_args(data_format) except NoOpenerError as e: raise LimitationError(str(e)) # register a private 'open' command that handles the provider's keywords registry = RegisteredCommandInfo() def database_names(mgr=mgr): return mgr.database_names keywords = { 'format': DynamicEnum(lambda ses=session:format_names(ses)), 'from_database': DynamicEnum(database_names), 'ignore_cache': BoolArg, 'name': StringArg } for keyword, annotation in provider_args.items(): if keyword in keywords: raise ValueError("Open-provider keyword '%s' conflicts with builtin arg of" " same name" % keyword) keywords[keyword] = annotation desc = CmdDesc(required=[('names', OpenFileNamesArg)], keyword=keywords.items(), synopsis="read and display data") register("open", desc, provider_open, registry=registry) except BaseException as e: # want to log command even for keyboard interrupts log_command(session, "open", provider_cmd_text, url=_main_open_CmdDesc.url) raise return Command(session, registry=registry).run(provider_cmd_text, log=log)