diff --git a/core/pymod/core/pm3argparse.py b/core/pymod/core/pm3argparse.py index 51f55bca7287eea8c45fd27508eae0ac8b724b44..59cc9485139069166841e3f68650b0ad30064a40 100644 --- a/core/pymod/core/pm3argparse.py +++ b/core/pymod/core/pm3argparse.py @@ -121,6 +121,14 @@ def _AssembleTrgTplAln(target, template): new_aln.SetSequenceOffset(1, start) return new_aln +class PM3StoreOnceAction(argparse.Action): + """Action for argument parsing to prevent multiple calls to an option. + """ + def __call__(self, parser, namespace, values, option_string=None): + if getattr(namespace, self.dest, None) is not None: + raise argparse.ArgumentError(self, 'may only be used once.') + setattr(namespace, self.dest, values) + class PM3ArgumentParser(argparse.ArgumentParser): """ This class is a child of :class:`argparse.ArgumentParser`. It provides a @@ -224,12 +232,18 @@ class PM3ArgumentParser(argparse.ArgumentParser): Options/ arguments added: - * ``--fasta trg:<NAME> <FILE>`` - describing a target-template alignment - with ``trg:`` marking the target sequence inside :file:`<FILE>` + * ``-f/ --fasta trg:<NAME> <FILE>`` - describing a target-template + alignment with ``trg:`` marking the target sequence inside + :file:`<FILE>`. The order of arguments is arbitrary which means file + names starting with :file:`trg:` will not work. + + * ``-j/ --json <OBJECT>|<FILE>`` - target-template alignments in JSON + format. Either an object string or a file name. The string variant is + limited to how many characters your command line can gobble. Exit codes related to alignment input: - * 11 - no prefix ``trg:`` found for an argument to '--fasta' + * 11 - no prefix ``trg:`` found for an argument to ``--fasta`` * 12 - a given alignment file does not exist @@ -248,9 +262,12 @@ class PM3ArgumentParser(argparse.ArgumentParser): Attributes added to the namespace returned by :meth:`Parse`: - * :attr:`fasta` - filled with the input of the '--fasta' argument, a + * :attr:`fasta` - filled with the input of the ``--fasta`` option, a :class:`list` with multiple :class:`list` objects + * :attr:`json` - argument of the ``--json`` option, :class:`str`. May + be a filename of a JSON object string. + * :attr:`alignments` - :class:`ost.AlignmentList`, same order as :attr:`fasta` @@ -264,17 +281,26 @@ class PM3ArgumentParser(argparse.ArgumentParser): """ Actually add alignment arguments/ options """ + aln_grp = self.add_mutually_exclusive_group() # FastA input: - always pairwise alignments # - callable multiple times # - goes by 'trg:<SEQNAME> <FILE>' # - excludes JSON file/ object - # - leading whitespaces will be deleted - self.add_argument('-f', '--fasta', nargs=2, action='append', - metavar=('trg:<NAME>', '<FILE>'), - help='Pairwise alignment in FastA format, needs to '+ - 'declare what is the target sequence.') - # input: FastA/ JSON - # determined by extension: if we are wrong, the whole loading fails + # - leading whitespaces of FastA headers will be deleted + aln_grp.add_argument('-f', '--fasta', nargs=2, action='append', + metavar=('trg:<NAME>', '<FILE>'), + help='Pairwise alignment in FastA format, needs '+ + 'to declare what is the target sequence.') + # JSON input: - right as string or file + # - object starts with {, so not allowed for files + # - callable only ONCE: should include everything needed + # - fields/ objects used: ... + # - goes by '--json <OBJECT>|<FILE>' + # - excludes '--fasta' + # - leading whitespaces of sequence names will be deleted + aln_grp.add_argument('-j', '--json', metavar='<OBJECT>|<FILE>', + help='Alignments provided as JSON file/ object.', + action=PM3StoreOnceAction) # possibility to add JSON: mention limitation! class PM3OptionsNamespace(object): diff --git a/core/tests/test_pm3argparse.py b/core/tests/test_pm3argparse.py index 8bd20a84b36636fc702c63119a7d5d0e7537e686..271dfc26e683ae1a8591791f8d2cab681b41df6c 100644 --- a/core/tests/test_pm3argparse.py +++ b/core/tests/test_pm3argparse.py @@ -87,8 +87,9 @@ class PM3ArgParseTests(unittest.TestCase): self.assertEqual(ecd.exception.code, 2) self.assertEqual(self.log.messages['ERROR'], ['usage: test_pm3argparse.py [-h] [-f trg:<NAME> '+ - '<FILE>]', 'test_pm3argparse.py: error: argument '+ - '-f/--fasta: expected 2 argument(s)']) + '<FILE> | -j <OBJECT>|<FILE>]', + 'test_pm3argparse.py: error: argument -f/--fasta: '+ + 'expected 2 argument(s)']) def testAddAlignmentNoFileArg(self): # check failure on missing file argument @@ -100,8 +101,9 @@ class PM3ArgParseTests(unittest.TestCase): self.assertEqual(ecd.exception.code, 2) self.assertEqual(self.log.messages['ERROR'], ['usage: test_pm3argparse.py [-h] [-f trg:<NAME> '+ - '<FILE>]', 'test_pm3argparse.py: error: argument '+ - '-f/--fasta: expected 2 argument(s)']) + '<FILE> | -j <OBJECT>|<FILE>]', + 'test_pm3argparse.py: error: argument -f/--fasta: '+ + 'expected 2 argument(s)']) def testAddAlignemntNoTrgPfx(self): # checking that we fail on missing 'trg:' prefix for arguments of @@ -272,6 +274,38 @@ class PM3ArgParseTests(unittest.TestCase): self.assertEqual(opts.alignments[0].GetSequence(1).name, '1AKE.B') self.assertEqual(opts.aln_sources[0], 'data/fasta/1ake.fas') + def testAddAlignmentFastaAndJson(self): + # testing that --fasta and --json DO NOT work together + parser = pm3argparse.PM3ArgumentParser(__doc__, action=False) + parser.AddAlignment() + parser.AssembleParser() + with self.assertRaises(SystemExit) as ecd: + opts = parser.Parse(['--fasta', 'trg:target', + 'data/fasta/1ake.fas', '--json', 'foo']) + self.assertEqual(ecd.exception.code, 2) + self.assertEqual(self.log.messages['ERROR'][0], + 'usage: test_pm3argparse.py [-h] [-f trg:<NAME> '+ + '<FILE> | -j <OBJECT>|<FILE>]', + 'test_pm3argparse.py: error: argument -j/--json: '+ + 'not allowed with argument -f/--fasta') + + def testAddAlignmentJsonMulti(self): + # passing --json multiple times is not allowed + # also serves as a unit test for PM3StoreOnceAction + parser = pm3argparse.PM3ArgumentParser(__doc__, action=False) + parser.AddAlignment() + parser.AssembleParser() + with self.assertRaises(SystemExit) as ecd: + opts = parser.Parse(['--json', 'foo', '--json', 'bar']) + self.assertEqual(ecd.exception.code, 2) + self.assertEqual(self.log.messages['ERROR'][0], + 'usage: test_pm3argparse.py [-h] [-f trg:<NAME> '+ + '<FILE> | -j <OBJECT>|<FILE>]', + 'test_pm3argparse.py: error: argument -j/--json: '+ + 'may only be used once.') + +# test no multiple --json + if __name__ == "__main__": from ost import testutils testutils.RunTests()