diff --git a/.gitignore b/.gitignore index b2d5f03e4c..23cca88e03 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ __pycache__ .idea token.txt dist +**/*.rs.pending-snap diff --git a/docs/guide/config.md b/docs/guide/config.md index bda7499c7c..173c1efef8 100644 --- a/docs/guide/config.md +++ b/docs/guide/config.md @@ -90,6 +90,11 @@ use-uv = true # to `true` when uv is enabled and `false` otherwise. autosync = true +# Enable or disable automatic `sync` ahead of `run` and `test`. This defaults +# to `true` when uv is enabled and `false` otherwise. Note that autosync invoked +# before `run` and `test` will never update your lockfiles. +autosync-before-run = true + # Marks the managed .venv in a way that cloud-based synchronization systems # like Dropbox and iCloud Files will not upload it. This defaults to `true` # as a .venv in cloud storage typically does not make sense. Set this to diff --git a/rye/src/cli/add.rs b/rye/src/cli/add.rs index 9fa56f2ad2..230cfd0176 100644 --- a/rye/src/cli/add.rs +++ b/rye/src/cli/add.rs @@ -16,7 +16,7 @@ use crate::consts::VENV_BIN; use crate::lock::KeyringProvider; use crate::pyproject::{BuildSystem, DependencyKind, ExpandedSources, PyProject}; use crate::sources::py::PythonVersion; -use crate::sync::{autosync, sync, SyncOptions}; +use crate::sync::{autosync, sync, SyncOptions, SyncMode}; use crate::utils::{format_requirement, get_venv_python_bin, set_proxy_variables, CommandOutput}; use crate::uv::UvBuilder; @@ -322,6 +322,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> { autosync( &pyproject_toml, output, + SyncMode::Regular, cmd.pre, cmd.with_sources, cmd.generate_hashes, diff --git a/rye/src/cli/remove.rs b/rye/src/cli/remove.rs index f950bdbfcc..1fedac4a32 100644 --- a/rye/src/cli/remove.rs +++ b/rye/src/cli/remove.rs @@ -7,7 +7,7 @@ use pep508_rs::Requirement; use crate::config::Config; use crate::lock::KeyringProvider; use crate::pyproject::{DependencyKind, PyProject}; -use crate::sync::autosync; +use crate::sync::{autosync, SyncMode}; use crate::utils::{format_requirement, CommandOutput}; /// Removes a package from this project. @@ -82,6 +82,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> { autosync( &pyproject_toml, output, + SyncMode::Regular, cmd.pre, cmd.with_sources, cmd.generate_hashes, diff --git a/rye/src/cli/run.rs b/rye/src/cli/run.rs index 7db917d72b..03cac06a02 100644 --- a/rye/src/cli/run.rs +++ b/rye/src/cli/run.rs @@ -8,10 +8,11 @@ use anyhow::{bail, Context, Error}; use clap::Parser; use console::style; +use crate::config::Config; use crate::pyproject::{PyProject, Script}; -use crate::sync::{sync, SyncOptions}; +use crate::sync::{autosync, sync, SyncOptions, SyncMode}; use crate::tui::redirect_to_stderr; -use crate::utils::{exec_spawn, get_venv_python_bin, success_status, IoPathContext}; +use crate::utils::{exec_spawn, get_venv_python_bin, success_status, CommandOutput, IoPathContext}; /// Runs a command installed into this package. #[derive(Parser, Debug)] @@ -26,6 +27,18 @@ pub struct Args { /// Use this pyproject.toml file #[arg(long, value_name = "PYPROJECT_TOML")] pyproject: Option, + /// Runs `sync` even if auto-sync is disabled. + #[arg(long)] + sync: bool, + /// Does not run `sync` even if auto-sync is enabled. + #[arg(long, conflicts_with = "sync")] + no_sync: bool, + /// Enables verbose diagnostics. + #[arg(short, long)] + verbose: bool, + /// Turns off all output. + #[arg(short, long, conflicts_with = "verbose")] + quiet: bool, } #[derive(Parser, Debug)] @@ -36,11 +49,16 @@ enum Cmd { pub fn execute(cmd: Args) -> Result<(), Error> { let _guard = redirect_to_stderr(true); + let output = CommandOutput::from_quiet_and_verbose(cmd.quiet, cmd.verbose); let pyproject = PyProject::load_or_discover(cmd.pyproject.as_deref())?; - // make sure we have the minimal virtualenv. - sync(SyncOptions::python_only().pyproject(cmd.pyproject)) - .context("failed to sync ahead of run")?; + if (Config::current().autosync_before_run() && !cmd.no_sync) || cmd.sync { + autosync(&pyproject, output, SyncMode::OneOffLock)?; + } else { + // make sure we have the minimal virtualenv. + sync(SyncOptions::python_only().pyproject(cmd.pyproject)) + .context("failed to sync ahead of run")?; + } if cmd.list || cmd.cmd.is_none() { return list_scripts(&pyproject); diff --git a/rye/src/cli/test.rs b/rye/src/cli/test.rs index bb2a0042b1..3ba527c0dc 100644 --- a/rye/src/cli/test.rs +++ b/rye/src/cli/test.rs @@ -12,7 +12,7 @@ use crate::config::Config; use crate::consts::VENV_BIN; use crate::lock::KeyringProvider; use crate::pyproject::{locate_projects, normalize_package_name, DependencyKind, PyProject}; -use crate::sync::autosync; +use crate::sync::{autosync, SyncMode}; use crate::utils::{CommandOutput, QuietExit}; /// Run the tests on the project. @@ -32,6 +32,12 @@ pub struct Args { /// Disable test output capture to stdout. #[arg(long = "no-capture", short = 's')] no_capture: bool, + /// Runs `sync` even if auto-sync is disabled. + #[arg(long)] + sync: bool, + /// Does not run `sync` even if auto-sync is enabled. + #[arg(long, conflicts_with = "sync")] + no_sync: bool, /// Enables verbose diagnostics. #[arg(short, long)] verbose: bool, @@ -90,6 +96,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> { autosync( &projects[0], output, + SyncMode::OneOffLock, cmd.pre, cmd.with_sources, cmd.generate_hashes, diff --git a/rye/src/config.rs b/rye/src/config.rs index 2d151d9966..b603f71e5c 100644 --- a/rye/src/config.rs +++ b/rye/src/config.rs @@ -251,7 +251,7 @@ impl Config { Ok(rv) } - /// Enable autosync. + /// Enable autosync for `add` and `remove`. pub fn autosync(&self) -> bool { self.doc .get("behavior") @@ -259,6 +259,15 @@ impl Config { .and_then(|x| x.as_bool()) .unwrap_or_else(|| self.use_uv()) } + + /// Enable autosync for `run` and `test`. + pub fn autosync_before_run(&self) -> bool { + self.doc + .get("behavior") + .and_then(|x| x.get("autosync-before-run")) + .and_then(|x| x.as_bool()) + .unwrap_or_else(|| self.use_uv()) + } /// Indicates if uv should be used instead of pip-tools. pub fn use_uv(&self) -> bool { diff --git a/rye/src/sync.rs b/rye/src/sync.rs index 5ab816a616..164630fb43 100644 --- a/rye/src/sync.rs +++ b/rye/src/sync.rs @@ -37,6 +37,8 @@ pub enum SyncMode { Regular, /// recreate everything Full, + /// Recreate if no lock file present, otherwise install without updating + OneOffLock, } /// Updates the virtualenv based on the pyproject.toml @@ -179,16 +181,19 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> { // hack to make this work for now. We basically sym-link pip itself // into a folder all by itself and place a second file in there which we // can pass to pip-sync to install the local package. + let target_lockfile = if cmd.dev { &dev_lockfile } else { &lockfile }; + let has_lock = target_lockfile.is_file(); if recreate || cmd.mode != SyncMode::PythonOnly { let sources = ExpandedSources::from_sources(&pyproject.sources()?)?; if cmd.no_lock { - let lockfile = if cmd.dev { &dev_lockfile } else { &lockfile }; - if !lockfile.is_file() { + if !has_lock { bail!( "Locking is disabled but lockfile '{}' does not exist", - lockfile.display() + target_lockfile.display() ); } + } else if cmd.mode == SyncMode::OneOffLock && has_lock { + // do nothing } else if let Some(workspace) = pyproject.workspace() { // make sure we have an up-to-date lockfile update_workspace_lockfile( @@ -319,6 +324,7 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> { pub fn autosync( pyproject: &PyProject, output: CommandOutput, + sync_mode: SyncMode, pre: bool, with_sources: bool, generate_hashes: bool, @@ -327,7 +333,7 @@ pub fn autosync( sync(SyncOptions { output, dev: true, - mode: SyncMode::Regular, + mode: sync_mode, force: false, no_lock: false, lock_options: LockOptions { diff --git a/rye/tests/test_cli.rs b/rye/tests/test_cli.rs index bd754a8657..806e212170 100644 --- a/rye/tests/test_cli.rs +++ b/rye/tests/test_cli.rs @@ -48,5 +48,9 @@ fn test_dotenv() { 42 23 ----- stderr ----- + Reusing already existing virtualenv + Installing dependencies + Audited 1 package in [EXECUTION_TIME] + Done! "###); } diff --git a/rye/tests/test_init.rs b/rye/tests/test_init.rs index d20a9522ab..16fb5eed1b 100644 --- a/rye/tests/test_init.rs +++ b/rye/tests/test_init.rs @@ -42,6 +42,10 @@ fn test_init_lib() { Hello from my-project! ----- stderr ----- + Reusing already existing virtualenv + Installing dependencies + Audited 1 package in [EXECUTION_TIME] + Done! "###); assert!( @@ -91,6 +95,10 @@ fn test_init_default() { Hello from my-project! ----- stderr ----- + Reusing already existing virtualenv + Installing dependencies + Audited 1 package in [EXECUTION_TIME] + Done! "###); assert!( @@ -141,6 +149,10 @@ fn test_init_script() { Hello from my-project! ----- stderr ----- + Reusing already existing virtualenv + Installing dependencies + Audited 1 package in [EXECUTION_TIME] + Done! "###); rye_cmd_snapshot!(space.rye_cmd().arg("run").arg("python").arg("-mmy_project"), @r###" @@ -150,6 +162,10 @@ fn test_init_script() { Hello from my-project! ----- stderr ----- + Reusing already existing virtualenv + Installing dependencies + Audited 1 package in [EXECUTION_TIME] + Done! "###); }