Click: use click.command in a class

Created on 22 Jun 2016  路  11Comments  路  Source: pallets/click

hey, is it possible to do

class Foo(object):
    @click.command()
    def cmd(self):
        pass

this code would fail because click will validate that a "argument self" is needed, is it possible to let click not check this in a easy way?

All 11 comments

What's the point of doing this? Anyway, you can probably use @staticmethod

I am manipulating the sys.argv and use getattr(obj, cmd_name) to call the cmd, obj is used to share some states between commands, it is not a staticmethod.

@woosley You can use hand-made decorator (instead of click.command()). Like this. With it you can decorate any callable, populating its arguments with those extracted from sys.argv. Probably, it's worth considering adding smth like this to click.

I would like this to work. I am trying to integrate click with another library which requires I define the option parser as a method of a class. Using @staticmethod isn't a great option since it leaves me with nowhere to put the resulting state (except, obviously, I could dump it into a global somewhere - yuck).

Closing as I don't think this is an intended use case. If you want to build a cli in a class, you should probably do that in __init__ by calling cli.add_command() so that the methods are bound.

@davidism can you provide a functionnal example for your thoughts about class and click ?

#!/usr/bin/python
# -*- coding: utf-8 -*-

# standard libs
import os
import sys
import traceback
#import exceptions

# dependency libs
import click

class clii(click.Group) :

    def __init__(self):
        commandes = { "encode": self.encode,
                                  "decode": self.decode}
        click.Group.__init__(self, name="toto", commands = commandes, **{ "chain": False})
        #self.add_command(self.encode, "encode")
        #self.add_command(self.decode, "decode")

    @click.option('--name', prompt='Your name',
              help='The person to greet.')
    def encode(self, name):
        """Simple program that greets NAME for a total of COUNT times."""
        click.echo('Hello %s!' % name)

    @click.option('--name', prompt='Your name',
              help='The person to greet.')
    def decode(self, name):
        """Simple program that greets NAME for a total of COUNT times."""
        click.echo('Hello %s!' % name)


if __name__ == '__main__':
    # class test
    clii()
    click.pause()

I try below, but that never handle my command

I am trying do to exactly the same. To answer the question of @ThiefMaster "What's the point of doing this?". I am much more confident in using OOP than imperative programming for this kind of development.

Suppose I am writing a systemd service in Python. This Service features:

  • A CLI command
  • A command start, stop, run, restart, status
  • A demonization system
  • A way to specify the --pid file

Now I would like to write my own service that connect to some server:

class MyService(Service):
    @option('-h', '--host')
    def host(self, host):        
        self.host = host

    @option('-p', '--port')
    def port(self, port):
        self.port = port

    @command('test-connection')
    @option('-q', '--quiet')
    def test(self, quiet):       
        try:
            self.connect()
        except Exception as e:
            return 1
        return 0

    ...

And of course to run the service:

if __name__ == '__main__':
    MyService().run()

It is very convenient to extend existing command line applications which is not possible with the current Click implementation.

Well, I have quickly coded something that kind of work as I would like to

#!/usr/bin/python3
import click

class command:
    def __init__(self, name=None, cls=click.Command, **attrs):
        self.name = name
        self.cls = cls
        self.attrs = attrs

    def __call__(self, method):
        def __command__(this):
            def wrapper(*args, **kwargs):
                return method(this, *args, **kwargs)

            return self.cls(self.name, callback=wrapper, **self.attrs)

        method.__command__ = __command__
        return method


class option:
    def __init__(self, *param_decls, **attrs):
        self.param_decls = param_decls
        self.attrs = attrs

    def __call__(self, method):
        if not hasattr(method, '__option__'):
            method.__options__ = []

        method.__options__.append(
            click.Option(param_decls=self.param_decls, **self.attrs))
        return method


class Cli:
    def __new__(cls, *args, **kwargs):
        self = super(Cli, cls).__new__(cls, *args, **kwargs)

        def cli(*args, **options):
            for callback in self.__option_callbacks__:
                callback(self, **options)

        self._cli = click.Group(callback=cli)

        # Wrap commands
        for attr_name in dir(cls):
            attr = getattr(cls, attr_name)
            if hasattr(attr, '__command__'):
                self._cli.add_command(attr.__command__(self))

        # Wrap instance options
        self.__option_callbacks__ = set()
        for attr_name in dir(cls):
            attr = getattr(cls, attr_name)
            if hasattr(attr, '__options__'):
                self._cli.params.extend(attr.__options__)
                self.__option_callbacks__.add(attr)
        return self

    def run(self):
        """Run the CLI application."""
        self()

    def __call__(self):
        """Run the CLI application."""
        self._cli()


class HeyDude(Cli):
    @option('-n', '--name', default="Dude")
    def set_name(self, name):

        self._name = name

    @command('shout')
    def shout(self):
        print("HEY %s!" % self._name.upper())

    @command('greet')
    def bar(self):
        print("Hello %s!" % self._name)

HeyDude()()

It is a little late for this conversation, but I ran into a similar problem, and worked it out with a bit of metaprogramming and functional wrapping. My solution only works for Python3.x, but there is nothing there that would not work for Python2.7 with a change in how the Metaclasses is assigned.

Closing as I don't think this is an intended use case. If you want to build a cli in a class, you should probably do that in __init__ by calling cli.add_command() so that the methods are bound.

@davidism Yes, I thought this way too, but if you'll try to do that all you'll receive is just traceback like this one:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "C:\Users\asalynskiy\AppData\Local\Programs\Python\Python37\lib\importlib\__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "C:\Users\asalynskiy\Documents\Python\temp\click-example\clickexample\cli.py", line 54, in <module>
    @click.group(cls=MathCli)
  File "C:\Users\asalynskiy\Documents\Python\temp\click-example\.venv\lib\site-packages\click\decorators.py", line 130, in decorator
    cmd = _make_command(f, name, attrs, cls)
  File "C:\Users\asalynskiy\Documents\Python\temp\click-example\.venv\lib\site-packages\click\decorators.py", line 102, in _make_command
    **attrs
  File "C:\Users\asalynskiy\Documents\Python\temp\click-example\clickexample\cli.py", line 35, in __init__
    func = click.argument(arg[0], type=arg[1])(func)
  File "C:\Users\asalynskiy\Documents\Python\temp\click-example\.venv\lib\site-packages\click\decorators.py", line 168, in decorator
    _param_memo(f, ArgumentClass(param_decls, **attrs))
  File "C:\Users\asalynskiy\Documents\Python\temp\click-example\.venv\lib\site-packages\click\decorators.py", line 151, in _param_memo
    f.__click_params__ = []
AttributeError: 'method' object has no attribute '__click_params__'

It's not quite obvious functionality, but for bunch of usecases it would be quite useful to be able "just do it in __init__" instead of searching for some workarounds or reinventing the wheel. 馃槈

@davidism Agree with others here. Python allows for functional and imperative programming, but OOP is predominant. I'm now at my third company where this very issue is being brought up. I really think you should document your suggested approach with a thorough working example.

Was this page helpful?
0 / 5 - 0 ratings