-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
feat(derive): derive clap::Args
for enum types
#5700
base: master
Are you sure you want to change the base?
Conversation
Args
for enum typesclap::Args
for enum types
19629c4
to
bd0e12f
Compare
let Fields::Named(ref fields) = variant.fields else { | ||
abort! { variant.span(), | ||
"`#[derive(Args)]` only supports named enum variants if used on an enum", | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restricting this to named enums only, allows to reuse the struct codegen with little modification.
Unnamed
variants, with a single item might somehow dispatch the parsing bits to the contained Type and somehow set the group conflicts using ArgGroup::mut_group
on the augment side and ??? on the from arg matches (that might simply forward?).
Unit
variants need to be parsed as flags?- What about
enum Something { Variant(String) }
would we expect this to parse as 1) a positional, 2) a flag, 3) not at all bc we cant forward toString
s implementations?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm fine with scoping the first PR toNamed variants. We should have compile error tests for all of the other kinds of variants.
I could see soon after support single-element tuple variants with #[command(flatten)] Variant(MoreArgs)
to avoid the need for Variant { #[command(flatten)] args: MoreArgs }
. I'm assuming supporting specifically that wouldn't be too bad.
Longer term...
Unit
variant should probably be discussed in the issue. My first guess is to use it as an "else" case.
For other single-element tuples, I see it working just like a field. They are positional by default and need #[arg(short, long)]
on the variant to change it to something else. We'd use value_parser!
to understand what to do with the type inside of the variant.
For N-element tuples, we have an issue for supporting those on fields and I'd point to that for variants as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that all makes sense in my book.
I'm still getting familiar with Item
and the parsing around it where Named variants were just the closest to intuitively scrape together.
Happy to look into that soon after.
// when generating mutably exclusive arguments, | ||
// ids of arguments that should conflict | ||
conflicts: &[Name], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not super clean to sqeeze this in here esp, since this is also used by the subcommand
deriver, but i tried to avoid rewriting the entire method for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This goes away if we rely on groups, rather than conflicts
@@ -70,6 +70,66 @@ impl Item { | |||
Ok(res) | |||
} | |||
|
|||
pub(crate) fn from_args_enum_variant( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this method is basically a copy of from_subcommand_enum_variant
.
I'm not yet entirely clear if that is sensible since my picture of what Item
is is still somewhat blurry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Item
is a mess. It is a storage for all of our highlevel attribute information for a single type, field, variant, etc. We use the same type for all of those and for whichever trait or attribute type is used.
Making this function fits exactly within the current model
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good to know, is there any better documentation on Item
that I can read up on?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really. A lot of this is ad hoc. It'd be good to clean up at some point.
flake.nix
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't mind this it just helps me get a rust compiler using nix...
will be removed before this is ready to go anywhere
|
||
if __clap_arg_matches.contains_id(#group_id) { | ||
let #item_name::#variant_name { #( #field_names ),* } = self else { | ||
unreachable!(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i guess this might fail if the current variant is different, should it be possible to "change" variants using update_from_arg_matches_mut
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be an error, or instead of trying to update we'd attempt to parse all fields of the "other" variant and replace self with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for a slightly more concrete discussion:
this currently generates:
fn update_from_arg_matches_mut(
&mut self,
__clap_arg_matches: &mut clap::ArgMatches,
) -> ::std::result::Result<(), clap::Error> {
#![allow(deprecated)]
if __clap_arg_matches.contains_id("A") {
let Source::A { a, aaa } = self else {
unreachable!();
};
if __clap_arg_matches.contains_id("a") {
*a = __clap_arg_matches.remove_one::<bool>("a").ok_or_else(|| {
clap::Error::raw(
clap::error::ErrorKind::MissingRequiredArgument,
concat!("The following required argument was not provided: ", "a"),
)
})?
}
if __clap_arg_matches.contains_id("aaa") {
*aaa = __clap_arg_matches
.remove_one::<bool>("aaa")
.ok_or_else(|| {
clap::Error::raw(
clap::error::ErrorKind::MissingRequiredArgument,
concat!("The following required argument was not provided: ", "aaa"),
)
})?
};
}
if __clap_arg_matches.contains_id("B") {
let Source::B { b } = self else {
unreachable!();
};
if __clap_arg_matches.contains_id("b") {
*b = __clap_arg_matches.remove_one::<bool>("b").ok_or_else(|| {
clap::Error::raw(
clap::error::ErrorKind::MissingRequiredArgument,
concat!("The following required argument was not provided: ", "b"),
)
})?
};
}
::std::result::Result::Ok(())
}
for my test enum:
#[derive(clap::Args, Clone, Debug, PartialEq, Eq)]
enum Source {
A {
#[arg(short)]
a: bool,
#[arg(long)]
aaa: bool,
},
B {
#[arg(short)]
b: bool,
},
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would look to subcommands for a starting point in designing how to model this.
I also am really tempted to remove all of the update
code...
fn from_arg_matches_mut(__clap_arg_matches: &mut clap::ArgMatches) -> ::std::result::Result<Self, clap::Error> { | ||
#raw_deprecated | ||
#constructors | ||
unreachable!() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we actually do get here if neither variant was explcitly constructed.
In a way that makes sense, if all variants have defaultable arguments, either is a valid candidate. However I cant decide if that's a conflict or resolvable by defining a default varaint for such situations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the user has an Option<Enum>
, we'll check if the group is present and do None
if it isn't, so this is only a problem with Enum
. I lean towards hand constructing a clap error like we do if you have #[arg(required = False)] String
. If we make unit variants a fallback (or add a fallback attribute). then users can avoid the error here.
Especially if I've not looked at it yet, feel free to squash all of your fixups. In general, its a big help to review things when there is a clean commit history and changes are made as small as possible (while still being atomic). |
assert_eq!( | ||
clap::error::ErrorKind::ArgumentConflict, | ||
Opt::try_parse_from(["test", "-b", "-a"]) | ||
.unwrap_err() | ||
.kind(), | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When adding a test commit separate from a feature, it
needs to still pass tests / compile. I'm assuming this commit does neither.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How would i add a test that does compile for a thing that doesn't yet compile, or where the subject of the PR is allowing these constructs to compile in the first place?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"it depends"
Let's step back to the goals of why we encourage tests in a commit before the feature
- Help test the tests (when fixing a bug, did the tests actually need to change?)
- Through a commit's diff, making it clear to reviewers and the wider community what the intended behavior is
All of this is trivial when its a bug fix to help output. You show the existing behavior in the tests, then change the test when you change the behavior.
When the behavior doesn't exist yet, your options are
- Just don't do this and have the test added in the commit.
- Capture the feature failing spectacularly
- Find the closest parallel feature and test that
The ideal is the last but sometimes there isn't always a close enough parallel and it can take the most work.
As an example of the last, take https://github.com/rust-lang/cargo/pull/14435/commits. This adds a new feature to Cargo. Originally, the test commit used the new features which didn't exist and errored out quickly. The commit that implemented the feature then made them not error. This was still more helpful than having them in the same commit because I only had to review the test output and not all of the test. However, I found a parallel feature and suggested it to the contributor. The tests are now much easier to see what the intended behavior is.
Thats easy when its all textual output and you aren't dealing with compile errors. One option is to write the tests with structs, rater than enums. The commit that adds the feature could also switch the structs to enums, causing some cases to still work while other cases will error.
That might or might not be worth it. If that seems like it will be too much or not show enough, feel free to say so, squash the commits, and move on.
Hej @epage thanks for having a look |
8c3b3b0
to
bd54e23
Compare
let conflicts = variants | ||
.iter() | ||
.filter_map(|(_, v)| { | ||
if v.ident == variant.ident { | ||
None | ||
} else { | ||
Some(Name::Derived(v.ident.clone())) | ||
} | ||
}) | ||
.collect::<Vec<_>>(); | ||
|
||
let fields = collect_args_fields(item, fields)?; | ||
|
||
let augmentation = gen_augment(&fields, &app_var, item, &conflicts, false)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we collecting and manually specifying conflicts rather than having group.multiple(false)
?
There are bugs in nested group support but we should instead focus on those.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah specify multiple(false) for the "outer" group generated for the enum itself?
Guess that could work.
I explicitly stated the conflicts more out of intuition and reading that nested groups are behaving weirdly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So to elaborate on "behaving weirdly", adding a group as an argument of a group doesnt seem to work at all currently
if i build something like this:
Command::new()
.arg(clap::Arg::new("a") ...)
.arg(clap::Arg::new("b") ...)
.group(clap::ArgGroup("A").args(["a"])
.group(clap::ArgGroup("B").args(["b"])
.group(clap::ArgGroup("Outer").args(["A", "B"])
i get a runtime error: Command clap: Argument group 'Outer' contains non-existent argument 'A'
.
Before I'm digging into the ArgGroup parser which seems to specifically address args (rather than args or groups), i'm looking to confirm that this is indeed how we would want to specify nested groups to begin with.
Continue reading here if i haven't gone astray yet.
From thereon i'm looking at clap_builder::parser::validator::gather_arg_direct_conflicts
which would recursively resolve all ancestor chains, and then from the root(s) down check each group whether it is a multiple == false
group, and if it is add all args from other branches not on the current path as conflicts; or in pseudocode:
conflicts(arg_id: &Id, path: Vec<&Id>, conf: &mut Vec<Id>)
parent_id = path[-1] ? arg_id
for group in groups_for_arg(parnt_id) {
for conflict in group.conflicts {
if conflict.is_group {
for member in members_recursive_ignore_path(path, group) {
conf.push(member.id)
}
} else {
conf.push(conflict.id)
}
}
if !group.multiple {
for member in members_recursive_ignore_path(path, group) {
conf.push(member.id)
}
}
path = path.clone()
path.push(group.id)
conflicts(arg_id, path, conf)
}
that is if the goal is to support arbitrary nesting
I'm not too happy with the amount of potential duplicates iff groups are densely nested.
Also keep calling groups_for_arg
(or the impl of it) may hold potential for optimization down the road.
I believe a similar impl is also necessary for require
ing with nested groups.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh wow, I thought we supported that. There was some case where we had a bug related to ArgGroup
s and I thought we had broader support for ArgGroup
s than we apparently do (which also means I blocked #4697 on the wrong thing...)
I would like for this to move forward with groups, rather than conflicts, so we make sure we stabilize the right semantics. So that means we need nested groups first...
One approach
- Deprecate
ArgGroup::arg
andArgGroup::args
and addArgGroup::member
orArgGroup::child
- Update the assertions related to ensuring group members are present
- Add associated tests to make sure nested arg groups work, fixing bugs along the way
If someone wants to take this on, this should be its own PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd be up to look into that.
Do you think my understanding of conflict detection aligns with your intended changes to arggroup?
If not, how/where could we plan this in more detail than 3 bullet points?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Created #5711. I don't have more specific guidance at this time. In general, a child group should behave like a child arg. I don't more more specific guidance than that at this time.
// when generating mutably exclusive arguments, | ||
// ids of arguments that should conflict | ||
conflicts: &[Name], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This goes away if we rely on groups, rather than conflicts
@@ -70,6 +70,66 @@ impl Item { | |||
Ok(res) | |||
} | |||
|
|||
pub(crate) fn from_args_enum_variant( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Item
is a mess. It is a storage for all of our highlevel attribute information for a single type, field, variant, etc. We use the same type for all of those and for whichever trait or attribute type is used.
Making this function fits exactly within the current model
Hello thank you for your efforts to bring simplicity of enums into args. What are future perspective of this PR? Is is possible to have it finished in principle(no blockers of by design or because of backward compatibility)? |
As far as I understand the blocking issue for this is #5711. |
Current work on #5711 is here: https://github.com/ysndr/clap/tree/nested-arg-groups, specifically the last commits. Its not the most efficient in terms of avoiding checking paths multiple times potentially, but the first fairly simple group conflict test succeeds. |
Implement
derive(clap::Args)
support for enum types, where each variant is a mutually exclusiveArgGroup
.Relevant discussion and motivation for this PR is in #2621.
Impl notes:
At the moment this is a rough initial implementation.
Named
/ struct like enum variants such as this:subcommand
impl and deriving from structsRegardless this is a dear feature to me so i'm looking for some guidance to polish this into completion.