Skip to content

Commit a731f02

Browse files
authored
MAINT: CLI end of prov lib refactor (#384)
1 parent 256f250 commit a731f02

File tree

3 files changed

+175
-18
lines changed

3 files changed

+175
-18
lines changed

q2cli/core/usage.py

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,15 @@
1212
import re
1313
import shlex
1414
import textwrap
15-
from typing import Any, Callable, Dict, List, Tuple
15+
from typing import Any, Callable, Dict, List, Tuple, Type
1616

1717
from qiime2 import ResultCollection
1818
import qiime2.sdk.usage as usage
1919
from qiime2.sdk.usage import (
20-
UsageVariable, Usage, UsageInputs, UsageOutputs, UsageOutputNames
20+
UsageVariable, Usage, UsageInputs, UsageOutputs
2121
)
22-
from qiime2.sdk import Action
2322
from qiime2.core.archive.provenance_lib.usage_drivers import (
24-
build_header, build_footer
23+
build_header, build_footer, ReplayUsageAction
2524
)
2625
from qiime2.core.archive.provenance_lib import ProvDAG
2726

@@ -439,6 +438,8 @@ def to_interface_name(self):
439438

440439

441440
class ReplayCLIUsage(CLIUsage):
441+
UsageAction: Type[ReplayUsageAction] = ReplayUsageAction
442+
442443
shebang = '#!/usr/bin/env bash'
443444
header_boundary = ('#' * 79)
444445
set_ex = [
@@ -506,11 +507,16 @@ def _append_action_line(self, signature, param_name: str, value):
506507
'when the plugin version you have\n # installed does not '
507508
'match the version used in the original analysis.\n # '
508509
'Please see the docs and correct the parameter name '
509-
'before running.\n')
510-
cli_name = re.sub('_', '-', param_name)
511-
line += self.INDENT + '--?-' + cli_name + ' ' + str(value)
512-
line += ' \\'
510+
'before running.')
513511
self.recorder.append(line)
512+
self._append_unknown_param(param_name, value)
513+
514+
def _append_unknown_param(self, param_name, value):
515+
line = ''
516+
cli_name = re.sub('_', '-', param_name)
517+
line += self.INDENT + '--?-' + cli_name + ' ' + str(value)
518+
line += ' \\'
519+
self.recorder.append(line)
514520

515521
def _make_param(self, value: Any, state: Dict) -> List[Tuple]:
516522
'''
@@ -630,9 +636,9 @@ def comment(self, text):
630636

631637
def action(
632638
self,
633-
action: Action,
639+
action: ReplayUsageAction,
634640
inputs: UsageInputs,
635-
outputs: UsageOutputNames
641+
outputs: UsageOutputs
636642
) -> UsageOutputs:
637643
'''
638644
Overrides parent method to fill in missing outputlines from
@@ -641,18 +647,40 @@ def action(
641647
642648
Parameters
643649
----------
644-
action : Action
645-
The underlying sdk.Action object.
650+
action : ReplayUsageAction
651+
The object representing the Action we are replaying.
646652
inputs : UsageInputs
647653
Mapping of parameter names to arguments for the action.
648-
outputs : UsageOutputNames
654+
outputs : ReplayUsageOutputNames
649655
Mapping of registered output names to usage variable names.
650656
651657
Returns
652658
-------
653659
UsageOutputs
654660
The results returned by the action.
655661
'''
662+
plugin_name = q2cli.util.to_cli_name(action.plugin_id)
663+
action_name = q2cli.util.to_cli_name(action.action_id)
664+
665+
if action.node._action_present_:
666+
variables = self._action_found(
667+
plugin_name, action_name, action, inputs, outputs
668+
)
669+
else:
670+
variables = self._action_not_found(
671+
plugin_name, action_name, action, inputs, outputs
672+
)
673+
674+
return variables
675+
676+
def _action_found(
677+
self, plugin_name: str, action_name: str,
678+
action: ReplayUsageAction,
679+
inputs: UsageInputs,
680+
outputs: UsageOutputs
681+
):
682+
self.recorder.append('qiime %s %s \\' % (plugin_name, action_name))
683+
656684
variables = Usage.action(self, action, inputs, outputs)
657685
vars_dict = variables._asdict()
658686

@@ -668,11 +696,6 @@ def action(
668696
# Otherwise, we should add filler values to missing_outputs
669697
missing_outputs[output] = f'XX_{output}'
670698

671-
plugin_name = q2cli.util.to_cli_name(action.plugin_id)
672-
action_name = q2cli.util.to_cli_name(action.action_id)
673-
self.recorder.append('qiime %s %s \\' % (plugin_name, action_name))
674-
675-
action_f = action.get_action()
676699
action_state = get_action_state(action_f)
677700

678701
ins = inputs.map_variables(lambda v: v.to_interface_name())
@@ -699,6 +722,39 @@ def action(
699722
self.recorder.append('')
700723
return variables
701724

725+
def _action_not_found(
726+
self, plugin_name: str, action_name: str,
727+
action: ReplayUsageAction,
728+
inputs: UsageInputs,
729+
outputs: UsageOutputs
730+
):
731+
variables = Usage.action_not_found(self, action, inputs, outputs)
732+
vars_dict = variables._asdict()
733+
734+
ins = inputs.map_variables(lambda v: v.to_interface_name())
735+
736+
self.recorder.append(
737+
'# FIXME: The following action was not found in your current '
738+
'QIIME 2\n# environment. Please ensure the action and its '
739+
'parameters are correct before\n# running.'
740+
)
741+
self.recorder.append('qiime %s %s \\' % (plugin_name, action_name))
742+
743+
for param_name, value in ins.items():
744+
self._append_unknown_param(param_name, value)
745+
746+
dir_name = self._build_output_dir_name(plugin_name, action_name)
747+
self.recorder.append(
748+
self.INDENT + '--output-dir %s \\' % (dir_name)
749+
)
750+
self._rename_outputs(vars_dict, dir_name)
751+
752+
self.recorder[-1] = self.recorder[-1][:-2] # remove trailing `\`
753+
754+
self.recorder.append('')
755+
756+
return variables
757+
702758
def render(self, flush=False):
703759
'''
704760
Return a newline-seperated string of CLI script.
81 KB
Binary file not shown.

q2cli/tests/test_tools.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import os
1010
import gc
1111
import re
12+
import yaml
13+
import pytest
1214
import shutil
1315
import unittest
1416
from unittest.mock import patch
@@ -1277,6 +1279,105 @@ def test_replay_supplement_zipfile(self):
12771279

12781280
self.assertEqual(os.listdir(unzipped_path), ['supplement'])
12791281

1282+
# Leave me alone I know the checksums don't match
1283+
@pytest.mark.filterwarnings('ignore::UserWarning')
1284+
def test_replay_param_not_found(self):
1285+
qiime_cli = RootCommand()
1286+
command = qiime_cli.get_command(ctx=None, name='dummy-plugin')
1287+
1288+
in_fp = os.path.join(self.tempdir, 'concated_ints.qza')
1289+
left_path = os.path.join(self.tempdir, 'left.qza')
1290+
right_path = os.path.join(self.tempdir, 'right.qza')
1291+
1292+
result = self.runner.invoke(
1293+
command, ['split-ints', '--i-ints', in_fp,
1294+
'--o-left', left_path, '--o-right', right_path,
1295+
'--verbose'])
1296+
1297+
self.assertEqual(result.exit_code, 0)
1298+
1299+
left = Artifact.load(left_path)
1300+
action_fp = os.path.join(
1301+
left._archiver.provenance_dir, 'action', 'action.yaml'
1302+
)
1303+
with open(action_fp, 'r+') as action_fh:
1304+
action_string = action_fh.read()
1305+
action_yaml = yaml.safe_load(action_string)
1306+
action_yaml['action']['parameters'].append({'not': 'real'})
1307+
1308+
action_fh.write(yaml.dump(action_yaml))
1309+
1310+
with tempfile.TemporaryDirectory() as tmpdir:
1311+
out_fp = os.path.join(tmpdir, 'rendered.txt')
1312+
result = self.runner.invoke(
1313+
tools,
1314+
['replay-provenance', '--in-fp', left_path, '--out-fp',
1315+
out_fp, '--no-dump-recorded-metadata']
1316+
)
1317+
self.assertEqual(result.exit_code, 0)
1318+
1319+
with open(out_fp, 'r') as fh:
1320+
rendered = fh.read()
1321+
1322+
MISSING_PARAM = \
1323+
"""
1324+
# FIXME: The following parameter name was not found in your current
1325+
# QIIME 2 environment. This may occur when the plugin version you have
1326+
# installed does not match the version used in the original analysis.
1327+
# Please see the docs and correct the parameter name before running.
1328+
--?-not real \\
1329+
""" # noqa: E128
1330+
self.assertIn(MISSING_PARAM, rendered)
1331+
1332+
def test_replay_action_not_found(self):
1333+
datadir = os.path.join(
1334+
os.path.dirname(os.path.abspath(__file__)), 'data'
1335+
)
1336+
artifact_fp = os.path.join(datadir, 'rarefied_table.qza')
1337+
1338+
with tempfile.TemporaryDirectory() as tmpdir:
1339+
out_fp = os.path.join(tmpdir, 'rendered.txt')
1340+
result = self.runner.invoke(
1341+
tools,
1342+
['replay-provenance', '--in-fp', artifact_fp, '--out-fp',
1343+
out_fp, '--no-dump-recorded-metadata']
1344+
)
1345+
self.assertEqual(result.exit_code, 0)
1346+
1347+
with open(out_fp, 'r') as fh:
1348+
rendered = fh.read()
1349+
1350+
imports = \
1351+
"""
1352+
qiime tools import \\
1353+
--type 'Phylogeny[Rooted]' \\
1354+
--input-path <your data here> \\
1355+
--output-path phylogeny-rooted-0.qza
1356+
1357+
qiime tools import \\
1358+
--type 'FeatureTable[Frequency]' \\
1359+
--input-path <your data here> \\
1360+
--output-path feature-table-frequency-0.qza
1361+
""" # noqa: E128
1362+
self.assertIn(imports, rendered)
1363+
1364+
FIXME_action = \
1365+
"""
1366+
# FIXME: The following action was not found in your current QIIME 2
1367+
# environment. Please ensure the action and its parameters are correct before
1368+
# running.
1369+
qiime diversity core-metrics-phylogenetic \\
1370+
--?-table feature-table-frequency-0.qza \\
1371+
--?-phylogeny phylogeny-rooted-0.qza \\
1372+
--?-sampling-depth 13 \\
1373+
--?-metadata <your metadata filepath>.tsv \\
1374+
--?-with-replacement False \\
1375+
--?-n-jobs-or-threads 1 \\
1376+
--?-ignore-missing-samples False \\
1377+
--output-dir diversity-core-metrics-phylogenetic
1378+
""" # noqa: E128
1379+
self.assertIn(FIXME_action, rendered)
1380+
12801381

12811382
class TestAnnotations(unittest.TestCase):
12821383
def setUp(self):

0 commit comments

Comments
 (0)