★ wanayoo — archive 1999 https://github.com/python-cmd2/cmd2/issues/522Nouvelle recherche | Portail wanayoo
Skip to content
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

Cannot use per-function docstring in help messages when argparse description is not set #522

Closed
xNinjaKittyx opened this issue Sep 17, 2018 · 9 comments
Assignees
Labels
Milestone

Comments

@xNinjaKittyx
Copy link
Contributor

@xNinjaKittyx xNinjaKittyx commented Sep 17, 2018

I have been noticing help messages of only a single docstring will apply to its argparser. So any commands that use a common parser will always have the same docstring, which is fairly inconvenient if someone wants to use the same parser for multiple commands.

import argparse

from cmd2 import Cmd, with_argparser


parser = argparse.ArgumentParser()
parser.add_argument('-s', '--something', type=str, help='some argument')


class SomeClass(Cmd):
    @with_argparser(parser)
    def do_foo(self, opts):
        """This is foo docstring"""
        pass


    @with_argparser(parser)
    def do_bar(self, opts):
        """This is bar docstring"""
        pass


if __name__ == "__main__":
    SomeClass().cmdloop()

Now using the Cmd

(Cmd) foo -h
usage: bar [-h] [-s SOMETHING]

This is foo docstring

optional arguments:
  -h, --help            show this help message and exit
  -s SOMETHING, --something SOMETHING
                        some argument
(Cmd) bar -h
usage: bar [-h] [-s SOMETHING]

This is foo docstring

optional arguments:
  -h, --help            show this help message and exit
  -s SOMETHING, --something SOMETHING
                        some argument
(Cmd)

Looking at the code, it could be fixed by setting the argparser's description inside the function wrapper, rather than outside of it. I don't know how this will affect the rest of the code though.
Nevermind, it's not that straightforward. There needs to be a way to know if argparse.description was not declared on init vs declaring it dynamically.

If people want all of their functions to have the same help message, they can always set the parser's description. However, if people want to have unique messages for each one, it would be nice to be setting that docstring based on what function is being used.

@xNinjaKittyx xNinjaKittyx changed the title Unwanted overriding of help messages. Cannot use per-function docstring in help messages when argparse description is not set Sep 17, 2018
@tleonhardt
Copy link
Member

@tleonhardt tleonhardt commented Sep 17, 2018

@xNinjaKittyx Thank you for reporting this issue. It is certainly undesirable for one command to inadvertently pick up the docstring help from another command.

@anselor @kmvanbrunt Would either of you want to take a look at this? It sounds up your alley.

@tleonhardt
Copy link
Member

@tleonhardt tleonhardt commented Sep 17, 2018

Since this Issue and #515 are somewhat related, it might be a good idea to work on a solution for them at the same time.

@tleonhardt tleonhardt added this to the 0.9.5 milestone Sep 17, 2018
@kmvanbrunt
Copy link
Member

@kmvanbrunt kmvanbrunt commented Sep 18, 2018

This isn't really a cmd2 or argparse issue. The same parser object is being passed into both function wrappers. Even if you did change the description in between the function declarations, both functions would still print the new description because the same object is being altered.

A deep copy of the parser needs to be made for each function you wish to use it with before the description gets set. I tried this in Python 3.5 on Debian 9 and didn't have much luck.

I received lots of exceptions doing this with an ACArgumentParser object:
parser_copy = copy.deepcopy(orig_parser)

@kmvanbrunt kmvanbrunt removed the bug label Sep 18, 2018
@kmvanbrunt
Copy link
Member

@kmvanbrunt kmvanbrunt commented Sep 18, 2018

I found a decent workaround if deepcopy doesn't work. Write a function to create and return the type of parser you want to reuse. Then call it for each function being wrapped.

def make_parser():
    new_parser = argparse.ArgumentParser()
    new_parser.add_argument('-s', '--something', type=str, help='some argument')
    return new_parser

