Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add autosync support #677

Merged
merged 14 commits into from
Feb 20, 2024
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ _Unreleased_

- Sync latest PyPy releases. #683

- When `uv` is enabled, rye will now automatically sync on `add` and `remove`. #677

<!-- released start -->

## 0.25.0
Expand Down
14 changes: 11 additions & 3 deletions docs/guide/commands/add.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ but provides additional helper arguments to make this process more user friendly
instance instead of passing git references within the requiement string, the `--git`
parameter can be used.

After a dependency is added it's not automatically installed. To do that, you need to
invoke the [`sync`](sync.md) command. To remove a dependency again use the [`remove`](remove.md)
command.
If auto sync is disabled, after a dependency is added it's not automatically
installed. To do that, you need to invoke the [`sync`](sync.md) command or pass
`--sync`. To remove a dependency again use the [`remove`](remove.md) command.

+++ 0.26.0

Added support for auto-sync and the `--sync` / `--no-sync` flags.

## Example

Expand Down Expand Up @@ -64,6 +68,10 @@ Added flask @ git+https://github.com/pallets/flask as regular dependency

* `--pin <PIN>`: Overrides the pin operator [possible values: `equal`, `tilde-equal``, `greater-than-equal``]

* `--sync`: Runs `sync` automatically even if auto-sync is disabled.

* `--no-sync`: Does not run `sync` automatically even if auto-sync is enabled.

* `-v, --verbose`: Enables verbose diagnostics

* `-q, --quiet`: Turns off all output
Expand Down
12 changes: 12 additions & 0 deletions docs/guide/commands/remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
Removes a package from this project. This removes a package from the `pyproject.toml`
dependency list.

If auto sync is disabled, after a dependency is removed it's not automatically
uninstalled. To do that, you need to invoke the [`sync`](sync.md) command or pass
`--sync`.

+++ 0.26.0

Added support for auto-sync and the `--sync` / `--no-sync` flags.

## Example

