Source code for capsul.qt_gui.widgets.activation_inspector

# -*- coding: utf-8 -*-
'''
A tool to debug and inspect nodes activation / deactivation in a pipeline.

Classes
=======
:class:`ActivationInspectorApp`
-------------------------------
:class:`ActivationInspector`
----------------------------
'''

# System import
from __future__ import print_function
from __future__ import absolute_import
import os
import re
import logging
import tempfile
import six

# Define the logger
logger = logging.getLogger(__name__)

# Soma import
from soma.qt_gui import qt_backend
from soma.qt_gui.qt_backend import QtGui
from soma.qt_gui.controller_widget import ScrollControllerWidget

# Capsul import
from capsul.qt_apps.utils.application import Application
import capsul.qt_apps.resources as resources
from capsul.api import get_process_instance
from capsul.qt_gui.widgets import PipelineDeveloperView
from capsul.pipeline.pipeline_nodes import PipelineNode


[docs] class ActivationInspectorApp(Application): """ ActivationInspector Application. While developing a pipeline, nodes are connected through links. Nodes will be automatically activated or disabled depending on their connections (a mandatory link to a disabled node will disable the current one). You will often wonder why a node will not be activated. This tool helps to determine when and why by "playing" activation rules sequences step-by-step and displaying nodes which activate or deactivate at each step. """ # Load some meta information from capsul.info import __version__ as _version from capsul.info import NAME as _application_name from capsul.info import ORGANISATION as _organisation_name def __init__(self, pipeline_path, record_file=None, *args, **kwargs): """ Method to initialize the ActivationInspectorApp class. Parameters ---------- pipeline_path: str (mandatory) the name of the pipeline we want to load. record_file: str (optional) a file where the pipeline activation steps are stored. """ # Inhetritance super(ActivationInspectorApp, self).__init__(*args, **kwargs) # Load the pipeline self.pipeline = get_process_instance(pipeline_path) # Initialize the application self.record_file = record_file self.window = None self.init_window()
[docs] def init_window(self): """ Method to initialize the main window. """ # First set some meta information self.setApplicationName(self._application_name) self.setOrganizationName(self._organisation_name) self.setApplicationVersion(self._version) # Get the user interface description from capsul resources ui_file = os.path.join( os.path.dirname(__file__), "activation_inspector.ui") #ui_file = os.path.join(resources.__path__[0], "activation_inspector.ui") # Create and show the activation/pipeline/controller windows self.pipeline_window = PipelineDeveloperView(self.pipeline, show_sub_pipelines=True) self.controller_window = ScrollControllerWidget(self.pipeline,live=True) self.activation_window = ActivationInspector( self.pipeline, ui_file, self.record_file, developper_view=self.pipeline_window) self.pipeline_window.show() self.activation_window.show() self.controller_window.show() return True
[docs] class ActivationInspector(QtGui.QWidget): """ A Widget to display the pipeline activation process step by step. """ def __init__(self, pipeline, ui_file=None, record_file=None, developper_view=None, parent=None): """ Initialize the ActivationInspector class. Parameters ---------- pipeline: capsul.Pipeline (mandatory) the pipeline we want to inspect. ui_file: str (optional) the path to the qt user interface description file. If not specified, it will find its standard one. record_file: str (optional) a file path where the activation steps are recorded. If not specified (None), it will create a temporary file. developper_view: PipelineDeveloperView (optional) if specified it is possible to click on a plug to set a filter pattern and to update the pipeline activation accordingly. """ # Inheritance: create the application QtGui.QWidget.__init__(self, parent) # load the user interface window if ui_file is None: ui_file = os.path.join( os.path.dirname(__file__), "activation_inspector.ui") self.ui = qt_backend.loadUi(ui_file) # Define dynamic controls self.controls = { QtGui.QListWidget: ["events"], QtGui.QPushButton: ["btnUpdate", "next", "previous"], QtGui.QLineEdit: ["pattern"] } if record_file is None: record_file_s = tempfile.mkstemp() record_file = record_file_s[1] os.close(record_file_s[0]) print('temporary record file:', record_file) class AutoDeleteFile(object): def __init__(self, record_file): self.record_file = record_file def __del__(self): try: os.unlink(self.record_file) except OSError: pass self._autodelete_record_file = AutoDeleteFile(record_file) # Add ui class parameter with the dynamic controls and initialize # default values self.add_controls_to_ui() # Store class parameters self.pipeline = pipeline self.record_file = record_file self.developper_view = developper_view # Set the pipeline record file if folder exists if os.path.isdir(os.path.dirname(self.record_file)): self.pipeline._debug_activations = self.record_file else: raise ValueError( "The record file '{0}' can't be created since the " "base directory does not exist.".format(self.record_file)) # Execute the pipeline activation method self.pipeline.update_nodes_and_plugs_activation() # Refresh the pipeline activation displayed list self.refresh_activation_from_record() # Signals for window interface self.ui.events.currentRowChanged.connect( self.update_pipeline_activation) self.ui.btnUpdate.clicked.connect( self.refresh_activation_from_record) self.ui.next.clicked.connect(self.find_next) self.ui.previous.clicked.connect(self.find_previous) # Dynamically select a filter rule by clicking on the pipeline view # plugs if developper_view is not None: developper_view.plug_clicked.connect(self.set_filter_pattern)
[docs] def show(self): """ Shows the widget and its child widgets. """ self.ui.show()
[docs] def add_controls_to_ui(self): """ Method to find dynamic controls """ # Error message template error_message = "{0} has no attribute '{1}'" # Got through the class dynamic controls for control_type, control_item in six.iteritems(self.controls): # Get the dynamic control name for control_name in control_item: # Try to set the control value to the ui class parameter try: value = self.ui.findChild(control_type, control_name) if value is None: logger.error(error_message.format( type(self.ui), control_name)) setattr(self.ui, control_name, value) except Exception: logger.error(error_message.format( type(self.ui), control_name))
########################################################################### # Slots ###########################################################################
[docs] def refresh_activation_from_record(self): """ Method to display pipeline activation steps from the recorded file. """ # Open the last recorded activation file with open(self.record_file) as openrecord: # Get the header of the file that contains the pipeline identifier # of the recorded activation record_pipeline_id = openrecord.readline().strip() if record_pipeline_id != self.pipeline.id: raise ValueError( "'{0}' recorded activations for pipeline '{1}' but not for " "'{2}'".format(self.record_file, record_pipeline_id, self.pipeline.id)) # Clear the list where the recorded activation is displayed self.ui.events.clear() # Parse the recorded activation file # > Store the activation stack step by step to dynamically replay # the activation self.activations = [] current_activations = {} # > Go through all the activation steps parser = re.compile(r"(\d+)([+-=])([^:]*)(:([a-zA-Z_0-9]+))?") for activation_step in openrecord.readlines(): # > Parse the line iteration, activation, node, x, plug = parser.match( activation_step.strip()).groups() plug = plug or "" # > Store the current activation stack if activation == "+": current_activations["{0}:{1}".format(node, plug)] = True else: del current_activations["{0}:{1}".format(node, plug)] self.activations.append(current_activations.copy()) # > Add a line to the activation display self.ui.events.addItem("{0}{1} {2}:{3}".format( iteration, activation, node, plug)) # Select the last activation step so the pipeline will be # in his final configuration self.ui.events.setCurrentRow(self.ui.events.count() - 1)
[docs] def update_pipeline_activation(self, index): """ Method that is used to replay the activation step by step. When a specific activation step is selected, the pipeline will reflect the selected activation status """ # Get the activation associated to the 'index' stack level activations = self.activations[index] # Update the pipeline activation to meet the current selection for node in self.pipeline.all_nodes(): # Restore the plugs and nodes activations node_name = node.full_name for plug_name, plug in six.iteritems(node.plugs): plug.activated = activations.get( "{0}:{1}".format(node_name, plug_name), False) node.activated = activations.get("{0}:".format(node_name), False) # Refresh views relying on plugs and nodes selection for node in self.pipeline.all_nodes(): if isinstance(node, PipelineNode): node.process.selection_changed = True
[docs] def find_next(self): """ Forward search for a pattern in the activation list. Returns ------- is_found: int 1 if a match has been found, 0 otherwise. """ # Build the search pattern pattern = re.compile(self.ui.pattern.text()) # Forward search # > Get the next (n+1) activation row next_activation_row = self.ui.events.currentRow() + 1 # > Search recursively until a match is found while next_activation_row < self.ui.events.count(): if pattern.search(self.ui.events.item(next_activation_row).text()): self.ui.events.setCurrentRow(next_activation_row) return 1 next_activation_row += 1 return 0
[docs] def find_previous(self): """ Backward search for a pattern in the activation list. Returns ------- is_found: bool True if a match has been found, False otherwise. """ # Build the search pattern pattern = re.compile(self.ui.pattern.text()) # Backward search # > Get the previous (n-1) activation row previous_activation_row = self.ui.events.currentRow() - 1 # > Search recursively until a match is found while previous_activation_row > 0: if pattern.search(self.ui.events.item(previous_activation_row).text()): self.ui.events.setCurrentRow(previous_activation_row) return 1 previous_activation_row -= 1 return 0
[docs] def set_filter_pattern(self, filter_pattern): """ Method that set a filter pattern. Try a forward search followed by a backward one. Parameters ---------- filter_pattern: str (mandatory) the filter pattern we want to set. """ # Display the filter pattern self.ui.pattern.setText(filter_pattern) # Try to select an item corresponding to the filter pattern if not self.find_next(): self.find_previous()
if __name__ == "__main__": """ Command example: >>> python activation_inspector.py -p funtk.connectivity.conn.Conn -r ~/tmp/conn_activation.txt """ # Create a tool we can control easily import sys from optparse import OptionParser # Define activation inspector options parser = OptionParser() parser.add_option("-p", "--pipeline", dest="pipeline_path", help=("the pipeline path we want to investigate: " "module1.module2.Pipeline.")) parser.add_option("-r", "--record", dest="record_file", help="the file where the activation steps are recorded.") (options, args) = parser.parse_args() sys.argv = [] # Start the application if QtGui.QApplication.instance() is None: app = ActivationInspectorApp(options.pipeline_path, options.record_file) sys.exit(app.exec_())