AracKnight
AracKnight

Reputation: 397

missing 1 required positional argument in click CLI

I've written a CLI with click originally as a module and it worked fine. But since my project got bigger I now need to have attributes the CLI can work with, so I tried to turn it into a class, but I'm running into an error doing it. My code is like the following:

import click
import click_repl
import os
from prompt_toolkit.history import FileHistory

class CLI:
    def __init__(self):
        pass

    @click.group(invoke_without_command=True)
    @click.pass_context
    def cli(self, ctx):
        if ctx.invoked_subcommand is None:
            ctx.invoke(self.repl)

    @cli.command()
    def foo(self):
        print("foo")

    @cli.command()
    def repl(self):
        prompt_kwargs = {
            'history': FileHistory(os.path.expanduser('~/.repl_history'))
        }
        click_repl.repl(click.get_current_context(), prompt_kwargs)

    def main(self):
        while True:
            try:
                self.cli(obj={})
            except SystemExit:
                pass


if __name__ == "__main__":
    foo = CLI()
    foo.main()

Without all the selfs and the class CLI: the CLI is working as expected, but as a class it runs into an error: TypeError: cli() missing 1 required positional argument: 'ctx' I don't understand why this happens. As far as I know calling self.cli() should pass self automatically, thus obj={} should be passed as ctx.obj, so it shouldn't make any difference to cli if it's wrapped in a class or not.

Can someone explain to me, why this happens and more important, how I can fix it?

In case it's relevant here is the complete error stack trace:

Traceback (most recent call last):
    File "C:/Users/user/.PyCharmCE2018.2/config/scratches/exec.py", line 
     37, in <module>
      foo.main()
    File "C:/Users/user/.PyCharmCE2018.2/config/scratches/exec.py", line 
     30, in main
      self.cli(obj={})
    File "C:\Users\user\AppData\Local\Programs\Python\Python37\lib\site- packages\click\core.py", line 764, in __call__
      return self.main(*args, **kwargs)
    File "C:\Users\user\AppData\Local\Programs\Python\Python37\lib\site-packages\click\core.py", line 717, in main
      rv = self.invoke(ctx)
    File "C:\Users\user\AppData\Local\Programs\Python\Python37\lib\site-packages\click\core.py", line 1114, in invoke
      return Command.invoke(self, ctx)
    File "C:\Users\user\AppData\Local\Programs\Python\Python37\lib\site-packages\click\core.py", line 956, in invoke
      return ctx.invoke(self.callback, **ctx.params)
    File "C:\Users\user\AppData\Local\Programs\Python\Python37\lib\site-packages\click\core.py", line 555, in invoke
      return callback(*args, **kwargs)
    File "C:\Users\user\AppData\Local\Programs\Python\Python37\lib\site-packages\click\decorators.py", line 17, in new_func
      return f(get_current_context(), *args, **kwargs)
TypeError: cli() missing 1 required positional argument: 'ctx'

EDIT: The problem seems to be the pass_context call. Usually pass_context would provide the current context as first parameter to the function, so that obj={} would be passed to the context instance. But since I wrapped the click-group into a class, the first spot is taken by the self-reference, so that the current context can't be passed to the function. Any ideas of how to work around that?

I tried changing def cli() the following way:

@click.group(invoke_without_command=True)
def cli(self):
    ctx = click.get_current_context()
    ctx.obj = {}
    if ctx.invoked_subcommand is None:
        ctx.invoke(self.repl)

So I don't pass the context by call avoiding a conflict with self, but if I try to run this with self.cli() error TypeError: cli() missing 1 required positional argument: 'self' happens. Calling it with self.cli(self) runs into TypeError: 'CLI' object is not iterable

Upvotes: 4

Views: 5738

Answers (1)

Hielke Walinga
Hielke Walinga

Reputation: 2844

I am afraid the click library is not designed to work as a class. Click makes use of decorators. Don't take decorators too lightly. Decorators literally take your function as argument and return a different function.

For example:

@cli.command()
def foo(self):

Is something in line of

foo = cli.command()(foo)

So, I am afraid that click has not support to decorate functions bound to classes, but can only decorate functions that are unbound. So, basically the solution to your answer is, don't use a class.

You might be wondering how to organize your code now. Most languages present you the class as an unit of organization.

Python however goes one step further and gives you modules as well. Basically a file is a module and within this file everything you put in there is automatically associated with that file as a module.

So, just name a file cli.py and create your attributes as global variables. This might give you other problems, since you cannot alter global variables in a function scope, but you can use a class to contain your variables instead.

class Variables:
    pass

variables = Variables()
variables.something = "Something"

def f():
    variables.something = "Nothing"

Upvotes: 4

Related Questions