Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.0.0] multiparameter is broken #247

Closed
TakWolf opened this issue Nov 12, 2024 · 1 comment
Closed

[3.0.0] multiparameter is broken #247

TakWolf opened this issue Nov 12, 2024 · 1 comment

Comments

@TakWolf
Copy link

TakWolf commented Nov 12, 2024

The following code cannot pass in 3.0.0

from typing import Literal

from cyclopts import App

app = App()


@app.default
def main(
        font_sizes: set[Literal[10, 12, 16]] | None = None,
):
    print(font_sizes)


if __name__ == '__main__':
    app()
(.venv) takwolf@TakWolf-MacBook-Pro-2 ark-pixel-font % python -m tools.cli --font-sizes 10 
{10}
(.venv) takwolf@TakWolf-MacBook-Pro-2 ark-pixel-font % python -m tools.cli --font-sizes 10 --font-sizes 12    
{10, 12}
(.venv) takwolf@TakWolf-MacBook-Pro-2 ark-pixel-font % python -m tools.cli --font-sizes 10 12 16
╭─ Error ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Unused Tokens: ['12', '16'].                                                                                                                                                      │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
@BrianPugh
Copy link
Owner

BrianPugh commented Nov 12, 2024

Hi @TakWolf!

This change is intentional; from the v3.0.0 changelog:

When an iterable-like datatype is specified by keyword, only a single element's worth of tokens will be consumed. To restore the old behavior where tokens are consumed until an option-like argument is reached, set Parameter.consume_multiple = True.

This change was made because it led to ambiguous/unintuitive CLIs when the user specifies keyword arguments first. Lets expand your example to the following:

from typing import Literal
from cyclopts import App

app = App()

@app.default
def main(
    font_name: Literal["opensans", "helvetica", "roboto"],
    font_sizes: set[Literal[10, 12, 16]] | None = None,
):
    print(f"{font_name=} {font_sizes=}")

if __name__ == '__main__':
    app()

And this works as expected:

$ python issue-247.py roboto 12 16
font_name='roboto' font_sizes={16, 12}

In cyclopts v2.9.9, if you specify the --font-sizes first, then the positionally supplied font_name will get consumed.

# cyclopts v2.9.9
$ python issue-247.py --font-sizes 12 16 roboto
╭─ Error ───────────────────────────────────────────────────────╮
│ Error converting value "roboto" to Literal[10, 12, 16] for    │
│ "--font-sizes,--empty-font-sizes".                            │
╰───────────────────────────────────────────────────────────────╯

So, in v3, cyclopts does the same as other CLI libraries and only consumes one element worth of tokens at a time when specified by keyword:

# cyclopts v3.0.0
# same command as before
$ python issue-247.py --font-sizes 12 16 roboto
╭─ Error ───────────────────────────────────────────────────────╮
│ Invalid value for "FONT-NAME": unable to convert "16" into    │
│ one of {'opensans', 'helvetica', 'roboto'}.                   │
╰───────────────────────────────────────────────────────────────╯

# specifying it "correctly" now:
$ python issue-247.py --font-sizes 12 --font-sizes 16 roboto
font_name='roboto' font_sizes={16, 12}

If you do not like this new behavior, we can either set Parameter.consume_multiple directly for the parameter, or at an app-level via default_parameter. Updating the original script with default_parameter so that the parsing is more in-line with what you were seeing in cyclopts v2.9.9:

from typing import Literal
from cyclopts import App, Parameter

app = App(
    default_parameter=Parameter(consume_multiple=True),
)

@app.default
def main(
    font_name: Literal["opensans", "helvetica", "roboto"],
    font_sizes: set[Literal[10, 12, 16]] | None = None,
):
    print(f"{font_name=} {font_sizes=}")

if __name__ == '__main__':
    app()

EDIT: removed stuff about things not working as expected; it was a local problem to my setup 🙈

@TakWolf TakWolf closed this as completed Nov 13, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants