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

Closed
wants to merge 2 commits into from
Closed

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 charliermarsh force-pushed the charlie/merge branch 2 times, most recently from 508c266 to 92717e6 Compare February 11, 2024 03:32
@charliermarsh
Copy link
Member Author

Adding a test...

Copy link
Contributor

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

@MichaReiser MichaReiser Feb 11, 2024

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
@MichaReiser MichaReiser changed the base branch from main to ruff-0.5 June 24, 2024 13:42
@MichaReiser
Copy link
Member

To confirm the behavior change. We'll now start merging the following options:

  • per_file_ignores
  • extend-per-file-ignores (Do we still need the extend?)
  • flake8_import_convention_options.aliases
  • flake8_import_convention_options.extend_aliases
  • flake8_import_convention_options.banned_aliases
  • flake8_import_convention_options.banned_from
  • flake8_tidy_imports.banned_api
  • isort.sections

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 extend- options when "extending" becomes the new default for tables.

The new CombineOptions allows us to use different merging behaviors for different settings, but I'm worried this will be confusing.

This makes me hesitant to merge this change. @charliermarsh what are your thoughts on this?

@MichaReiser MichaReiser self-assigned this Jun 25, 2024
@MichaReiser
Copy link
Member

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 extend semantics. We need to holistically rethink the semantics of extending configurations.

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