diff --git a/docs/source/dev_guide/atom_enaml.rst b/docs/source/dev_guide/atom_enaml.rst index 1cc07547..8c4261fa 100644 --- a/docs/source/dev_guide/atom_enaml.rst +++ b/docs/source/dev_guide/atom_enaml.rst @@ -26,3 +26,5 @@ Enaml .. todo:: Need to describe the syntax for overriding declarative functions. + +See the package documentation at http://enaml.readthedocs.io \ No newline at end of file diff --git a/docs/source/dev_guide/measurement.rst b/docs/source/dev_guide/measurement.rst index 78930403..eed863da 100644 --- a/docs/source/dev_guide/measurement.rst +++ b/docs/source/dev_guide/measurement.rst @@ -44,8 +44,8 @@ pre-hook can have two purposes : Adding a pre-hook requires to : -- implement the logic by subclassing |BasePreExecutionHook|. The methods that can be - overridden are : +- implement the logic by subclassing |BasePreExecutionHook|. The methods that + can be overridden are : - check: make sure that the measurement is in a proper state to be executed. - run: execute any custom logic. If any task is to be executed it should be @@ -53,7 +53,7 @@ Adding a pre-hook requires to : - pause/resume/stop: to implement if the run method execution can take a long time (typically if tasks are involved). - list_runtimes: let the measurement know the runtime dependencies (such as - instrument drivers) if any. + instrument drivers) if any. They are then collected by the measurement. Additionally if any entry is contributed to the task hierarchy they should be added when the tool is linked (or later during edition of the tool). @@ -68,14 +68,16 @@ Adding a pre-hook requires to : - If a make_view method has been declared then one needs to create the associated widget which should inherit of |Container|. + The syntax of the make_view is defined in the |BaseToolDeclaration| class. Monitors ^^^^^^^^ -Monitors are used to follow the progress of a measurement. They specify a number of -database entries they are interested in and will receive notifications when -the concerned entry is updated during the execution of the task hierarchy. +Monitors are used to follow the progress of a measurement. They specify a +number of database entries they are interested in and will receive +notifications whenthe concerned entry is updated during the execution of the +task hierarchy. Adding a monitor requires to : @@ -119,8 +121,8 @@ asked not to run them). They are hence perfectly fitted to run clean up. Adding a post-hook requires to : -- implement the logic by subclassing |BasePostExecutionHook|. The methods that can be - overridden are : +- implement the logic by subclassing |BasePostExecutionHook|. The methods that + can be overridden are : - check: make sure that the measurement is in a proper state to be executed. - run: execute any custom logic. If any task is to be executed it should be @@ -130,9 +132,10 @@ Adding a post-hook requires to : - pause/resume/stop: to implement if the run method execution can take a long time (typically if tasks are involved). - list_runtimes: let the measurement know the runtime dependencies (such as - instrument drivers) if any. To access those dependencies inside the - `run` method one can use the |Measurement.get_runtime_dependencies| method - called with the id of the hook. + instrument drivers) if any. They are then collected by the measurement. + To access those dependencies inside the `run` method one can use + the |Measurement.get_runtime_dependencies| method called with the + id of the hook. Additionally if any entry is contributed to the task hierarchy they should be added when the tool is linked (or later during edition of the tool). @@ -147,6 +150,7 @@ Adding a post-hook requires to : - If a make_view method has been declared then one needs to create the associated widget which should inherit of |Container|. + The syntax of the make_view is defined in the |BaseToolDeclaration| class. .. note :: diff --git a/exopy/app/dependencies/plugin.py b/exopy/app/dependencies/plugin.py index d488ba33..36c7d3a7 100644 --- a/exopy/app/dependencies/plugin.py +++ b/exopy/app/dependencies/plugin.py @@ -284,7 +284,7 @@ def validate_dependencies(self, kind, dependencies): return not container.errors, container.errors def collect_dependencies(self, kind, dependencies, owner=None): - """Collect that a set of dependencies. + """Collect a set of dependencies. For runtime dependencies if permissions are necessary to use a dependence they are requested and should released when they are no diff --git a/exopy/measurement/hooks/addtask_hook.py b/exopy/measurement/hooks/addtask_hook.py new file mode 100644 index 00000000..1155e7bd --- /dev/null +++ b/exopy/measurement/hooks/addtask_hook.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2015-2018 by Exopy Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Implementaion of the AddTaskHook hook. + +""" +from enaml.workbench.api import Workbench +from atom.api import Typed, Unicode, Tuple +from .base_hooks import BasePostExecutionHook +from ...tasks.api import RootTask +from ..engines.base_engine import ExecutionInfos, BaseEngine + + +class AddTasksHook(BasePostExecutionHook): + """Post-execusion hook to add a hierarchy of tasks. + + """ + #: Reference to the root task at the base of the hierarchy + root_task = Typed(RootTask) + + #: Reference to the measurement workbench + workbench = Typed(Workbench) + + #: Reference to the measurement engine + engine = Typed(BaseEngine) + + #: Reference to the hook root task path; + #: it is the same as the one of the measurement and will not be displayed + default_path = Unicode() + + #: Reference to the build and runtime dependencies of the hook tasks + dependencies = Tuple() + + def __init__(self, declaration, workbench): + self.root_task = RootTask() + self.workbench = workbench + super().__init__(declaration=declaration) + + def check(self, workbench, **kwargs): + """ Check that the post-hook task can be executed + + """ + # set the root_task default path to the one of the measure + self.root_task.default_path = self.measurement.root_task.default_path + res, traceback = self.root_task.check() + return res, traceback + + def run(self, workbench, engine): + """ Execute the post-hook task + + """ + # measure has collected the runtime dependencies given by list_runtimes + meas_deps = self.measurement.dependencies + runtime_deps = meas_deps.get_runtime_dependencies(self.declaration.id) + # on the other hand, we need to collect the build dependencies + build_deps = self.dependencies[0].dependencies + cmd = 'exopy.app.dependencies.collect' + core = workbench.get_plugin('enaml.workbench.core') + deps = core.invoke_command(cmd, dict(dependencies=build_deps, + kind='build')) + if deps.errors: + raise RuntimeError('Error when collecting the build dependencies') + + infos = ExecutionInfos(id=self.measurement.id+'.posttask', + task=self.root_task, + build_deps=deps.dependencies, + runtime_deps=runtime_deps, + observed_entries=[], # no monitor for the hooks + checks=not self.measurement.forced_enqueued, + ) + execution_result = engine.perform(infos) + self.engine = engine + return execution_result + + def pause(self): + """ Pause the task + + """ + self.engine.pause() + + def resume(self): + """ Resume the task + + """ + self.engine.resume() + + def stop(self, force=False): + """ Stop the task + + """ + self.engine.stop(force) + + def list_runtimes(self, workbench): + """ Returns the run_time dependencies + + """ + cmd = 'exopy.app.dependencies.analyse' + core = workbench.get_plugin('enaml.workbench.core') + deps = core.invoke_command(cmd, + {'obj': self.root_task, + 'dependencies': ['build', 'runtime']}) + self.dependencies = deps + return deps[1] + + def get_state(self): + """ Return the informations to save the post hook + + """ + core = self.workbench.get_plugin('enaml.workbench.core') + cmd = 'exopy.tasks.save' + task_prefs = core.invoke_command(cmd, {'task': self.root_task}, self) + return task_prefs + + def set_state(self, state): + """ Load the post hook + + """ + cmd = 'exopy.tasks.build_root' + kwarg = {'mode': 'from config', 'config': state, + 'build_dep': self.workbench} + try: + core = self.workbench.get_plugin('enaml.workbench.core') + self.root_task = core.invoke_command(cmd, kwarg) + except Exception: + msg = 'Building %s, failed to restore post hook task : %s' + errors['post hook'] = msg % (state.get('name'), format_exc()) + return None, errors diff --git a/exopy/measurement/hooks/addtask_view.enaml b/exopy/measurement/hooks/addtask_view.enaml new file mode 100644 index 00000000..ca721054 --- /dev/null +++ b/exopy/measurement/hooks/addtask_view.enaml @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2015-2018 by Exopy Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Widget associated with the AddTaskHook. + +""" +from atom.api import Typed +from enaml.widgets.api import Container, PushButton, Label +from ...tasks.tasks.base_views import RootTaskView + + +enamldef AddTasksView(Container): + """ Widget used for the AddTaskHook + + """ + #: Reference to the hook edited with this view + attr hook + + #: Reference to the corresponding declaration + attr declaration + + #: Reference to the corresponding measurement workbench + attr workbench + + hug_width = 'ignore' + hug_height = 'ignore' + RootTaskView: view: + core = workbench.get_plugin('enaml.workbench.core') + task = hook.root_task + show_path = False diff --git a/exopy/measurement/manifest.enaml b/exopy/measurement/manifest.enaml index 3aa3917c..7ec5df85 100644 --- a/exopy/measurement/manifest.enaml +++ b/exopy/measurement/manifest.enaml @@ -26,7 +26,7 @@ from ..instruments.api import InstrUser from .engines.process_engine import ProcessEngine from .editors.api import Editor -from .hooks.api import PreExecutionHook +from .hooks.api import PreExecutionHook, PostExecutionHook logger = logging.getLogger(__name__) @@ -216,6 +216,25 @@ enamldef MeasureManifest(PluginManifest): manifest: from .hooks.internal_checks import InternalChecksHook return InternalChecksHook(declaration=self) + Extension: + id = 'post-execution' + point = manifest.id + '.post-execution' + PostExecutionHook: + id = 'exopy.addtask_hook' + description = ('Run an additional task at the end of a measure,' + 'even if it is stopped.') + + new => (workbench, default=False): + from .hooks.addtask_hook import AddTasksHook + return AddTasksHook(declaration=self, + workbench=workbench) + + make_view => (workbench, hook): + with enaml.imports(): + from .hooks.addtask_view import AddTasksView + return AddTasksView(declaration=self, hook=hook, + workbench=workbench) + Extension: id = 'preferences' point = 'exopy.app.preferences.plugin' diff --git a/exopy/measurement/measurement.py b/exopy/measurement/measurement.py index c9992f7f..4dfa339f 100644 --- a/exopy/measurement/measurement.py +++ b/exopy/measurement/measurement.py @@ -322,8 +322,8 @@ def save(self, path): config.update(self.preferences_from_members()) # First save the task. - core = self.plugin.workbench.get_plugin(u'enaml.workbench.core') - cmd = u'exopy.tasks.save' + core = self.plugin.workbench.get_plugin('enaml.workbench.core') + cmd = 'exopy.tasks.save' task_prefs = core.invoke_command(cmd, {'task': self.root_task, 'mode': 'config'}, self) config['root_task'] = {} diff --git a/exopy/measurement/workspace/tools_edition.enaml b/exopy/measurement/workspace/tools_edition.enaml index 70a9c281..524c959a 100644 --- a/exopy/measurement/workspace/tools_edition.enaml +++ b/exopy/measurement/workspace/tools_edition.enaml @@ -122,8 +122,10 @@ enamldef ToolsEditor(DestroyableContainer): main: attr _names = ids_to_unique_names(getattr(measurement, _kind), reverse=True) constraints << [hbox(tools, *(list(inc.objects) + - [vbox(add, remove, up, down, spacer)])) - ] + [vbox(add, remove, up, down, spacer)])), + add.right == contents_right, + tools.bottom == contents_bottom, + tools.top == contents_top] func update_items(): """Update the list of tools. @@ -132,6 +134,7 @@ enamldef ToolsEditor(DestroyableContainer): main: self._names = ids_to_unique_names(getattr(measurement, _kind), reverse=True) tools.items = list(_names) + if not _names: tools.selected_item = None @@ -181,13 +184,13 @@ enamldef ToolsEditor(DestroyableContainer): main: Include: inc: objects << (make_view(tools.selected_item) if tools.selected_item - else []) + else [Container()]) destroy_old = False PushButton: add: text = 'Add' enabled << not all([id in tools.items - for id in getattr(measurement.plugin, _kind)]) + for id in getattr(measurement.plugin, _kind)]) clicked :: selector = ToolSelector(measurement=measurement, kind=kind) res = selector.exec_() @@ -263,27 +266,22 @@ enamldef ToolsEditorDockItem(DockItem): main: Page: title = 'Pre-execution' name = 'exopy.measurement.workspace.tools.pre_hooks' - Container: - constraints << [hbox(pre_ed, spacer)] - ToolsEditor: pre_ed: - kind = 'pre-hook' - measurement << main.measurement - mandatory_tools = ['exopy.internal_checks'] + ToolsEditor: pre_ed: + kind = 'pre-hook' + measurement << main.measurement + mandatory_tools = ['exopy.internal_checks'] Page: title = 'Monitors' name = 'exopy.measurement.workspace.tools.monitors' - Container: - constraints << [hbox(mon_ed, spacer)] - ToolsEditor: mon_ed: - kind = 'monitor' - measurement << main.measurement + ToolsEditor: mon_ed: + kind = 'monitor' + measurement << main.measurement Page: title = 'Post-execution' name = 'exopy.measurement.workspace.tools.post_hooks' - Container: - constraints << [hbox(post_ed, spacer)] - ToolsEditor: post_ed: - kind = 'post-hook' - measurement << main.measurement + ToolsEditor: post_ed: + kind = 'post-hook' + measurement << main.measurement + diff --git a/exopy/tasks/tasks/base_views.enaml b/exopy/tasks/tasks/base_views.enaml index b957a022..292b74b5 100644 --- a/exopy/tasks/tasks/base_views.enaml +++ b/exopy/tasks/tasks/base_views.enaml @@ -18,7 +18,7 @@ from atom.api import Event from enaml.widgets.api import (GroupBox, Stack, StackItem, FileDialogEx, Label, Field, ToolButton, CheckBox) from enaml.core.api import d_, d_func -from enaml.layout.api import hbox, vbox, align +from enaml.layout.api import hbox, vbox, align, spacer from ...app.icons.api import get_icon from ...utils.enaml_destroy_hook import add_destroy_hook @@ -79,6 +79,9 @@ enamldef RootTaskView(BaseTaskView): main: #: Reference to the core plugin of the application. attr core + #: Option to display or not the RootTask path + attr show_path = True + root = main refresh => (): @@ -179,14 +182,19 @@ enamldef RootTaskView(BaseTaskView): main: view.root = None self.root = None - constraints = [vbox(hbox(p_lab, p_val, p_exp, prof), editor), + constraints = [vbox(hbox(p_lab, p_val, p_exp, prof), editor, spacer), align('v_center', p_lab, p_val)] Label: p_lab: text = 'Root path' + visible = show_path + Field: p_val: text := task.default_path + visible = show_path + ToolButton: p_exp: + visible = show_path icon = get_icon(core.workbench, 'folder-open') clicked :: curr_path = task.default_path @@ -198,6 +206,7 @@ enamldef RootTaskView(BaseTaskView): main: task.default_path = path CheckBox: prof: text = 'Profile' + visible = show_path checked := task.should_profile tool_tip = 'Profile the execution of the task and dump the result.' diff --git a/exopy/tasks/utils/saving.py b/exopy/tasks/utils/saving.py index 7d75af2d..65ced909 100644 --- a/exopy/tasks/utils/saving.py +++ b/exopy/tasks/utils/saving.py @@ -21,15 +21,15 @@ def save_task(event): - """Save a task in memory or in an .ini file. + """Save a task in memory. Parameters ---------- task : BaseTask Task to save. - mode : {'config', 'template'} - Should the task be returned as a dict (ConfigObj) or saved as a, + mode : {'config', 'template'}, optional + Should the task be returned as a dict (ConfigObj) or saved as a template. widget : optional @@ -42,7 +42,7 @@ def save_task(event): A dict is returned if the mode is 'config'. """ - mode = event.parameters['mode'] + mode = event.parameters.get('mode', 'config') if mode == 'template': manager = event.workbench.get_plugin('exopy.tasks') saver = TemplateSaverDialog(event.parameters.get('widget'), diff --git a/tests/measurement/hooks/test_addtask_hook.py b/tests/measurement/hooks/test_addtask_hook.py new file mode 100644 index 00000000..de1100c6 --- /dev/null +++ b/tests/measurement/hooks/test_addtask_hook.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- +# ----------------------------------------------------------------------------- +# Copyright 2015-2018 by Exopy Authors, see AUTHORS for more details. +# +# Distributed under the terms of the BSD license. +# +# The full license is in the file LICENCE, distributed with this software. +# ----------------------------------------------------------------------------- +"""Test the AddTask post hook. + +""" +import pytest +from atom.api import Tuple +from exopy.tasks.api import RootTask, SimpleTask +from enaml.workbench.api import Workbench +from exopy.testing.measurement.dummies import DummyEngine +from exopy.tasks.tasks.base_views import RootTaskView + +from exopy.testing.util import show_widget +pytest_plugins = str('exopy.testing.tasks.fixtures') + + +@pytest.fixture +def addtaskhook(measurement): + """Create an AddTaskHook. + + """ + hook = measurement.plugin.create('post-hook', 'exopy.addtask_hook') + return hook + + +@pytest.fixture +def addtaskview(measurement_workbench, addtaskhook): + """Create an AddTaskView. + + """ + # on est obligé d'aller chercher dans le vrai manifest de measurement pour + # trouver le make_view + meas = measurement_workbench.get_plugin('exopy.measurement') + decl = meas.get_declarations('exopy.addtask_hook') # check how to get the declaration + view = decl.make_view(measurement_workbench, addtaskhook) + return view + + +def test_new_addtask_hook(addtaskhook): + """Testing the creation of an AddTask post-hook + + """ + assert type(addtaskhook.root_task) == RootTask + assert type(addtaskhook.workbench) == Workbench + assert type(addtaskhook.dependencies) == Tuple + assert addtaskhook.default_path + assert addtaskhook.engine + + +def test_get_set_state(addtaskhook): + """Testing saving and loading the hook + + """ + root = addtaskhook.root_task + # adding a task to the hook + root.children = [SimpleTask(name='task', + database_entries={'val': 1}, + root=root, parent=root, + database=root.database)] + task_prefs = addtaskhook.get_state() + print(task_prefs) + assert task_prefs # blabla selon sa structure ? + + root.children = [] + assert len(root.children) == 0 + addtaskhook.set_state(task_prefs) + assert len(root.children) == 1 + assert isinstance(root.children[0], SimpleTask) + + +def test_pause(addtaskhook): + """Testing the engine pause + + """ + addtaskhook.engine = DummyEngine() + addtaskhook.pause() + assert addtaskhook.engine.should_pause == True + + +def test_resume(addtaskhook): + """Testing the engine resume + + """ + addtaskhook.engine = DummyEngine() + addtaskhook.resume() + assert addtaskhook.engine.should_resume == True + + +def test_stop(addtaskhook): + """Testing the engine stop + + """ + addtaskhook.engine = DummyEngine() + addtaskhook.stop(force=False) + assert addtaskhook.engine._stop == True + + +def test_force_stop(addtaskhook): + """Testing the engine force stop + + """ + addtaskhook.engine = DummyEngine() + addtaskhook.stop(force=True) + assert addtaskhook.engine._stop == True + +def test_view(addtaskview, addtaskhook, exopy_qtbot, dialog_sleep): + """Testing the view + + """ + assert addtaskview.hook + assert addtaskview.declaration.id == 'exopy.addtask_hook' + assert addtaskview.workbench + assert type(addtaskview.widget()[0]) == RootTaskView + rootview = addtaskview.widget()[0] + assert rootview.show_path == False + # ca devrait marcher pcq on appelle une seule fois la fixture, ensuite c'est le même objet + assert rootview.task == addtaskhook.root_task + + # test the widget display + win = show_widget(exopy_qtbot, rootview) + exopy_qtbot.wait(dialog_sleep) + win.close() + + +def test_list_runtimes(): + """Testing list_runtimes + + TODO + """ diff --git a/tests/measurement/workspace/test_tools_edition.py b/tests/measurement/workspace/test_tools_edition.py index 3d6f9cee..cebb5dde 100644 --- a/tests/measurement/workspace/test_tools_edition.py +++ b/tests/measurement/workspace/test_tools_edition.py @@ -44,7 +44,7 @@ def assert_selected(): def test_navigation_in_tools_editor(measurement, exopy_qtbot, dialog_sleep): - """Test navigating among the different measurement tools and accessing + """Test navigating among the different measurement tools and accessing their editors. """ @@ -82,7 +82,7 @@ def test_manipulating_tools(measurement, exopy_qtbot, dialog_sleep): exopy_qtbot.wait(dialog_sleep) nb = item.dock_widget().widgets()[0] - pre_hook_ed = nb.pages()[0].page_widget().widgets()[0] + pre_hook_ed = nb.pages()[0].page_widget() # Add a tool def add_tool_1(bot, dial): @@ -157,7 +157,7 @@ def test_ending_with_no_tools(measurement, exopy_qtbot, dialog_sleep): exopy_qtbot.wait(dialog_sleep) nb = item.dock_widget().widgets()[0] - mon_ed = nb.pages()[1].page_widget().widgets()[0] + mon_ed = nb.pages()[1].page_widget() # Add a tool def add_tool_1(bot, dial):