4

I'm using Typer logo Typer to write a command line program using python.

Code

Here's an example script of were I'm running into trouble.

import typer

app = typer.Typer()

@app.command()
def hello(name):
    typer.echo(f'Hello!')

@app.command()
def goodbye():
    typer.echo(f'Goodbye.')

class Grettings:

    @app.command()
    def aloha(self):
        typer.echo('Aloha!')

    @app.command()
    def bonjour(self):
        typer.echo('Bonjour!')

if __name__ == '__main__':
    app()

When the following commands are entered into the terminal the expected output is given.

python main.py hello   
python main.py goodbye

Issue

However, when the class methods are called, I get the following exception.

python main.py aloha  
python main.py bonjour

    Usage: main.py aloha [OPTIONS] SELF
    Try 'main.py aloha --help' for help.
    
    Error: Missing argument 'SELF'.

Clearly, this is from the class not being initialized, yet. But it seems like it'd be a common problem, so I assume there's a simple solution to the issue.

Research

Possible solutions I've found include using decorators on the class/methods being used, or using a special class that'd need to be inherited to "expose" the class methods.

Hint from answer of Can a decorator of an instance method access the class? :

Any decorator is called BEFORE class is built, so is unknown to the decorator.

4
  • 1
    Make it a static method, then it doesn't need a class instance. Commented Jan 20, 2022 at 21:54
  • 4
    It's not a particularly sensible thing to do. typer would have to know how to create an instance of the class, when that's really your job. All you need is a short shim method: @app.command / def aloha(): / Grettings().aloha() . Commented Jan 20, 2022 at 22:01
  • Terminology note, these are not "class methods". They are instance methods Commented May 24, 2022 at 19:20
  • I would posit the best solution here is to simply not make those functions methods, and define them at the module level Commented May 24, 2022 at 19:25

2 Answers 2

4

Issue with decorated instance methods

Typer tries to invoke the callback as Grettings().aloha(). This will fail in Python with error:

TypeError: hallo() missing 1 required positional argument: 'self'

Demo of command invocation in Typer

See following demo recorded in Python shell:

Part 1: How it works (with static functions, no self argument)

>>> import typer
>>> app = typer.Typer()
>>> app
<typer.main.Typer object at 0x7f0713f59c18>
>>> app.__dict__
{'_add_completion': True, 'info': <typer.models.TyperInfo object at 0x7f0713f59c50>, 'registered_groups': [], 'registered_commands': [], 'registered_callback': None}
>>> @app.command()
... def hello():
...     typer.echo('hello')
... 
>>> app.__dict__['registered_commands']
[<typer.models.CommandInfo object at 0x7f0711e69cf8>]
>>> app.__dict__['registered_commands'][0].cls
<class 'typer.core.TyperCommand'>
>>> app.__dict__['registered_commands'][0].callback
<function hello at 0x7f070f539378>
>>> app.__dict__['registered_commands'][0].callback()
hello

Part 2: How it wont work (with instance methods, self argument required)

>>> class German:
...     @app.command()
...     def hallo(self):
...         typer.echo('Hallo')
... 
>>> app.__dict__['registered_commands'][1]
<typer.models.CommandInfo object at 0x7f070f59ccf8>
>>> app.__dict__['registered_commands'][1].callback
<function German.hallo at 0x7f070f539158>
>>> app.__dict__['registered_commands'][1].callback()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: hallo() missing 1 required positional argument: 'self'
>>> app.__dict__['registered_commands'][1].callback(German())
Hallo

Note: In the last statement I a fresh instance was passed as argument self to the callback and the invocation succeeded with expected output.

Fixed code

I changed 3 things:

  1. Renamed your class Grettings into Greetings (spelling)
  2. Redefined the 2 existing methods as static class methods like Barmar's comment suggested.
  3. Additionally I added a new instance method nihao(self) to demonstrate the failure.
import typer

app = typer.Typer()


@app.command()
def hello(name):
    typer.echo(f'Hello!')

@app.command()
def goodbye():
    typer.echo(f'Goodbye.')

class Greetings:
    @app.command()
    def aloha():              # function or class-method (implicitly static)
        typer.echo('Aloha!')

    @staticmethod             # explicitly static
    @app.command()
    def bonjour():            # no self argument!
        typer.echo('Bonjour!')

    @app.command()
    def nihao(self):          # callback invocation fails because missing self argument
        typer.echo('Nihao!')


if __name__ == '__main__':
    app()

Behavior and output as expected

Although the offered commands still list nihao as available, the invocation of it will fail equally as you experienced.

But the command-decorated static methods can be invoked now.

$ python3 SO_typer.py --help
Usage: SO_typer.py [OPTIONS] COMMAND [ARGS]...

Options:
  --install-completion [bash|zsh|fish|powershell|pwsh]
                                  Install completion for the specified shell.
  --show-completion [bash|zsh|fish|powershell|pwsh]
                                  Show completion for the specified shell, to
                                  copy it or customize the installation.

  --help                          Show this message and exit.

Commands:
  aloha
  bonjour
  goodbye
  hello
  nihao

🇨🇳️ Chinese greeting fails because no argument self passed with invocation:

$ python3 SO_typer.py nihao
Usage: SO_typer.py nihao [OPTIONS] SELF
Try 'SO_typer.py nihao --help' for help.

Error: Missing argument 'SELF'.

🏴󠁵󠁳󠁨󠁩󠁿 Hawaiian greeting works because static invocation possible now:

$ python3 SO_typer.py aloha
Aloha!

See also

Sign up to request clarification or add additional context in comments.

Comments

0

The skycaptain was replying it - https://github.com/tiangolo/typer/issues/309#issuecomment-1934028088

I've implemented a pattern to auto get the methods that is a command.

import inspect

import typer

class MyKlass:
    some_default_value: str

    def __init__(self, some_default_value: str):
        self.some_default_value = some_default_value
        self.app = typer.Typer()

        for method, _ in inspect.getmembers(self, predicate=inspect.ismethod):
            if not method.startswith('cmd'):
                continue

            cmd_name = method.strip('cmd_')
            self.app.command(
                name=cmd_name,
                help=self.some_default_value
            )(eval(f'self.{method}'))

    def cmd_run(self):
        print("Running")

    def cmd_sleep(self):
        print("sleep")


if __name__ == "__main__":
    MyKlass(some_default_value="it's a command").app()

Usage: python -m legalops_commons.utils.foo [OPTIONS] COMMAND [ARGS]...

Options:
  --install-completion  Install completion for the current shell.
  --show-completion     Show completion for the current shell, to copy it or
                        customize the installation.
  --help                Show this message and exit.

Commands:
  run    it's a command
  sleep  it's a command

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.