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

Disallow pyproject.toml from pip uninstall #2663

Merged
merged 1 commit into from
Mar 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions crates/uv-requirements/src/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,44 @@ impl RequirementsSource {
}
}

/// Parse a [`RequirementsSource`] from a constraints file.
pub fn from_constraints_file(path: PathBuf) -> Self {
if path.ends_with("pyproject.toml") {
warn_user!(
"The file `{}` appears to be a `pyproject.toml` file, but constraints must be specified in `requirements.txt` format.", path.user_display()
);
/// Parse a [`RequirementsSource`] from a `requirements.txt` file.
pub fn from_requirements_txt(path: PathBuf) -> Self {
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(filename) {
warn_user!(
"The file `{}` appears to be a `{}` file, but requirements must be specified in `requirements.txt` format.",
path.user_display(),
filename
);
}
}
Self::RequirementsTxt(path)
}

/// Parse a [`RequirementsSource`] from an overrides file.
pub fn from_overrides_file(path: PathBuf) -> Self {
if path.ends_with("pyproject.toml") {
warn_user!(
"The file `{}` appears to be a `pyproject.toml` file, but overrides must be specified in `requirements.txt` format.", path.user_display()
);
/// Parse a [`RequirementsSource`] from a `constraints.txt` file.
pub fn from_constraints_txt(path: PathBuf) -> Self {
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(filename) {
warn_user!(
"The file `{}` appears to be a `{}` file, but constraints must be specified in `requirements.txt` format.",
path.user_display(),
filename
);
}
}
Self::RequirementsTxt(path)
}

/// Parse a [`RequirementsSource`] from an `overrides.txt` file.
pub fn from_overrides_txt(path: PathBuf) -> Self {
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(filename) {
warn_user!(
"The file `{}` appears to be a `{}` file, but overrides must be specified in `requirements.txt` format.",
path.user_display(),
filename
);
}
}
Self::RequirementsTxt(path)
}
Expand Down
8 changes: 4 additions & 4 deletions crates/uv/src/commands/pip_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ use uv_installer::{
is_dynamic, Downloader, NoBinary, Plan, Planner, Reinstall, ResolvedEditable, SitePackages,
};
use uv_interpreter::{Interpreter, PythonEnvironment};
use uv_requirements::{
ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification,
SourceTreeResolver,
};
use uv_resolver::InMemoryIndex;
use uv_traits::{BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy};
use uv_warnings::warn_user;
Expand All @@ -31,10 +35,6 @@ use crate::commands::reporters::{
};
use crate::commands::{compile_bytecode, elapsed, ChangeEvent, ChangeEventKind, ExitStatus};
use crate::printer::Printer;
use uv_requirements::{
ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification,
SourceTreeResolver,
};

/// Install a set of locked requirements into the current Python environment.
#[allow(clippy::too_many_arguments, clippy::fn_params_excessive_bools)]
Expand Down
10 changes: 5 additions & 5 deletions crates/uv/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1469,12 +1469,12 @@ async fn run() -> Result<ExitStatus> {
let constraints = args
.constraint
.into_iter()
.map(RequirementsSource::from_constraints_file)
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
let overrides = args
.r#override
.into_iter()
.map(RequirementsSource::from_overrides_file)
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();
let index_urls = IndexLocations::new(
args.index_url.and_then(Maybe::into_option),
Expand Down Expand Up @@ -1624,12 +1624,12 @@ async fn run() -> Result<ExitStatus> {
let constraints = args
.constraint
.into_iter()
.map(RequirementsSource::from_constraints_file)
.map(RequirementsSource::from_constraints_txt)
.collect::<Vec<_>>();
let overrides = args
.r#override
.into_iter()
.map(RequirementsSource::from_overrides_file)
.map(RequirementsSource::from_overrides_txt)
.collect::<Vec<_>>();
let index_urls = IndexLocations::new(
args.index_url.and_then(Maybe::into_option),
Expand Down Expand Up @@ -1714,7 +1714,7 @@ async fn run() -> Result<ExitStatus> {
.chain(
args.requirement
.into_iter()
.map(RequirementsSource::from_requirements_file),
.map(RequirementsSource::from_requirements_txt),
)
.collect::<Vec<_>>();
commands::pip_uninstall(
Expand Down
106 changes: 106 additions & 0 deletions crates/uv/tests/pip_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,112 @@ fn empty_requirements_txt() -> Result<()> {
Ok(())
}

#[test]
fn missing_pyproject_toml() {
let context = TestContext::new("3.12");

uv_snapshot!(command(&context)
.arg("-r")
.arg("pyproject.toml"), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: failed to read from file `pyproject.toml`
Caused by: No such file or directory (os error 2)
"###
);
}

#[test]
fn invalid_pyproject_toml_syntax() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str("123 - 456")?;

uv_snapshot!(command(&context)
.arg("-r")
.arg("pyproject.toml"), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 1, column 5
|
1 | 123 - 456
| ^
expected `.`, `=`

"###
);

Ok(())
}

#[test]
fn invalid_pyproject_toml_schema() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str("[project]")?;

uv_snapshot!(command(&context)
.arg("-r")
.arg("pyproject.toml"), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 1, column 1
|
1 | [project]
| ^^^^^^^^^
missing field `name`

"###
);

Ok(())
}

#[test]
fn invalid_pyproject_toml_requirement() -> Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(
r#"[project]
name = "project"
dependencies = ["flask==1.0.x"]
"#,
)?;

uv_snapshot!(command(&context)
.arg("-r")
.arg("pyproject.toml"), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 3, column 16
|
3 | dependencies = ["flask==1.0.x"]
| ^^^^^^^^^^^^^^^^
after parsing 1.0, found ".x" after it, which is not part of a valid version
flask==1.0.x
^^^^^^^

"###
);

Ok(())
}

#[test]
fn no_solution() {
let context = TestContext::new("3.12");
Expand Down
120 changes: 0 additions & 120 deletions crates/uv/tests/pip_uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,126 +149,6 @@ fn invalid_requirements_txt_requirement() -> Result<()> {
Ok(())
}

#[test]
fn missing_pyproject_toml() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;

uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.arg("-r")
.arg("pyproject.toml")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: failed to read from file `pyproject.toml`
Caused by: No such file or directory (os error 2)
"###
);

Ok(())
}

#[test]
fn invalid_pyproject_toml_syntax() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str("123 - 456")?;

uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.arg("-r")
.arg("pyproject.toml")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 1, column 5
|
1 | 123 - 456
| ^
expected `.`, `=`

"###);

Ok(())
}

#[test]
fn invalid_pyproject_toml_schema() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str("[project]")?;

uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.arg("-r")
.arg("pyproject.toml")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 1, column 1
|
1 | [project]
| ^^^^^^^^^
missing field `name`

"###);

Ok(())
}

#[test]
fn invalid_pyproject_toml_requirement() -> Result<()> {
let temp_dir = assert_fs::TempDir::new()?;
let pyproject_toml = temp_dir.child("pyproject.toml");
pyproject_toml.touch()?;
pyproject_toml.write_str(
r#"[project]
name = "project"
dependencies = ["flask==1.0.x"]
"#,
)?;

uv_snapshot!(Command::new(get_bin())
.arg("pip")
.arg("uninstall")
.arg("-r")
.arg("pyproject.toml")
.current_dir(&temp_dir), @r###"
success: false
exit_code: 2
----- stdout -----

----- stderr -----
error: Failed to parse `pyproject.toml`
Caused by: TOML parse error at line 3, column 16
|
3 | dependencies = ["flask==1.0.x"]
| ^^^^^^^^^^^^^^^^
after parsing 1.0, found ".x" after it, which is not part of a valid version
flask==1.0.x
^^^^^^^

"###);

Ok(())
}

#[test]
fn uninstall() -> Result<()> {
let context = TestContext::new("3.12");
Expand Down
Loading