Skip to content

Commit 858e8d3

Browse files
committed
Reject pyproject.toml in --config-file
1 parent fae9a70 commit 858e8d3

File tree

6 files changed

+271
-1
lines changed

6 files changed

+271
-1
lines changed

crates/uv-settings/src/combine.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,9 @@ impl Combine for Option<ConfigSettings> {
9797
}
9898
}
9999
}
100+
101+
impl Combine for serde::de::IgnoredAny {
102+
fn combine(self, _other: Self) -> Self {
103+
self
104+
}
105+
}

crates/uv-settings/src/settings.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pub(crate) struct Tools {
3131
/// A `[tool.uv]` section.
3232
#[allow(dead_code)]
3333
#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)]
34-
#[serde(rename_all = "kebab-case")]
34+
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
3535
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
3636
pub struct Options {
3737
#[serde(flatten)]
@@ -49,6 +49,24 @@ pub struct Options {
4949
)]
5050
pub override_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
5151
pub constraint_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
52+
53+
// NOTE(charlie): These fields should be kept in-sync with `ToolUv` in
54+
// `crates/uv-workspace/src/pyproject.rs`.
55+
#[serde(default, skip_serializing)]
56+
#[cfg_attr(feature = "schemars", schemars(skip))]
57+
workspace: serde::de::IgnoredAny,
58+
59+
#[serde(default, skip_serializing)]
60+
#[cfg_attr(feature = "schemars", schemars(skip))]
61+
sources: serde::de::IgnoredAny,
62+
63+
#[serde(default, skip_serializing)]
64+
#[cfg_attr(feature = "schemars", schemars(skip))]
65+
dev_dependencies: serde::de::IgnoredAny,
66+
67+
#[serde(default, skip_serializing)]
68+
#[cfg_attr(feature = "schemars", schemars(skip))]
69+
managed: serde::de::IgnoredAny,
5270
}
5371

5472
/// Global settings, relevant to all invocations.

crates/uv-workspace/src/pyproject.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,14 @@ pub struct Tool {
7676
pub uv: Option<ToolUv>,
7777
}
7878

