New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
using subcommands feels strange #17
Comments
Could you elaborate? From docs:
|
the thing is that each subcommand gets an boolean value for convient lookup something like a attribute to show the current subcommand would be nice |
Your approach would be fine if there could only be one subcommand at the same time. But you can have many of them:
If you have an API idea for return type—you are welcome. |
sounds like a good job for a list an interesting way to chain tem for the given example could be something having each subcommand have the next command as value that evaluates to true and can be chained for naval_fate.py that would mean something like the following as result:
|
if args[args["naval_fate.py"]] == "new" ? Or what usage case do you see? |
no, more something like :
|
+1 on that — having activated command as an attribute value is more intuitive and useful to me |
So you want to call a function with same name as the command? How about alternative discussed in #4 (kind of): from docopt import command
@command
def ship_new(args):
""""Usage: naval_fate.py ship new ......."""
@command
def ship_move(args):
"""Usage: naval_fate.py ship [<name>] move .........""""
if __name__ == '__main__':
command.run() |
then i'd have to split the docs again |
What about something like this: 440,443d439
< class Func():
< def __init__(self, value):
< self.name = '__docoptfunc__'
< self.value = value
445c441
< def docopt(doc, argv=sys.argv[1:], help=True, version=None, funcs=None):
---
> def docopt(doc, argv=sys.argv[1:], help=True, version=None):
455,463d450
<
< cmds = frozenset([a.name for a in (pot_arguments+arguments)
< if type(a) == Command and a.value==True])
<
< try:
< func = [Func(funcs[cmds])]
< except (KeyError, TypeError):
< func = [Func(None)]
<
466c453
< (pot_options + options + pot_arguments + arguments + func))
---
> (pot_options + options + pot_arguments + arguments)) Which you then call from your code like so: funcs={frozenset(['ship', 'new']):ship_new, frozenset(['ship', 'move']):ship_move, frozenset(['mine', 'remove']):mine_remove}
args = docopt(__doc__, funcs=funcs)
if args['__docoptfunc__']:
args['__docoptfunc__'](args) I used the weird name ( So the obvious extension of this is how to get it to only pass back that args/opts that go with the given command(s)? I haven't looked at this yet, but i'm wondering if there is a way to "filter" the list? If you think about it as a standard parse tree - we need to find the branch that contains all of the Commands given in |
@mattbathje I think your solution is more complicated than necessary. Compare: funcs={frozenset(['ship', 'new']):ship_new,
frozenset(['ship', 'move']):ship_move,
frozenset(['mine', 'remove']):mine_remove}
args = docopt(__doc__, funcs=funcs)
if args['__docoptfunc__']:
args['__docoptfunc__'](args) args = docopt(__doc__)
if args['ship'] and args['new']:
ship_new(args)
elif args['ship'] and args['move']:
ship_move(args)
elif args['mine'] and args['remove']:
mine_remove(args) No clear advantage. |
What about a combination of the two? @command('ship', 'move')
def ship_move(x, y, name=None, speed=None):
pass
@command('ship', 'shoot')
def ship_shoot(x, y):
pass
if __name__ == '__main__':
command.run(__doc__, version='Naval Fate 2.0') You'd still have one docstring and all the relevant arguments could be passed as kwargs. It could also simplify function re-use for related operations: @command('mine', 'set|remove') # or perhaps @command('mine', ('set', 'remove'))
def mine(x, y, set=False, remove=False, moored=None, drifting=None):
if remove:
print("Remove Mine:", x, y)
else:
print("Set Mine:", x, y) |
@Met48 Kinda like your idea. Do you have any neat idea of API that would allow dispatch in both cases of 1 docstring and several docstrings? |
How's this? """Naval Fate
Usage:
naval_fate.py ship new <name>...
naval_fate.py mine (set|remove) <x> <y> [--moored|--drifting]
Options:
...
"""
from docopt import command
@command()
def ship_move(x, y, name=None, speed=None):
"""Usage: naval_fate.py ship [<name>] move <x> <y> [--speed=<kn>]"""
@command('ship new')
def ship_new(name):
pass
@command('mine (set|remove)')
def mine(x, y, set=False, remove=False, moored=None, drifting=None):
if remove:
print("Remove Mine:", x, y)
else:
print("Set Mine:", x, y)
if __name__ == '__main__':
command.run(__doc__, version='Naval Fate 2.0') It differentiates between functions with their own docstring and functions for the main docstring. It also has the benefit of reusing the existing parser for subcommand matching. |
imho the proposals get more and more confusing and comlpex - i really think all that should be there is a way to discover the chain of commands without lots of bool checks and building the rest on top of that |
@RonnyPfannschmidt do you have any example in mind? |
i think just puting a ':command' key into the dict that contains the ordered list of command parts would suffice |
@RonnyPfannschmidt how about if docopt returned this kind of dictionary: class Dict(dict):
def __getitem__(self, key):
if type(key) == tuple:
return tuple(self[k] for k in key)
return dict.__getitem__(self, key) It is a backwards-compatible subclass of >>> args = Dict({'ship': True, 'move': True, 'new': False})
>>> args['ship']
True
>>> args['ship', 'move']
(True, True)
>>> all(args['ship', 'move'])
True
>>> any(args['ship', 'new'])
True
>>> all(args['ship', 'new'])
False I.e. you can dispatch stuff as: if all(args['ship', 'move']):
ship_move()
elif all(args['ship', 'new']):
ship_new() What do you think? |
are you kidding?! |
ok, looks like I'm very confused. Anyway, I can't see how this could be used in practice: # python naval_fate.py ship Guardian move 10 20 --speed=20
{'commands:': ['ship', 'move']
'--drifting': False,
'--help': False,
'--moored': False,
'--speed': '20',
'--version': False,
'<name>': ['Guardian'],
'<x>': '10',
'<y>': '20',
'mine': False,
'move': True,
'new': False,
'remove': False,
'set': False,
'ship': True,
'shoot': False} |
it enables to take the list of commands in correct order and do name based lookup that can be done to do stuff like also note the : in front, so it can be distinguished from the booleans |
Why not add the With that said, I think a more complete solution also needs to be present. The programmer should not have to maintain their own dispatch code if it can be avoided. For comparison, here are all the mentioned dispatch methods, with docstrings and functions omitted: #Current
if __name__ == '__main__':
args = docopt.docopt(__doc__)
if args['ship'] and args['add']:
ship_add(args)
elif args['ship'] and args['move']:
ship_move(args)
elif args['mine']:
mine(args)
#Multi-match custom dict (with all() moved to the custom class)
if __name__ == '__main__':
args = docopt.docopt(__doc__)
if args['ship', 'add']:
ship_add(args)
elif args['ship', 'move']:
ship_move(args)
elif args['mine']:
mine(args)
#Commands array with user-enforced naming scheme
#Does not support partial matches
class cmdmod(object): # or an import of cmdmod
if __name__ == '__main__':
args = docopt.docopt(__doc__)
getattr(cmdmod, '_'.join(args[':commands']))
#Decorators for functions, with naming scheme or multiple docstrings
#Does not support partial matches
@command
@command
@command
if __name__ == '__main__':
command.run() # __doc__)
#Decorators for functions
@command('ship add')
@command('ship move')
@command('mine (set|remove)')
if __name__ == '__main__':
command.run(__doc__)
#Function dictionary, modification of above frozenset method to simplify
#Does not support partial matches
if __name__ == '__main__':
docopt.run(__doc__, funcs={
('ship', 'add'): ship_add,
('ship', 'move'): ship_move,
('mine', 'set'): mine,
('mine', 'remove'): mine,
}) Of these options I think the decorator methods are the most minimal and flexible. Although the most complicated to implement, they aren't that confusing to use - take a look at Flask's routing for a similar example. The other methods have far more boilerplate or force naming conventions. As an aside, if |
@Met48 what do you mean by "partial matches"? I don't like the
if args['ship'] and args['new']:
ship = Ship(args['<name>'] or 'Mayflower')
elif args['ship'] and args['move']:
ship.move(args['<x>'], args['<y>']) how would having I like the idea of having some kinda decorator-based dispatch much more. Almost sure, docopt will end up with something like Since I'm for TSBOAPOOOWTDI, having both decorators and The problems to be solved:
|
@halst By "partial matches" I mean being able to handle multiple subcommand combinations with the same function easily. I think this is worth having, as some operations may differ only slightly based on the subcommand. I also think it is worth exposing the subcommands list through some means. If the dispatch method does not fit the needs of the programmer, there should be a fallback. To avoid the keyword problem, it could be an attribute on the returned Dict, or the module. It could also be exposed using the dispatch solution. As for the dispatch solution, the import docopt
# ... Naval Fate functions ...
def other_command(commands):
...
if __name__ == '__main__':
docopt.dispatch(__doc__, {
'ship new': ship_new,
'ship move': ship_move,
None: other_command,
}) Again, this would be a fallback for when decorators do not fit a more unusual use case. I am not sure how to best handle the TSBOAPOOOWTDI guideline here, but the decorator method could be prioritized as the obvious route. Incorporating the help messages is possible with both dispatch solutions. The most straightforward would be as a second argument to the As for the naval_fate example above, I'm not sure any dispatch solution would work for it. If there are only a few lines of code per subcommand, the dispatch code would be far longer than the bool checks already implemented. The nested checks are a more interested problem, one that should be better considered when exposing a dispatch method. |
maybe it is better if args got attributes instead of magic keys so we can go and check directly for after all the command decorator could just turn a expression into a number of tuples to register the function for them |
+1 for RonnyPfannschmidt solution. |
args is and must stay a dict subclass |
Why? And namedtuples have the _asdict() method. |
we have keys that start with -- or -, so normal attributes are a no go to begin with, |
Well, the namedtuple could have attributes that contains keys. |
please mock up a usage example just for the feel of it and a comparison i think you will see that it will get more complicated for no real gain |
This is essentially same as #4, further discussion moves there. |
Write to supplied ostream instead of cout
having flags instead of some kind of lookup adds some strange loops to figure whats actually going on with the cli
The text was updated successfully, but these errors were encountered: