typer-cli: How to use custom data type with typer

Viewed 597

Click have click.ParamType, for defining custom parameter types, but it doesn't work with typer (Example snippet below)

I want to use, my own formatting for datetime, ex: today %H:%M, %M:%H(automatically uses date as today). Typer already allows setting, custom datetime formats but it doesn't cover my use case.

This is example from click docs that I tried using with typer.

class BasedIntParamType(click.ParamType):
    name = "integer"

    def convert(self, value, param, ctx):
        if isinstance(value, int):
            return value

        try:
            if value[:2].lower() == "0x":
                return int(value[2:], 16)
            elif value[:1] == "0":
                return int(value, 8)
            return int(value, 10)
        except ValueError:
            self.fail(f"{value!r} is not a valid integer", param, ctx)

BASED_INT = BasedIntParamType()

def main(based_int: BASED_INT):
    print(based_int)
    
if __name__=="__main__":
    typer.run(main)

It gives this error:

Traceback (most recent call last):
  File "/home/yashrathi/test.py", line 26, in <module>
    typer.run(main)
  File "/home/yashrathi/.local/lib/python3.9/site-packages/typer/main.py", line 859, in run
    app()
  File "/home/yashrathi/.local/lib/python3.9/site-packages/typer/main.py", line 214, in __call__
    return get_command(self)(*args, **kwargs)
  File "/home/yashrathi/.local/lib/python3.9/site-packages/typer/main.py", line 239, in get_command
    click_command = get_command_from_info(typer_instance.registered_commands[0])
  File "/home/yashrathi/.local/lib/python3.9/site-packages/typer/main.py", line 423, in get_command_from_info
    ) = get_params_convertors_ctx_param_name_from_function(command_info.callback)
  File "/home/yashrathi/.local/lib/python3.9/site-packages/typer/main.py", line 404, in get_params_convertors_ctx_param_name_from_function
    click_param, convertor = get_click_param(param)
  File "/home/yashrathi/.local/lib/python3.9/site-packages/typer/main.py", line 656, in get_click_param
    parameter_type = get_click_type(
  File "/home/yashrathi/.local/lib/python3.9/site-packages/typer/main.py", line 587, in get_click_type
    raise RuntimeError(f"Type not yet supported: {annotation}")  # pragma no cover
RuntimeError: Type not yet supported: <__main__.BasedIntParamType object at 0x7f7295822fd0>

typer.main.get_click_type, doesn't seem to be compatible with custom click type. But click knows how to handle click.ParamType, typer don't need to convert it to click type.

What would be the best way to implement my own datetime format?

  • Would it be good to create a new class that inherits from datetime, and overrides strptime to fit my usecase?
  • Could this be implemented by typer itself? Because it's easy to do this with click

Thank you

1 Answers

I'm not familiar with typer, however, here is how you could approach it with click.

One way to implement your datetime format is to indeed extend from click.ParamType and do all the hard parsing yourself.

Alternatively, if you just want to make some minor tweaks to how click.DateTime works, you can also directly extend from click.DateTime.
That way you can have click.DateTime do most of the heavy lifting.

To simplify your case a bit, let's say we want to enable the user to specify:

$ cli --newdate today    # which converts today to a click.DateTime
2021-12-30               # Note: below I also show how to return "today" 

Moreover, we want to retain the old behavior of click.DateTime, such that this also works:

$ cli --newdate 2042-12-29
2042-12-29

Then we could implement this as follows:

import datetime
from typing import Optional, Sequence, Any
import click

class EnhancedDate(click.DateTime):
    name = "enhanceddate"

    def __init__(self, formats: Optional[Sequence[str]] = None):
        super().__init__(formats)
        self.formats += ["today"]  # add your formats here

    def convert(self, value: Any, param: Optional["Parameter"], ctx: Optional["Context"]) -> Any:
        # And in the convert, we'll handle our own formats first
        if value is None or value == "today":
            return datetime.date.today()
            # Alternatively, if you return "today" here 
            # you also get the "today" string in your commands
            # when you use type=EnhancedDate()
              
        # we'll let click handle all the other stuff
        return super().convert(value, param, ctx)

@click.command()
@click.option("--newdate", type=EnhancedDate())
def cli(newdate):
    click.echo(newdate)


cli()

The nice thing about this approach is that you get click documentation out of the box:

python3 cli.py --help
Usage: cli.py [OPTIONS]

Options:
  --newdate [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S|today]
  --help                          Show this message and exit.
# Notice how `today` is added automatically over here:       ^

I hope this helps you out.

Related