kootenpv / cliche

Build a simple command-line interface from your functions :computer:
MIT License
107 stars 9 forks source link

Ensure lists of items work #3

Open kootenpv opened 4 years ago

kootenpv commented 4 years ago

Determine the right format, probably space separated instead of comma separated though:

--some-list 1,2,3,4
antimirov commented 4 years ago

How to know when the list stops then? The comma is more explicit. Or you can make it hackable, like a separator definition in a doctring.

kootenpv commented 4 years ago

It's already implemented, it's done by spaces actually as is custom in bash:

--some-list 1 2 3 4

and you have to use quotes to separate the args if a space occurs in them.

I think it would be difficult to implement in a different way.

pykenny commented 2 years ago

Just saw two issues with list options/arguments:

My current use case for this type of requirement is appending a set of names to internal reference list for the application. Take the command function above as example, the generated application should behave like this:

pykenny commented 2 years ago

Also it's kind of awkward to use a list parameter with default value to indicate variable number of arguments with "1 or more" constraint like this...

def command(items: List[str] = ['default_00']):
  # ...

... since Python linters (such as PyCharm's builtin linter) often captures mutable default arguments to avoid possible runtime errors.

Of course workaround exists, like using tuple, which is how *args exactly works in Python:

from typing import Tuple

def command(items: Tuple[str, ...] = ('default_00', ))
  # ...

Or even using more generic type in typing annotation:

from collections.abc import Sequence

def command(items: Sequence[str] = ('default_00', ))
  # ...

But to me they're not so intuitive as the one proposed earlier:

def command(
  item: str,   # At least one string
  *items: str  # and more are accepted as well
):
  # ...

Plus in my use case there's no default value when users pass nothing to the list argument. They should be warned instead of accidentally appending extra data (from default values) to the internal list.

kootenpv commented 2 years ago

Yea I have been using tuple for this which pleases mypy.

If you want to do verification of how many elements, it's best to use that in the function itself (as also the function could be called not as cli but from within a program)

@cli
def fn(items: tuple[str, ...] = ()):
    if not items:
        raise ValueError()
pykenny commented 2 years ago

Just realized that cliche still prints "1 or more" in help document even if passing empty collection as default value for an option/kwarg... The problem is usage generated from help doc will become -i [ITEMS [ITEMS ...]], which contradicts with the "1 or more" statement.

Also flag is strictly required for this approach, so you'll need to write fn -i item_01 item_02 item_03 instead of fn item_01 item_02 item_03, which is kind of inconvenient.

In this case I'll prefer implementing "1 or more" positional argument constraint in this way as workaround:

# my_application.py
@cli
def command(
  item: str,
  items: Tuple[str, ...],
):
  # ...
pykenny commented 2 years ago

Another problem when defining positional arguments in this way below (should be invalid since we don't know how to set the boundary between items and some_other_items on trailing positional arguments, or one of the list arguments should be converted to option with "0 or more" constraint):

# my_application.py
@cli
def command(
  item: str,
  items: Tuple[str, ...],
  some_other_items: Tuple[str, ...],
):
  """

  :param item: An item
  :param items: Additional items
  :param some_other_items: Some other items
  """
  # ...

Generated usage (from ArgumentParser?):

my_application command [-h] [--notraceback] [--cli] [--pdb] [--timing] [--raw] [-s]

Positional argument section in help doc:

POSITIONAL ARGUMENTS:
  item             |str| An item
  items            |0 or more of: str| Additional items
  some-other-items |0 or more of: str| Some other items