Skip to content

Added config file settings validation for preview features#17202

Open
j-helland wants to merge 9 commits into
astral-sh:mainfrom
j-helland:jwh/config-file-validation
Open

Added config file settings validation for preview features#17202
j-helland wants to merge 9 commits into
astral-sh:mainfrom
j-helland:jwh/config-file-validation

Conversation

@j-helland

@j-helland j-helland commented Dec 21, 2025

Copy link
Copy Markdown

Stacked on #16452

Summary

Note

For reviewers: see the last commit

Per discussion #16452 (comment), it was determined that we need to have additional config validation for the preview and preview-features fields.

This is a sketch of how we might approach that via a Validator trait implemented for Options and GlobalOptions.

Test Plan

New and existing automated tests.

Example

# Setup
> mkdir example && cd example
> cargo run -- init
> echo '\n[tool.uv]\npreview = false\npreview-features = ["pylock"]' >> pyproject.toml
> tail -n3 pyproject.toml
[tool.uv]
preview = false
preview-features = ["pylock"]

# New behavior
> cargo run -- format
error: Failed to parse: `pyproject.toml`. Cannot specify both `preview` and `preview-features`.

j-helland and others added 5 commits December 10, 2025 00:41
Preview features are specified as a list of strings in TOML, which parse
to PreviewFeatures values. CLI arguments like --preview and --no-preview
take precedence over config values. --preview-features combines with
config values.
…e fields

* Instead of merging commandline `--preview-features` with config file `preview-features`, now the commandline arguments take precedence. This matches the semantics of how uv handles other commandline arguments.
* Removed unnecessary `PreviewFeaturesMode` type.
* Fixed bug with `PreviewFeatures` serde serializer that produced a list of lists instead of a flat list as expected.
Comment on lines +7 to +9
pub(crate) trait Validator<Context = (), Err = ValidationError> {
fn validate(&self, ctx: &Context) -> Result<(), Err>;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This trait might be overkill. If we think that there will be lots of additional validation rules in the future, something like this makes sense. If not, simplifying to something like validate_uv_toml would be better.

YAGNI?

Comment on lines 52 to +53
tracing::debug!("Found user configuration in: `{}`", file.display());
options.validate(&Context { path: &file })?;

@j-helland j-helland Dec 21, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't consolidate validate_uv_toml into the validator trait impl because it actually doesn't get called in the pyproject.toml branch and tests would fail if it was called there.

@j-helland j-helland force-pushed the jwh/config-file-validation branch from ab5f1cd to b24e193 Compare December 23, 2025 21:05
EliteTK and others added 3 commits February 6, 2026 21:13
Specifically, disallow specifying both `preview` and `preview-features` in the config files. The implementation is intended to be extensible, should more validation rules be needed in the future.
@EliteTK EliteTK force-pushed the jwh/config-file-validation branch from b24e193 to 259a9d4 Compare February 6, 2026 22:00
@EliteTK

EliteTK commented Feb 8, 2026

Copy link
Copy Markdown
Member

For this kind of validation, I feel like having to remember to validate the options everywhere is a bit error prone. Since all sources of this data will need this validation, it's best to implement it with a custom Deserialize implementation on Options.

In fact, the best way to do this is to just repurpose the From<OptionsWire> implementation. Something like this:

impl<'de> Deserialize<'de> for Options {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let wire = OptionsWire::deserialize(deserializer)?;

        if wire.preview_features.is_some() && wire.preview.is_some() {
            return Err(serde::de::Error::custom(
                "Cannot specify both `preview` and `preview-features`.",
            ));
        }

        let OptionsWire {
            required_version,
            native_tls,
            ...

See also #16452 (comment).

@j-helland

Copy link
Copy Markdown
Author

In fact, the best way to do this is to just repurpose the From implementation...

Nice, thanks for the pointer! Centralizing into the deserializer is good.

It would make the most sense to me, then, to consolidate what you suggest into the original PR #16452 once the design there is clarified. I can do it in this PR if you all would prefer, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants