Skip to content

Commit

Permalink
Add keyring support for uv (#1016)
Browse files Browse the repository at this point in the history
`uv` has support for keyrings, as long as the option is
`--keyring-provider=subprocess`. But this isn't exposed in the `rye`
CLI.

Some things I haven't addressed, happy to discuss:

1. I haven't implemented it for piptools. I expect it's possible, in
fact more features could be implemented using pip-compile's `--pip-args`
argument, but I think the plan is to move to `uv` as the default going
forward, so I don't know how much desire there is to extend the piptools
backend. But if it's needed, I can do it, either in this PR or as
followup work.

2. In practice, I'd expect that people would want to always pass this
`--keyring-provider` value for all calls, and it does pollute the code a
bit, passing round the CLI options. I think another way to go could be
to put it in the global config and/or the project TOML, and just read it
from there. (I'd actually prefer this interface for my own uses.) But I
wanted to stay close to the existing CLI of `uv` and `pip`, so stuck
with that interface. Again, happy to iterate.

# Test

From a `rye init` project:

```
❯ ../rye/target/debug/rye lock
Generating production lockfile: /home/emarsden/src/rye-proj/test-proj/requirements.lock
error: Failed to build editables
  Caused by: Failed to build editable: /home/emarsden/src/rye-proj/test-proj
  Caused by: Failed to install requirements from build-system.requires (resolve)
  Caused by: No solution found when resolving: hatchling
  Caused by: HTTP status client error (401 Unauthorized) for url (https://[snip]/hatchling/)
error: could not write production lockfile for project

Caused by:
    Failed to run uv compile /tmp/.tmplGQmy9/requirements.txt. uv exited with status: exit status: 2

❯ ../rye/target/debug/rye lock --keyring-provider=subprocess
Generating production lockfile: /home/emarsden/src/rye-proj/test-proj/requirements.lock
Generating dev lockfile: /home/emarsden/src/rye-proj/test-proj/requirements-dev.lock
Done!
```
  • Loading branch information
emarsden-iso committed May 6, 2024
1 parent 27e392b commit 45b7bd3
Show file tree
Hide file tree
Showing 8 changed files with 98 additions and 15 deletions.
34 changes: 26 additions & 8 deletions rye/src/cli/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use url::Url;
use crate::bootstrap::ensure_self_venv;
use crate::config::Config;
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};
Expand Down Expand Up @@ -137,7 +138,7 @@ impl ReqExtras {
};
req.version_or_url = match req.version_or_url {
Some(_) => bail!("requirement already has a version marker"),
None => Some(pep508_rs::VersionOrUrl::Url(
None => Some(VersionOrUrl::Url(
format!("git+{}{}", git, suffix).parse().with_context(|| {
format!("unable to interpret '{}{}' as git reference", git, suffix)
})?,
Expand All @@ -146,10 +147,11 @@ impl ReqExtras {
} else if let Some(ref url) = self.url {
req.version_or_url = match req.version_or_url {
Some(_) => bail!("requirement already has a version marker"),
None => Some(pep508_rs::VersionOrUrl::Url(
url.parse()
.with_context(|| format!("unable to parse '{}' as url", url))?,
)),
None => {
Some(VersionOrUrl::Url(url.parse().with_context(|| {
format!("unable to parse '{}' as url", url)
})?))
}
};
} else if let Some(ref path) = self.path {
// For hatchling build backend, it use {root:uri} for file relative path,
Expand All @@ -175,7 +177,7 @@ impl ReqExtras {
};
req.version_or_url = match req.version_or_url {
Some(_) => bail!("requirement already has a version marker"),
None => Some(pep508_rs::VersionOrUrl::Url(file_url)),
None => Some(VersionOrUrl::Url(file_url)),
};
}
for feature in self.features.iter().flat_map(|x| x.split(',')) {
Expand Down Expand Up @@ -212,6 +214,9 @@ pub struct Args {
/// Overrides the pin operator
#[arg(long)]
pin: Option<Pin>,
/// Attempt to use `keyring` for authentication for index URLs.
#[arg(long, value_enum, default_value_t)]
keyring_provider: KeyringProvider,
/// Runs `sync` even if auto-sync is disabled.
#[arg(long)]
sync: bool,
Expand Down Expand Up @@ -259,6 +264,8 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
requirements.push(requirement);
}

let keyring_provider = cmd.keyring_provider;

if !cmd.excluded {
if cfg.use_uv() {
sync(SyncOptions::python_only().pyproject(None))
Expand All @@ -270,8 +277,12 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
cmd.pre,
output,
&default_operator,
keyring_provider,
)?;
} else {
if keyring_provider != KeyringProvider::Disabled {
bail!("`--keyring-provider` option requires the uv backend");
}
for requirement in &mut requirements {
resolve_requirements_with_unearth(
&pyproject_toml,
Expand Down Expand Up @@ -303,7 +314,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}

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

Ok(())
Expand Down Expand Up @@ -448,6 +459,7 @@ fn resolve_requirements_with_uv(
pre: bool,
output: CommandOutput,
default_operator: &Operator,
keyring_provider: KeyringProvider,
) -> Result<(), Error> {
let venv_path = pyproject_toml.venv_path();
let py_bin = get_venv_python_bin(&venv_path);
Expand All @@ -460,7 +472,13 @@ fn resolve_requirements_with_uv(
.venv(&venv_path, &py_bin, py_ver, None)?;

for req in requirements {
let mut new_req = uv.resolve(py_ver, req, pre, env::var("__RYE_UV_EXCLUDE_NEWER").ok())?;
let mut new_req = uv.resolve(
py_ver,
req,
pre,
env::var("__RYE_UV_EXCLUDE_NEWER").ok(),
keyring_provider,
)?;

// if a version or URL is already provided we just use the normalized package name but
// retain all old information.
Expand Down
6 changes: 5 additions & 1 deletion rye/src/cli/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::PathBuf;
use anyhow::Error;
use clap::Parser;

use crate::lock::LockOptions;
use crate::lock::{KeyringProvider, LockOptions};
use crate::sync::{sync, SyncMode, SyncOptions};
use crate::utils::CommandOutput;

Expand Down Expand Up @@ -34,6 +34,9 @@ pub struct Args {
/// Set to true to lock with sources in the lockfile.
#[arg(long)]
with_sources: bool,
/// Attempt to use `keyring` for authentication for index URLs.
#[arg(long, value_enum, default_value_t)]
keyring_provider: KeyringProvider,
/// Reset prior lock options.
#[arg(long)]
reset: bool,
Expand All @@ -57,6 +60,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
reset: cmd.reset,
},
pyproject: cmd.pyproject,
keyring_provider: cmd.keyring_provider,
..SyncOptions::default()
})?;
Ok(())
Expand Down
6 changes: 5 additions & 1 deletion rye/src/cli/remove.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use clap::Parser;
use pep508_rs::Requirement;

use crate::config::Config;
use crate::lock::KeyringProvider;
use crate::pyproject::{DependencyKind, PyProject};
use crate::sync::autosync;
use crate::utils::{format_requirement, CommandOutput};
Expand All @@ -27,6 +28,9 @@ pub struct Args {
/// Does not run `sync` even if auto-sync is enabled.
#[arg(long, conflicts_with = "sync")]
no_sync: bool,
/// Attempt to use `keyring` for authentication for index URLs.
#[arg(long, value_enum, default_value_t)]
keyring_provider: KeyringProvider,
/// Enables verbose diagnostics.
#[arg(short, long)]
verbose: bool,
Expand Down Expand Up @@ -65,7 +69,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
}

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

Ok(())
Expand Down
6 changes: 5 additions & 1 deletion rye/src/cli/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::path::PathBuf;
use anyhow::Error;
use clap::Parser;

use crate::lock::LockOptions;
use crate::lock::{KeyringProvider, LockOptions};
use crate::sync::{sync, SyncMode, SyncOptions};
use crate::utils::CommandOutput;

Expand Down Expand Up @@ -43,6 +43,9 @@ pub struct Args {
/// Set to true to lock with sources in the lockfile.
#[arg(long)]
with_sources: bool,
/// Attempt to use `keyring` for authentication for index URLs.
#[arg(long, value_enum, default_value_t)]
keyring_provider: KeyringProvider,
/// Use this pyproject.toml file
#[arg(long, value_name = "PYPROJECT_TOML")]
pyproject: Option<PathBuf>,
Expand Down Expand Up @@ -72,6 +75,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
with_sources: cmd.with_sources,
reset: cmd.reset,
},
keyring_provider: cmd.keyring_provider,
pyproject: cmd.pyproject,
})?;
Ok(())
Expand Down
6 changes: 5 additions & 1 deletion rye/src/cli/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use same_file::is_same_file;

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::utils::{CommandOutput, QuietExit};
Expand All @@ -28,6 +29,9 @@ pub struct Args {
/// Use this pyproject.toml file
#[arg(long, value_name = "PYPROJECT_TOML")]
pyproject: Option<PathBuf>,
/// Attempt to use `keyring` for authentication for index URLs.
#[arg(long, value_enum, default_value_t)]
keyring_provider: KeyringProvider,
// Disable test output capture to stdout
#[arg(long = "no-capture", short = 's')]
no_capture: bool,
Expand Down Expand Up @@ -73,7 +77,7 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
let has_pytest = has_pytest_dependency(&projects)?;
if has_pytest {
if Config::current().autosync() {
autosync(&projects[0], output)?;
autosync(&projects[0], output, cmd.keyring_provider)?;
} else {
bail!("pytest not installed but in dependencies. Run `rye sync`.")
}
Expand Down
24 changes: 24 additions & 0 deletions rye/src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::sync::Arc;
use std::{env, fmt, fs};

use anyhow::{anyhow, bail, Context, Error};
use clap::ValueEnum;
use minijinja::render;
use once_cell::sync::Lazy;
use pep508_rs::Requirement;
Expand Down Expand Up @@ -59,6 +60,18 @@ impl fmt::Display for LockMode {
}
}

/// Keyring provider type to use for credential lookup.
#[derive(ValueEnum, Copy, Clone, Serialize, Debug, Default, PartialEq)]
#[value(rename_all = "snake_case")]
#[serde(rename_all = "snake_case")]
pub enum KeyringProvider {
/// Do not use keyring for credential lookup.
#[default]
Disabled,
/// Use the `keyring` command for credential lookup.
Subprocess,
}

/// Controls how locking should work.
#[derive(Debug, Clone, Default, Serialize)]
pub struct LockOptions {
Expand Down Expand Up @@ -128,6 +141,7 @@ impl LockOptions {
}

/// Creates lockfiles for all projects in the workspace.
#[allow(clippy::too_many_arguments)]
pub fn update_workspace_lockfile(
py_ver: &PythonVersion,
workspace: &Arc<Workspace>,
Expand All @@ -136,6 +150,7 @@ pub fn update_workspace_lockfile(
output: CommandOutput,
sources: &ExpandedSources,
lock_options: &LockOptions,
keyring_provider: KeyringProvider,
) -> Result<(), Error> {
echo!(if output, "Generating {} lockfile: {}", lock_mode, lockfile.display());

Expand Down Expand Up @@ -189,6 +204,7 @@ pub fn update_workspace_lockfile(
&lock_options,
&exclusions,
true,
keyring_provider,
)?;

Ok(())
Expand Down Expand Up @@ -308,6 +324,7 @@ fn dump_dependencies(
}

/// Updates the lockfile of the current project.
#[allow(clippy::too_many_arguments)]
pub fn update_single_project_lockfile(
py_ver: &PythonVersion,
pyproject: &PyProject,
Expand All @@ -316,6 +333,7 @@ pub fn update_single_project_lockfile(
output: CommandOutput,
sources: &ExpandedSources,
lock_options: &LockOptions,
keyring_provider: KeyringProvider,
) -> Result<(), Error> {
echo!(if output, "Generating {} lockfile: {}", lock_mode, lockfile.display());

Expand Down Expand Up @@ -356,6 +374,7 @@ pub fn update_single_project_lockfile(
&lock_options,
&exclusions,
false,
keyring_provider,
)?;

Ok(())
Expand All @@ -372,6 +391,7 @@ fn generate_lockfile(
lock_options: &LockOptions,
exclusions: &HashSet<Requirement>,
no_deps: bool,
keyring_provider: KeyringProvider,
) -> Result<(), Error> {
let use_uv = Config::current().use_uv();
let scratch = tempfile::tempdir()?;
Expand Down Expand Up @@ -409,8 +429,12 @@ fn generate_lockfile(
lock_options.pre,
env::var("__RYE_UV_EXCLUDE_NEWER").ok(),
upgrade,
keyring_provider,
)?;
} else {
if keyring_provider != KeyringProvider::Disabled {
bail!("`--keyring-provider` option requires the uv backend");
}
let mut cmd = Command::new(get_pip_compile(py_ver, output)?);
// legacy pip tools requires some extra parameters
if get_pip_tools_version(py_ver) == PipToolsVersion::Legacy {
Expand Down
15 changes: 13 additions & 2 deletions rye/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use crate::config::Config;
use crate::consts::VENV_BIN;
use crate::lock::{
make_project_root_fragment, update_single_project_lockfile, update_workspace_lockfile,
LockMode, LockOptions,
KeyringProvider, LockMode, LockOptions,
};
use crate::piptools::{get_pip_sync, get_pip_tools_venv_path};
use crate::platform::get_toolchain_python_bin;
Expand Down Expand Up @@ -56,6 +56,8 @@ pub struct SyncOptions {
pub lock_options: LockOptions,
/// Explicit pyproject location (Only usable by PythonOnly mode)
pub pyproject: Option<PathBuf>,
/// Keyring provider to use for credential lookup.
pub keyring_provider: KeyringProvider,
}

impl SyncOptions {
Expand Down Expand Up @@ -197,6 +199,7 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> {
cmd.output,
&sources,
&cmd.lock_options,
cmd.keyring_provider,
)
.context("could not write production lockfile for workspace")?;
update_workspace_lockfile(
Expand All @@ -207,6 +210,7 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> {
cmd.output,
&sources,
&cmd.lock_options,
cmd.keyring_provider,
)
.context("could not write dev lockfile for workspace")?;
} else {
Expand All @@ -219,6 +223,7 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> {
cmd.output,
&sources,
&cmd.lock_options,
cmd.keyring_provider,
)
.context("could not write production lockfile for project")?;
update_single_project_lockfile(
Expand All @@ -229,6 +234,7 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> {
cmd.output,
&sources,
&cmd.lock_options,
cmd.keyring_provider,
)
.context("could not write dev lockfile for project")?;
}
Expand Down Expand Up @@ -310,7 +316,11 @@ pub fn sync(mut cmd: SyncOptions) -> Result<(), Error> {
}

/// Performs an autosync.
pub fn autosync(pyproject: &PyProject, output: CommandOutput) -> Result<(), Error> {
pub fn autosync(
pyproject: &PyProject,
output: CommandOutput,
keyring_provider: KeyringProvider,
) -> Result<(), Error> {
sync(SyncOptions {
output,
dev: true,
Expand All @@ -319,6 +329,7 @@ pub fn autosync(pyproject: &PyProject, output: CommandOutput) -> Result<(), Erro
no_lock: false,
lock_options: LockOptions::default(),
pyproject: Some(pyproject.toml_path().to_path_buf()),
keyring_provider,
})
}

Expand Down
Loading

0 comments on commit 45b7bd3

Please sign in to comment.