def affected_wf_module_delta_ids( cls, workflow: Workflow, old_slugs: List[str], new_slugs: List[str] ) -> List[Tuple[int, int]]: """ Find WfModule+Delta IDs whose output may change with this reordering. Reordering tabs changes the ordering of 'Multitab' params. Any WfModule with a 'Multitab' param can change as a result of this delta. There are very few 'Multitab' params in the wild: as of 2019-02-12, the only one is in "concattabs". TODO optimize this method to look one level deep for _only_ 'Multitab' params that depend on the changed ordering, not 'Tab' params, essentially making ReorderTabsCommand _not_ change any WfModules unless there's a "concattabs" module. """ # Calculate `moved_slugs`: just the slugs whose `position` changed. # # There's no need to re-render a WfModule that only depends on tabs # whose `position`s _haven't_ changed: its input tab order certainly # hasn't changed. first_change_index = None last_change_index = None for i, old_slug_and_new_slug in enumerate(zip(old_slugs, new_slugs)): old_slug, new_slug = old_slug_and_new_slug if old_slug != new_slug: if first_change_index is None: first_change_index = i last_change_index = i moved_slugs = set(old_slugs[first_change_index : last_change_index + 1]) # Figure out which params depend on those. graph = DependencyGraph.load_from_workflow(workflow) wf_module_ids = graph.get_step_ids_depending_on_tab_slugs(moved_slugs) q = models.Q(id__in=wf_module_ids) return cls.q_to_wf_module_delta_ids(q)
def test_read_graph_happy_path(self): workflow = Workflow.objects.create() tab1 = workflow.tabs.create(position=0, slug="tab-1") tab2 = workflow.tabs.create(position=1, slug="tab-2") ModuleVersion.create_or_replace_from_spec( { "id_name": "simple", "name": "Simple", "category": "Add data", "parameters": [{"id_name": "str", "type": "string"}], } ) ModuleVersion.create_or_replace_from_spec( { "id_name": "tabby", "name": "Tabby", "category": "Add data", "parameters": [{"id_name": "tab", "type": "tab"}], } ) wfm1 = tab1.wf_modules.create( order=0, slug="step-1", module_id_name="simple", params={"str": "A"} ) wfm2 = tab1.wf_modules.create( order=1, slug="step-2", module_id_name="tabby", params={"tab": "tab-2"} ) wfm3 = tab2.wf_modules.create( order=0, slug="step-3", module_id_name="simple", params={"str": "B"} ) graph = DependencyGraph.load_from_workflow(workflow) self.assertEqual( graph.tabs, [ DependencyGraph.Tab("tab-1", [wfm1.id, wfm2.id]), DependencyGraph.Tab("tab-2", [wfm3.id]), ], ) self.assertEqual( graph.steps, { wfm1.id: DependencyGraph.Step(set()), wfm2.id: DependencyGraph.Step(set(["tab-2"])), wfm3.id: DependencyGraph.Step(set()), }, )
def affected_wf_modules_from_tab(cls, tab: Tab) -> Q: """ Filter for WfModules depending on `tab`. In other words: all WfModules that use `tab` in a 'tab' parameter, plus all WfModules that depend on them. This uses the tab's workflow's `DependencyGraph`. """ graph = DependencyGraph.load_from_workflow(tab.workflow) tab_slug = tab.slug wf_module_ids = graph.get_step_ids_depending_on_tab_slug(tab_slug) # You'd _think_ a Delta could change the dependency graph in a way we # can't detect. But [adamhooper, 2019-02-07] I don't think it can. In # particular, if this Delta is about to create or fix a cycle, then all # the nodes in the cycle are there both before _and_ after the change. # # So assume `wf_module_ids` is complete here. If we notice some modules # not updating correctly, we'll have to revisit this. I haven't proved # anything, and I don't know whether future Deltas might break this # assumption. return Q(id__in=wf_module_ids)
def test_read_graph_happy_path(self, load_module): workflow = Workflow.objects.create() tab1 = workflow.tabs.create(position=0, slug='tab-1') tab2 = workflow.tabs.create(position=1, slug='tab-2') ModuleVersion.create_or_replace_from_spec({ 'id_name': 'simple', 'name': 'Simple', 'category': 'Add data', 'parameters': [{ 'id_name': 'str', 'type': 'string' }] }) ModuleVersion.create_or_replace_from_spec({ 'id_name': 'tabby', 'name': 'Tabby', 'category': 'Add data', 'parameters': [{ 'id_name': 'tab', 'type': 'tab' }] }) wfm1 = tab1.wf_modules.create(order=0, module_id_name='simple', params={'str': 'A'}) wfm2 = tab1.wf_modules.create(order=1, module_id_name='tabby', params={'tab': 'tab-2'}) wfm3 = tab2.wf_modules.create(order=0, module_id_name='simple', params={'str': 'B'}) # DependencyGraph.load_from_workflow needs to call migrate_params() so # it can check for tab values. That means it needs to load the 'tabby' # module. class MockLoadedModule: def migrate_params(self, schema, values): return values load_module.return_value = MockLoadedModule() graph = DependencyGraph.load_from_workflow(workflow) self.assertEqual(graph.tabs, [ DependencyGraph.Tab('tab-1', [wfm1.id, wfm2.id]), DependencyGraph.Tab('tab-2', [wfm3.id]), ]) self.assertEqual( graph.steps, { wfm1.id: DependencyGraph.Step(set()), wfm2.id: DependencyGraph.Step(set(['tab-2'])), wfm3.id: DependencyGraph.Step(set()), })