classification
Title: Add "necessarily inclusive" groups to argparse
Type: enhancement Stage: needs patch
Components: Versions: Python 3.3
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: John.Didion, bethard, cjmayo, manveru, paul.j3, xuanji
Priority: normal Keywords: patch

Created on 2011-03-17 15:51 by John.Didion, last changed 2016-05-16 21:52 by paul.j3.

Files
File name Uploaded Description Edit
patch_w_mxg2.diff paul.j3, 2014-02-21 22:05 review
example1.py paul.j3, 2014-02-22 19:56
issue11588_4.py paul.j3, 2014-03-04 19:01
usagegroup.py paul.j3, 2014-06-13 04:36
usage_2.patch paul.j3, 2014-09-06 02:58 review
Messages (15)
msg131262 - (view) Author: John Didion (John.Didion) Date: 2011-03-17 15:51
Just as some options are mutually exclusive, there are others that are "necessarily inclusive," i.e. all or nothing. I propose the addition of ArgumentParser.add_necessarily_inclusive_group(required=True).

This also means that argparse will need to support nested groups. For example, if I want to set up options such that the user has to provide an output file OR (an output directory AND (an output file pattern OR an output file extension)):

output_group = parser.add_mutually_exclusive_group(required=True)
output_group.add_argument("-o", "--outfile")
outdir_group = output_group.add_necessarily_inclusive_group()
outdir_group.add_argument("-O", "--outdir")
outfile_group = outdir_group.add_mutually_exclusive_group(required=True)
outfile_group.add_argument("-p", "--outpattern")
outfile_group.add_argument("-s", "--outsuffix")

The usage should then look like:

(-o FILE | (-O DIR & (-p PATTERN | -s SUFFIX))
msg132241 - (view) Author: Steven Bethard (bethard) * (Python committer) Date: 2011-03-26 14:09
I think this is a great suggestion. Care to work on a patch?
msg134560 - (view) Author: Manveru (manveru) Date: 2011-04-27 12:55
I am subscribing to this idea as I've just fall into such use case where I need it. I would like to submit a patch, but I still have difficulties to understand argparse code not much spare time to spent on this.
msg211240 - (view) Author: paul j3 (paul.j3) * Date: 2014-02-14 21:26
The suggestion in this issue is to add a 'mutually_inclusive_group' mechanism, one that would let users specify that certain sets of arguments must occur together.  Furthermore there was mention of allowing some sort of nesting.

Modeling it on the mutually_exclusive_group would be straight forward.  But should it affect the usage and help display?mutually_exclusive_groups add a messy layer to the usage formatting.

The only place such a group would act would be at the end of '_parse_known_args', where the current code checks for things like required actions (and mxgroups). A test at this point could use 'namespace', 'seen_actions' and 'seen_non_default_actions' to check whether the required group actions were seen.

But the only thing that the argument_group contributes to this test is a list of argument names ('dest'?).  Why not provide this list directly?  And what if the user wants A to occur together with either B or C, but not both?  Or make the inclusivity conditional on the value of A?

Currently users can define argument interactions in a couple of ways.  They can define custom Actions. In test_argparse.py there's a custom Actions test that does something like this (using '--spam' and 'badger').  But tests in Actions depend on the order in which arguments are given.

An alternative is to test for interactions of arguments after `parse_args`.  However the only information that the user has at this point is the args namespace.  Reliably distinguishing between non-occurrence of arguments and default values can be difficult.

I am proposing 'cross_test' mechanism that would give the user access to the 'seen_actions' and 'seen_non_default_actions' sets that 'mutually_exclusive_groups' use.  Specifically an optional function can be called at the end of '_parse_known_args' that has access to these sets as well as the parser and the namespace.

The core of the change would be adding

    cross_test = getattr(self, 'cross_test', None)
    if cross_test:
        cross_test(self, namespace, extras, seen_actions, seen_non_default_actions)

at the end of 'parser._parse_known_args'.  In addition 'cross_test' (or some other name) could be added to the 'ArgumentParser.__init__' arguments.

The feature could be used by defining such a 'cross_test' function and adding it to the parser (either instance or subclass)

    def foobar(self, namespace, extras, seen_actions, seen_non_default_actions):
        ...
        (raise self.error(...))

    parser.cross_test = foobar

The patch proposed http://bugs.python.org/issue18943 should be included
with any changes here since it refines the setting of 'seen_non_default_actions'.

I am working on tests and examples of such functionality.
msg211261 - (view) Author: paul j3 (paul.j3) * Date: 2014-02-15 07:11
Regarding a usage line like:

    (-o FILE | (-O DIR & (-p PATTERN | -s SUFFIX))

The simplest option is to just a custom written 'usage' parameter.

With the existing HelpFormatter, a nested grouping like this is next to impossible.  It formats the arguments (e.g.'-O DIR'), interleaves the group symbols, and then trims out the excess spaces and symbols.

http://bugs.python.org/issue10984  is a request to allow overlapping mutually_exclusive_groups.  It loops on the groups, formatting each.  It would be easier with that to format several different types of groups, and to handle nested ones.

What would it take to convert a usage string like that into a logical expression that tests for the proper occurrence (or non-occurrence) of the various arguments.  It might, for example be converted to

    exc(file, inc(dir, exc(pattern, suffix)))

where 'exc' and 'inc' are exclusive and inclusive tests, and 'file','dir' etc are booleans.  And what would be the error message(s) if this expression fails?

I can imagine a factory function that would take usage line (or other expression of groupings), and produce a function that would implement a cross_test (as outlined in my previous post).  It would be, in effect, a micro-language compiler.
msg211878 - (view) Author: paul j3 (paul.j3) * Date: 2014-02-21 22:05
This patch uses:

    tests = self._registries['cross_tests'].values()

to get a list of functions to run at the end of '_parse_known_args'.

I replaced all of the mutually_exclusive_group tests (3 places) in the ArgumentParser with a static function defined in class _MutuallyExclusiveGroup, and registered this function.  This refactoring should make it easier to add other specialized groups (e.g. inclusive) in the future.

I'm using the _registries because they are already being shared among groups.  

A user can also register a custom testing function.  For example:

    def inclusive_test(parser, seen, *args):
        # require an argument from one of the 2 groups, g0 and g1
        g0seen = seen.intersection(g0._group_actions)
        g1seen = seen.intersection(g1._group_actions)
        if len(g0seen.union(g1seen))==0:
            parser.error('one of the 2 groups is required')
    parser.register('cross_tests','inclusive', inclusive_test)

This patched 'argparse.py' runs 'test_argparse.py' without error.

This patch does not include the issue18943 changes, which make setting 'seen_non_default_actions' more reliable.
msg211947 - (view) Author: paul j3 (paul.j3) * Date: 2014-02-22 19:56
This is an example of using 'patch_w_mxg2.diff' to handle the inclusive group case proposed by the OP.

Since 'seen_non_default_actions' (and 'seen_actions') is a set of 'Actions', it is convenient to use 'set' methods with pointers to the actions that a collected during setup.  Tests could also be done with the 'dest' or other action attributes.

In this example I wrote 3 simple tests corresponding to the 3 proposed groups, but they could also have been written as one test.

    a_file= parser.add_argument("-o", "--outfile", metavar='FILE')
    a_dir = parser.add_argument("-O", "--outdir", metavar='DIR')
    a_pat = parser.add_argument("-p", "--outpattern", metavar='PATTERN')
    a_suf = parser.add_argument("-s", "--outsuffix", metavar='SUFFIX')
    ...
    def dir_inclusive(parser, seen_actions, *args):
        if a_dir in seen_actions:
            if 0==len(seen_actions.intersection([a_pat, a_suf])):
                parser.error('DIR requires PATTERN or SUFFIX')
    parser.register('cross_tests', 'dir_inclusive', dir_inclusive)
    ...

In theory tests like this could be generated from groups as proposed by the OP. There is one case in 'test_argparse.py' where a mutually_exclusive_group is nested in an argument_group.  But the current groups do not implement nesting.  A (plain) argument_group does not share its '_group_actions' list with its 'container'.  A mutually_exclusive_group shares its '_group_actions' but the result is a flat list (no nesting).

For now I think it is more useful to give users tools to write custom 'cross_tests' than to generalize the 'group' classes.
msg212228 - (view) Author: paul j3 (paul.j3) * Date: 2014-02-25 23:33
http://stackoverflow.com/questions/11455218
python, argparse: enable input parameter when another one has been specified

    $ python myScript.py --parameter1 value1
    $ python myScript.py --parameter1 value1 --parameter2 value2
    $ python myScript.py --parameter2 value2  # error

This is an example where a 'mutually inclusive group' wouldn't quite do the job.  

The proposed answers mostly use a custom Action.  I'd lean toward an after-the-parse test of the namespace.

With the patch I proposed this could be implemented with:

    a1 = parser.add_argument("--parameter1")
    a2 = parser.add_argument("--parameter2")
    def test(parser, seen_actions, *args):
        if a2 in seen_actions and a1 not in seen_actions:
            parser.error('parameter2 requires parameter1')
    parser.register('cross_tests', 'test', test)

One poster on that thread claimed that the use of 'a1 = parser.add_argument...' is using an undocumented feature.  The fact that `add_argument` returns an `action` object, should be illustrated in the documentation, and may be explicitly noted.  I'll have to review the documentation to see if this is the case.
msg212243 - (view) Author: paul j3 (paul.j3) * Date: 2014-02-26 07:24
The addition of a simple decorator to the 'ArgumentParser' class, would simplify registering the tests:

    def crosstest(self, func):
        # decorator to facilitate adding these functions
        name = func.__name__
        self.register('cross_tests', name, func)

which would be used as:

    @parser.crosstest
    def pat_or_suf(parser, seen_actions, *args):
        if 2==len(seen_actions.intersection([a_pat, a_suf])):
            parser.error('only one of PATTERN and SUFFIX allowed')
msg212733 - (view) Author: paul j3 (paul.j3) * Date: 2014-03-04 19:01
A couple more thoughts on an expanded argument testing mechanism:

- do we need both 'seen_actions' and 'seen_non_default_actions'?  

'seen_actions' is used only to test whether all required actions have been seen.  These 2 sets differ in how positionals with '?*' are categorized.  Positionals like this are always 'seen', even if they just get the default value.  But they are not required (the case of a '*' positional without default needs to be revisited.)

- If 'seen_non_default_actions' is changed to a list (currently its a set), users could test for repeated use on an optional, or even the order of arguments.  

- One way to make this testing mechanism more user-friendly is to provide convenience functions via a decorator.  

For example the decorator could wrap the 'seen_non_default_actions' argument in a 'seen' function.  Such a function could accept either an Action or a 'dest' string, it could accept a single Action, or a list of them, etc.  There could be other functions like 'count', 'unique', 'mutually_exclusive', 'inclusive', etc.

    def testwfnc(func):
        # decorator to register function and provide 'seen'
        name = func.__name__
        def wrapped(parser, seen_actions, *args):
            def seen(*args):
                actions = seen_actions
                if isinstance(args[0], str):
                    actions = [a.dest for a in actions]
                if len(args)>1:
                    return [a in actions for a in args]
                else:
                    return args[0] in actions
            return func(parser, seen)
        parser.register('cross_tests', name, wrapped)
        return wrapped

    #@testwfnc
    def test(parser, seen, *args):
        if seen(a_file):
            print(seen(a_dir, a_pat, a_suf))
            cnt = sum(seen(a_dir, a_pat, a_suf))
        if cnt>0:
            parser.error('FILE cannot have DIR, PATTERN or SUFFIX')
        ...

The attached script experiments with several versions of decorators.  Some sort of testing Class is probably the way to go if we want to provide many convenience methods.
msg215777 - (view) Author: paul j3 (paul.j3) * Date: 2014-04-08 20:53
http://stackoverflow.com/questions/22929087
A question that could be addressed with this patch(es)

In a subparser:

    I have 4 arguments: -g, -wid, -w1, and -w2.

    -w1 and -w2 always appear together

    -wid and (-w1 -w2) are mutually exclusive, but one or the other is required

    -g is optional; if it is not specified only (-w1 -w2) can appear, but not -wid

The `g` case does not fit either the inclusive or exclusive group patterns.
msg220419 - (view) Author: paul j3 (paul.j3) * Date: 2014-06-13 04:36
I have developed a UsageGroup class that can implement nested 'inclusive' tests.  Using this, the original example in this issue could be coded as 3 groups, 2 mutually_exclusive and inclusive one.

    parser = ArgumentParser(prog='PROG', formatter_class=UsageGroupHelpFormatter)

    g1 = parser.add_usage_group(dest='FILE or DIR', kind='mxg', required=True)
    a_file= g1.add_argument("-o", "--outfile", metavar='FILE')

    g2 = g1.add_usage_group(dest='DIR and PS', kind='inc')
    a_dir = g2.add_argument("-O", "--outdir", metavar='DIR')

    g3 = g2.add_usage_group(dest='P or S', kind='mxg')
    a_pat = g3.add_argument("-p", "--outpattern", metavar='PATTERN')
    a_suf = g3.add_argument("-s", "--outsuffix", metavar='SUFFIX')
    # usage: PROG [-h] (-o FILE | (-O DIR & (-p PATTERN | -s SUFFIX)))

UsageGroup is like MutuallyExclusiveGroup, except that:

- A nested group is added to self._group_actions, but its actions are not.  Those actions are added separately to the parser._actions list.  Thus the '_group_actions' list can be a mix of actions and groups.

- Each group has a 'testfn', a function that tests its own '_group_actions' against the 'seen_non_default_actions' provided by the parser. These are similar to the 'cross_test' functions I developed
earlier.  The 'testfn' for primary level usage groups are registered with cross_tests (here renamed 'usage_tests')).  'testfn' for nested groups are handled recursively in the containing testfn.

- Normally 'testfn' is chosen based on 'kind' and 'required' parameters. 'kind' can implement logical actions like 'xor' (mutually exclusive), 'and' (inclusive), 'or' (any), 'not'.  These seem to cover most possibilities, though custom tests are possible.

- Each group has several variables defining how it is to be formatted.  The basic ones are 'joiner' (e.g. '|&^,') and 'parens' (e.g. (),[],{}).  Or a totally custom 'usage' string could be used.

- The Usage Formatter is a derivative of the MultiGroupHelpFormatter I wrote for issue 10984.  Each group is formatted individually (and possibly recursively).

- Existing actions and usage groups can be added to a usage group.  This adds to the flexibility of the usage testing, though the formatted usage line could easily become unwieldy.

- A 'mxg' UsageGroup effectively replaces a MutuallyExclusiveGroup.

- self._add_container_actions(parent) knows nothing about this class yet.

The attached 'usagegroup.py' file has the core code for this change.  The full working code is at

    https://github.com/hpaulj/argparse_issues/tree/nested

It incorporates too many other changes (from other bug issues) to post
here as a simple patch file (yet).  I have a number of test scripts, but haven't cast those as unittests.  Same goes for documentation.
msg226324 - (view) Author: paul j3 (paul.j3) * Date: 2014-09-03 20:58
http://stackoverflow.com/questions/25626109/python-argparse-conditionally-required-arguments

asks about implementing a 'conditionally-required-arguments' case in `argparse`.  The post-parsing test is simple enough:

    if args.argument and (args.a is None or args.b is None):
        # raise argparse error here

I believe the clearest and shortest expression using Groups is:

    p = ArgumentParser(formatter_class=UsageGroupHelpFormatter)
    g1 = p.add_usage_group(kind='nand', dest='nand1')
    g1.add_argument('--arg', metavar='C')
    g11 = g1.add_usage_group(kind='nand', dest='nand2')
    g11.add_argument('-a')
    g11.add_argument('-b')

The usage is (using !() to mark a 'nand' test):

    usage: issue25626109.py [-h] !(--arg C & !(-a A & -b B))

This uses a 'nand' group, with a 'not-all' test (False if all its actions are present, True otherwise).
msg226469 - (view) Author: paul j3 (paul.j3) * Date: 2014-09-06 02:58
Attached is a patch for 3.5.0 dev that adds UsageGroups.  Now different 'kinds' are implemented as subclasses, which are accessed via the registry:

     _XorUsageGroup - replicate action of MutuallyExclusiveGroups
     _AndUsageGroup - an inclusive group
     _OrUsageGroup -  an 'any' group
     _NorUsageGroup - NotOr - also works as Not
     _NandUsageGroup - NotAnd

Open issues:

    - what difference should the 'required' parameter make?
    - how should 'errors' in nested groups propagate?
    - formatting of error messages
    - formatting the usage line with these groups
    - any additional 'help' display
    - framework for custom test and/or subclasses
    - documentation, sample scripts, and formal testing
msg265734 - (view) Author: paul j3 (paul.j3) * Date: 2016-05-16 21:52
So far I've proposed adding a 'hook' at the end of '_parse_known_args', that would give the user access to the 'seen_non_default_actions' variable.  This function could perform an almost arbitrarily complex set of logical co-occurrence tests on this set (or list) of Actions.

The rest of my proposed patches (nested groups, etc) are user interface components that attempt make this testing more user-friendly, both in specification and usage display.

It just occurred to me that an alternate stop-gap fix is to make 'seen_non_default_actions' available to the user for his own testing after parsing.  Adding it to the method return is not backward compatible.  But it could be added as an attribute to parser.

     self._seen_actions = seen_non_default_actions

It would be the first case of giving the parser a memory of past parsing actions, but I don't think that's a problem.

Another possibility is to conditionally add it to the 'namespace'. 

     if hasattr(namespace, 'seen_actions'):
        setattr(namespace, 'seen_actions', seen_non_default_actions)

The user could initial this attribute with a custom 'Namespace' object or with a 'set_defaults' call.

(I'm  proposing to save 'seen_non_default_actions' because in my earlier tests that seemed to be more useful than 'seen_actions'.  It's the one used by mutually_exclusive_group testing.)
History
Date User Action Args
2016-05-16 21:52:31paul.j3setmessages: + msg265734
2015-12-22 19:41:50cjmayosetnosy: + cjmayo
2014-09-06 02:58:26paul.j3setfiles: + usage_2.patch

messages: + msg226469
2014-09-03 20:58:57paul.j3setmessages: + msg226324
2014-06-13 04:36:49paul.j3setfiles: + usagegroup.py

messages: + msg220419
2014-04-08 20:53:05paul.j3setmessages: + msg215777
2014-03-04 19:01:46paul.j3setfiles: + issue11588_4.py

messages: + msg212733
2014-02-26 07:24:09paul.j3setmessages: + msg212243
2014-02-25 23:33:01paul.j3setmessages: + msg212228
2014-02-22 19:56:01paul.j3setfiles: + example1.py

messages: + msg211947
2014-02-21 22:05:35paul.j3setfiles: + patch_w_mxg2.diff
keywords: + patch
messages: + msg211878
2014-02-15 07:11:02paul.j3setmessages: + msg211261
2014-02-14 21:26:59paul.j3setnosy: + paul.j3
messages: + msg211240
2011-04-27 15:27:34xuanjisetnosy: + xuanji
2011-04-27 12:55:54manverusetnosy: + manveru
messages: + msg134560
2011-03-26 14:09:13bethardsetmessages: + msg132241
stage: needs patch
2011-03-17 23:52:41eric.araujosetnosy: bethard, John.Didion
versions: + Python 3.3, - Python 2.7
2011-03-17 16:11:12SilentGhostsetnosy: + bethard
2011-03-17 15:51:29John.Didioncreate