Source code for schrodinger.project.utils

import uuid
from contextlib import contextmanager

from schrodinger import get_maestro
from schrodinger import project
from schrodinger.infra import mm
from schrodinger.project import ProjectException
from schrodinger.ui.qt import utils as qt_utils

maestro = get_maestro()
PROPNAME_MARK = 'b_m_Mark'
NOT_IN_WORKSPACE = project.NOT_IN_WORKSPACE
IN_WORKSPACE = project.IN_WORKSPACE
LOCKED_IN_WORKSPACE = project.LOCKED_IN_WORKSPACE
PROP_TRJ = mm.M2IO_DATA_TRAJECTORY_FILE


[docs]def get_PT(): """ Safe getter for the project instance that swallows exceptions that occur if the project has been closed. Using this function avoids unnecessary tracebacks that occur if the project is requested in the brief period after the project has been closed before a new one is created. :return: a project instance, if a project is available in Maestro :rtype: project.Project or NoneType """ if not maestro: return None try: return maestro.project_table_get() except ProjectException: # project was closed return None
[docs]def set_entries_pinned(entry_ids, pin): """ Pin or unpin entries from the workspace. :param entry_ids: a list of entry IDs :type entry_ids: list(str) :param pin: whether to pin or unpin the specified entries to the workspace :type pin: bool """ pt = maestro.project_table_get() for entry_id in entry_ids: row = pt.getRow(entry_id) if pin: row.in_workspace = LOCKED_IN_WORKSPACE elif row.in_workspace == LOCKED_IN_WORKSPACE: row.in_workspace = IN_WORKSPACE
[docs]def set_entry_pinned(entry_id, pin): """ Pin or unpin an entry from the workspace. :param entry_id: an entry ID :type entry_id: str :param pin: whether to pin or unpin the specified entry to the workspace :type pin: bool """ set_entries_pinned([entry_id], pin)
[docs]def set_entries_included(entry_ids, include, override_pin=False): """ For the specified entries, either include them in or exclude them from the workspace. :param entry_ids: a list of entry IDs :type entry_ids: list(str) :param include: whether to include (`True`) or exclude (`False`) :type include: bool :param override_pin: if `False`, ignore pinned entries. If `True`, exclude entries that are set to be excluded even if they are pinned. If pinned entries are set to be included, leave them as pinned (because they are also included as pinned entries) :type override_pin: bool """ pt = maestro.project_table_get() for entry_id in entry_ids: row = pt.getRow(entry_id) is_pinned = row.in_workspace == LOCKED_IN_WORKSPACE if not override_pin and is_pinned: continue elif include and not is_pinned: # If an entry is already pinned, setting it to be IN_WORKSPACE will # un-pin it row.in_workspace = IN_WORKSPACE elif not include: row.in_workspace = NOT_IN_WORKSPACE
[docs]def set_entry_included(entry_id, include, override_pin=False): """ For the specified entry, either include it in or exclude it from the workspace. :param entry_id: an entry ID :type entry_id: str :param include: whether to include (`True`) or exclude (`False`) :type include: bool :param override_pin: if `False`, ignore pinned entries. If `True`, exclude entries that are set to be excluded even if they are pinned. If pinned entries are set to be included, leave them as pinned (because they are also included as pinned entries) :type override_pin: bool """ set_entries_included([entry_id], include, override_pin)
[docs]def entry_is_pinned(entry_id): """ Return whether the specified entry is pinned in the workspace. :param entry_id: the entry ID of a row in the project :type entry_id: str :return: whether the entry is pinned in the workspace :rtype: bool """ row = get_row(entry_id) return row.in_workspace == LOCKED_IN_WORKSPACE
[docs]def entry_is_included(entry_id): """ Return whether the specified entry is included in the workspace. :param entry_id: the entry ID of a row in the project :type entry_id: str :return: whether the entry is included in the workspace :rtype: bool """ row = get_row(entry_id) return row.in_workspace in (IN_WORKSPACE, LOCKED_IN_WORKSPACE)
[docs]def entry_is_selected(entry_id): """ Return whether the specified entry is selected in the project. :param entry_id: the entry ID of a row in the project :type entry_id: str :return: whether the entry is selected in the project :rtype: bool """ row = get_row(entry_id) return row.is_selected
[docs]def set_entry_locked(entry_id, lock): """ Set the supplied project table entry to be "locked" or "unlocked." These operations are grouped because they are both performed when locking an entry in the visual interface. :param entry_id: the entry ID of a row in the project :type entry_id: str :param lock: whether to lock or unlock the specified entry :type lock: bool """ row = get_row(entry_id) row.read_only = lock row.deletable = not lock
[docs]def entry_is_locked(entry_id): """ Return whether the supplied project entry is read-only and not deletable. These properties are both tested because both values are set when locking an entry in the visual interface. :param entry_id: the entry ID of a row in the project :type entry_id: str :return: whether the entry is locked :rtype: bool """ row = get_row(entry_id) return row.read_only and not row.deletable
[docs]def remove_entries(entry_ids): """ Cleanly remove the specified entries from the project. If an entry cannot be found, do not raise an exception. :param entry_id: a list of entry IDs :type entry_id: list(str) """ if not entry_ids: return pt = maestro.project_table_get() # Unlock rows and make them deletable valid_entry_ids = set() for entry_id in entry_ids: row = pt.getRow(entry_id) if row is None: continue set_entry_locked(entry_id, False) valid_entry_ids.add(entry_id) with qt_utils.suppress_signals(pt.project_model): if valid_entry_ids: entries_str = ', '.join(valid_entry_ids) maestro.command('entrywsexclude entry_id "{0}"'.format(entries_str)) for entry_id in valid_entry_ids: pt.deleteRow(entry_id) pt.update()
[docs]def create_subgroup(entry_ids, group_name, subgroup_title): """ Create a subgroup of the specified project group, and place the supplied entries into it. :param entry_ids: entry IDs for project entries to be placed into the new subgroup :type entry_ids: list(str) :param group_name: name of the group to which this new subgroup will belong :type group_name: str :param subgroup_title: the title of the new subgroup :type subgroup_title: str :return: the new group object :rtype: project.EntryGroup """ pt = maestro.project_table_get() with preserve_selection(pt): pt.selectRows(entry_ids=entry_ids) cmd = ('entrygroupcreatewithselected "{0}" autodetectparentgroup=false' ' parentgroup="{1}"').format(subgroup_title, group_name) maestro.command(cmd) entry_id = next(iter(entry_ids)) return pt.getRow(entry_id).group
[docs]def move_group(group, row_number): """ Move all of the entries in the specified group to be below a specified row number in the project, while still retaining their group identity. :param group: a non-empty entry group :type group: project.EntryGroup :param row_number: the number of a row in the project: one that designates order within the project, not an entry ID :type row_number: int """ pt = maestro.project_table_get() entry_ids = {row.entry_id for row in group.all_rows} group_name = group.name parent_group = group.getParentGroup() parent_group_name = None if parent_group is not None: parent_group_name = parent_group.name with preserve_selection(pt): pt.selectRows(entry_ids=entry_ids) # Move all group entries to the desired position cmd = f'entrymoveselection {row_number} above_row=false' maestro.command(cmd) # Re-create the group with the same entries (the above command will have # moved all entries out of the group) eid_str = ', '.join(entry_ids) cmd = f'entrygroupcreate "{group_name}" entry_id "{eid_str}"' if parent_group_name is not None: cmd += f' parentgroup="{parent_group_name}"' maestro.command(cmd)
[docs]def get_rows(entry_ids): """ :param entry_id: a list of entry IDs :type entry_id: list(str) :return: a list of either project rows corresponding to the provided entry IDs, if they exist, or `None` otherwise :rtype: list(project.ProjectRow or None) """ # TODO modify this function to raise exception if no such entry exists. # TODO: Consider returning a generator instead of a list. pt = maestro.project_table_get() return [pt.getRow(entry_id) for entry_id in entry_ids]
[docs]def get_row(entry_id): """ :param entry_id: an entry ID :type entry_id: str :return: row corresponding to the entry ID, if it exists :rtype: project.ProjectRow or None """ # TODO modify this function to raise exception if no such entry exists. rows = get_rows([entry_id]) return rows[0]
[docs]def get_structures_for_entry_ids(entry_ids, copy=True, props=True, pt=None): """ Iterate over structures for the given entry IDs. """ if not entry_ids: return if maestro: # if not running in unit tests maestro.project_table_synchronize() if pt is None: pt = maestro.project_table_get() for eid in entry_ids: yield pt[eid].getStructure(workspace_sync=False, copy=copy, props=props)
[docs]def get_included_structures(copy=True, props=True, pt=None): """ Iterate over structures for entries that are included n the Workspace. """ if maestro: # if not running in unit tests maestro.project_table_synchronize() if pt is None: pt = maestro.project_table_get() for row in pt.included_rows: yield row.getStructure(workspace_sync=False, copy=copy, props=props)
[docs]def get_included_entry_ids(pt=None): """ Iterate over entry IDs for entries that are included in the Workspace. :rtype: Iterator[str] :return: Each iteration yields the entry id of an included project entry """ if maestro: # if not running in unit tests maestro.project_table_synchronize() if pt is None: pt = maestro.project_table_get() for row in pt.included_rows: yield row.entry_id
[docs]def get_selected_structures(copy=True, props=True, pt=None): """ Iterate over structures for entries that are selected in the Project Table. """ if maestro: # if not running in unit tests maestro.project_table_synchronize() if pt is None: pt = maestro.project_table_get() for row in pt.selected_rows: yield row.getStructure(workspace_sync=False, copy=copy, props=True)
[docs]def get_selected_entry_ids(pt=None): """ Iterate over entry IDs for entries that are selected in the Project Table. :rtype: Iterator[str] :return: Each iteration yields the entry id of an included project entry """ if maestro: # if not running in unit tests maestro.project_table_synchronize() if pt is None: pt = maestro.project_table_get() for row in pt.selected_rows: yield row.entry_id
[docs]def get_structure(entry_id): """ :param entry_ids: an entry ID :type entry_ids: str :return: a structure from the specified entry, if available :rtype: structure.Structure or None """ # TODO modify this function to raise exception if no such entry exists. pt = maestro.project_table_get() row = pt.getRow(entry_id) if row is None: return None return row.getStructure()
[docs]@contextmanager def preserve_selection(pt=None): """ Save the selection state of the project, then restore it on exit. :param pt: optionally, a project instance :type pt: project.Project or NoneType """ pt = pt or maestro.project_table_get() entry_ids = [row.entry_id for row in pt.selected_rows] yield pt.selectRows(entry_ids=entry_ids)
[docs]@contextmanager def unlock_entries(entry_ids, update_pt=True): """ Temporarily unlock the specified entries, if they are locked. Otherwise, do not modify their lock state. Suppress signals from the project model during this process. :param entry_ids: a list of entry IDs :type entry_ids: list(str) :param update_pt: whether to update the project table on exiting the context environment :type update_pt: bool """ pt = maestro.project_table_get() lock_map = {entry_id: entry_is_locked(entry_id) for entry_id in entry_ids} with qt_utils.suppress_signals(pt.project_model): for entry_id in entry_ids: set_entry_locked(entry_id, False) yield with qt_utils.suppress_signals(pt.project_model): for entry_id, lock in lock_map.items(): if pt.getRow(entry_id) is not None: set_entry_locked(entry_id, lock) if update_pt: pt.update()
[docs]def create_child_group(entry_ids, parent_group_name, group_name=None, before_entry_id=None): """ Create a new group using the supplied entry IDs as a child group of the specified parent group. :raise ValueError: if no entry IDs are provided :param entry_ids: a list of entry IDs to put into the new group :type entry_ids: list(str) :param parent_group_name: the name of the group that should be the parent of the group created by this function :type parent_group_name: str :param group_name: the name of the group to be created; if not supplied, a unique name will be randomly generated :type group_name: str or None :param before_entry_id: optionally, the entry ID of a project entry above which the new subgroup should be created :type before_entry_id: str or NoneType :return: the new group :rtype: project.EntryGroup """ if not entry_ids: raise ValueError('Cannot create a group with no entries.') group_name = group_name or generate_unique_group_name() esl = 'entry_id ' + ', '.join(entry_ids) cmd = f'entrygroupcreate {group_name} {esl} parentgroup={parent_group_name}' if before_entry_id is not None: cmd += f' positionbeforeentryid={before_entry_id}' maestro.command(cmd) return get_entry_group(group_name)
[docs]def get_entry_group(group_name): """ Return the group in the project with the specified name. :param group_name: the name of the desired group :type group_name: str :return: the group with the specified name, if available :rtype: str or None """ pt = maestro.project_table_get() for group in pt.groups: if group.name == group_name: return group
[docs]def generate_unique_group_name(): """ :return: a unique entry group name :rtype: str """ pt = maestro.project_table_get() extant_group_names = {group.name for group in pt.groups} group_name = None while group_name is None or group_name in extant_group_names: group_name = str(uuid.uuid4()) return group_name
[docs]def get_top_entry(entry_ids): """ Given a list of project entry IDs, return the one that appears in the highest visual row in the project. :param entry_ids: a list of project entry IDs :type entry_ids: list(str) :return: the entry ID corresponding to the "highest" entry in `entry_ids` :rtype: str """ rows = [get_row(entry_id) for entry_id in entry_ids] rows = sorted(rows, key=lambda row: row.row_number) return rows[0].entry_id
[docs]def entry_is_marked(entry_id): """ Return whether the specified entry is "marked" with the Maestro property `b_m_Mark`. :param entry_id: a project entry ID :type entry_id: str :return: whether the specified entry has the Mark property, and that the value of that property is `True` :rtype: bool """ row = get_row(entry_id) if PROPNAME_MARK in row.property: return row.property[PROPNAME_MARK] return False
[docs]def set_entries_marked(entry_ids): """ Programmatically mimic the behavior of the Maestro command "entrymarkincluded", but for an arbitrary group of entries. If all entries are marked, unmark all entries. Otherwise, mark all entries. :param entry_ids: a list of entry IDs :type entry_ids: list(str) """ set_marked = not all(entry_is_marked(entry_id) for entry_id in entry_ids) for entry_id in entry_ids: row = get_row(entry_id) row.property[PROPNAME_MARK] = set_marked
[docs]def get_base_entry_group(entry_id): """ For the specified entry, return the highest-level parent group to which it belongs, if possible. :param entry_id: a project entry ID :type entry_id: str :return: the top-level entry group that contains the entry, if the entry belongs to any group :rtype: project.EntryGroup or NoneType """ row = get_row(entry_id) group = row.group if group is None: return None parent_group = group.getParentGroup() while parent_group is not None: new_parent_group = parent_group.getParentGroup() group = parent_group parent_group = new_parent_group return group
[docs]def move_row(entry_id, row_number): """ Move the specified entry row to the specified position in the project. :param entry_id: an entry ID :type entry_id: str :param row_number: the position to which the specified entry should be moved :type row_number: int """ row = get_row(entry_id) if row.row_number == row_number: return with preserve_selection(): row.selectOnly() cmd = f'entrymoveselection {row_number} above_row=true' maestro.command(cmd)
[docs]@contextmanager def modify_row_structure(entry_id=None, row=None): """ Context which returns a structure from the row corresponding to the given entry id and sets the structure back to the row afterwards. Also preserves the structure's trajectory property. :param entry_id: entry id of rows to modify :type entry_id: str :param `project.ProjectRow` row: The project row for this entry. Either row or entry_id must be supplied :return: a structure in a context manager that will set the structure back onto the project row when control is returned to this function :rtype: structure.Structure or NoneType """ if entry_id: row = get_row(entry_id) if row: st = row.getStructure() # Workaround for MAE-41472 row_traj = row.property.get(PROP_TRJ) if row_traj: st.property[PROP_TRJ] = row_traj yield st row.setStructure(st) else: yield None
[docs]def has_valid_wscore_block(row): """ Given a ProjectRow instance, return True if the entry has associated WScore data and a valid receptor; False otherwise. """ struc_handle = project.mmproj.mmproj_index_entry_get_ct( row._project_handle, row._entry_index) try: urh = mm.mmct_ct_m2io_get_unrequested_handle(struc_handle) except mm.MmException: return False try: mm.m2io_goto_block(urh, "m_wsviz_data", 1) except mm.MmException: return False else: mm.m2io_leave_block(urh) return True
# There is some feature overlap between the ProjectStructure class and # modify_row_structure. However, ProjectStructure is safe to use in included # entry callbacks and modify_row_structure is not (PANEL-16125)
[docs]class ProjectStructure(object): """ A Context manager that gets the structure for an entry from the project table and then optionally puts it back in the PT upon exit """
[docs] def __init__(self, row=None, eid=None, modify=True): """ Create a ProjectStructure instance :param `project.ProjectRow` row: The project row for this entry. Either row or eid must be supplied :param str eid: The entry ID for this entry. Either row or eid must be suppled. If the eid is not found in the project, the self.row attribute will be None. :param bool modify: If True (default), set the structure back in the project on exit. If False, do not modify the structure in the project. :raise AttributeError: If neither row nor eid are supplied """ if eid: try: self.row = maestro.project_table_get().getRow(eid) except project.ProjectException: # Invalid projects can happen during project close self.row = None else: self.row = row self.modify = modify
def __enter__(self): """ :rtype: `structure.Structure` or None :return: The structure for this project row or None if the originally entry ID is no longer valid """ if self.row is None: return None self.struct = self.row.getStructure() # This is a workaround for MAE-41472 row_traj = self.row.property.get(PROP_TRJ) if row_traj: self.struct.property[PROP_TRJ] = row_traj return self.struct def __exit__(self, *args): if self.row and self.modify: self.row.setStructure(self.struct)
[docs]def get_trajectory_path(proj, eid): """ Method will return None if passed a falsey entry ID. Return trajectory file path if any, or None. :param: proj: Project on which to operate. :type: proj: Project :param: eid: Entry id associated with the given project. :type: eid: int or str """ return proj.project_model.getTrajectoryPath(int(eid)) if eid else None
[docs]def has_trajectory(proj, eid): """ Whether the entry with given entry id has a trajectory associated with it :param: proj: Project on which to operate. :type: proj: Project :param: eid: Entry id associated with the given project. :type: eid: int or str """ return proj.project_model.hasTrajectoryData(int(eid))
[docs]def has_desmond_trajectory(proj, eid): """ Whether the entry with given entry id has desmond trajectory :param: proj: Project on which to operate. :type: proj: Project :param: eid: Entry id associated with the given project. :type: eid: int or str """ return proj.project_model.hasDesmondTrajectory(int(eid))
[docs]def has_materials_trajectory(proj, eid): """ Whether the entry with given entry id has materials trajectory :param: proj: Project on which to operate. :type: proj: Project :param: eid: Entry id associated with the given project. :type: eid: int or str """ return proj.project_model.hasMaterialsTrajectory(int(eid))
[docs]@contextmanager def entry_excluded(entry_id: str): """ Exclude the given entry temporarily. :param entry_id: Entry id to be excluded temporarily. """ is_included = entry_is_included(entry_id) is_pinned = entry_is_pinned(entry_id) if is_included: set_entry_included(entry_id, False, override_pin=True) yield if is_included: set_entry_included(entry_id, True, override_pin=True) set_entries_pinned([entry_id], is_pinned)