mdklatt / cookiecutter-python-app

Cookiecutter template for a Python application project.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

app is not working out of the box

luiscachog opened this issue · comments

Hi,

Excellent idea, just it is not working out of the box.

I've installed the pyapp running:

cd pyapp
pip install .

Then the error comes when I run the app with no arguments:

pyapp
Traceback (most recent call last):
  File "/Users/luis/.virtualenvs/ca-env/bin/pyapp", line 8, in <module>
    sys.exit(main())
  File "/Users/luis/.virtualenvs/ca-env/lib/python3.7/site-packages/pyapp/cli.py", line 31, in main
    command = args.command
AttributeError: 'Namespace' object has no attribute 'command'

However, if I run the app with the correct command hello, I believe the app works:

pyapp hello
echo $?
0

Now, if I added command as an argument on the parent parser:

parser.add_argument("command", action="store",help="test command")

The app runs as expected with no arguments:

pyapp
usage: pyapp [-h] [-c CONFIG] [-v] [-w WARN] command {hello} ...
pyapp: error: the following arguments are required: command

But generates errors with the command argument hello:

 pyapp hello
Traceback (most recent call last):
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/inspect.py", line 1126, in getfullargspec
    sigcls=Signature)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/inspect.py", line 2208, in _signature_from_callable
    raise TypeError('{!r} is not a callable object'.format(obj))
TypeError: 'hello' is not a callable object

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/luis/.virtualenvs/ca-env/bin/pyapp", line 8, in <module>
    sys.exit(main())
  File "/Users/luis/.virtualenvs/ca-env/lib/python3.7/site-packages/pyapp/cli.py", line 33, in main
    spec = getfullargspec(command)
  File "/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/inspect.py", line 1132, in getfullargspec
    raise TypeError('unsupported callable') from ex
TypeError: unsupported callable

I'm new at programming and don't know how to fix it.

Could you take a look, please?

@k4ch0
This template will give you an example of an application that has subcommands. It is not designed to be used without subcommands. Arguably, running it without a subcommand should give you the help message instead of exception, but I guess argparse doesn't work that way by default. ¯_(ツ)_/¯

If you don't want subcommands, you can simplify cli.py quite a bit. The examples in the argparse tutorial should be enough.

If you need subcommands and a no-command default action, something like Click might work better for you than argparse.

@k4ch0
Here's a quick untested example if your application has a single action (no subcommands).

def main(argv=None) -> int:
    """ Execute the application CLI.

    :param argv: argument list to parse (sys.argv by default)
    :return: exit status
    """
    args = _args(argv)
    logger.start(args.warn or "DEBUG")  # can't use default from config yet
    logger.debug("starting execution")
    config.load(args.config)
    config.core.config = args.config
    if args.warn:
        config.core.logging = args.warn
    logger.stop()  # clear handlers to prevent duplicate records
    logger.start(config.core.logging)
    try:
        hello(args.name)
    except RuntimeError as err:
        logger.critical(err)
        return 1
    logger.debug("successful completion")
    return 0
 

def _args(argv):
    """ Parse command line arguments.

    :param argv: argument list to parse
    """
    parser = ArgumentParser()
    parser.add_argument("-c", "--config", action="append",
            help="config file [etc/config.yml]")
    parser.add_argument("-v", "--version", action="version",
            version="{{ cookiecutter.app_name }} {:s}".format(__version__),
            help="print version and exit")
    parser.add_argument("-w", "--warn", default="WARN",
            help="logger warning level [WARN]")
    parser.add_argument("name", nargs="?", default="World", help="name for greeting message")
    args = parser.parse_args(argv)
    if not args.config:
        # Don't specify this as an argument default or else it will always be
        # included in the list.
        args.config = "etc/config.yml"
    return args
 

Thank you for your answer @mdklatt. I need the subcommands but also when I run the app with no command I need to show the help message and not an exception.

@k4ch0

For starters, if you run myapp --help without a subcommand you will get the help message.

If you want the exact behavior you describe, I think this will work (again, untested); see ArgumentParser.print_help():

def _args(argv):
    """ Parse command line arguments.
    :param argv: argument list to parse
    """
    parser = ArgumentParser()
    parser.add_argument("-c", "--config", action="append",
            help="config file [etc/config.yml]")
    parser.add_argument("-v", "--version", action="version",
            version="{{ cookiecutter.app_name }} {:s}".format(__version__),
            help="print version and exit")
    parser.add_argument("-w", "--warn", default="WARN",
            help="logger warning level [WARN]")
    common = ArgumentParser(add_help=False)  # common subcommand arguments
    common.add_argument("--name", "-n", default="World", help="greeting name")
    subparsers = parser.add_subparsers(title="subcommands")
    _hello(subparsers, common)
    args = parser.parse_args(argv)
    if not args.command:
        # User didn't enter a subcommand, display help message.
        parser.print_help()
    if not args.config:
        # Don't specify this as an argument default or else it will always be
        # included in the list.
        args.config = "etc/config.yml"
    return args

Hi @mdklatt,

Thank you for the update, after some debugging, I come with the next solution:

def _args(argv):
    """ Parse command line arguments.
    :param argv: argument list to parse
    """
    parser = ArgumentParser()
    parser.add_argument("-c", "--config", action="append",
            help="config file [etc/config.yml]")
    parser.add_argument("-v", "--version", action="version",
            version="{{ cookiecutter.app_name }} {:s}".format(__version__),
            help="print version and exit")
    parser.add_argument("-w", "--warn", default="WARN",
            help="logger warning level [WARN]")
    common = ArgumentParser(add_help=False)  # common subcommand arguments
    common.add_argument("--name", "-n", default="World", help="greeting name")
    subparsers = parser.add_subparsers(title="subcommands")
    _hello(subparsers, common)
    args = parser.parse_args(argv)
    if not hasattr(args, 'command'):
        parser.print_help()
        exit(1)
    if not args.config:
        # Don't specify this as an argument default or else it will always be
        # included in the list.
        args.config = "etc/config.yml"
    return args

Issue resolved by commit bbaf7b2.