click icon indicating copy to clipboard operation
click copied to clipboard

Use function both as API and click function?

Open michaelaye opened this issue 7 years ago • 7 comments

I am trying to reduce boilerplate code and it was my idea to just decorate an existing function with click decorators to make it available via click. It's unclear to me from the documentation if this use case is supported. I also search the issues for the below error message and couldn't find anything related.

When I decorate a function with click, I am not able to use it as a function within Python:

Here's the function code:

@click.command()
@click.argument('datestr')
def nasa_date_to_iso(datestr):
    """Convert the day-number based NASA format to ISO.

    Parameters
    ----------
    datestr : str
        Date string in the form Y-j

    Returns
    -------
    Datestring in ISO standard yyyy-mm-ddTHH:MM:SS.MMMMMM
    """
    date = dt.datetime.strptime(datestr, nasa_date_format)
    click.echo(date.isoformat())
    return date.isoformat()

Simple enough, right? I install it via setup.py like so:

    entry_points={
        "console_scripts": [
            'nasa_date_to_iso = planetpy.utils:nasa_date_to_iso',
        ]
    },

but when I call it within ipython I get this:

In [1]: from planetpy.utils import nasa_date_to_iso

In [2]: nasa_date_to_iso('2017-090')
Usage: ipython [OPTIONS] DATESTR

Error: Got unexpected extra arguments (0 1 7 - 0 9 0)
An exception has occurred, use %tb to see the full traceback.

SystemExit: 2

/Users/klay6683/miniconda3/envs/stable/lib/python3.6/site-packages/IPython/core/interactiveshell.py:2971: UserWarning: To exit: use 'exit', 'quit', or Ctrl-D.
  warn("To exit: use 'exit', 'quit', or Ctrl-D.", stacklevel=1)

So, is it not possible to have the same function both as a click-cli function and an python-internal API?

System: Py3.6 via conda on macOS 10.12.6, click version 6.7

michaelaye avatar Jun 23 '18 12:06 michaelaye

It's no longer a plain function, it's a command. You can run a command by calling it, which invokes its main method. Pass a list of args for the command to parse, and if you don't want it to exit, pass standalone_mode=False.

# will invoke Click pipeline
nasa_date_to_iso(['2017-090'], standalone_mode=False)
# equivalent
nasa_date_to_iso.main(['2017-090'], standalone_mode=False)

Assuming you know the given command is a direct wrapper for a function you wrote (and not a group or other type of command), you can get at the function with command.callback. However, calling it will just call the function, it won't invoke any of the Click pipeline for validation, callbacks, etc.

# won't invoke Click pipeline
nasa_date_to_iso.callback('2017-090')

davidism avatar Jun 23 '18 14:06 davidism

Hm, that's of course not optimal, because now I have to decide between adding a boilerplate function with a different name just for click or change all my Python-internal API calls to include the callback method. Both not ideal. :/

michaelaye avatar Jun 26 '18 14:06 michaelaye

def nasa_date_to_iso(value):
    ...

@click.command('nasa_date_to_iso')
def nasa_date_to_iso_command(value):
    click.echo(nasa_date_to_iso(value))

This would be pretty easy to write a decorator for to automate.

davidism avatar Jun 26 '18 15:06 davidism

Thank you, that looks like the smallest extra code possible. I was just hoping that that wouldn't be necessary. Thanks anyway!

michaelaye avatar Jun 26 '18 16:06 michaelaye

I would still call this a bad experience as a user. Why do we have to write the boilerplate? You have 5 args, you need to duplicate 5 args. If you go kwargs then you lose the signature.

kootenpv avatar Feb 05 '20 17:02 kootenpv

A command is no longer a regular function. It may assume all sorts of other behaviors now, such as argument validation, the availability of the Click context, or that the function is being run interactively. Extracting a behavior into a common function that's used in multiple situations is a common and perfectly acceptable refactor.

davidism avatar Feb 05 '20 17:02 davidism

I ran into this today. Obvious in retrospect, but probably worth an addition to docs faq.

Rowlando13 avatar May 16 '25 20:05 Rowlando13