diff --git a/Cargo.lock b/Cargo.lock index 886baa622..e48010f6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -448,6 +448,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" +dependencies = [ + "is-terminal", + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "configparser" version = "3.0.2" @@ -1966,13 +1977,13 @@ version = "0.0.1" dependencies = [ "anyhow", "clap", + "colored", "directories", "flate2", "fs-err", "gourgeist", "indoc 2.0.4", "itertools", - "owo-colors", "pep508_rs", "platform-host", "platform-tags", @@ -2006,6 +2017,7 @@ dependencies = [ "bitflags 2.4.1", "cacache", "clap", + "colored", "directories", "fs-err", "futures", @@ -2015,7 +2027,6 @@ dependencies = [ "install-wheel-rs", "itertools", "miette", - "owo-colors", "pep440_rs 0.3.12", "pep508_rs", "platform-host", diff --git a/Cargo.toml b/Cargo.toml index 6201f0c11..ff200c6d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ bitflags = { version = "2.4.0" } cacache = { version = "11.7.1", default-features = false, features = ["tokio-runtime"] } camino = { version = "1.1.6", features = ["serde1"] } clap = { version = "4.4.6" } +colored = { version = "2.0.4" } configparser = { version = "3.0.2" } csv = { version = "1.3.0" } data-encoding = { version = "2.4.0" } @@ -37,7 +38,6 @@ mailparse = { version = "0.14.0" } memchr = { version = "2.6.4" } miette = { version = "5.10.0" } once_cell = { version = "1.18.0" } -owo-colors = { version = "3.5.0" } platform-info = { version = "2.0.2" } plist = { version = "1.5.0" } pyproject-toml = { version = "0.7.0" } diff --git a/crates/puffin-build/Cargo.toml b/crates/puffin-build/Cargo.toml index 85f18cc13..2648d9651 100644 --- a/crates/puffin-build/Cargo.toml +++ b/crates/puffin-build/Cargo.toml @@ -24,12 +24,12 @@ puffin-workspace = { path = "../puffin-workspace" } anyhow = { workspace = true } clap = { workspace = true, features = ["derive"] } +colored = { workspace = true } directories = { workspace = true } flate2 = { workspace = true } fs-err = { workspace = true } indoc = { workspace = true } itertools = { workspace = true } -owo-colors = { workspace = true } pyproject-toml = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/puffin-build/src/main.rs b/crates/puffin-build/src/main.rs index c18ffb4f9..248de6a98 100644 --- a/crates/puffin-build/src/main.rs +++ b/crates/puffin-build/src/main.rs @@ -2,9 +2,9 @@ use anyhow::Context; use clap::Parser; +use colored::Colorize; use directories::ProjectDirs; use fs_err as fs; -use owo_colors::OwoColorize; use puffin_build::{Error, SourceDistributionBuilder}; use std::path::PathBuf; use std::process::ExitCode; diff --git a/crates/puffin-cli/Cargo.toml b/crates/puffin-cli/Cargo.toml index c64deaa78..7d154427e 100644 --- a/crates/puffin-cli/Cargo.toml +++ b/crates/puffin-cli/Cargo.toml @@ -26,13 +26,13 @@ anyhow = { workspace = true } bitflags = { workspace = true } cacache = { workspace = true } clap = { workspace = true, features = ["derive"] } +colored = { workspace = true } directories = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } indicatif = { workspace = true } itertools = { workspace = true } miette = { workspace = true, features = ["fancy"] } -owo-colors = { workspace = true } pyproject-toml = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } diff --git a/crates/puffin-cli/src/commands/pip_compile.rs b/crates/puffin-cli/src/commands/pip_compile.rs index c380102ec..632d3de00 100644 --- a/crates/puffin-cli/src/commands/pip_compile.rs +++ b/crates/puffin-cli/src/commands/pip_compile.rs @@ -3,9 +3,9 @@ use std::io::{stdout, BufWriter}; use std::path::Path; use anyhow::Result; +use colored::Colorize; use fs_err::File; use itertools::Itertools; -use owo_colors::OwoColorize; use pubgrub::report::Reporter; use tracing::debug; @@ -28,6 +28,13 @@ pub(crate) async fn pip_compile( ) -> Result { let start = std::time::Instant::now(); + // Read all requirements from the provided sources. + let requirements = sources + .iter() + .map(RequirementsSource::requirements) + .flatten_ok() + .collect::>>()?; + // Detect the current Python interpreter. let platform = Platform::current()?; let python = PythonExecutable::from_env(platform, cache)?; @@ -36,13 +43,6 @@ pub(crate) async fn pip_compile( python.executable().display() ); - // Read all requirements from the provided sources. - let requirements = sources - .iter() - .map(RequirementsSource::requirements) - .flatten_ok() - .collect::>>()?; - // Determine the current environment markers. let markers = python.markers(); diff --git a/crates/puffin-cli/src/commands/pip_sync.rs b/crates/puffin-cli/src/commands/pip_sync.rs index 3f8c97da6..691ec2b32 100644 --- a/crates/puffin-cli/src/commands/pip_sync.rs +++ b/crates/puffin-cli/src/commands/pip_sync.rs @@ -2,8 +2,8 @@ use std::fmt::Write; use std::path::Path; use anyhow::{Context, Result}; +use colored::Colorize; use itertools::Itertools; -use owo_colors::OwoColorize; use tracing::debug; use pep508_rs::Requirement; @@ -247,7 +247,7 @@ pub(crate) async fn sync_requirements( printer, " {} {}{}", "+".green(), - event.distribution.name().white().bold(), + event.distribution.name().as_ref().white().bold(), format!("@{}", event.distribution.version()).dimmed() )?; } @@ -256,7 +256,7 @@ pub(crate) async fn sync_requirements( printer, " {} {}{}", "-".red(), - event.distribution.name().white().bold(), + event.distribution.name().as_ref().white().bold(), format!("@{}", event.distribution.version()).dimmed() )?; } diff --git a/crates/puffin-cli/src/commands/pip_uninstall.rs b/crates/puffin-cli/src/commands/pip_uninstall.rs index e0eda41a8..b4c0f458a 100644 --- a/crates/puffin-cli/src/commands/pip_uninstall.rs +++ b/crates/puffin-cli/src/commands/pip_uninstall.rs @@ -2,8 +2,8 @@ use std::fmt::Write; use std::path::Path; use anyhow::Result; +use colored::Colorize; use itertools::Itertools; -use owo_colors::OwoColorize; use tracing::debug; use pep508_rs::Requirement; @@ -23,6 +23,13 @@ pub(crate) async fn pip_uninstall( ) -> Result { let start = std::time::Instant::now(); + // Read all requirements from the provided sources. + let requirements = sources + .iter() + .map(RequirementsSource::requirements) + .flatten_ok() + .collect::>>()?; + // Detect the current Python interpreter. let platform = Platform::current()?; let python = PythonExecutable::from_env(platform, cache)?; @@ -31,13 +38,6 @@ pub(crate) async fn pip_uninstall( python.executable().display() ); - // Read all requirements from the provided sources. - let requirements = sources - .iter() - .map(RequirementsSource::requirements) - .flatten_ok() - .collect::>>()?; - // Index the current `site-packages` directory. let site_packages = puffin_installer::SitePackages::from_executable(&python).await?; @@ -64,7 +64,7 @@ pub(crate) async fn pip_uninstall( "{}{} Skipping {} as it is not installed.", "warning".yellow().bold(), ":".bold(), - package.bold() + package.as_ref().bold() ); None } diff --git a/crates/puffin-cli/src/commands/venv.rs b/crates/puffin-cli/src/commands/venv.rs index 5e1589acd..6a66c8db7 100644 --- a/crates/puffin-cli/src/commands/venv.rs +++ b/crates/puffin-cli/src/commands/venv.rs @@ -2,8 +2,8 @@ use std::fmt::Write; use std::path::Path; use anyhow::Result; +use colored::Colorize; use fs_err::tokio as fs; -use owo_colors::OwoColorize; use crate::commands::ExitStatus; use crate::printer::Printer; @@ -25,7 +25,7 @@ pub(crate) async fn venv( writeln!( printer, "Using Python interpreter: {}", - base_python.display().cyan() + format!("{}", base_python.display()).cyan() )?; // If the path already exists, remove it. @@ -35,7 +35,7 @@ pub(crate) async fn venv( writeln!( printer, "Creating virtual environment at: {}", - path.display().cyan() + format!("{}", path.display()).cyan() )?; // Create the virtual environment. diff --git a/crates/puffin-cli/src/main.rs b/crates/puffin-cli/src/main.rs index 37a441d16..6e4e7ec3f 100644 --- a/crates/puffin-cli/src/main.rs +++ b/crates/puffin-cli/src/main.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; use std::process::ExitCode; use clap::{Args, Parser, Subcommand}; +use colored::Colorize; use directories::ProjectDirs; -use owo_colors::OwoColorize; use crate::commands::ExitStatus; use crate::requirements::RequirementsSource; diff --git a/crates/puffin-cli/src/requirements.rs b/crates/puffin-cli/src/requirements.rs index db1f79f7f..8a5a06ea2 100644 --- a/crates/puffin-cli/src/requirements.rs +++ b/crates/puffin-cli/src/requirements.rs @@ -1,7 +1,9 @@ +//! A standard interface for working with heterogeneous sources of requirements. + use std::path::PathBuf; use std::str::FromStr; -use anyhow::{bail, Result}; +use anyhow::{bail, Context, Result}; use fs_err as fs; use pep508_rs::Requirement; @@ -37,7 +39,8 @@ impl RequirementsSource { /// Return an iterator over the requirements in this source. pub(crate) fn requirements(&self) -> Result> { let iter_name = if let Self::Name(name) = self { - let requirement = Requirement::from_str(name)?; + let requirement = + Requirement::from_str(name).with_context(|| format!("Failed to parse `{name}`"))?; Some(std::iter::once(requirement)) } else { None @@ -59,8 +62,9 @@ impl RequirementsSource { }; let iter_pyproject_toml = if let Self::PyprojectToml(path) = self { - let pyproject_toml = - toml::from_str::(&fs::read_to_string(path)?)?; + let contents = fs::read_to_string(path)?; + let pyproject_toml = toml::from_str::(&contents) + .with_context(|| format!("Failed to read `{}`", path.display()))?; Some( pyproject_toml .project diff --git a/crates/puffin-cli/tests/pip_uninstall.rs b/crates/puffin-cli/tests/pip_uninstall.rs new file mode 100644 index 000000000..5123e2aff --- /dev/null +++ b/crates/puffin-cli/tests/pip_uninstall.rs @@ -0,0 +1,125 @@ +use std::process::Command; + +use anyhow::Result; +use assert_fs::prelude::*; +use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; + +const BIN_NAME: &str = "puffin"; + +#[test] +fn no_arguments() -> Result<()> { + let tempdir = assert_fs::TempDir::new()?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-uninstall") + .current_dir(&tempdir)); + + Ok(()) +} + +#[test] +fn invalid_requirement() -> Result<()> { + let tempdir = assert_fs::TempDir::new()?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-uninstall") + .arg("flask==1.0.x") + .current_dir(&tempdir)); + + Ok(()) +} + +#[test] +fn missing_requirements_txt() -> Result<()> { + let tempdir = assert_fs::TempDir::new()?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-uninstall") + .arg("-r") + .arg("requirements.txt") + .current_dir(&tempdir)); + + Ok(()) +} + +#[test] +fn invalid_requirements_txt_requirement() -> Result<()> { + let tempdir = assert_fs::TempDir::new()?; + let requirements_txt = tempdir.child("requirements.txt"); + requirements_txt.touch()?; + requirements_txt.write_str("flask==1.0.x")?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-uninstall") + .arg("-r") + .arg("requirements.txt") + .current_dir(&tempdir)); + + Ok(()) +} + +#[test] +fn missing_pyproject_toml() -> Result<()> { + let tempdir = assert_fs::TempDir::new()?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-uninstall") + .arg("-r") + .arg("pyproject.toml") + .current_dir(&tempdir)); + + Ok(()) +} + +#[test] +fn invalid_pyproject_toml_syntax() -> Result<()> { + let tempdir = assert_fs::TempDir::new()?; + let pyproject_toml = tempdir.child("pyproject.toml"); + pyproject_toml.touch()?; + pyproject_toml.write_str("123 - 456")?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-uninstall") + .arg("-r") + .arg("pyproject.toml") + .current_dir(&tempdir)); + + Ok(()) +} + +#[test] +fn invalid_pyproject_toml_schema() -> Result<()> { + let tempdir = assert_fs::TempDir::new()?; + let pyproject_toml = tempdir.child("pyproject.toml"); + pyproject_toml.touch()?; + pyproject_toml.write_str("[project]")?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-uninstall") + .arg("-r") + .arg("pyproject.toml") + .current_dir(&tempdir)); + + Ok(()) +} + +#[test] +fn invalid_pyproject_toml_requirement() -> Result<()> { + let tempdir = assert_fs::TempDir::new()?; + let pyproject_toml = tempdir.child("pyproject.toml"); + pyproject_toml.touch()?; + pyproject_toml.write_str( + r#"[project] +name = "project" +dependencies = ["flask==1.0.x"] +"#, + )?; + + assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME)) + .arg("pip-uninstall") + .arg("-r") + .arg("pyproject.toml") + .current_dir(&tempdir)); + + Ok(()) +} diff --git a/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_pyproject_toml_requirement.snap b/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_pyproject_toml_requirement.snap new file mode 100644 index 000000000..cee972bde --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_pyproject_toml_requirement.snap @@ -0,0 +1,24 @@ +--- +source: crates/puffin-cli/tests/pip_uninstall.rs +info: + program: puffin + args: + - pip-uninstall + - "-r" + - pyproject.toml +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: Failed to read `pyproject.toml` + Caused by: TOML parse error at line 3, column 16 + | +3 | dependencies = ["flask==1.0.x"] + | ^^^^^^^^^^^^^^^^ +Version specifier `==1.0.x` doesn't match PEP 440 rules +flask==1.0.x + ^^^^^^^ + + diff --git a/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_pyproject_toml_schema.snap b/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_pyproject_toml_schema.snap new file mode 100644 index 000000000..03935413f --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_pyproject_toml_schema.snap @@ -0,0 +1,22 @@ +--- +source: crates/puffin-cli/tests/pip_uninstall.rs +info: + program: puffin + args: + - pip-uninstall + - "-r" + - pyproject.toml +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: Failed to read `pyproject.toml` + Caused by: TOML parse error at line 1, column 1 + | +1 | [project] + | ^^^^^^^^^ +missing field `name` + + diff --git a/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_pyproject_toml_syntax.snap b/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_pyproject_toml_syntax.snap new file mode 100644 index 000000000..86190dfe3 --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_pyproject_toml_syntax.snap @@ -0,0 +1,22 @@ +--- +source: crates/puffin-cli/tests/pip_uninstall.rs +info: + program: puffin + args: + - pip-uninstall + - "-r" + - pyproject.toml +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: Failed to read `pyproject.toml` + Caused by: TOML parse error at line 1, column 5 + | +1 | 123 - 456 + | ^ +expected `.`, `=` + + diff --git a/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_requirement.snap b/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_requirement.snap new file mode 100644 index 000000000..7b565e7ac --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_requirement.snap @@ -0,0 +1,18 @@ +--- +source: crates/puffin-cli/tests/pip_uninstall.rs +info: + program: puffin + args: + - pip-uninstall + - flask==1.0.x +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: Failed to parse `flask==1.0.x` + Caused by: Version specifier `==1.0.x` doesn't match PEP 440 rules +flask==1.0.x + ^^^^^^^ + diff --git a/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_requirements_txt_requirement.snap b/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_requirements_txt_requirement.snap new file mode 100644 index 000000000..5631a0975 --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_uninstall__invalid_requirements_txt_requirement.snap @@ -0,0 +1,19 @@ +--- +source: crates/puffin-cli/tests/pip_uninstall.rs +info: + program: puffin + args: + - pip-uninstall + - "-r" + - requirements.txt +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: Couldn't parse requirement in requirements.txt position 0 to 12 + Caused by: Version specifier `==1.0.x` doesn't match PEP 440 rules +flask==1.0.x + ^^^^^^^ + diff --git a/crates/puffin-cli/tests/snapshots/pip_uninstall__missing_pyproject_toml.snap b/crates/puffin-cli/tests/snapshots/pip_uninstall__missing_pyproject_toml.snap new file mode 100644 index 000000000..8aebdc28b --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_uninstall__missing_pyproject_toml.snap @@ -0,0 +1,17 @@ +--- +source: crates/puffin-cli/tests/pip_uninstall.rs +info: + program: puffin + args: + - pip-uninstall + - "-r" + - pyproject.toml +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: failed to open file `pyproject.toml` + Caused by: No such file or directory (os error 2) + diff --git a/crates/puffin-cli/tests/snapshots/pip_uninstall__missing_requirements_txt.snap b/crates/puffin-cli/tests/snapshots/pip_uninstall__missing_requirements_txt.snap new file mode 100644 index 000000000..192d473c7 --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_uninstall__missing_requirements_txt.snap @@ -0,0 +1,17 @@ +--- +source: crates/puffin-cli/tests/pip_uninstall.rs +info: + program: puffin + args: + - pip-uninstall + - "-r" + - requirements.txt +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: failed to open file `requirements.txt` + Caused by: No such file or directory (os error 2) + diff --git a/crates/puffin-cli/tests/snapshots/pip_uninstall__no_arguments.snap b/crates/puffin-cli/tests/snapshots/pip_uninstall__no_arguments.snap new file mode 100644 index 000000000..83cbcee0a --- /dev/null +++ b/crates/puffin-cli/tests/snapshots/pip_uninstall__no_arguments.snap @@ -0,0 +1,19 @@ +--- +source: crates/puffin-cli/tests/pip_uninstall.rs +info: + program: puffin + args: + - pip-uninstall +--- +success: false +exit_code: 2 +----- stdout ----- + +----- stderr ----- +error: the following required arguments were not provided: + > + +Usage: puffin pip-uninstall > + +For more information, try '--help'. +