class SomeClass(Cmd):
    @with_argparser(make_parser())
    def do_foo(self, opts):
        """This is foo docstring"""
        pass

    @with_argparser(make_parser())
    def do_bar(self, opts):
        """This is bar docstring"""
        pass
@xNinjaKittyx
Copy link
Contributor Author

@xNinjaKittyx xNinjaKittyx commented Sep 18, 2018

Sure, that seems like a reasonable workaround.

The way I originally was thinking of was something like this:

def with_argparser(argparser: argparse.ArgumentParser) -> Callable:
    """A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
    with the given instance of argparse.ArgumentParser.

    :param argparser: argparse.ArgumentParser - given instance of ArgumentParser
    :return: function that gets passed parsed args
    """
    import functools

    # noinspection PyProtectedMember
    def arg_decorator(func: Callable):
        if argparser.description is None:
            argparser.init_with_description = False
        @functools.wraps(func)
        def cmd_wrapper(instance, cmdline):
            lexed_arglist = parse_quoted_string(cmdline)
            try:
                # argparser defaults the program name to sys.argv[0]
                # we want it to be the name of our command
                argparser.prog = func.__name__[3:]
                # If the description has not been set, then use the method docstring if one exists

                if not argparser.init_with_description and func.__doc__:
                    argparser.description = func.__doc__
                args = argparser.parse_args(lexed_arglist)
            except SystemExit:
                return
            else:
                return func(instance, args)

        if func.__doc__:
            setattr(cmd_wrapper, HELP_SUMMARY, func.__doc__)

        cmd_wrapper.__doc__ = argparser.format_help()

        # Mark this function as having an argparse ArgumentParser
        setattr(cmd_wrapper, 'argparser', argparser)

        return cmd_wrapper

    return arg_decorator

Where the function name and doc string were dynamically changed based on the command.
Your suggestion is okay, and it means each command has to have its own unique argparser object, which is totally reasonable.

Feel free to close based on your decision.

@anselor
Copy link
Contributor

@anselor anselor commented Sep 18, 2018

I'm inclined to handle it inside with_argparser rather than creating a separate argparser per command. On phone right now so difficult to look at this specific implementation. I was pretty sure that this could be done without making multiple argparser objects.

@kmvanbrunt
Copy link
Member

@kmvanbrunt kmvanbrunt commented Sep 18, 2018

The solution proposed by @xNinjaKittyx does work. However, I'm not sure it sets a good precedent. Argparse has many parameters that a user could want overridden between commands (ex: usage and epilog). How many cases do the decorators need to handle?

@tleonhardt tleonhardt added the bug label Sep 18, 2018
@tleonhardt
Copy link
Member

@tleonhardt tleonhardt commented Sep 18, 2018

@kmvanbrunt For whatever its worth, the inability to deepcopy an argparse.ArgumentParser is a known issue and has been fixed in Python 3.7. Unfortunately, I don't believe there are any plans to back port that fix to Python 3.5 or 3.6.

I would prefer for cmd2 to handle correct behavior within the argparse-based decorators if it is reasonable. If not, then we need to add a big bold glaring warning to the documentation telling end users that each command needs a unique ArgumentParser.

@tleonhardt
Copy link
Member

@tleonhardt tleonhardt commented Oct 1, 2018

Then general consensus among the cmd2 core maintainers is that it would be a bad idea to try to write workarounds to allow the same argparser to be used on multiple do_ methods because this could introduce unpredictability if we didn't design and implement this perfectly.

For now we will document that a separate instance of an argparser needs to be used for each command. We should make sure to also demonstrate best practices. Perhaps we could keep track of id's of all argparsers used so far and disallow one to be used again, but maybe that would be overkill?

In the future if cmd2 gets to the point of only supporting Python 3.7 or newer, then we could revisit this Issue because then we would be able to make a deepcopy of an argparse.ArgumentParser.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
4 participants
You can’t perform that action at this time.