diff --git a/README.md b/README.md index 35f84bab..90b17e5d 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,6 @@ cargo install --git https://github.com/cnpryer/huak.git huak ## Usage ```console -❯ huak help - A Python package manager written in Rust and inspired by Cargo. Usage: huak [OPTIONS] @@ -60,8 +58,7 @@ Commands: completion Generates a shell completion script for supported shells fix Auto-fix fixable lint conflicts fmt Format the project's Python code - init Initialize the existing project - install Install the dependencies of an existing project + init Initialize the current project lint Lint the project's Python code new Create a new project at publish Builds and uploads current project to a registry diff --git a/crates/huak-cli/src/cli.rs b/crates/huak-cli/src/cli.rs index 077632a5..7590e634 100644 --- a/crates/huak-cli/src/cli.rs +++ b/crates/huak-cli/src/cli.rs @@ -80,7 +80,7 @@ enum Commands { #[arg(last = true)] trailing: Option>, }, - /// Initialize the existing project. + /// Initialize the current project. Init { /// Use an application template. #[arg(long, conflicts_with = "lib")] @@ -91,12 +91,24 @@ enum Commands { /// Don't initialize VCS in the project #[arg(long)] no_vcs: bool, - }, - /// Install the dependencies of an existing project. - Install { - /// Install optional dependency groups - #[arg(long, num_args = 1..)] - groups: Option>, + /// Initialize with a project manifest. + #[arg(long)] + manifest: Option, + // TODO(cnpryer): https://github.com/cnpryer/huak/issues/853 + // /// Initialize with requirements files. + // #[arg(short, long)] + // requirements: Option>, + // /// Initialize with development requirements files. + // dev_requirements: Option>, + /// Initialize without setting up a Python environment. + #[arg(long)] + no_env: bool, + /// Optional dependency groups to install. + #[arg(long)] + optional_dependencies: Option>, + /// Force the initialization. + #[arg(short, long)] + force: bool, /// Pass trailing arguments with `--`. #[arg(last = true)] trailing: Option>, @@ -337,14 +349,36 @@ fn exec_command(cmd: Commands, config: &mut Config) -> HuakResult<()> { }; fmt(config, &options) } - Commands::Init { app, lib, no_vcs } => { + Commands::Init { + app, + lib, + no_vcs, + manifest, + no_env, + optional_dependencies, + trailing, + force, + } => { config.workspace_root = config.cwd.clone(); - let options = WorkspaceOptions { uses_git: !no_vcs }; - init(app, lib, config, &options) - } - Commands::Install { groups, trailing } => { - let options = InstallOptions { values: trailing }; - install(groups, config, &options) + let workspace_options = WorkspaceOptions { + uses_git: !no_vcs, + values: None, + }; + + let install_options = InstallOptions { values: trailing }; // TODO(cnpryer) + + // TODO(cnpryer): Use `WorkspaceOptions` where possible. + init( + app, + lib, + manifest, + no_env, + optional_dependencies, + force, + config, + &workspace_options, + &install_options, + ) } Commands::Lint { fix, @@ -373,7 +407,10 @@ fn exec_command(cmd: Commands, config: &mut Config) -> HuakResult<()> { no_vcs, } => { config.workspace_root = PathBuf::from(path); - let options = WorkspaceOptions { uses_git: !no_vcs }; + let options = WorkspaceOptions { + uses_git: !no_vcs, + values: None, + }; new(app, lib, config, &options) } Commands::Publish { trailing } => { @@ -478,21 +515,44 @@ fn fmt(config: &Config, options: &FormatOptions) -> HuakResult<()> { ops::format_project(config, options) } -fn init(app: bool, _lib: bool, config: &Config, options: &WorkspaceOptions) -> HuakResult<()> { - if app { - ops::init_app_project(config, options) - } else { - ops::init_lib_project(config, options) - } -} - -#[allow(clippy::needless_pass_by_value)] -fn install( - groups: Option>, +#[allow(clippy::too_many_arguments)] +#[allow(clippy::fn_params_excessive_bools)] +fn init( + app: bool, + _lib: bool, + manifest: Option, + no_env: bool, + optional_dependencies: Option>, + force: bool, config: &Config, - options: &InstallOptions, + workspace_options: &WorkspaceOptions, + install_options: &InstallOptions, ) -> HuakResult<()> { - ops::install_project_dependencies(groups.as_ref(), config, options) + let res = if app { + ops::init_app_project(config, workspace_options) + } else { + ops::init_lib_project(config, workspace_options) + }; + + // If initialization failed because a manifest file already exists and the project + // initialization option 'no-env' is 'false' then we attempt to inititialize the + // project's Python environment. + if res + .as_ref() + .err() + .map_or(true, |it| matches!(it, HuakError::ManifestFileFound)) + && !no_env + { + ops::init_python_env( + manifest, + optional_dependencies, + force, + install_options, + config, + ) + } else { + res + } } fn lint(config: &Config, options: &LintOptions) -> HuakResult<()> { diff --git a/crates/huak-cli/tests/mod.rs b/crates/huak-cli/tests/mod.rs index b6bbe091..06af1ea0 100644 --- a/crates/huak-cli/tests/mod.rs +++ b/crates/huak-cli/tests/mod.rs @@ -49,10 +49,10 @@ mod tests { assert_cmd_snapshot!(Command::new("huak").arg("init").arg("--help")); } - #[test] - fn test_install_help() { - assert_cmd_snapshot!(Command::new("huak").arg("install").arg("--help")); - } + // #[test] + // fn test_install_help() { + // assert_cmd_snapshot!(Command::new("huak").arg("install").arg("--help")); + // } #[test] fn test_lint_help() { diff --git a/crates/huak-cli/tests/snapshots/r#mod__tests__help-2.snap b/crates/huak-cli/tests/snapshots/r#mod__tests__help-2.snap index 314c23fc..341bfd52 100644 --- a/crates/huak-cli/tests/snapshots/r#mod__tests__help-2.snap +++ b/crates/huak-cli/tests/snapshots/r#mod__tests__help-2.snap @@ -20,8 +20,7 @@ Commands: completion Generates a shell completion script for supported shells fix Auto-fix fixable lint conflicts fmt Format the project's Python code - init Initialize the existing project - install Install the dependencies of an existing project + init Initialize the current project lint Lint the project's Python code new Create a new project at publish Builds and uploads current project to a registry diff --git a/crates/huak-cli/tests/snapshots/r#mod__tests__help.snap b/crates/huak-cli/tests/snapshots/r#mod__tests__help.snap index 3cf11eb4..32a5db5a 100644 --- a/crates/huak-cli/tests/snapshots/r#mod__tests__help.snap +++ b/crates/huak-cli/tests/snapshots/r#mod__tests__help.snap @@ -20,8 +20,7 @@ Commands: completion Generates a shell completion script for supported shells fix Auto-fix fixable lint conflicts fmt Format the project's Python code - init Initialize the existing project - install Install the dependencies of an existing project + init Initialize the current project lint Lint the project's Python code new Create a new project at publish Builds and uploads current project to a registry diff --git a/crates/huak-cli/tests/snapshots/r#mod__tests__init_help.snap b/crates/huak-cli/tests/snapshots/r#mod__tests__init_help.snap index 4ecfad43..f7b4a727 100644 --- a/crates/huak-cli/tests/snapshots/r#mod__tests__init_help.snap +++ b/crates/huak-cli/tests/snapshots/r#mod__tests__init_help.snap @@ -1,5 +1,5 @@ --- -source: crates/huak_cli/tests/mod.rs +source: crates/huak-cli/tests/mod.rs info: program: huak args: @@ -9,17 +9,34 @@ info: success: true exit_code: 0 ----- stdout ----- -Initialize the existing project +Initialize the current project -Usage: huak init [OPTIONS] +Usage: huak init [OPTIONS] [-- ...] + +Arguments: + [TRAILING]... Pass trailing arguments with `--` Options: - --app Use an application template - --lib Use a library template [default] - --no-vcs Don't initialize VCS in the project - -q, --quiet - --no-color - -h, --help Print help + --app + Use an application template + --lib + Use a library template [default] + --no-vcs + Don't initialize VCS in the project + --manifest + Initialize with a project manifest + --no-env + Initialize without setting up a Python environment + --optional-dependencies + Optional dependency groups to install + -f, --force + Force the initialization + -q, --quiet + + --no-color + + -h, --help + Print help ----- stderr ----- diff --git a/crates/huak-cli/tests/snapshots/r#mod__tests__install_help.snap b/crates/huak-cli/tests/snapshots/r#mod__tests__install_help.snap deleted file mode 100644 index 5436fb3a..00000000 --- a/crates/huak-cli/tests/snapshots/r#mod__tests__install_help.snap +++ /dev/null @@ -1,26 +0,0 @@ ---- -source: crates/huak_cli/tests/mod.rs -info: - program: huak - args: - - install - - "--help" ---- -success: true -exit_code: 0 ------ stdout ----- -Install the dependencies of an existing project - -Usage: huak install [OPTIONS] [-- ...] - -Arguments: - [TRAILING]... Pass trailing arguments with `--` - -Options: - --groups ... Install optional dependency groups - -q, --quiet - --no-color - -h, --help Print help - ------ stderr ----- - diff --git a/crates/huak-package-manager/src/error.rs b/crates/huak-package-manager/src/error.rs index c736316b..ed22c2e2 100644 --- a/crates/huak-package-manager/src/error.rs +++ b/crates/huak-package-manager/src/error.rs @@ -52,6 +52,8 @@ pub enum Error { ManifestFileFound, #[error("a manifest file could not be found")] ManifestFileNotFound, + #[error("a manifest file is not supported: {0}")] + ManifestFileNotSupported(PathBuf), #[error("a package version could not be found")] PackageVersionNotFound, #[error("a project already exists")] diff --git a/crates/huak-package-manager/src/lib.rs b/crates/huak-package-manager/src/lib.rs index 0bd5f16c..dba03d8d 100644 --- a/crates/huak-package-manager/src/lib.rs +++ b/crates/huak-package-manager/src/lib.rs @@ -12,42 +12,7 @@ //! 2. Making some change to the project //! 3. Running tests //! 4. Distributing the project -//! -//!```zsh -//! ❯ huak help -//! -//! A Python package manager written in Rust and inspired by Cargo. -//! -//! Usage: huak [OPTIONS] -//! -//! Commands: -//! activate Activate the virtual environment -//! add Add dependencies to the project -//! build Build tarball and wheel for the project -//! clean Remove tarball and wheel from the built project -//! completion Generates a shell completion script for supported shells -//! fix Auto-fix fixable lint conflicts -//! fmt Format the project's Python code -//! init Initialize the existing project -//! install Install the dependencies of an existing project -//! lint Lint the project's Python code -//! new Create a new project at -//! publish Builds and uploads current project to a registry -//! python Manage Python installations -//! remove Remove dependencies from the project -//! run Run a command with Huak -//! test Test the project's Python code -//! toolchain Manage toolchains -//! update Update the project's dependencies -//! version Display the version of the project -//! help Print this message or the help of the given subcommand(s) -//! -//! Options: -//! -q, --quiet -//! --no-color -//! -h, --help Print help -//! -V, --version Print version -//!``` + mod config; mod dependency; mod environment; diff --git a/crates/huak-package-manager/src/ops/init.rs b/crates/huak-package-manager/src/ops/init.rs index 2a15f02c..debbd4fa 100644 --- a/crates/huak-package-manager/src/ops/init.rs +++ b/crates/huak-package-manager/src/ops/init.rs @@ -2,10 +2,11 @@ use toml_edit::{Item, Table}; use super::init_git; use crate::{ - default_package_entrypoint_string, importable_package_name, last_path_component, Config, - Dependency, Error, HuakResult, LocalManifest, WorkspaceOptions, + default_package_entrypoint_string, directory_is_venv, importable_package_name, + last_path_component, Config, Dependency, Error, HuakResult, InstallOptions, LocalManifest, + WorkspaceOptions, }; -use std::str::FromStr; +use std::{path::PathBuf, str::FromStr}; pub fn init_app_project(config: &Config, options: &WorkspaceOptions) -> HuakResult<()> { init_lib_project(config, options)?; @@ -51,10 +52,111 @@ pub fn init_lib_project(config: &Config, options: &WorkspaceOptions) -> HuakResu manifest.write_file() } +// TODO(cnpryer): Remove current huak install ops +pub fn init_python_env( + manifest: Option, + optional_dependencies: Option>, + force: bool, + options: &InstallOptions, + config: &Config, +) -> HuakResult<()> { + let ws = config.workspace(); + + // TODO(cnpryer): Can't remember if clap parses "." as curr dir + let mut manifest_path = manifest.unwrap_or(ws.root().join("pyproject.toml")); + if manifest_path + .file_name() + .is_some_and(|it| !it.eq_ignore_ascii_case("pyproject.toml")) + { + return Err(Error::ManifestFileNotSupported(manifest_path)); + } + + manifest_path.set_file_name("pyproject.toml"); + + let Ok(manifest) = LocalManifest::new(manifest_path) else { + return config + .terminal() + .print_warning("a manifest file could not be resolved"); + }; + + let mut dependencies = Vec::new(); + + if let Some(gs) = optional_dependencies { + // If the group "required" is passed and isn't a valid optional dependency group + // then install just the required dependencies. + // TODO(cnpryer): Refactor/move + if manifest + .manifest_data() + .project_optional_dependency_groups() + .map_or(false, |it| it.iter().any(|s| s == "required")) + { + if let Some(reqs) = manifest.manifest_data().project_dependencies() { + dependencies.extend(reqs); + } + } else if let Some(optional_deps) = manifest.manifest_data().project_optional_dependencies() + { + for g in gs { + // TODO(cnpryer): Perf + if let Some(deps) = optional_deps.get(&g.to_string()) { + dependencies.extend(deps.iter().cloned()); + } + } + } + } else { + // If no groups are passed then install all dependencies listed in the manifest file + // including the optional dependencies. + if let Some(reqs) = manifest.manifest_data().project_dependencies() { + dependencies.extend(reqs); + } + + // TODO(cnpryer): Install optional as opt-in + if let Some(groups) = manifest + .manifest_data() + .project_optional_dependency_groups() + { + for key in groups { + if let Some(g) = manifest.manifest_data().project_optional_dependencies() { + if let Some(it) = g.get(&key) { + dependencies.extend(it.iter().cloned()); + } + } + } + } + } + + dependencies.dedup(); + + if dependencies.is_empty() { + return Ok(()); + } + + // TODO(cnpryer): Relax this by attempting to use existing environments + if force { + // Remove the current Python virtual environment if one exists. + match ws.current_python_environment() { + Ok(it) if directory_is_venv(it.root()) => std::fs::remove_dir_all(it.root())?, + // TODO(cnpryer): This might be a clippy bug. + #[allow(clippy::no_effect)] + Ok(_) + | Err(Error::PythonEnvironmentNotFound | Error::UnsupportedPythonEnvironment(_)) => { + (); + } + Err(e) => return Err(e), + }; + } + + let python_env = ws.resolve_python_environment()?; + python_env.install_packages(&dependencies, options, config) +} + #[cfg(test)] mod tests { use super::*; - use crate::{default_pyproject_toml_contents, TerminalOptions, Verbosity}; + use crate::{ + copy_dir, default_pyproject_toml_contents, initialize_venv, CopyDirOptions, Package, + TerminalOptions, Verbosity, + }; + use huak_dev::dev_resources_dir; use tempfile::tempdir; #[test] @@ -73,7 +175,10 @@ mod tests { terminal_options, ..Default::default() }; - let options = WorkspaceOptions { uses_git: false }; + let options = WorkspaceOptions { + uses_git: false, + values: None, + }; init_lib_project(&config, &options).unwrap(); let ws = config.workspace(); @@ -101,7 +206,10 @@ mod tests { terminal_options, ..Default::default() }; - let options = WorkspaceOptions { uses_git: false }; + let options = WorkspaceOptions { + uses_git: false, + values: None, + }; init_app_project(&config, &options).unwrap(); @@ -125,4 +233,78 @@ mock-project = "mock_project.main:main" "# ); } + + #[test] + fn test_install_project_dependencies() { + let dir = tempdir().unwrap(); + copy_dir( + &dev_resources_dir().join("mock-project"), + &dir.path().join("mock-project"), + &CopyDirOptions::default(), + ) + .unwrap(); + let workspace_root = dir.path().join("mock-project"); + let cwd = workspace_root.clone(); + let terminal_options = TerminalOptions { + verbosity: Verbosity::Quiet, + ..Default::default() + }; + let config = Config { + workspace_root, + cwd, + terminal_options, + ..Default::default() + }; + let ws = config.workspace(); + initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); + let options = InstallOptions { values: None }; + let venv = ws.resolve_python_environment().unwrap(); + let test_package = Package::from_str("click==8.1.3").unwrap(); + let had_package = venv.contains_package(&test_package); + + init_python_env(None, None, true, &options, &config).unwrap(); + + assert!(!had_package); + assert!(venv.contains_package(&test_package)); + } + + #[test] + fn test_install_project_optional_dependencies() { + let dir = tempdir().unwrap(); + copy_dir( + &dev_resources_dir().join("mock-project"), + &dir.path().join("mock-project"), + &CopyDirOptions::default(), + ) + .unwrap(); + let workspace_root = dir.path().join("mock-project"); + let cwd = workspace_root.clone(); + let terminal_options = TerminalOptions { + verbosity: Verbosity::Quiet, + ..Default::default() + }; + let config = Config { + workspace_root, + cwd, + terminal_options, + ..Default::default() + }; + let ws = config.workspace(); + initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); + let options = InstallOptions { values: None }; + let venv = ws.resolve_python_environment().unwrap(); + let had_package = venv.contains_module("pytest").unwrap(); + + init_python_env( + None, + Some(vec![String::from("dev")]), + true, + &options, + &config, + ) + .unwrap(); + + assert!(!had_package); + assert!(venv.contains_module("pytest").unwrap()); + } } diff --git a/crates/huak-package-manager/src/ops/install.rs b/crates/huak-package-manager/src/ops/install.rs index 445301ce..0dcf958a 100644 --- a/crates/huak-package-manager/src/ops/install.rs +++ b/crates/huak-package-manager/src/ops/install.rs @@ -1,139 +1,6 @@ -use crate::{Config, HuakResult, InstallOptions}; +use crate::HuakResult; -pub fn install_project_dependencies( - groups: Option<&Vec>, - config: &Config, - options: &InstallOptions, -) -> HuakResult<()> { - let workspace = config.workspace(); - let manifest = workspace.current_local_manifest()?; - - let mut dependencies = Vec::new(); - - if let Some(gs) = groups { - // If the group "required" is passed and isn't a valid optional dependency group - // then install just the required dependencies. - // TODO(cnpryer): Refactor/move - if manifest - .manifest_data() - .project_optional_dependency_groups() - .map_or(false, |it| it.iter().any(|s| s == "required")) - { - if let Some(reqs) = manifest.manifest_data().project_dependencies() { - dependencies.extend(reqs); - } - } else if let Some(optional_deps) = manifest.manifest_data().project_optional_dependencies() - { - for g in gs { - // TODO(cnpryer): Perf - if let Some(deps) = optional_deps.get(&g.to_string()) { - dependencies.extend(deps.iter().cloned()); - } - } - } - } else { - // If no groups are passed then install all dependencies listed in the manifest file - // including the optional dependencies. - if let Some(reqs) = manifest.manifest_data().project_dependencies() { - dependencies.extend(reqs); - } - - // TODO(cnpryer): Install optional as opt-in - if let Some(groups) = manifest - .manifest_data() - .project_optional_dependency_groups() - { - for key in groups { - if let Some(g) = manifest.manifest_data().project_optional_dependencies() { - if let Some(it) = g.get(&key) { - dependencies.extend(it.iter().cloned()); - } - } - } - } - } - - dependencies.dedup(); - - if dependencies.is_empty() { - return Ok(()); - } - - let python_env = workspace.resolve_python_environment()?; - python_env.install_packages(&dependencies, options, config) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{copy_dir, initialize_venv, CopyDirOptions, Package, TerminalOptions, Verbosity}; - use huak_dev::dev_resources_dir; - use tempfile::tempdir; - - #[test] - fn test_install_project_dependencies() { - let dir = tempdir().unwrap(); - copy_dir( - &dev_resources_dir().join("mock-project"), - &dir.path().join("mock-project"), - &CopyDirOptions::default(), - ) - .unwrap(); - let workspace_root = dir.path().join("mock-project"); - let cwd = workspace_root.clone(); - let terminal_options = TerminalOptions { - verbosity: Verbosity::Quiet, - ..Default::default() - }; - let config = Config { - workspace_root, - cwd, - terminal_options, - ..Default::default() - }; - let ws = config.workspace(); - initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); - let options = InstallOptions { values: None }; - let venv = ws.resolve_python_environment().unwrap(); - let test_package = Package::from_str("click==8.1.3").unwrap(); - let had_package = venv.contains_package(&test_package); - - install_project_dependencies(None, &config, &options).unwrap(); - - assert!(!had_package); - assert!(venv.contains_package(&test_package)); - } - - #[test] - fn test_install_project_optional_dependencies() { - let dir = tempdir().unwrap(); - copy_dir( - &dev_resources_dir().join("mock-project"), - &dir.path().join("mock-project"), - &CopyDirOptions::default(), - ) - .unwrap(); - let workspace_root = dir.path().join("mock-project"); - let cwd = workspace_root.clone(); - let terminal_options = TerminalOptions { - verbosity: Verbosity::Quiet, - ..Default::default() - }; - let config = Config { - workspace_root, - cwd, - terminal_options, - ..Default::default() - }; - let ws = config.workspace(); - initialize_venv(ws.root().join(".venv"), &ws.environment()).unwrap(); - let options = InstallOptions { values: None }; - let venv = ws.resolve_python_environment().unwrap(); - let had_package = venv.contains_module("pytest").unwrap(); - - install_project_dependencies(Some(&vec![String::from("dev")]), &config, &options).unwrap(); - - assert!(!had_package); - assert!(venv.contains_module("pytest").unwrap()); - } +// TODO(cnpryer): https://github.com/cnpryer/huak/issues/850 +pub fn _install() -> HuakResult<()> { + todo!() } diff --git a/crates/huak-package-manager/src/ops/mod.rs b/crates/huak-package-manager/src/ops/mod.rs index 5a0d8fcd..b3d0ddda 100644 --- a/crates/huak-package-manager/src/ops/mod.rs +++ b/crates/huak-package-manager/src/ops/mod.rs @@ -24,8 +24,7 @@ pub use add::{add_project_dependencies, add_project_optional_dependencies, AddOp pub use build::{build_project, BuildOptions}; pub use clean::{clean_project, CleanOptions}; pub use format::{format_project, FormatOptions}; -pub use init::{init_app_project, init_lib_project}; -pub use install::install_project_dependencies; +pub use init::{init_app_project, init_lib_project, init_python_env}; pub use lint::{lint_project, LintOptions}; pub use new::{new_app_project, new_lib_project}; pub use publish::{publish_project, PublishOptions}; diff --git a/crates/huak-package-manager/src/ops/new.rs b/crates/huak-package-manager/src/ops/new.rs index 20e3bdd7..251bbc64 100644 --- a/crates/huak-package-manager/src/ops/new.rs +++ b/crates/huak-package-manager/src/ops/new.rs @@ -98,7 +98,10 @@ mod tests { terminal_options, ..Default::default() }; - let options = WorkspaceOptions { uses_git: false }; + let options = WorkspaceOptions { + uses_git: false, + values: None, + }; new_lib_project(&config, &options).unwrap(); @@ -145,7 +148,10 @@ def test_version(): terminal_options, ..Default::default() }; - let options = WorkspaceOptions { uses_git: false }; + let options = WorkspaceOptions { + uses_git: false, + values: None, + }; new_app_project(&config, &options).unwrap(); diff --git a/crates/huak-package-manager/src/workspace.rs b/crates/huak-package-manager/src/workspace.rs index 6a6a9c24..716e15e2 100644 --- a/crates/huak-package-manager/src/workspace.rs +++ b/crates/huak-package-manager/src/workspace.rs @@ -158,6 +158,8 @@ impl Workspace { pub struct WorkspaceOptions { /// Inidcate the `Workspace` should use git. pub uses_git: bool, + /// Trailing argument values. + pub values: Option>, } /// Search for a Python virtual environment.