-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Merge maps when combining options #9927
Conversation
508c266
to
92717e6
Compare
Adding a test... |
|
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 is one of those bug fixes that are, theoretically, a breaking change 😟
)), | ||
ident: type_ident, | ||
arguments, | ||
}) if type_ident == "Option" => { |
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 prefer "dumb" derive macros that, ideally, call a trait method rather than that they contain the entire implementation.
This is achievable for CombinePluginOptions
, although it requires more boilerplate code.
handle_field
in macro:
Ok(quote_spanned!(
ident.span() => #ident: crate::configuration::CombinePluginOptions::combine(self.#ident, other.#ident)
))
next to CombinePluginOptions
macro_rules! or_combine_plugin_impl {
($ty:ident) => {
impl CombinePluginOptions for Option<$ty> {
#[inline]
fn combine(self, other: Self) -> Self {
self.or(other)
}
}
};
}
or_combine_plugin_impl!(bool);
or_combine_plugin_impl!(u8);
or_combine_plugin_impl!(u16);
or_combine_plugin_impl!(u32);
or_combine_plugin_impl!(u64);
or_combine_plugin_impl!(usize);
or_combine_plugin_impl!(i8);
or_combine_plugin_impl!(i16);
or_combine_plugin_impl!(i32);
or_combine_plugin_impl!(i64);
or_combine_plugin_impl!(isize);
or_combine_plugin_impl!(String);
impl<T: CombinePluginOptions> CombinePluginOptions for Option<T> {
fn combine(self, other: Self) -> Self {
match (self, other) {
(Some(base), Some(other)) => Some(base.combine(other)),
(Some(base), None) => Some(base),
(None, Some(other)) => Some(other),
(None, None) => None,
}
}
}
impl<T> CombinePluginOptions for Option<Vec<T>> {
fn combine(self, other: Self) -> Self {
self.or(other)
}
}
impl<T, S> CombinePluginOptions for Option<HashSet<T, S>> {
fn combine(self, other: Self) -> Self {
self.or(other)
}
}
impl<K, V, S> CombinePluginOptions for HashMap<K, V, S>
where
K: Eq + Hash,
S: BuildHasher,
{
fn combine(mut self, other: Self) -> Self {
self.extend(other);
self
}
}
or_combine_plugin_impl!(ParametrizeNameType);
or_combine_plugin_impl!(ParametrizeValuesType);
or_combine_plugin_impl!(ParametrizeValuesRowType);
or_combine_plugin_impl!(Quote);
or_combine_plugin_impl!(Strictness);
or_combine_plugin_impl!(RelativeImportsOrder);
or_combine_plugin_impl!(LineLength);
or_combine_plugin_impl!(Convention);
or_combine_plugin_impl!(IndentStyle);
or_combine_plugin_impl!(QuoteStyle);
or_combine_plugin_impl!(LineEnding);
or_combine_plugin_impl!(DocstringCodeLineWidth);
We could avoid all the boilerplate if Rust supported specialization... (it would boil down to impl<T> CombinePluginOptions for Option<T>
and one specialization impl<T: CombinePluginOptions> for Option<T>
). Maybe @BurntSushi knows a better way to avoid some of the boilerplate.
The advantage of being explicit about how CombinePluginOptions
is implemented is that we can have custom implementations (like for HashMap) or even other types we don't know yet. It also ensures that we explicitly consider how Option<T>
should be merged rather than assuming Option::or
is the right choice.
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.
Sounds good, I can do 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.
Yeah I like @MichaReiser's approach here.
We could avoid all the boilerplate if Rust supported specialization... (it would boil down to
impl<T> CombinePluginOptions for Option<T>
and one specializationimpl<T: CombinePluginOptions> for Option<T>
). Maybe @BurntSushi knows a better way to avoid some of the boilerplate.
Nothing is really coming to me here. And specialization is unfortunately probably not going to land any time soon.
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 applied the changes for you
Yeah. I think this behavior is more intuitive when you consider how TOML is structured, but it also means there's now no way to "unset" values in a map when extending. |
You should still be able to "unset" by explicitly writing the extended value and setting it to a null value, perhaps. The issue would be that there isn't (I am not aware of) a "universal safe value". I think TOML explicitly doesn't like or expects to handle empty/null/nil values. I think an explicit unset could still be expressed by:
./subdir/.ruff.toml:
|
a75ad9e
to
ccf797e
Compare
ccf797e
to
1d2d0c6
Compare
To confirm the behavior change. We'll now start merging the following options:
I'm not sure whether the new behavior is correct for all settings, or at least it becomes less clear why we would still need the The new This makes me hesitant to merge this change. @charliermarsh what are your thoughts on this? |
I don't feel confident that this is the right solution to justify a breaking change because it doesn't play nice with Ruff's |
Summary
If a user provides a value via a sub-table in a configuration file, we should combine that table when extending, rather than overwriting it.
Closes #9872.
Test Plan
Creating a directory following the structure in #9872. Verified that
--show-settings
showed the custom section as a known package.