```
Expand All @@ -20,6 +28,10 @@ Removed flask>=3.0.1

* `--optional <OPTIONAL>`: Remove this from the optional dependency group

* `--sync`: Runs `sync` automatically even if auto-sync is disabled.

* `--no-sync`: Does not run `sync` automatically even if auto-sync is enabled.

* `-v, --verbose`: Enables verbose diagnostics

* `-q, --quiet`: Turns off all output
Expand Down
4 changes: 4 additions & 0 deletions docs/guide/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ global-python = false
# for pip-tools. Learn more about uv here: https://github.com/astral-sh/uv
use-uv = false

# Enable or disable automatic `sync` after `add` and `remove`. This defaults
# to `true` when uv is enabled and `false` otherwise.
autosync = 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
Expand Down
20 changes: 13 additions & 7 deletions docs/guide/sync.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
# Syncing and Locking

Rye currently uses [pip-tools](https://github.com/jazzband/pip-tools) to download and install
dependencies. For this purpose it creates two "lockfiles" (called `requirements.lock` and
`requirements-dev.lock`). These are not real lockfiles but they fulfill a similar purpose
until a better solution has been implemented.

Whenever `rye sync` is called, it will update lockfiles as well as the virtualenv. If you only
want to update the lockfiles, then `rye lock` can be used.
Rye supports two systems to manage dependencies:
[uv](https://github.com/astral-sh/uv) and
[pip-tools](https://github.com/jazzband/pip-tools). It currently defaults to
`pip-tools` but will offer you the option to use `uv` instead. `uv` will become
the default choice once it stabilzes as it offers significantly better performance.

In order to download dependencies rye creates two "lockfiles" (called
`requirements.lock` and `requirements-dev.lock`). These are not real lockfiles
but they fulfill a similar purpose until a better solution has been implemented.

Whenever `rye sync` is called, it will update lockfiles as well as the
virtualenv. If you only want to update the lockfiles, then `rye lock` can be
used.

## Lock

Expand Down
15 changes: 13 additions & 2 deletions rye/src/cli/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use crate::config::Config;
use crate::consts::VENV_BIN;
use crate::pyproject::{BuildSystem, DependencyKind, ExpandedSources, PyProject};
use crate::sources::PythonVersion;
use crate::sync::{sync, SyncOptions};
use crate::sync::{autosync, sync, SyncOptions};
use crate::utils::{format_requirement, set_proxy_variables, CommandOutput};

const PACKAGE_FINDER_SCRIPT: &str = r#"
Expand Down Expand Up @@ -210,6 +210,12 @@ pub struct Args {
/// Overrides the pin operator
#[arg(long)]
pin: Option<Pin>,
/// 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,
Expand All @@ -222,6 +228,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
let output = CommandOutput::from_quiet_and_verbose(cmd.quiet, cmd.verbose);
let self_venv = ensure_self_venv(output).context("error bootstrapping venv")?;
let python_path = self_venv.join(VENV_BIN).join("python");
let cfg = Config::current();

let mut pyproject_toml = PyProject::discover()?;
let py_ver = pyproject_toml.venv_python_version()?;
Expand Down Expand Up @@ -251,7 +258,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}

if !cmd.excluded {
if Config::current().use_uv() {
if cfg.use_uv() {
sync(SyncOptions::python_only().pyproject(None))
.context("failed to sync ahead of add")?;
resolve_requirements_with_uv(
Expand Down Expand Up @@ -294,6 +301,10 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}
}

if (cfg.autosync() && !cmd.no_sync) || cmd.sync {
autosync(&pyproject_toml, output)?;
}

Ok(())
}

Expand Down
4 changes: 4 additions & 0 deletions rye/src/cli/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ pub struct Args {
/// Set to true to lock with sources in the lockfile.
#[arg(long)]
with_sources: bool,
/// Reset prior lock options.
#[arg(long)]
reset: bool,
/// Use this pyproject.toml file
#[arg(long, value_name = "PYPROJECT_TOML")]
pyproject: Option<PathBuf>,
Expand All @@ -51,6 +54,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
features: cmd.features,
all_features: cmd.all_features,
with_sources: cmd.with_sources,
reset: cmd.reset,
},
pyproject: cmd.pyproject,
..SyncOptions::default()
Expand Down
12 changes: 12 additions & 0 deletions rye/src/cli/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use anyhow::Error;
use clap::Parser;
use pep508_rs::Requirement;

use crate::config::Config;
use crate::pyproject::{DependencyKind, PyProject};
use crate::sync::autosync;
use crate::utils::{format_requirement, CommandOutput};

/// Removes a package from this project.
Expand All @@ -19,6 +21,12 @@ pub struct Args {
/// Remove this from an optional dependency group.
#[arg(long, conflicts_with = "dev")]
optional: Option<String>,
/// 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,
Expand Down Expand Up @@ -56,5 +64,9 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}
}

if (Config::current().autosync() && !cmd.no_sync) || cmd.sync {
autosync(&pyproject_toml, output)?;
}

Ok(())
}
4 changes: 4 additions & 0 deletions rye/src/cli/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ pub struct Args {
/// Use this pyproject.toml file
#[arg(long, value_name = "PYPROJECT_TOML")]
pyproject: Option<PathBuf>,
/// Do not reuse (reset) prior lock options.
#[arg(long)]
reset: bool,
}

pub fn execute(cmd: Args) -> Result<(), Error> {
Expand All @@ -67,6 +70,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
features: cmd.features,
all_features: cmd.all_features,
with_sources: cmd.with_sources,
reset: cmd.reset,
},
pyproject: cmd.pyproject,
})?;
Expand Down
9 changes: 9 additions & 0 deletions rye/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,15 @@ impl Config {
Ok(rv)
}

/// Enable autosync.
pub fn autosync(&self) -> bool {
self.doc
.get("behavior")
.and_then(|x| x.get("autosync"))
.and_then(|x| x.as_bool())
.unwrap_or_else(|| self.use_uv())
}

/// Indicates if the experimental uv support should be used.
pub fn use_uv(&self) -> bool {
self.doc
Expand Down
81 changes: 69 additions & 12 deletions rye/src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ static REQUIREMENTS_HEADER: &str = r#"# generated by rye
# use `rye lock` or `rye sync` to update this lockfile
#
# last locked with the following flags:
# pre: {{ lock_options.pre }}
# features: {{ lock_options.features }}
# all-features: {{ lock_options.all_features }}
# with-sources: {{ lock_options.with_sources }}
# pre: {{ lock_options.pre|tojson }}
# features: {{ lock_options.features|tojson }}
# all-features: {{ lock_options.all_features|tojson }}
# with-sources: {{ lock_options.with_sources|tojson }}

"#;
static PARAM_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^# (pre|features|all-features|with_sources):\s*(.*?)$").unwrap());

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum LockMode {
Expand Down Expand Up @@ -73,6 +75,55 @@ pub struct LockOptions {
pub all_features: bool,
/// Should locking happen with sources?
pub with_sources: bool,
/// Do not reuse (reset) prior lock options.
pub reset: bool,
}

impl LockOptions {
/// Writes the lock options as header.
pub fn write_header<W: Write>(&self, mut w: W) -> Result<(), Error> {
writeln!(w, "{}", render!(REQUIREMENTS_HEADER, lock_options => self))?;
Ok(())
}

/// Restores lock options from a requirements file.
///
/// This also applies overrides from the command line.
pub fn restore<'o>(s: &str, opts: &'o LockOptions) -> Result<Cow<'o, LockOptions>, Error> {
// nothing to do here
if opts.reset {
return Ok(Cow::Borrowed(opts));
}

let mut rv = opts.clone();
for line in s
.lines()
.skip_while(|x| *x != "# last locked with the following flags:")
{
if let Some(m) = PARAM_RE.captures(line) {
let value = &m[2];
match &m[1] {
"pre" => rv.pre = rv.pre || serde_json::from_str(value)?,
"features" => {
if rv.features.is_empty() {
rv.features = serde_json::from_str(value)?;
}
}
"all-features" => {
rv.all_features = rv.all_features || serde_json::from_str(value)?
}
"with-sources" => rv.with_sources = serde_json::from_str(value)?,
_ => unreachable!(),
}
}
}

if rv.all_features {
rv.features = Vec::new();
}

Ok(Cow::Owned(rv))
}
}

/// Creates lockfiles for all projects in the workspace.
Expand Down Expand Up @@ -310,11 +361,14 @@ fn generate_lockfile(
) -> Result<(), Error> {
let scratch = tempfile::tempdir()?;
let requirements_file = scratch.path().join("requirements.txt");
if lockfile.is_file() {
fs::copy(lockfile, &requirements_file)?;
let lock_options = if lockfile.is_file() {
let requirements = fs::read_to_string(lockfile)?;
fs::write(&requirements_file, &requirements)?;
LockOptions::restore(&requirements, lock_options)?
} else {
fs::write(&requirements_file, b"")?;
}
Cow::Borrowed(lock_options)
};

let mut cmd = if Config::current().use_uv() {
let self_venv = ensure_self_venv(output)?;
Expand All @@ -331,6 +385,9 @@ fn generate_lockfile(
} else if output == CommandOutput::Quiet {
cmd.arg("-q");
}
if lock_options.pre {
cmd.arg("--prerelease=allow");
}
// this primarily exists for testing
if let Ok(dt) = env::var("__RYE_UV_EXCLUDE_NEWER") {
cmd.arg("--exclude-newer").arg(dt);
Expand Down Expand Up @@ -359,6 +416,9 @@ fn generate_lockfile(
} else {
"-q"
});
if lock_options.pre {
cmd.arg("--pre");
}
cmd
};

Expand All @@ -376,9 +436,6 @@ fn generate_lockfile(
if lock_options.update_all {
cmd.arg("--upgrade");
}
if lock_options.pre {
cmd.arg("--pre");
}
sources.add_as_pip_args(&mut cmd);
set_proxy_variables(&mut cmd);
let status = cmd.status().context("unable to run pip-compile")?;
Expand All @@ -392,7 +449,7 @@ fn generate_lockfile(
workspace_path,
exclusions,
sources,
lock_options,
&lock_options,
)?;

Ok(())
Expand All @@ -407,7 +464,7 @@ fn finalize_lockfile(
lock_options: &LockOptions,
) -> Result<(), Error> {
let mut rv = BufWriter::new(fs::File::create(out)?);
writeln!(rv, "{}", render!(REQUIREMENTS_HEADER, lock_options))?;
lock_options.write_header(&mut rv)?;

// only if we are asked to include sources we do that.
if lock_options.with_sources {
Expand Down
13 changes: 13 additions & 0 deletions rye/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,19 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> {
Ok(())
}

/// Performs an autosync.
pub fn autosync(pyproject: &PyProject, output: CommandOutput) -> Result<(), Error> {
sync(SyncOptions {
output,
dev: true,
mode: SyncMode::Regular,
force: false,
no_lock: false,
lock_options: LockOptions::default(),
pyproject: Some(pyproject.toml_path().to_path_buf()),
})
}

pub fn create_virtualenv(
output: CommandOutput,
self_venv: &Path,
Expand Down