diff --git a/.prototools b/.prototools index 5eca81bb9c3..f8dbadd86a3 100644 --- a/.prototools +++ b/.prototools @@ -5,6 +5,8 @@ deno = "2.1.7" node = "20.8.0" npm = "10.1.0" pkl = "0.27.2" +python = "3.11.10" +uv = "0.5.26" [plugins] pkl = "https://raw.githubusercontent.com/milesj/proto-plugins/refs/heads/master/pkl.toml" diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b76c5397f3..fee8561c0ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,11 @@ - Added support for v3 and v4 lockfiles (we now use the `deno_lockfile` crate). - Added basic support for workspaces. - Added `deno.installArgs` setting. +- Improved the Python toolchain. + - Added unstable uv support. Can be enabled with the new `python.packageManager` setting. + - Right now, has basic toolchain support, including dependency install and virtual environments. + - Renamed `python.rootRequirementsOnly` to `python.rootVenvOnly`. + - Will now inherit versions from the root `.prototools`. - Improved the Rust toolchain. - The root-level project is now properly taken into account when detecting the package workspaces. - Project dependencies (`dependsOn`) are now automatically inferred from `Cargo.toml` diff --git a/Cargo.lock b/Cargo.lock index d15494d23bf..d5081a37409 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -654,6 +654,12 @@ dependencies = [ "piper", ] +[[package]] +name = "boxcar" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2721c3c5a6f0e7f7e607125d963fedeb765f545f67adc9d71ed934693881eb42" + [[package]] name = "bstr" version = "1.11.3" @@ -984,19 +990,6 @@ dependencies = [ "phf_codegen", ] -[[package]] -name = "chumsky" -version = "1.0.0-alpha.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c28d4e5dd9a9262a38b231153591da6ce1471b818233f4727985d3dd0ed93c" -dependencies = [ - "hashbrown 0.14.5", - "regex-automata 0.3.9", - "serde", - "stacker", - "unicode-ident", -] - [[package]] name = "chunked_transfer" version = "1.5.0" @@ -4476,9 +4469,14 @@ dependencies = [ "cached", "miette 7.4.0", "moon_lang", + "moon_logger", "moon_test_utils", - "pep-508", + "pep508_rs", + "pyproject-toml", "rustc-hash 2.1.0", + "serde", + "starbase_styles", + "starbase_utils", ] [[package]] @@ -5394,12 +5392,38 @@ dependencies = [ ] [[package]] -name = "pep-508" -version = "0.4.0" +name = "pep440_rs" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56536b95df75cc5801a27ae2b53381d5d295fb30837be65f72916ecef5d1e4f" +checksum = "31095ca1f396e3de32745f42b20deef7bc09077f918b085307e8eab6ddd8fb9c" dependencies = [ - "chumsky", + "once_cell", + "serde", + "unicode-width 0.2.0", + "unscanny", + "version-ranges", +] + +[[package]] +name = "pep508_rs" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faee7227064121fcadcd2ff788ea26f0d8f2bd23a0574da11eca23bc935bcc05" +dependencies = [ + "boxcar", + "indexmap 2.7.1", + "itertools 0.13.0", + "once_cell", + "pep440_rs", + "regex", + "rustc-hash 2.1.0", + "serde", + "smallvec", + "thiserror 1.0.69", + "unicode-width 0.2.0", + "url", + "urlencoding", + "version-ranges", ] [[package]] @@ -5874,6 +5898,20 @@ dependencies = [ "sptr", ] +[[package]] +name = "pyproject-toml" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643af57c3f36ba90a8b53e972727d8092f7408a9ebfbaf4c3d2c17b07c58d835" +dependencies = [ + "indexmap 2.7.1", + "pep440_rs", + "pep508_rs", + "serde", + "thiserror 1.0.69", + "toml", +] + [[package]] name = "quinn" version = "0.11.6" @@ -6062,17 +6100,6 @@ dependencies = [ "regex-syntax 0.6.29", ] -[[package]] -name = "regex-automata" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax 0.7.5", -] - [[package]] name = "regex-automata" version = "0.4.9" @@ -6090,12 +6117,6 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" -[[package]] -name = "regex-syntax" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -6895,19 +6916,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "stacker" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799c883d55abdb5e98af1a7b3f23b9b6de8ecada0ecac058672d7635eb48ca7b" -dependencies = [ - "cc", - "cfg-if", - "libc", - "psm", - "windows-sys 0.59.0", -] - [[package]] name = "starbase" version = "0.9.9" @@ -7849,6 +7857,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unscanny" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47" + [[package]] name = "untrusted" version = "0.9.0" @@ -7884,6 +7898,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf16_iter" version = "1.0.5" @@ -7929,6 +7949,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version-ranges" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8d079415ceb2be83fc355adbadafe401307d5c309c7e6ade6638e6f9f42f42d" +dependencies = [ + "smallvec", +] + [[package]] name = "version_check" version = "0.1.5" diff --git a/crates/cli/tests/run_python_test.rs b/crates/cli/tests/run_python_test.rs index f3367cda688..0913a6f1a86 100644 --- a/crates/cli/tests/run_python_test.rs +++ b/crates/cli/tests/run_python_test.rs @@ -1,27 +1,21 @@ -use moon_config::{PartialPipConfig, PartialPythonConfig}; +use moon_config::{PartialPipConfig, PartialPythonConfig, PartialUvConfig, PythonPackageManager}; use moon_test_utils::{ - assert_snapshot, create_sandbox_with_config, get_python_fixture_configs, Sandbox, + assert_snapshot, create_sandbox_with_config, get_python_fixture_configs, + predicates::prelude::*, Sandbox, }; use proto_core::UnresolvedVersionSpec; fn python_sandbox(config: PartialPythonConfig) -> Sandbox { - python_sandbox_with_config(|_| {}, config) + python_sandbox_with_config("python", config) } -fn python_sandbox_with_config(callback: C, config: PartialPythonConfig) -> Sandbox -where - C: FnOnce(&mut PartialPythonConfig), -{ +fn python_sandbox_with_config(fixture: &str, config: PartialPythonConfig) -> Sandbox { let (workspace_config, mut toolchain_config, tasks_config) = get_python_fixture_configs(); toolchain_config.python = Some(config); - if let Some(python_config) = &mut toolchain_config.python { - callback(python_config); - } - let sandbox = create_sandbox_with_config( - "python", + fixture, Some(workspace_config), Some(toolchain_config), Some(tasks_config), @@ -31,39 +25,76 @@ where sandbox } -#[test] -fn runs_standard_script() { - let sandbox = python_sandbox(PartialPythonConfig { - version: Some(UnresolvedVersionSpec::parse("3.11.10").unwrap()), - ..PartialPythonConfig::default() - }); - let assert = sandbox.run_moon(|cmd| { - cmd.arg("run").arg("python:standard"); - }); - - assert_snapshot!(assert.output()); -} +mod python { + use super::*; + + #[test] + fn runs_standard_script() { + let sandbox = python_sandbox(PartialPythonConfig { + version: Some(UnresolvedVersionSpec::parse("3.11.10").unwrap()), + ..PartialPythonConfig::default() + }); + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("python:standard"); + }); + + assert_snapshot!(assert.output()); + } + + mod pip { + use super::*; + + #[test] + fn runs_install_deps_via_args() { + let sandbox = python_sandbox(PartialPythonConfig { + version: Some(UnresolvedVersionSpec::parse("3.11.10").unwrap()), + pip: Some(PartialPipConfig { + install_args: Some(vec![ + "--quiet".to_string(), + "--disable-pip-version-check".to_string(), + "poetry==1.8.4".to_string(), + ]), + }), + ..PartialPythonConfig::default() + }); + + // Needed for venv + sandbox.create_file("base/requirements.txt", ""); -#[test] -fn runs_install_deps_via_args() { - let sandbox = python_sandbox(PartialPythonConfig { - version: Some(UnresolvedVersionSpec::parse("3.11.10").unwrap()), - pip: Some(PartialPipConfig { - install_args: Some(vec![ - "--quiet".to_string(), - "--disable-pip-version-check".to_string(), - "poetry==1.8.4".to_string(), - ]), - }), - ..PartialPythonConfig::default() - }); - - // Needed for venv - sandbox.create_file("base/requirements.txt", ""); - - let assert = sandbox.run_moon(|cmd| { - cmd.arg("run").arg("python:poetry"); - }); - - assert_snapshot!(assert.output()); + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("python:poetry"); + }); + + assert_snapshot!(assert.output()); + } + } + + mod uv { + use super::*; + + #[test] + fn runs_install_deps_via_args() { + let sandbox = python_sandbox_with_config( + "python-uv", + PartialPythonConfig { + version: Some(UnresolvedVersionSpec::parse("3.11.10").unwrap()), + package_manager: Some(PythonPackageManager::Uv), + uv: Some(PartialUvConfig { + version: Some(UnresolvedVersionSpec::parse("0.5.26").unwrap()), + ..PartialUvConfig::default() + }), + ..PartialPythonConfig::default() + }, + ); + + let assert = sandbox.run_moon(|cmd| { + cmd.arg("run").arg("python:uv"); + }); + + let output = assert.output(); + + assert!(predicate::str::contains("uv 0.5.26").eval(&output)); + assert!(predicate::str::contains("Creating virtual environment").eval(&output)); + } + } } diff --git a/crates/cli/tests/snapshots/run_python_test__python__pip__runs_install_deps_via_args.snap b/crates/cli/tests/snapshots/run_python_test__python__pip__runs_install_deps_via_args.snap new file mode 100644 index 00000000000..19b2cf9e1d1 --- /dev/null +++ b/crates/cli/tests/snapshots/run_python_test__python__pip__runs_install_deps_via_args.snap @@ -0,0 +1,12 @@ +--- +source: crates/cli/tests/run_python_test.rs +expression: assert.output() +--- +▪▪▪▪ python -m venv +▪▪▪▪ pip install +▪▪▪▪ python:poetry +Poetry (version 1.8.4) +▪▪▪▪ python:poetry (100ms) + +Tasks: 1 completed + Time: 100ms diff --git a/crates/cli/tests/snapshots/run_python_test__python__runs_standard_script.snap b/crates/cli/tests/snapshots/run_python_test__python__runs_standard_script.snap new file mode 100644 index 00000000000..745a3427f2f --- /dev/null +++ b/crates/cli/tests/snapshots/run_python_test__python__runs_standard_script.snap @@ -0,0 +1,11 @@ +--- +source: crates/cli/tests/run_python_test.rs +expression: assert.output() +--- +▪▪▪▪ pip install +▪▪▪▪ python:standard +Python 3.11.10 +▪▪▪▪ python:standard (100ms) + +Tasks: 1 completed + Time: 100ms diff --git a/crates/config/src/toolchain/python_config.rs b/crates/config/src/toolchain/python_config.rs index 30b7c21b7af..ffb78985107 100644 --- a/crates/config/src/toolchain/python_config.rs +++ b/crates/config/src/toolchain/python_config.rs @@ -1,27 +1,61 @@ -// use super::bin_config::BinEntry; -use schematic::Config; +use schematic::{derive_enum, Config, ConfigEnum}; use serde::Serialize; use version_spec::UnresolvedVersionSpec; use warpgate_api::PluginLocator; +#[cfg(feature = "proto")] +use crate::inherit_tool; + +derive_enum!( + /// The available package managers for Python. + #[derive(ConfigEnum, Copy, Default)] + pub enum PythonPackageManager { + #[default] + Pip, + Uv, + } +); + #[derive(Clone, Config, Debug, PartialEq, Serialize)] pub struct PipConfig { /// List of arguments to append to `pip install` commands. - pub install_args: Option>, + pub install_args: Vec, +} + +#[derive(Clone, Config, Debug, PartialEq, Serialize)] +pub struct UvConfig { + /// Location of the WASM plugin to use for uv support. + pub plugin: Option, + + /// List of arguments to append to `uv sync` commands. + pub sync_args: Vec, + + /// The version of uv to download, install, and run `uv` tasks with. + #[setting(env = "MOON_UV_VERSION")] + pub version: Option, } #[derive(Clone, Config, Debug, PartialEq)] pub struct PythonConfig { - /// Location of the WASM plugin to use for Python support. - pub plugin: Option, + /// The package manager to use for installing dependencies and managing + /// the virtual environment. + pub package_manager: PythonPackageManager, /// Options for pip, when used as a package manager. #[setting(nested)] - pub pip: Option, + pub pip: PipConfig, - /// Assumes only the root `requirements.txt` is used for dependencies. + /// Location of the WASM plugin to use for Python support. + pub plugin: Option, + + /// Assumes a workspace root virtual environment is used for dependencies. /// Can be used to support the "one version policy" pattern. - pub root_requirements_only: bool, + #[setting(alias = "rootRequirementsOnly")] + pub root_venv_only: bool, + + /// Options for uv, when used as a package manager. + #[setting(nested)] + pub uv: Option, /// Defines the virtual environment name, which will be created in the workspace root. /// Project dependencies will be installed into this. @@ -32,3 +66,25 @@ pub struct PythonConfig { #[setting(env = "MOON_PYTHON_VERSION")] pub version: Option, } + +#[cfg(feature = "proto")] +impl PythonConfig { + inherit_tool!(UvConfig, uv, "uv", inherit_proto_uv); + + pub fn inherit_proto(&mut self, proto_config: &proto_core::ProtoConfig) -> miette::Result<()> { + match &self.package_manager { + PythonPackageManager::Pip => { + // Built-in + } + PythonPackageManager::Uv => { + if self.uv.is_none() { + self.uv = Some(UvConfig::default()); + } + + self.inherit_proto_uv(proto_config)?; + } + } + + Ok(()) + } +} diff --git a/crates/config/src/toolchain_config.rs b/crates/config/src/toolchain_config.rs index 37f176d9945..7846863bab4 100644 --- a/crates/config/src/toolchain_config.rs +++ b/crates/config/src/toolchain_config.rs @@ -185,6 +185,12 @@ impl ToolchainConfig { if let Some(version) = &python_config.version { inject("PROTO_PYTHON_VERSION", version); } + + if let Some(uv_config) = &python_config.uv { + if let Some(version) = &uv_config.version { + inject("PROTO_UV_VERSION", version); + } + } } // We don't include Rust since it's a special case! @@ -220,6 +226,7 @@ impl ToolchainConfig { is_using_tool_version!(self, node, pnpm); is_using_tool_version!(self, node, yarn); is_using_tool_version!(self, python); + is_using_tool_version!(self, python, uv); is_using_tool_version!(self, rust); // Special case @@ -266,6 +273,10 @@ impl ToolchainConfig { }; } + if let Some(python_config) = &mut self.python { + python_config.inherit_proto(proto_config)?; + } + Ok(()) } } diff --git a/crates/hash/src/deps_hash.rs b/crates/hash/src/deps_hash.rs index c604dcdded0..3fe03add708 100644 --- a/crates/hash/src/deps_hash.rs +++ b/crates/hash/src/deps_hash.rs @@ -6,9 +6,9 @@ pub type DepsAliasesMap = BTreeMap; hash_content!( pub struct DepsHash<'cfg> { - aliases: BTreeMap<&'cfg str, BTreeMap<&'cfg str, &'cfg str>>, - dependencies: BTreeMap<&'cfg str, &'cfg str>, - name: String, + pub aliases: BTreeMap<&'cfg str, BTreeMap<&'cfg str, &'cfg str>>, + pub dependencies: BTreeMap<&'cfg str, &'cfg str>, + pub name: String, } ); diff --git a/legacy/core/test-utils/src/configs.rs b/legacy/core/test-utils/src/configs.rs index 5336ea0bf74..659e72b2ea2 100644 --- a/legacy/core/test-utils/src/configs.rs +++ b/legacy/core/test-utils/src/configs.rs @@ -498,6 +498,7 @@ pub fn get_python_fixture_configs() -> ( }; let mut toolchain_config = get_default_toolchain(); + toolchain_config.node = None; toolchain_config.python = Some(PartialPythonConfig { version: Some(UnresolvedVersionSpec::parse("3.11.10").unwrap()), ..PartialPythonConfig::default() diff --git a/legacy/python/lang/Cargo.toml b/legacy/python/lang/Cargo.toml index 4ca4c5cd01b..1a9eef194f6 100644 --- a/legacy/python/lang/Cargo.toml +++ b/legacy/python/lang/Cargo.toml @@ -6,10 +6,15 @@ publish = false [dependencies] moon_lang = { path = "../../core/lang" } +moon_logger = { path = "../../core/logger" } cached = { workspace = true } miette = { workspace = true } -pep-508 = "0.4.0" +pep508_rs = "0.9.2" +pyproject-toml = "0.13.4" rustc-hash = { workspace = true } +serde = { workspace = true } +starbase_styles = { workspace = true } +starbase_utils = { workspace = true, features = ["toml"] } [dev-dependencies] moon_test_utils = { path = "../../core/test-utils" } diff --git a/legacy/python/lang/src/lib.rs b/legacy/python/lang/src/lib.rs index 90a0e45a446..84f432ef667 100644 --- a/legacy/python/lang/src/lib.rs +++ b/legacy/python/lang/src/lib.rs @@ -1,4 +1,4 @@ -pub mod pip_requirements; +pub mod pip; +pub mod uv; pub use moon_lang::LockfileDependencyVersions; -pub use pip_requirements::*; diff --git a/legacy/python/lang/src/pip.rs b/legacy/python/lang/src/pip.rs new file mode 100644 index 00000000000..c007112221b --- /dev/null +++ b/legacy/python/lang/src/pip.rs @@ -0,0 +1,25 @@ +use cached::proc_macro::cached; +use moon_lang::LockfileDependencyVersions; +use pep508_rs::{Requirement, VerbatimUrl}; +use rustc_hash::FxHashMap; +use starbase_utils::fs; +use std::io; +use std::io::BufRead; +use std::path::PathBuf; +use std::str::FromStr; + +#[cached(result)] +pub fn load_lockfile_dependencies(path: PathBuf) -> miette::Result { + let mut deps: LockfileDependencyVersions = FxHashMap::default(); + let file = fs::open_file(&path)?; + + for line in io::BufReader::new(file).lines().map_while(Result::ok) { + if let Ok(parsed) = Requirement::::from_str(&line) { + deps.entry(parsed.name.to_string()) + .or_default() + .push(line.clone()); + } + } + + Ok(deps) +} diff --git a/legacy/python/lang/src/pip_requirements.rs b/legacy/python/lang/src/pip_requirements.rs deleted file mode 100644 index c581227c6d2..00000000000 --- a/legacy/python/lang/src/pip_requirements.rs +++ /dev/null @@ -1,33 +0,0 @@ -use cached::proc_macro::cached; -use moon_lang::LockfileDependencyVersions; -use pep_508::parse; -use rustc_hash::FxHashMap; -use std::fs::File; -use std::io; -use std::io::BufRead; -use std::path::{Path, PathBuf}; - -fn read_lines

(filename: P) -> io::Result>> -where - P: AsRef, -{ - let file = File::open(filename)?; - Ok(io::BufReader::new(file).lines()) -} - -#[cached(result)] -pub fn load_lockfile_dependencies(path: PathBuf) -> miette::Result { - let mut deps: LockfileDependencyVersions = FxHashMap::default(); - - if let Ok(lines) = read_lines(&path) { - for line in lines.map_while(Result::ok) { - if let Ok(parsed) = parse(&line) { - deps.entry(parsed.name.to_string()) - .or_default() - .push(line.clone()); - } - } - } - - Ok(deps) -} diff --git a/legacy/python/lang/src/uv.rs b/legacy/python/lang/src/uv.rs new file mode 100644 index 00000000000..4efce22dbd7 --- /dev/null +++ b/legacy/python/lang/src/uv.rs @@ -0,0 +1,53 @@ +use cached::proc_macro::cached; +use moon_lang::{config_cache_container, LockfileDependencyVersions}; +use pyproject_toml::PyProjectToml; +use rustc_hash::FxHashMap; +use serde::{Deserialize, Serialize}; +use starbase_utils::{fs, toml}; +use std::path::{Path, PathBuf}; + +fn read_file(path: &Path) -> miette::Result { + Ok(toml::parse(fs::read_file(path)?)?) +} + +config_cache_container!( + PyProjectTomlCache, + PyProjectToml, + "package-lock.json", + read_file +); + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(default, rename_all = "kebab-case")] +pub struct UvLockPackageSdist { + pub url: String, + pub hash: String, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(default, rename_all = "kebab-case")] +pub struct UvLockPackage { + pub name: String, + pub version: String, + pub sdist: UvLockPackageSdist, +} + +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(default, rename_all = "kebab-case")] +pub struct UvLock { + pub packages: Vec, +} + +#[cached(result)] +pub fn load_lockfile_dependencies(path: PathBuf) -> miette::Result { + let mut deps: LockfileDependencyVersions = FxHashMap::default(); + let lockfile: UvLock = toml::read_file(&path)?; + + for package in lockfile.packages { + let dep = deps.entry(package.name).or_default(); + dep.push(package.version); + dep.push(package.sdist.hash); + } + + Ok(deps) +} diff --git a/legacy/python/platform/src/actions/install_deps.rs b/legacy/python/platform/src/actions/install_deps.rs index cfcb4ebd8cf..ee1533b3f01 100644 --- a/legacy/python/platform/src/actions/install_deps.rs +++ b/legacy/python/platform/src/actions/install_deps.rs @@ -1,7 +1,9 @@ use moon_action::Operation; +use moon_common::is_test_env; +use moon_config::PythonPackageManager; use moon_console::{Checkpoint, Console}; use moon_logger::error; -use moon_python_tool::{find_requirements_txt, PythonTool}; +use moon_python_tool::PythonTool; use std::path::Path; pub async fn install_deps( @@ -11,80 +13,76 @@ pub async fn install_deps( console: &Console, ) -> miette::Result> { let mut operations = vec![]; - let requirements_path = find_requirements_txt(working_dir, workspace_root); + let venv_parent = python.find_venv_root(working_dir, workspace_root); - let venv_root = if python.config.root_requirements_only { + let venv_root = if python.config.root_venv_only { workspace_root.join(&python.config.venv_name) } else { - requirements_path + venv_parent .as_ref() - .and_then(|rp| rp.parent()) + .and_then(|vp| vp.parent()) .unwrap_or(working_dir) .join(&python.config.venv_name) }; - if !venv_root.exists() && requirements_path.is_some() { - let args = vec!["-m", "venv", venv_root.to_str().unwrap_or_default()]; + if !venv_root.exists() && venv_parent.is_some() { + let command = match python.config.package_manager { + PythonPackageManager::Pip => "python -m venv", + PythonPackageManager::Uv => "uv venv", + }; operations.push( - Operation::task_execution(format!("python {}", args.join(" "))) + Operation::task_execution(command) .track_async(|| async { - console - .out - .print_checkpoint(Checkpoint::Setup, "python venv")?; + console.out.print_checkpoint(Checkpoint::Setup, command)?; - python.exec_python(args, working_dir, workspace_root).await + python + .exec_venv(&venv_root, working_dir, workspace_root) + .await }) .await?, ); } - if let Some(pip_config) = &python.config.pip { - let mut args = vec![]; + let package_manager = python.get_package_manager(); - // Add pip installArgs, if users have given - if let Some(install_args) = &pip_config.install_args { - args.extend(install_args.iter().map(|c| c.as_str())); - } - - // Add requirements.txt path, if found - if let Some(reqs_path) = requirements_path.as_ref().and_then(|req| req.to_str()) { - args.extend(["-r", reqs_path]); - } - - if !args.is_empty() { - args.splice(0..0, vec!["-m", "pip", "install"]); + // Install dependencies + { + let command = match python.config.package_manager { + PythonPackageManager::Pip => "pip install", + PythonPackageManager::Uv => "uv sync", + }; - for attempt in 1..=3 { - if attempt == 1 { - console - .out - .print_checkpoint(Checkpoint::Setup, "pip install")?; - } else { - console.out.print_checkpoint_with_comments( - Checkpoint::Setup, - "pip install", - [format!("attempt {attempt} of 3")], - )?; - } + for attempt in 1..=3 { + if attempt == 1 { + console.out.print_checkpoint(Checkpoint::Setup, command)?; + } else { + console.out.print_checkpoint_with_comments( + Checkpoint::Setup, + command, + [format!("attempt {attempt} of 3")], + )?; + } - let mut op = Operation::task_execution(format!("python {}", args.join(" "))); - let result = Operation::do_track_async(&mut op, || { - python.exec_python(&args, working_dir, workspace_root) - }) - .await; + let mut op = Operation::task_execution(command); + let result = Operation::do_track_async(&mut op, || { + package_manager.install_dependencies(python, working_dir, !is_test_env()) + }) + .await; - operations.push(op); + operations.push(op); - if let Err(error) = result { - if attempt == 3 { - return Err(error); - } else { - error!("Failed to install pip dependencies, retrying..."); - } + if let Err(error) = result { + if attempt == 3 { + return Err(error); } else { - break; + error!( + "Failed to install {} dependencies, retrying...", + python.config.package_manager + ); } + } else { + break; } } } diff --git a/legacy/python/platform/src/python_platform.rs b/legacy/python/platform/src/python_platform.rs index 5a36ab036c1..0b8bd5c042a 100644 --- a/legacy/python/platform/src/python_platform.rs +++ b/legacy/python/platform/src/python_platform.rs @@ -6,16 +6,16 @@ use moon_common::{ Id, }; use moon_config::{ - HasherConfig, PlatformType, ProjectConfig, ProjectsAliasesList, ProjectsSourcesList, - PythonConfig, UnresolvedVersionSpec, + HasherConfig, HasherOptimization, PlatformType, ProjectConfig, ProjectsAliasesList, + ProjectsSourcesList, PythonConfig, PythonPackageManager, UnresolvedVersionSpec, }; use moon_console::Console; -use moon_hash::ContentHasher; +use moon_hash::{ContentHasher, DepsHash}; use moon_platform::{Platform, Runtime, RuntimeReq}; use moon_process::Command; use moon_project::Project; -use moon_python_lang::pip_requirements::load_lockfile_dependencies; -use moon_python_tool::{find_requirements_txt, get_python_tool_paths, PythonTool}; +use moon_python_lang::{pip, uv}; +use moon_python_tool::{get_python_tool_paths, PythonTool}; use moon_task::Task; use moon_tool::{get_proto_version_env, prepend_path_env_var, Tool, ToolManager}; use moon_utils::async_trait; @@ -102,7 +102,7 @@ impl Platform for PythonPlatform { project_source: &str, ) -> miette::Result { // Single version policy / only a root requirements.txt - if self.config.root_requirements_only { + if self.config.root_venv_only { return Ok(true); } @@ -142,9 +142,12 @@ impl Platform for PythonPlatform { } fn get_dependency_configs(&self) -> miette::Result> { + let tool = self.toolchain.get()?; + let depman = tool.get_package_manager(); + Ok(Some(( - "requirements.txt".to_owned(), - "requirements.txt".to_owned(), + depman.get_lock_filename(), + depman.get_manifest_filename(), ))) } @@ -242,17 +245,38 @@ impl Platform for PythonPlatform { hasher: &mut ContentHasher, _hasher_config: &HasherConfig, ) -> miette::Result<()> { - let deps = BTreeMap::from_iter(load_lockfile_dependencies(manifest_path.to_path_buf())?); - - hasher.hash_content(PythonToolchainHash { - version: self - .config - .version - .as_ref() - .map(|v| v.to_string()) - .unwrap_or_default(), - dependencies: deps, - })?; + match self.config.package_manager { + PythonPackageManager::Pip => { + if let Ok(data) = pip::load_lockfile_dependencies(manifest_path.to_path_buf()) { + let mut hash = DepsHash::new("unknown".into()); + let mut project_deps = BTreeMap::default(); + + for (key, req) in data { + project_deps.insert(key, req.join("")); + } + + hash.add_deps(&project_deps); + hasher.hash_content(hash)?; + } + } + PythonPackageManager::Uv => { + if let Some(data) = uv::PyProjectTomlCache::read(manifest_path)? { + if let Some(project) = data.project { + let mut hash = DepsHash::new(project.name); + let mut project_deps = BTreeMap::default(); + + if let Some(deps) = project.dependencies { + for dep in deps { + project_deps.insert(dep.name.to_string(), dep.to_string()); + } + } + + hash.add_deps(&project_deps); + hasher.hash_content(hash)?; + } + } + } + }; Ok(()) } @@ -261,25 +285,62 @@ impl Platform for PythonPlatform { async fn hash_run_target( &self, project: &Project, - _runtime: &Runtime, + runtime: &Runtime, hasher: &mut ContentHasher, - _hasher_config: &HasherConfig, + hasher_config: &HasherConfig, ) -> miette::Result<()> { - let mut deps = BTreeMap::new(); - - if let Some(pip_requirements) = find_requirements_txt(&project.root, &self.workspace_root) { - deps = BTreeMap::from_iter(load_lockfile_dependencies(pip_requirements)?); - } - - hasher.hash_content(PythonToolchainHash { + let python_tool = self.toolchain.get_for_version(&runtime.requirement).ok(); + let mut content = PythonToolchainHash { version: self .config .version .as_ref() .map(|v| v.to_string()) .unwrap_or_default(), - dependencies: deps, - })?; + dependencies: BTreeMap::new(), + }; + + let resolved_dependencies = + if matches!(hasher_config.optimization, HasherOptimization::Accuracy) + && python_tool.is_some() + { + python_tool + .unwrap() + .get_package_manager() + .get_resolved_dependencies(&project.root) + .await? + } else { + FxHashMap::default() + }; + + match self.config.package_manager { + // Since the manifest and lockfile are the same (requirements.txt), + // just inherit the resolved dependencies as-is + PythonPackageManager::Pip => { + content.dependencies.extend(resolved_dependencies); + } + PythonPackageManager::Uv => { + if let Some(data) = uv::PyProjectTomlCache::read(&project.root)? { + if let Some(project) = data.project { + if let Some(deps) = project.dependencies { + for dep in deps { + let name = dep.name.to_string(); + + if let Some(resolved_versions) = resolved_dependencies.get(&name) { + let mut sorted_deps = resolved_versions.to_owned().clone(); + sorted_deps.sort(); + content.dependencies.insert(name, sorted_deps); + } else { + content.dependencies.insert(name, vec![dep.to_string()]); + } + } + } + } + } + } + }; + + hasher.hash_content(content)?; Ok(()) } diff --git a/legacy/python/tool/src/lib.rs b/legacy/python/tool/src/lib.rs index 84807fe6f11..5e1a45c3d8b 100644 --- a/legacy/python/tool/src/lib.rs +++ b/legacy/python/tool/src/lib.rs @@ -1,3 +1,7 @@ +mod pip_tool; mod python_tool; +mod uv_tool; +pub use pip_tool::*; pub use python_tool::*; +pub use uv_tool::*; diff --git a/legacy/python/tool/src/pip_tool.rs b/legacy/python/tool/src/pip_tool.rs new file mode 100644 index 00000000000..cd3ab490e7f --- /dev/null +++ b/legacy/python/tool/src/pip_tool.rs @@ -0,0 +1,180 @@ +use crate::python_tool::{get_python_tool_paths, PythonTool}; +use moon_config::PipConfig; +use moon_console::Console; +use moon_process::Command; +use moon_python_lang::{pip, LockfileDependencyVersions}; +use moon_tool::{ + async_trait, get_proto_env_vars, get_proto_version_env, prepend_path_env_var, + DependencyManager, Tool, +}; +use moon_utils::get_workspace_root; +use proto_core::{ProtoEnvironment, UnresolvedVersionSpec}; +use rustc_hash::FxHashMap; +use starbase_utils::fs; +use std::env; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tracing::instrument; + +pub fn find_requirements_txt(starting_dir: &Path, workspace_root: &Path) -> Option { + fs::find_upwards_until("requirements.txt", starting_dir, workspace_root) +} + +pub struct PipTool { + pub config: PipConfig, + + console: Arc, + + global: bool, + + #[allow(dead_code)] + proto_env: Arc, +} + +impl PipTool { + pub async fn new( + proto_env: Arc, + console: Arc, + config: &PipConfig, + global: bool, + ) -> miette::Result { + Ok(PipTool { + global, + config: config.to_owned(), + proto_env, + console, + }) + } + + fn inject_command_paths(&self, cmd: &mut Command, python: &PythonTool, working_dir: &Path) { + if !self.global { + cmd.env( + "PATH", + prepend_path_env_var(get_python_tool_paths( + python, + working_dir, + &get_workspace_root(), + )), + ); + } + } +} + +#[async_trait] +impl Tool for PipTool { + fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { + self + } + + #[instrument(skip_all)] + async fn setup( + &mut self, + _last_versions: &mut FxHashMap, + ) -> miette::Result { + Ok(0) + } + + async fn teardown(&mut self) -> miette::Result<()> { + Ok(()) + } +} + +#[async_trait] +impl DependencyManager for PipTool { + fn create_command(&self, python: &PythonTool) -> miette::Result { + let mut cmd = Command::new("python"); + cmd.with_console(self.console.clone()); + cmd.envs(get_proto_env_vars()); + cmd.args(["-m", "pip"]); + + if let Some(version) = get_proto_version_env(&python.tool) { + cmd.env("PROTO_PYTHON_VERSION", version); + } + + Ok(cmd) + } + + #[instrument(skip_all)] + async fn dedupe_dependencies( + &self, + _python: &PythonTool, + _working_dir: &Path, + _log: bool, + ) -> miette::Result<()> { + Ok(()) + } + + fn get_lock_filename(&self) -> String { + String::from("requirements.txt") + } + + fn get_manifest_filename(&self) -> String { + String::from("requirements.txt") + } + + #[instrument(skip_all)] + async fn get_resolved_dependencies( + &self, + project_root: &Path, + ) -> miette::Result { + let Some(reqs_path) = find_requirements_txt(project_root, &get_workspace_root()) else { + return Ok(FxHashMap::default()); + }; + + Ok(pip::load_lockfile_dependencies(reqs_path)?) + } + + #[instrument(skip_all)] + async fn install_dependencies( + &self, + python: &PythonTool, + working_dir: &Path, + log: bool, + ) -> miette::Result<()> { + let mut args: Vec<&str> = vec![]; + let reqs_path = find_requirements_txt(working_dir, &get_workspace_root()); + + if let Some(reqs_path) = &reqs_path { + args.extend(["-r", reqs_path.to_str().unwrap_or_default()]); + } + + args.extend( + self.config + .install_args + .iter() + .map(|arg| arg.as_str()) + .collect::>(), + ); + + if args.is_empty() { + return Ok(()); + } + + let mut cmd = self.create_command(python)?; + + self.inject_command_paths(&mut cmd, python, working_dir); + + cmd.arg("install") + .args(args) + .cwd(working_dir) + .set_print_command(log); + + if env::var("MOON_TEST_HIDE_INSTALL_OUTPUT").is_ok() { + cmd.exec_capture_output().await?; + } else { + cmd.exec_stream_output().await?; + } + + Ok(()) + } + + #[instrument(skip_all)] + async fn install_focused_dependencies( + &self, + _python: &PythonTool, + _package_names: &[String], + _production_only: bool, + ) -> miette::Result<()> { + Ok(()) + } +} diff --git a/legacy/python/tool/src/python_tool.rs b/legacy/python/tool/src/python_tool.rs index 37bf2fe21a1..0711991ff8e 100644 --- a/legacy/python/tool/src/python_tool.rs +++ b/legacy/python/tool/src/python_tool.rs @@ -1,10 +1,12 @@ -use moon_config::PythonConfig; +use crate::pip_tool::PipTool; +use crate::uv_tool::UvTool; +use moon_config::{PythonConfig, PythonPackageManager}; use moon_console::{Checkpoint, Console}; use moon_logger::debug; use moon_process::Command; use moon_tool::{ async_trait, get_proto_env_vars, get_proto_paths, get_proto_version_env, load_tool_plugin, - prepend_path_env_var, use_global_tool_on_path, Tool, + prepend_path_env_var, use_global_tool_on_path, DependencyManager, Tool, ToolError, }; use moon_toolchain::RuntimeReq; use proto_core::flow::install::InstallOptions; @@ -16,10 +18,6 @@ use std::sync::Arc; use std::{ffi::OsStr, path::Path}; use tracing::instrument; -pub fn find_requirements_txt(starting_dir: &Path, workspace_root: &Path) -> Option { - fs::find_upwards_until("requirements.txt", starting_dir, workspace_root) -} - pub fn get_python_tool_paths( python_tool: &PythonTool, working_dir: &Path, @@ -48,6 +46,10 @@ pub struct PythonTool { console: Arc, proto_env: Arc, + + pip: Option, + + uv: Option, } impl PythonTool { @@ -66,8 +68,10 @@ impl PythonTool { config.plugin.as_ref().unwrap(), ) .await?, - proto_env, - console, + proto_env: Arc::clone(&proto_env), + console: Arc::clone(&console), + pip: None, + uv: None, }; if use_global_tool_on_path("python") || req.is_global() { @@ -77,6 +81,25 @@ impl PythonTool { python.config.version = req.to_spec(); }; + match config.package_manager { + PythonPackageManager::Pip => { + python.pip = Some( + PipTool::new( + Arc::clone(&proto_env), + Arc::clone(&console), + &config.pip, + python.global, + ) + .await?, + ); + } + PythonPackageManager::Uv => { + python.uv = Some( + UvTool::new(Arc::clone(&proto_env), Arc::clone(&console), &config.uv).await?, + ); + } + }; + Ok(python) } @@ -110,6 +133,68 @@ impl PythonTool { Ok(()) } + + #[instrument(skip_all)] + pub async fn exec_venv( + &self, + venv_root: &Path, + working_dir: &Path, + workspace_root: &Path, + ) -> miette::Result<()> { + match self.config.package_manager { + PythonPackageManager::Pip => { + self.exec_python( + ["-m", "venv", venv_root.to_str().unwrap_or_default()], + working_dir, + workspace_root, + ) + .await?; + } + PythonPackageManager::Uv => { + let uv = self.get_uv()?; + + uv.create_command_with_paths(self, working_dir)? + .args(["venv", venv_root.to_str().unwrap_or_default()]) + .cwd(working_dir) + .exec_stream_output() + .await?; + } + }; + + Ok(()) + } + + pub fn get_pip(&self) -> miette::Result<&PipTool> { + match &self.pip { + Some(pip) => Ok(pip), + None => Err(ToolError::UnknownTool("pip".into()).into()), + } + } + + pub fn get_uv(&self) -> miette::Result<&UvTool> { + match &self.uv { + Some(uv) => Ok(uv), + None => Err(ToolError::UnknownTool("uv".into()).into()), + } + } + + pub fn get_package_manager(&self) -> &(dyn DependencyManager + Send + Sync) { + if self.uv.is_some() { + return self.get_uv().unwrap(); + } + + if self.pip.is_some() { + return self.get_pip().unwrap(); + } + + panic!("No package manager, how's this possible?"); + } + + pub fn find_venv_root(&self, starting_dir: &Path, workspace_root: &Path) -> Option { + let depman = self.get_package_manager(); + + fs::find_upwards_root_until(depman.get_manifest_filename(), starting_dir, workspace_root) + } } #[async_trait] @@ -163,6 +248,14 @@ impl Tool for PythonTool { self.tool.locate_globals_dirs().await?; + if let Some(pip) = &mut self.pip { + installed += pip.setup(last_versions).await?; + } + + if let Some(uv) = &mut self.uv { + installed += uv.setup(last_versions).await?; + } + Ok(installed) } diff --git a/legacy/python/tool/src/uv_tool.rs b/legacy/python/tool/src/uv_tool.rs new file mode 100644 index 00000000000..fa4350dceb0 --- /dev/null +++ b/legacy/python/tool/src/uv_tool.rs @@ -0,0 +1,230 @@ +use crate::python_tool::{get_python_tool_paths, PythonTool}; +use moon_config::UvConfig; +use moon_console::{Checkpoint, Console}; +use moon_logger::debug; +use moon_process::Command; +use moon_python_lang::{uv, LockfileDependencyVersions}; +use moon_tool::{ + async_trait, get_proto_env_vars, get_proto_version_env, load_tool_plugin, prepend_path_env_var, + use_global_tool_on_path, DependencyManager, Tool, +}; +use moon_utils::get_workspace_root; +use proto_core::flow::install::InstallOptions; +use proto_core::{Id, ProtoEnvironment, Tool as ProtoTool, UnresolvedVersionSpec}; +use rustc_hash::FxHashMap; +use starbase_utils::fs; +use std::env; +use std::path::Path; +use std::sync::Arc; +use tracing::instrument; + +pub struct UvTool { + pub config: UvConfig, + + pub global: bool, + + pub tool: ProtoTool, + + console: Arc, + + #[allow(dead_code)] + proto_env: Arc, +} + +impl UvTool { + pub async fn new( + proto_env: Arc, + console: Arc, + config: &Option, + ) -> miette::Result { + let config = config.to_owned().unwrap_or_default(); + + Ok(UvTool { + global: use_global_tool_on_path("uv") || config.version.is_none(), + tool: load_tool_plugin(&Id::raw("uv"), &proto_env, config.plugin.as_ref().unwrap()) + .await?, + config, + proto_env, + console, + }) + } + + pub fn create_command_with_paths( + &self, + python: &PythonTool, + working_dir: &Path, + ) -> miette::Result { + let mut cmd = self.create_command(python)?; + self.inject_command_paths(&mut cmd, python, working_dir); + Ok(cmd) + } + + fn inject_command_paths(&self, cmd: &mut Command, python: &PythonTool, working_dir: &Path) { + if !self.global { + cmd.env( + "PATH", + prepend_path_env_var(get_python_tool_paths( + python, + working_dir, + &get_workspace_root(), + )), + ); + } + } +} + +#[async_trait] +impl Tool for UvTool { + fn as_any(&self) -> &(dyn std::any::Any + Send + Sync) { + self + } + + #[instrument(skip_all)] + async fn setup( + &mut self, + last_versions: &mut FxHashMap, + ) -> miette::Result { + let mut count = 0; + let version = self.config.version.as_ref(); + + let Some(version) = version else { + return Ok(count); + }; + + if self.global { + debug!("Using global binary in PATH"); + + return Ok(count); + } + + if self.tool.is_setup(version).await? { + self.tool.locate_globals_dirs().await?; + + debug!("uv has already been setup"); + + return Ok(count); + } + + // When offline and the tool doesn't exist, fallback to the global binary + if proto_core::is_offline() { + debug!( + "No internet connection and uv has not been setup, falling back to global binary in PATH" + ); + + self.global = true; + + return Ok(count); + } + + if let Some(last) = last_versions.get("uv") { + if last == version && self.tool.get_product_dir().exists() { + return Ok(count); + } + } + + self.console + .out + .print_checkpoint(Checkpoint::Setup, format!("installing uv {version}"))?; + + if self.tool.setup(version, InstallOptions::default()).await? { + last_versions.insert("uv".into(), version.to_owned()); + count += 1; + } + + self.tool.locate_globals_dirs().await?; + + Ok(count) + } + + async fn teardown(&mut self) -> miette::Result<()> { + self.tool.teardown().await?; + + Ok(()) + } +} + +#[async_trait] +impl DependencyManager for UvTool { + fn create_command(&self, python: &PythonTool) -> miette::Result { + let mut cmd = Command::new("uv"); + cmd.with_console(self.console.clone()); + cmd.envs(get_proto_env_vars()); + + if let Some(version) = get_proto_version_env(&self.tool) { + cmd.env("PROTO_UV_VERSION", version); + } + + if let Some(version) = get_proto_version_env(&python.tool) { + cmd.env("PROTO_PYTHON_VERSION", version); + } + + Ok(cmd) + } + + #[instrument(skip_all)] + async fn dedupe_dependencies( + &self, + _python: &PythonTool, + _working_dir: &Path, + _log: bool, + ) -> miette::Result<()> { + Ok(()) + } + + fn get_lock_filename(&self) -> String { + String::from("uv.lock") + } + + fn get_manifest_filename(&self) -> String { + String::from("pyproject.toml") + } + + #[instrument(skip_all)] + async fn get_resolved_dependencies( + &self, + project_root: &Path, + ) -> miette::Result { + let Some(lockfile_path) = + fs::find_upwards_until("uv.lock", project_root, get_workspace_root()) + else { + return Ok(FxHashMap::default()); + }; + + Ok(uv::load_lockfile_dependencies(lockfile_path)?) + } + + #[instrument(skip_all)] + async fn install_dependencies( + &self, + python: &PythonTool, + working_dir: &Path, + log: bool, + ) -> miette::Result<()> { + let mut cmd = self.create_command(python)?; + + self.inject_command_paths(&mut cmd, python, working_dir); + + cmd.args(["sync"]) + .args(&self.config.sync_args) + .cwd(working_dir) + .set_print_command(log); + + if env::var("MOON_TEST_HIDE_INSTALL_OUTPUT").is_ok() { + cmd.exec_capture_output().await?; + } else { + cmd.exec_stream_output().await?; + } + + Ok(()) + } + + #[instrument(skip_all)] + async fn install_focused_dependencies( + &self, + _python: &PythonTool, + _packages: &[String], + _production_only: bool, + ) -> miette::Result<()> { + Ok(()) + } +} diff --git a/packages/types/src/toolchain-config.ts b/packages/types/src/toolchain-config.ts index 0382fd43ae4..c73a210cd1c 100644 --- a/packages/types/src/toolchain-config.ts +++ b/packages/types/src/toolchain-config.ts @@ -263,21 +263,47 @@ export interface NodeConfig { yarn: YarnConfig | null; } +/** The available package managers for Python. */ +export type PythonPackageManager = 'pip' | 'uv'; + export interface PipConfig { /** List of arguments to append to `pip install` commands. */ - installArgs: string[] | null; + installArgs: string[]; +} + +export interface UvConfig { + /** Location of the WASM plugin to use for uv support. */ + plugin: PluginLocator | null; + /** List of arguments to append to `uv sync` commands. */ + syncArgs: string[]; + /** + * The version of uv to download, install, and run `uv` tasks with. + * + * @envvar MOON_UV_VERSION + */ + version: UnresolvedVersionSpec | null; } export interface PythonConfig { + /** + * The package manager to use for installing dependencies and managing + * the virtual environment. + * + * @default 'pip' + * @type {'pip' | 'uv'} + */ + packageManager: PythonPackageManager; /** Options for pip, when used as a package manager. */ - pip: PipConfig | null; + pip: PipConfig; /** Location of the WASM plugin to use for Python support. */ plugin: PluginLocator | null; /** - * Assumes only the root `requirements.txt` is used for dependencies. + * Assumes a workspace root virtual environment is used for dependencies. * Can be used to support the "one version policy" pattern. */ - rootRequirementsOnly: boolean; + rootVenvOnly: boolean; + /** Options for uv, when used as a package manager. */ + uv: UvConfig | null; /** * Defines the virtual environment name, which will be created in the workspace root. * Project dependencies will be installed into this. @@ -661,16 +687,38 @@ export interface PartialPipConfig { installArgs?: string[] | null; } +export interface PartialUvConfig { + /** Location of the WASM plugin to use for uv support. */ + plugin?: PluginLocator | null; + /** List of arguments to append to `uv sync` commands. */ + syncArgs?: string[] | null; + /** + * The version of uv to download, install, and run `uv` tasks with. + * + * @envvar MOON_UV_VERSION + */ + version?: UnresolvedVersionSpec | null; +} + export interface PartialPythonConfig { + /** + * The package manager to use for installing dependencies and managing + * the virtual environment. + * + * @default 'pip' + */ + packageManager?: PythonPackageManager | null; /** Options for pip, when used as a package manager. */ pip?: PartialPipConfig | null; /** Location of the WASM plugin to use for Python support. */ plugin?: PluginLocator | null; /** - * Assumes only the root `requirements.txt` is used for dependencies. + * Assumes a workspace root virtual environment is used for dependencies. * Can be used to support the "one version policy" pattern. */ - rootRequirementsOnly?: boolean | null; + rootVenvOnly?: boolean | null; + /** Options for uv, when used as a package manager. */ + uv?: PartialUvConfig | null; /** * Defines the virtual environment name, which will be created in the workspace root. * Project dependencies will be installed into this. diff --git a/tests/fixtures/python-uv/base/.gitignore b/tests/fixtures/python-uv/base/.gitignore new file mode 100644 index 00000000000..1d17dae13b5 --- /dev/null +++ b/tests/fixtures/python-uv/base/.gitignore @@ -0,0 +1 @@ +.venv diff --git a/tests/fixtures/python-uv/base/moon.yml b/tests/fixtures/python-uv/base/moon.yml new file mode 100644 index 00000000000..51c454f42b0 --- /dev/null +++ b/tests/fixtures/python-uv/base/moon.yml @@ -0,0 +1,12 @@ +language: python + +tasks: + standard: + command: python + args: + - --version + + uv: + command: uv + args: + - --version diff --git a/tests/fixtures/python-uv/base/pyproject.toml b/tests/fixtures/python-uv/base/pyproject.toml new file mode 100644 index 00000000000..123593e8349 --- /dev/null +++ b/tests/fixtures/python-uv/base/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "python-uv" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +dependencies = [] diff --git a/website/static/schemas/toolchain.json b/website/static/schemas/toolchain.json index d521a525a90..9a627778a90 100644 --- a/website/static/schemas/toolchain.json +++ b/website/static/schemas/toolchain.json @@ -608,17 +608,10 @@ "installArgs": { "title": "installArgs", "description": "List of arguments to append to pip install commands.", - "anyOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ], + "type": "array", + "items": { + "type": "string" + }, "markdownDescription": "List of arguments to append to `pip install` commands." } }, @@ -672,15 +665,22 @@ "PythonConfig": { "type": "object", "properties": { + "packageManager": { + "title": "packageManager", + "description": "The package manager to use for installing dependencies and managing the virtual environment.", + "default": "pip", + "allOf": [ + { + "$ref": "#/definitions/PythonPackageManager" + } + ] + }, "pip": { "title": "pip", "description": "Options for pip, when used as a package manager.", - "anyOf": [ + "allOf": [ { "$ref": "#/definitions/PipConfig" - }, - { - "type": "null" } ] }, @@ -696,11 +696,22 @@ } ] }, - "rootRequirementsOnly": { - "title": "rootRequirementsOnly", - "description": "Assumes only the root requirements.txt is used for dependencies. Can be used to support the \"one version policy\" pattern.", - "type": "boolean", - "markdownDescription": "Assumes only the root `requirements.txt` is used for dependencies. Can be used to support the \"one version policy\" pattern." + "rootVenvOnly": { + "title": "rootVenvOnly", + "description": "Assumes a workspace root virtual environment is used for dependencies. Can be used to support the \"one version policy\" pattern.", + "type": "boolean" + }, + "uv": { + "title": "uv", + "description": "Options for uv, when used as a package manager.", + "anyOf": [ + { + "$ref": "#/definitions/UvConfig" + }, + { + "type": "null" + } + ] }, "venvName": { "title": "venvName", @@ -724,6 +735,14 @@ }, "additionalProperties": false }, + "PythonPackageManager": { + "description": "The available package managers for Python.", + "type": "string", + "enum": [ + "pip", + "uv" + ] + }, "RustConfig": { "description": "Configures and enables the Rust platform. Docs: https://moonrepo.dev/docs/config/toolchain#rust", "type": "object", @@ -926,6 +945,46 @@ "type": "string", "markdownDescription": "Represents an unresolved version or alias that must be resolved to a fully-qualified version." }, + "UvConfig": { + "type": "object", + "properties": { + "plugin": { + "title": "plugin", + "description": "Location of the WASM plugin to use for uv support.", + "anyOf": [ + { + "$ref": "#/definitions/PluginLocator" + }, + { + "type": "null" + } + ] + }, + "syncArgs": { + "title": "syncArgs", + "description": "List of arguments to append to uv sync commands.", + "type": "array", + "items": { + "type": "string" + }, + "markdownDescription": "List of arguments to append to `uv sync` commands." + }, + "version": { + "title": "version", + "description": "The version of uv to download, install, and run uv tasks with.", + "anyOf": [ + { + "$ref": "#/definitions/UnresolvedVersionSpec" + }, + { + "type": "null" + } + ], + "markdownDescription": "The version of uv to download, install, and run `uv` tasks with." + } + }, + "additionalProperties": false + }, "YarnConfig": { "description": "Options for Yarn, when used as a package manager.", "type": "object",