79+
// NOTE(charlie): When adding fields to this struct, mark them as ignored on `Options` in
80+
// `crates/uv-settings/src/settings.rs`.
7981
#[derive(Serialize, Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
8082
#[serde(rename_all = "kebab-case")]
8183
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
8284
pub struct ToolUv {
85+
/// The sources to use (e.g., workspace members, Git repositories, local paths) when resolving
86+
/// dependencies.
8387
pub sources: Option<BTreeMap<PackageName, Source>>,
8488
/// The workspace definition for the project, if any.
8589
#[option_group]

crates/uv/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
110110
// found, this file is combined with the user configuration file. In this case, we don't
111111
// search for `pyproject.toml` files, since we're not in a workspace.
112112
let filesystem = if let Some(config_file) = cli.config_file.as_ref() {
113+
if config_file
114+
.file_name()
115+
.is_some_and(|file_name| file_name == "pyproject.toml")
116+
{
117+
warn_user!("The `--config-file` argument expects to receive a `uv.toml` file, not a `pyproject.toml`. If you're trying to run a command from another project, use the `--directory` argument instead.");
118+
}
113119
Some(FilesystemOptions::from_file(config_file)?)
114120
} else if deprecated_isolated || cli.no_config {
115121
None

crates/uv/tests/show_settings.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2680,3 +2680,232 @@ fn resolve_both() -> anyhow::Result<()> {
26802680

26812681
Ok(())
26822682
}
2683+
2684+
/// Read from a `--config-file` command line argument.
2685+
#[test]
2686+
#[cfg_attr(
2687+
windows,
2688+
ignore = "Configuration tests are not yet supported on Windows"
2689+
)]
2690+
fn resolve_config_file() -> anyhow::Result<()> {
2691+
let context = TestContext::new("3.12");
2692+
2693+
// Write a `uv.toml` to a temporary location.
2694+
let config_dir = assert_fs::TempDir::new().expect("Failed to create temp dir");
2695+
let config = config_dir.child("uv.toml");
2696+
config.write_str(indoc::indoc! {r#"
2697+
[pip]
2698+
resolution = "lowest-direct"
2699+
generate-hashes = true
2700+
index-url = "https://pypi.org/simple"
2701+
"#})?;
2702+
2703+
let requirements_in = context.temp_dir.child("requirements.in");
2704+
requirements_in.write_str("anyio>3.0.0")?;
2705+
2706+
uv_snapshot!(context.filters(), command(&context)
2707+
.arg("--show-settings")
2708+
.arg("--config-file")
2709+
.arg(config.path())
2710+
.arg("requirements.in"), @r###"
2711+
success: true
2712+
exit_code: 0
2713+
----- stdout -----
2714+
GlobalSettings {
2715+
quiet: false,
2716+
verbose: 0,
2717+
color: Auto,
2718+
native_tls: false,
2719+
connectivity: Online,
2720+
show_settings: true,
2721+
preview: Disabled,
2722+
python_preference: OnlySystem,
2723+
python_fetch: Automatic,
2724+
no_progress: false,
2725+
}
2726+
CacheSettings {
2727+
no_cache: false,
2728+
cache_dir: Some(
2729+
"[CACHE_DIR]/",
2730+
),
2731+
}
2732+
PipCompileSettings {
2733+
src_file: [
2734+
"requirements.in",
2735+
],
2736+
constraint: [],
2737+
override: [],
2738+
constraints_from_workspace: [],
2739+
overrides_from_workspace: [],
2740+
build_constraint: [],
2741+
refresh: None(
2742+
Timestamp(
2743+
SystemTime {
2744+
tv_sec: [TIME],
2745+
tv_nsec: [TIME],
2746+
},
2747+
),
2748+
),
2749+
settings: PipSettings {
2750+
index_locations: IndexLocations {
2751+
index: Some(
2752+
Pypi(
2753+
VerbatimUrl {
2754+
url: Url {
2755+
scheme: "https",
2756+
cannot_be_a_base: false,
2757+
username: "",
2758+
password: None,
2759+
host: Some(
2760+
Domain(
2761+
"pypi.org",
2762+
),
2763+
),
2764+
port: None,
2765+
path: "/simple",
2766+
query: None,
2767+
fragment: None,
2768+
},
2769+
given: Some(
2770+
"https://pypi.org/simple",
2771+
),
2772+
},
2773+
),
2774+
),
2775+
extra_index: [],
2776+
flat_index: [],
2777+
no_index: false,
2778+
},
2779+
python: None,
2780+
system: false,
2781+
extras: None,
2782+
break_system_packages: false,
2783+
target: None,
2784+
prefix: None,
2785+
index_strategy: FirstIndex,
2786+
keyring_provider: Disabled,
2787+
no_build_isolation: false,
2788+
build_options: BuildOptions {
2789+
no_binary: None,
2790+
no_build: None,
2791+
},
2792+
allow_empty_requirements: false,
2793+
strict: false,
2794+
dependency_mode: Transitive,
2795+
resolution: LowestDirect,
2796+
prerelease: IfNecessaryOrExplicit,
2797+
output_file: None,
2798+
no_strip_extras: false,
2799+
no_strip_markers: false,
2800+
no_annotate: false,
2801+
no_header: false,
2802+
custom_compile_command: None,
2803+
generate_hashes: true,
2804+
setup_py: Pep517,
2805+
config_setting: ConfigSettings(
2806+
{},
2807+
),
2808+
python_version: None,
2809+
python_platform: None,
2810+
universal: false,
2811+
exclude_newer: Some(
2812+
ExcludeNewer(
2813+
2024-03-25T00:00:00Z,
2814+
),
2815+
),
2816+
no_emit_package: [],
2817+
emit_index_url: false,
2818+
emit_find_links: false,
2819+
emit_build_options: false,
2820+
emit_marker_expression: false,
2821+
emit_index_annotation: false,
2822+
annotation_style: Split,
2823+
link_mode: Clone,
2824+
compile_bytecode: false,
2825+
sources: Enabled,
2826+
hash_checking: None,
2827+
upgrade: None,
2828+
reinstall: None,
2829+
concurrency: Concurrency {
2830+
downloads: 50,
2831+
builds: 16,
2832+
installs: 8,
2833+
},
2834+
},
2835+
}
2836+
2837+
----- stderr -----
2838+
"###
2839+
);
2840+
2841+
// Write in `pyproject.toml` schema.
2842+
config.write_str(indoc::indoc! {r#"
2843+
[project]
2844+
name = "example"
2845+
version = "0.0.0"
2846+
2847+
[tool.uv.pip]
2848+
resolution = "lowest-direct"
2849+
generate-hashes = true
2850+
index-url = "https://pypi.org/simple"
2851+
"#})?;
2852+
2853+
// The file should be rejected for violating the schema.
2854+
uv_snapshot!(context.filters(), command(&context)
2855+
.arg("--show-settings")
2856+
.arg("--config-file")
2857+
.arg(config.path())
2858+
.arg("requirements.in"), @r###"
2859+
success: false
2860+
exit_code: 2
2861+
----- stdout -----
2862+
2863+
----- stderr -----
2864+
error: Failed to parse: `/var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/[TMP]/uv.toml`
2865+
Caused by: TOML parse error at line 1, column 1
2866+
|
2867+
1 | [project]
2868+
| ^
2869+
unknown field `project`
2870+
2871+
"###
2872+
);
2873+
2874+
// Write an _actual_ `pyproject.toml`.
2875+
let config = config_dir.child("pyproject.toml");
2876+
config.write_str(indoc::indoc! {r#"
2877+
[project]
2878+
name = "example"
2879+
version = "0.0.0"
2880+
2881+
[tool.uv.pip]
2882+
resolution = "lowest-direct"
2883+
generate-hashes = true
2884+
index-url = "https://pypi.org/simple"
2885+
"""#
2886+
})?;
2887+
2888+
// The file should be rejected for violating the schema, with a custom warning.
2889+
uv_snapshot!(context.filters(), command(&context)
2890+
.arg("--show-settings")
2891+
.arg("--config-file")
2892+
.arg(config.path())
2893+
.arg("requirements.in"), @r###"
2894+
success: false
2895+
exit_code: 2
2896+
----- stdout -----
2897+
2898+
----- stderr -----
2899+
warning: The `--config-file` argument expects to receive a `uv.toml` file, not a `pyproject.toml`. If you're trying to run a command from another project, use the `--directory` argument instead.
2900+
error: Failed to parse: `/var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/[TMP]/pyproject.toml`
2901+
Caused by: TOML parse error at line 9, column 3
2902+
|
2903+
9 | ""
2904+
| ^
2905+
expected `.`, `=`
2906+
2907+
"###
2908+
);
2909+
2910+
Ok(())
2911+
}

uv.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
native-tls = true
2+
3+
# [workspace]
4+
# members = []
5+
6+
# [sources]
7+
# foo = { path = "../" }

0 commit comments

Comments
 (0)