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

Merge maps when combining options #9927

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Merge maps when combining options #9927

wants to merge 2 commits into from

Conversation

charliermarsh
Copy link
Member

@charliermarsh charliermarsh commented Feb 11, 2024

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.

@charliermarsh charliermarsh added bug Something isn't working configuration Related to settings and configuration labels Feb 11, 2024
@charliermarsh charliermarsh marked this pull request as ready for review February 11, 2024 03:28
@charliermarsh
Copy link
Member Author

Adding a test...

Copy link

github-actions bot commented Feb 11, 2024

ruff-ecosystem results

Linter (stable)

✅ ecosystem check detected no linter changes.

Linter (preview)

✅ ecosystem check detected no linter changes.

Copy link
Member

@MichaReiser MichaReiser left a 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" => {
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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 specialization impl<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.

Copy link
Member

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

@charliermarsh
Copy link
Member Author

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.

@LTourinho
Copy link

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:
./.ruff.toml:

[sometag]
a = "a"

./subdir/.ruff.toml:

extend "../ruff.toml"
[sometag]
a = {}

@charliermarsh charliermarsh added the breaking Breaking API change label Feb 26, 2024
@charliermarsh charliermarsh added this to the v0.3.0 milestone Feb 26, 2024
@MichaReiser MichaReiser modified the milestones: v0.3.0, v0.4 Feb 29, 2024
@MichaReiser MichaReiser closed this Mar 6, 2024
@MichaReiser MichaReiser deleted the charlie/merge branch March 6, 2024 09:00
@MichaReiser MichaReiser restored the charlie/merge branch March 8, 2024 11:43
@MichaReiser MichaReiser reopened this Mar 8, 2024
@dhruvmanila dhruvmanila modified the milestones: v0.4.0, v0.5.0 Apr 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking Breaking API change bug Something isn't working configuration Related to settings and configuration
Projects
None yet
Development

Successfully merging this pull request may close these issues.

v0.1.6: Overriding sub-setting category nulls other inherited settings (#4348)
5 participants