Skip to content

Commit

Permalink
Reject pyproject.toml in --config-file
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Aug 6, 2024
1 parent fae9a70 commit 186b11a
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 1 deletion.
6 changes: 6 additions & 0 deletions crates/uv-settings/src/combine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,9 @@ impl Combine for Option<ConfigSettings> {
}
}
}

impl Combine for serde::de::IgnoredAny {
fn combine(self, _other: Self) -> Self {
self
}
}
20 changes: 19 additions & 1 deletion crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub(crate) struct Tools {
/// A `[tool.uv]` section.
#[allow(dead_code)]
#[derive(Debug, Clone, Default, Deserialize, CombineOptions, OptionsMetadata)]
#[serde(rename_all = "kebab-case")]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Options {
#[serde(flatten)]
Expand All @@ -49,6 +49,24 @@ pub struct Options {
)]
pub override_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,
pub constraint_dependencies: Option<Vec<Requirement<VerbatimParsedUrl>>>,

// NOTE(charlie): These fields should be kept in-sync with `ToolUv` in
// `crates/uv-workspace/src/pyproject.rs`.
#[serde(default, skip_serializing)]
#[cfg_attr(feature = "schemars", schemars(skip))]
workspace: serde::de::IgnoredAny,

#[serde(default, skip_serializing)]
#[cfg_attr(feature = "schemars", schemars(skip))]
sources: serde::de::IgnoredAny,

#[serde(default, skip_serializing)]
#[cfg_attr(feature = "schemars", schemars(skip))]
dev_dependencies: serde::de::IgnoredAny,

#[serde(default, skip_serializing)]
#[cfg_attr(feature = "schemars", schemars(skip))]
managed: serde::de::IgnoredAny,
}

/// Global settings, relevant to all invocations.
Expand Down
4 changes: 4 additions & 0 deletions crates/uv-workspace/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,14 @@ pub struct Tool {
pub uv: Option<ToolUv>,
}

