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
Consider changing when optional options become available, and how they're presented, in usage #3
Comments
I have been marinating on this for a while. And now I think I might be changing my mind. Back in the day, options were required to come before (positional) arguments on command-lines. You couldn't say So maybe "painful" isn't the word to describe the boundary condition described above. It's just a fact of life. If my example above: def o2(a, *, verbose=False): ...
@app.command()
def command2(arg, *things: o2): ... had usage like this:
where you were required to put option To be precise: Appeal would require the options to be specified on the command-line before the last positional argument mapping to the def o3(a, b, c, *, verbose=False): ...
@app.command()
def command3(arg, *things: o3): ... then usage would look like this:
but in actuality you could specify the So in this command-line:
we'd call As a reminder, in this new world order, options in an optional group get optimistically lazy-mapped to the command-line when we complete all the positional arguments from the previous group. -- I worry I'm not explaining this well, so I'm going to walk you--the reader--through it all the way. When Appeal parses the command-line, it's building function calls. At at the point that Appeal runs out of input, if the function call is valid, Appeal is happy. If the function call is invalid, Appeal is unhappy and throws an exception. Let's say Appeal is in the middle of parsing this command-line:
Internally, Appeal has finished At this exact moment--where we haven't noticed whether or not there's another positional argument on the command-line--Appeal will now provisionally map Anyway. After Appeal provisionally maps
So finally, here's the funny side-effect of these semantics. In this world, the following command-line:
is invalid, because Appeal wants to call -- There are really three possibilities for how to handle options in repeating optional groups (
I can see the benefits of each of these. I don't expect to do 3, because I think consistency is probably better than the mild convenience afforded by its inconsistency. And, as I said at the top of this message, I'm now tilting away from 2 and towards 1. |
Options are now going to be early-mapped whenever, which may make things a little odd when you have a *args parameter that maps options. But hey! That's on you.
Not done yet: usage is wrong, and a regression test is failing, and I think it's wrong in some details. But-- Rewrote when options get mapped in compiled code. Now they're provisionally mapped just *before* the first positional argument in that group. While I was in there, I cleaned up the compiler code some. Now it's a class instead of a function. It's still a bit messy overall... but it's improved.
* A new feature: Appeal can now read configuration files! Check out the new APIs `Appeal.read_mapping`, `Appeal.read_iterable`, and even `Appeal.read_csv`. This was a massive undertaking and involved a big overhaul of the compiler. * The biggest change to existing behavior: Appeal now early-maps options. (See issue #3.) In short: when options are only defined in an optional group, they get provisionally mapped (made available) *before* the first argument in that group. Using that option enters the group just like specifying the first argument in that group. You'll see the difference in usage; an optional group that mapped an option used to look like `[a [-v|--verbose] b c]` but now looks like `[[-v|--verbose] a b c]`. * Appeal now handles multiple short options smashed together (e.g. `-ace`) *identically* to them being specified separately (e.g. `-a -c -e`). This caused an observable change in behavior regarding when child options get unmapped. - Appeal only permits using child options in a limited context: it must be after the parent option is executed, it must be after the parent option has consumed all its required *or optional* opargs, and it must be before any top-level positional argument or option mapped before the parent option was executed. But Appeal was lax about enforcing these rules when using multiple short options smashed together (e.g. `-ace`); it would handle all the options and *then* unmap child options as needed. The good news: Appeal now enforces these rules here too. (The old behavior seems to have been *intentional* on my part--what was I *thinking?!)* * The usage message raised for an unknown option is now much better. If the option is defined anywhere in the program being run, it prints a different message telling you it can't be used here, but also tells you where it can be used. For example, if you use option `-x`, but that's a child option mapped by `--parent`, the message would say `-x can't be used here, it must be used immediately after --parent`. * Renamed `Appeal.argument` to `Appeal.parameter`. This was one of those "what was I *thinking?"* moments. The function affects the parameter, not the argument. The old name still works but will be removed before 1.0. * `short_option_concatenated_oparg` is now more strictly enforced: it's only permitted for short options that have *exactly one* **optional** oparg, as specified by POSIX.
Done! |
And for the record, I went with the early-binding (and therefore early-un-binding). I think the mild surprise folks might experience on the rare occasion an option is un-bound (or re-bound) earlier than they expected is minuscule, compared to the big win of options being available early the way folks would normally expect. |
Something I've been pondering. Consider this Appeal API:
Currently that would be rendered in usage as:
command arg [a [-v|--verbose] [-i|--ignore-case] b c ]
That is,
a
,b
,c
,-v
, and-i
are all in one "optional group". The options-v
and-i
only become available--only become "mapped"--once the user specifies the second positional argument,a
. And yes, this really is the command-line API Appeal would create; see the Recursive Converters section of the Appeal docs to understand why.This is consistent and understandable, but... it's also a little weird. I guess this is kind of a new command-line metaphor, having options that only become available after a certain number of positional parameters. But having them only become available after the first argument in the optional group? It's weird, right? It's not just me?
So, if we don't want that, what do we want? If we could start over and do anything, what's the usage string and command-line behavior of our dreams? I've convinced myself it's this:
command arg [[-v|--verbose] [-i|--ignore-case] a b c ]
That is, the optional part looks like conventional command-line usage, but with square brackets around it: options are shown first, then positional arguments.
But it's only fair to show this to the user if it actually works this way. If we show the user this usage string, they would quite reasonably expect this command to work:
% myscript.py command blah -v -i x y z
Can be made to work? Certainly. It means mapping the optional options at the judicious time (after
arg
is consumed), but not instantiating the call tooptional_stuff
until any of the options or arguments is specified. A little tricky but not impossible. Should it be made to work? It seems fine. If they specify-v
or-i
, they have to specify the three optional argumentsa
b
andc
. The hardest part seems like it'll be crafting an error message that gets this idea across to the user in an understandable way.But this gives rise to a painful boundary condition when combined with
*args
:Currently this would be presented in usage as:
command2 arg [a [-v|--verbose]]...
That is, you can specify additional
a
s as many times as you like, and each one can be followed by a-v
. Completely unambiguous.If we early-map optional options in this case, then what happens if the user runs this?
% myscript.py command2 meh first -v
Is this
-v
paired with the first instance ofo2
(the one that gets called witha=first
), or is it a preemptive option passed in to a second instance ofo2
that the user never completes? It's kind of ambiguous.In practice, it wouldn't be ambiguous--it'd consistently be one or the other. Either
-v
would be (re-)mapped beforea
was consumed, every time, or it would be (re-)mapped aftera
was consumed, every time. And since we're permitting-v
to be used beforea
, then-v
would have to be mapped beforea
was consumed, which means in the above command-line the-v
would be passed in to the second call too2()
, which is incomplete because the user doesn't provide a seconda
. So this command-line is invalid--which I think the user would find surprising.So I propose: we early-map optional options when they don't repeat, but we skip the early-mapping when they do repeat. I don't think that's amazingly wonderful, exactly,; it's a little inconsistent. But overall I think it minimizes unpleasantness and surprises to the user, and it's unambiguous.
There's one more thing to consider. Maybe it would be tidier if, for
*args
optional options, we display them in usage last. Consider:Which usage string is nicer?
1.
command3 arg [p [-v|--verbose] [-i|--ignore-case] q r]...
2.
command3 arg [p q r [-v|--verbose] [-i|--ignore-case]]...
I think 2. is prettier.
Note that I don't actually propose delaying mapping those optional options until the end. Between you and me, they'll still be mapped after the first optional argument (in this case
p
). It's just the usage string that we're tweaking.The text was updated successfully, but these errors were encountered: