From 81910e8d91364f9d10c6e7900007b2e8c2345f6d Mon Sep 17 00:00:00 2001 From: Chris Pryer Date: Tue, 21 Nov 2023 17:17:02 -0500 Subject: [PATCH 1/2] Small chore --- crates/huak-cli/src/cli.rs | 12 ++++++------ .../tests/snapshots/r#mod__tests__python_help.snap | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/huak-cli/src/cli.rs b/crates/huak-cli/src/cli.rs index c96da9ac..077632a5 100644 --- a/crates/huak-cli/src/cli.rs +++ b/crates/huak-cli/src/cli.rs @@ -178,6 +178,12 @@ enum Commands { #[derive(Subcommand)] enum Python { + /// Install a Python interpreter. + Install { + /// The version of Python to install. + #[arg(required = true)] + version: RequestedVersion, + }, /// List available Python interpreters. List, /// Use an available Python interpreter. @@ -186,12 +192,6 @@ enum Python { #[arg(required = true)] version: RequestedVersion, }, - /// Install a Python interpreter. - Install { - /// The version of Python to install. - #[arg(required = true)] - version: RequestedVersion, - }, } #[derive(Subcommand)] diff --git a/crates/huak-cli/tests/snapshots/r#mod__tests__python_help.snap b/crates/huak-cli/tests/snapshots/r#mod__tests__python_help.snap index f867c744..27359a81 100644 --- a/crates/huak-cli/tests/snapshots/r#mod__tests__python_help.snap +++ b/crates/huak-cli/tests/snapshots/r#mod__tests__python_help.snap @@ -14,9 +14,9 @@ Manage Python installations Usage: huak python [OPTIONS] Commands: + install Install a Python interpreter list List available Python interpreters use Use an available Python interpreter - install Install a Python interpreter help Print this message or the help of the given subcommand(s) Options: From 680bb1990ebdae468482e8d1da1d3001d1923b37 Mon Sep 17 00:00:00 2001 From: Chris Pryer Date: Tue, 21 Nov 2023 20:07:15 -0500 Subject: [PATCH 2/2] Add options to init command for installing project dependencies --- crates/huak-cli/src/cli.rs | 111 +++++++--- crates/huak-package-manager/src/error.rs | 2 + crates/huak-package-manager/src/ops/init.rs | 194 +++++++++++++++++- .../huak-package-manager/src/ops/install.rs | 141 +------------ crates/huak-package-manager/src/ops/mod.rs | 3 +- crates/huak-package-manager/src/ops/new.rs | 10 +- crates/huak-package-manager/src/workspace.rs | 2 + 7 files changed, 288 insertions(+), 175 deletions(-) diff --git a/crates/huak-cli/src/cli.rs b/crates/huak-cli/src/cli.rs index 077632a5..c6ae2c79 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,21 @@ 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. + 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. + no_env: bool, + /// Optional dependency groups to install. + optional_dependencies: Option>, + /// Force the initialization. + #[arg(short, long)] + force: bool, /// Pass trailing arguments with `--`. #[arg(last = true)] trailing: Option>, @@ -337,14 +346,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 +404,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 +512,42 @@ 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>, +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 'with-env' is 'true' 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-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/ops/init.rs b/crates/huak-package-manager/src/ops/init.rs index 2a15f02c..890d00b2 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)); + } else { + 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(()); + } + + 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.