// NOTE(charlie): When adding fields to this struct, mark them as ignored on `Options` in
// `crates/uv-settings/src/settings.rs`.
#[derive(Serialize, Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ToolUv {
/// The sources to use (e.g., workspace members, Git repositories, local paths) when resolving
/// dependencies.
pub sources: Option<BTreeMap<PackageName, Source>>,
/// The workspace definition for the project, if any.
#[option_group]
Expand Down
6 changes: 6 additions & 0 deletions crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,12 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
// found, this file is combined with the user configuration file. In this case, we don't
// search for `pyproject.toml` files, since we're not in a workspace.
let filesystem = if let Some(config_file) = cli.config_file.as_ref() {
if config_file
.file_name()
.is_some_and(|file_name| file_name == "pyproject.toml")
{
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.");
}
Some(FilesystemOptions::from_file(config_file)?)
} else if deprecated_isolated || cli.no_config {
None
Expand Down
229 changes: 229 additions & 0 deletions crates/uv/tests/show_settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2680,3 +2680,232 @@ fn resolve_both() -> anyhow::Result<()> {

Ok(())
}

/// Read from a `--config-file` command line argument.
#[test]
#[cfg_attr(
windows,
ignore = "Configuration tests are not yet supported on Windows"
)]
fn resolve_config_file() -> anyhow::Result<()> {
let context = TestContext::new("3.12");

// Write a `uv.toml` to a temporary location.
let config_dir = assert_fs::TempDir::new().expect("Failed to create temp dir");
let config = config_dir.child("uv.toml");
config.write_str(indoc::indoc! {r#"
[pip]
resolution = "lowest-direct"
generate-hashes = true
index-url = "https://pypi.org/simple"
"#})?;

let requirements_in = context.temp_dir.child("requirements.in");
requirements_in.write_str("anyio>3.0.0")?;

uv_snapshot!(context.filters(), command(&context)
.arg("--show-settings")
.arg("--config-file")
.arg(config.path())
.arg("requirements.in"), @r###"
success: true
exit_code: 0
----- stdout -----
GlobalSettings {
quiet: false,
verbose: 0,
color: Auto,
native_tls: false,
connectivity: Online,
show_settings: true,
preview: Disabled,
python_preference: OnlySystem,
python_fetch: Automatic,
no_progress: false,
}
CacheSettings {
no_cache: false,
cache_dir: Some(
"[CACHE_DIR]/",
),
}
PipCompileSettings {
src_file: [
"requirements.in",
],
constraint: [],
override: [],
constraints_from_workspace: [],
overrides_from_workspace: [],
build_constraint: [],
refresh: None(
Timestamp(
SystemTime {
tv_sec: [TIME],
tv_nsec: [TIME],
},
),
),
settings: PipSettings {
index_locations: IndexLocations {
index: Some(
Pypi(
VerbatimUrl {
url: Url {
scheme: "https",
cannot_be_a_base: false,
username: "",
password: None,
host: Some(
Domain(
"pypi.org",
),
),
port: None,
path: "/simple",
query: None,
fragment: None,
},
given: Some(
"https://pypi.org/simple",
),
},
),
),
extra_index: [],
flat_index: [],
no_index: false,
},
python: None,
system: false,
extras: None,
break_system_packages: false,
target: None,
prefix: None,
index_strategy: FirstIndex,
keyring_provider: Disabled,
no_build_isolation: false,
build_options: BuildOptions {
no_binary: None,
no_build: None,
},
allow_empty_requirements: false,
strict: false,
dependency_mode: Transitive,
resolution: LowestDirect,
prerelease: IfNecessaryOrExplicit,
output_file: None,
no_strip_extras: false,
no_strip_markers: false,
no_annotate: false,
no_header: false,
custom_compile_command: None,
generate_hashes: true,
setup_py: Pep517,
config_setting: ConfigSettings(
{},
),
python_version: None,
python_platform: None,
universal: false,
exclude_newer: Some(
ExcludeNewer(
2024-03-25T00:00:00Z,
),
),
no_emit_package: [],
emit_index_url: false,
emit_find_links: false,
emit_build_options: false,
emit_marker_expression: false,
emit_index_annotation: false,
annotation_style: Split,
link_mode: Clone,
compile_bytecode: false,
sources: Enabled,
hash_checking: None,
upgrade: None,
reinstall: None,
concurrency: Concurrency {
downloads: 50,
builds: 16,
installs: 8,
},
},
}
----- stderr -----
"###
);

// Write in `pyproject.toml` schema.
config.write_str(indoc::indoc! {r#"
[project]
name = "example"
version = "0.0.0"
[tool.uv.pip]
resolution = "lowest-direct"
generate-hashes = true
index-url = "https://pypi.org/simple"
"#})?;

// The file should be rejected for violating the schema.
uv_snapshot!(context.filters(), command(&context)
.arg("--show-settings")
.arg("--config-file")
.arg(config.path())
.arg("requirements.in"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to parse: `/var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/[TMP]/uv.toml`
Caused by: TOML parse error at line 1, column 1
|
1 | [project]
| ^
unknown field `project`
"###
);

// Write an _actual_ `pyproject.toml`.
let config = config_dir.child("pyproject.toml");
config.write_str(indoc::indoc! {r#"
[project]
name = "example"
version = "0.0.0"
[tool.uv.pip]
resolution = "lowest-direct"
generate-hashes = true
index-url = "https://pypi.org/simple"
"""#
})?;

// The file should be rejected for violating the schema, with a custom warning.
uv_snapshot!(context.filters(), command(&context)
.arg("--show-settings")
.arg("--config-file")
.arg(config.path())
.arg("requirements.in"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
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.
error: Failed to parse: `/var/folders/nt/6gf2v7_s3k13zq_t3944rwz40000gn/T/[TMP]/pyproject.toml`
Caused by: TOML parse error at line 9, column 3
|
9 | ""
| ^
expected `.`, `=`
"###
);

Ok(())
}

0 comments on commit 186b11a

Please sign in to comment.