Skip to content

Commit

Permalink
Support uv --override option (astral-sh#668)
Browse files Browse the repository at this point in the history
  • Loading branch information
idlsoft committed Apr 24, 2024
1 parent c62d0f4 commit 6830807
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 12 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ _Unreleased_

- Use uv in `rye build` when uv is enabled. #978

- Support dependency overrides via `tool.rye.override-dependencies` when using uv. #668


<!-- released start -->

## 0.33.0
Expand Down
24 changes: 22 additions & 2 deletions rye/src/cli/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,29 @@ pub struct Args {
#[arg(long)]
dev: bool,
/// Add this as an excluded dependency that will not be installed even if it's a sub dependency.
#[arg(long, conflicts_with = "dev", conflicts_with = "optional")]
#[arg(
long,
conflicts_with = "dev",
conflicts_with = "optional",
conflicts_with = "override"
)]
excluded: bool,
/// Add this to an optional dependency group.
#[arg(long, conflicts_with = "dev", conflicts_with = "excluded")]
#[arg(
long,
conflicts_with = "dev",
conflicts_with = "excluded",
conflicts_with = "override"
)]
optional: Option<String>,
/// Add this as an override dependency.
#[arg(
long,
conflicts_with = "dev",
conflicts_with = "optional",
conflicts_with = "excluded"
)]
r#override: bool,
/// Include pre-releases when finding a package version.
#[arg(long)]
pre: bool,
Expand Down Expand Up @@ -240,6 +258,8 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
DependencyKind::Excluded
} else if let Some(ref section) = cmd.optional {
DependencyKind::Optional(section.into())
} else if cmd.r#override {
DependencyKind::Override
} else {
DependencyKind::Normal
};
Expand Down
4 changes: 2 additions & 2 deletions rye/src/cli/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ pub fn execute(cmd: Args) -> Result<(), Error> {
fn has_pytest_dependency(projects: &[PyProject]) -> Result<bool, Error> {
for project in projects {
for dep in project
.iter_dependencies(DependencyKind::Dev)
.chain(project.iter_dependencies(DependencyKind::Normal))
.iter_dependencies(&DependencyKind::Dev)
.chain(project.iter_dependencies(&DependencyKind::Normal))
{
if let Ok(req) = dep.expand(|name| std::env::var(name).ok()) {
if normalize_package_name(&req.name) == "pytest" {
Expand Down
43 changes: 36 additions & 7 deletions rye/src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,15 @@ pub fn update_workspace_lockfile(

req_file.flush()?;

let exclusions = find_exclusions(&projects)?;
let exclusions = find_requirements(&projects, &DependencyKind::Excluded)?;
let overrides = find_requirements(&projects, &DependencyKind::Override)?;
let overrides_file = maybe_write_requirements_to_temp(&overrides)?;
generate_lockfile(
output,
py_ver,
&workspace.path(),
req_file.path(),
overrides_file.as_ref().map(|v| v.path()),
lockfile,
sources,
&lock_options,
Expand All @@ -194,6 +197,21 @@ pub fn update_workspace_lockfile(
Ok(())
}

fn maybe_write_requirements_to_temp(
requirements: &HashSet<Requirement>,
) -> Result<Option<NamedTempFile>, Error> {
if requirements.is_empty() {
Ok(None)
} else {
let mut nt_file = NamedTempFile::new()?;
for dep in requirements {
writeln!(&nt_file, "{}", dep)?;
}
nt_file.flush()?;
Ok(Some(nt_file))
}
}

/// Tries to restore the lock options from the given lockfile.
fn restore_lock_options<'o>(
lockfile: &Path,
Expand Down Expand Up @@ -263,10 +281,13 @@ fn collect_workspace_features(
Some(features_by_project)
}

fn find_exclusions(projects: &[PyProject]) -> Result<HashSet<Requirement>, Error> {
fn find_requirements(
projects: &[PyProject],
kind: &DependencyKind,
) -> Result<HashSet<Requirement>, Error> {
let mut rv = HashSet::new();
for project in projects {
for dep in project.iter_dependencies(DependencyKind::Excluded) {
for dep in project.iter_dependencies(kind) {
rv.insert(dep.expand(|name: &str| {
if name == "PROJECT_ROOT" {
Some(project.workspace_path().to_string_lossy().to_string())
Expand All @@ -285,7 +306,7 @@ fn dump_dependencies(
out: &mut fs::File,
dep_kind: DependencyKind,
) -> Result<(), Error> {
for dep in pyproject.iter_dependencies(dep_kind) {
for dep in pyproject.iter_dependencies(&dep_kind) {
if let Ok(expanded_dep) = dep.expand(|_| {
// we actually do not care what it expands to much, for as long
// as the end result parses
Expand Down Expand Up @@ -334,23 +355,26 @@ pub fn update_single_project_lockfile(
)?;
}

for dep in pyproject.iter_dependencies(DependencyKind::Normal) {
for dep in pyproject.iter_dependencies(&DependencyKind::Normal) {
writeln!(req_file, "{}", dep)?;
}
if lock_mode == LockMode::Dev {
for dep in pyproject.iter_dependencies(DependencyKind::Dev) {
for dep in pyproject.iter_dependencies(&DependencyKind::Dev) {
writeln!(req_file, "{}", dep)?;
}
}

req_file.flush()?;

let exclusions = find_exclusions(std::slice::from_ref(pyproject))?;
let exclusions = find_requirements(std::slice::from_ref(pyproject), &DependencyKind::Excluded)?;
let overrides = find_requirements(std::slice::from_ref(pyproject), &DependencyKind::Override)?;
let overrides_file = maybe_write_requirements_to_temp(&overrides)?;
generate_lockfile(
output,
py_ver,
&pyproject.workspace_path(),
req_file.path(),
overrides_file.as_ref().map(|v| v.path()),
lockfile,
sources,
&lock_options,
Expand All @@ -367,6 +391,7 @@ fn generate_lockfile(
py_ver: &PythonVersion,
workspace_path: &Path,
requirements_file_in: &Path,
overrides_file_in: Option<&Path>,
lockfile: &Path,
sources: &ExpandedSources,
lock_options: &LockOptions,
Expand Down Expand Up @@ -405,12 +430,16 @@ fn generate_lockfile(
.lockfile(
py_ver,
requirements_file_in,
overrides_file_in,
&requirements_file,
lock_options.pre,
env::var("__RYE_UV_EXCLUDE_NEWER").ok(),
upgrade,
)?;
} else {
if overrides_file_in.is_some() {
bail!("dependency overrides are only supported by uv");
}
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
11 changes: 10 additions & 1 deletion rye/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ pub enum DependencyKind<'a> {
Normal,
Dev,
Excluded,
Override,
Optional(Cow<'a, str>),
}

Expand All @@ -64,6 +65,7 @@ impl<'a> fmt::Display for DependencyKind<'a> {
DependencyKind::Normal => f.write_str("regular"),
DependencyKind::Dev => f.write_str("dev"),
DependencyKind::Excluded => f.write_str("excluded"),
DependencyKind::Override => f.write_str("override"),
DependencyKind::Optional(ref sect) => write!(f, "optional ({})", sect),
}
}
Expand Down Expand Up @@ -903,6 +905,7 @@ impl PyProject {
DependencyKind::Normal => &mut self.doc["project"]["dependencies"],
DependencyKind::Dev => &mut self.doc["tool"]["rye"]["dev-dependencies"],
DependencyKind::Excluded => &mut self.doc["tool"]["rye"]["excluded-dependencies"],
DependencyKind::Override => &mut self.doc["tool"]["rye"]["override-dependencies"],
DependencyKind::Optional(ref section) => {
// add this as a proper non-inline table if it's missing
let table = &mut self.doc["project"]["optional-dependencies"];
Expand Down Expand Up @@ -934,6 +937,7 @@ impl PyProject {
DependencyKind::Normal => &mut self.doc["project"]["dependencies"],
DependencyKind::Dev => &mut self.doc["tool"]["rye"]["dev-dependencies"],
DependencyKind::Excluded => &mut self.doc["tool"]["rye"]["excluded-dependencies"],
DependencyKind::Override => &mut self.doc["tool"]["rye"]["override-dependencies"],
DependencyKind::Optional(ref section) => {
&mut self.doc["project"]["optional-dependencies"][section as &str]
}
Expand All @@ -953,7 +957,7 @@ impl PyProject {
/// Iterates over all dependencies.
pub fn iter_dependencies(
&self,
kind: DependencyKind,
kind: &DependencyKind,
) -> impl Iterator<Item = DependencyRef> + '_ {
let sec = match kind {
DependencyKind::Normal => self.doc.get("project").and_then(|x| x.get("dependencies")),
Expand All @@ -967,6 +971,11 @@ impl PyProject {
.get("tool")
.and_then(|x| x.get("rye"))
.and_then(|x| x.get("excluded-dependencies")),
DependencyKind::Override => self
.doc
.get("tool")
.and_then(|x| x.get("rye"))
.and_then(|x| x.get("override-dependencies")),
DependencyKind::Optional(ref section) => self
.doc
.get("project")
Expand Down
4 changes: 4 additions & 0 deletions rye/src/uv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,12 @@ impl Uv {
Ok(UvWithVenv::new(self.clone(), venv_dir, version))
}

#[allow(clippy::too_many_arguments)]
pub fn lockfile(
&self,
py_version: &PythonVersion,
source: &Path,
overrides: Option<&Path>,
target: &Path,
allow_prerelease: bool,
exclude_newer: Option<String>,
Expand All @@ -341,6 +343,8 @@ impl Uv {

cmd.arg(source);

overrides.map(|ref value| cmd.arg("--override").arg(value));

let status = cmd.status().with_context(|| {
format!(
"Unable to run uv pip compile and generate {}",
Expand Down
47 changes: 47 additions & 0 deletions rye/tests/test_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,3 +261,50 @@ fn test_autosync_remember() {
werkzeug==3.0.1
"###);
}

#[test]
fn test_overrides() {
// enforce werkzeug==2.3.8 when flask==3.0.0 requires Werkzeug>=3.0.0

let space = Space::new();
space.init("my-project");

rye_cmd_snapshot!(space.rye_cmd().arg("add").arg("werkzeug==2.3.8").arg("--override").arg("--no-sync"), @r###"
success: true
exit_code: 0
----- stdout -----
Initializing new virtualenv in [TEMP_PATH]/project/.venv
Python version: [email protected]
Added werkzeug==2.3.8 as override dependency
----- stderr -----
"###);

rye_cmd_snapshot!(space.rye_cmd().arg("add").arg("flask==3.0.0").arg("colorama==0.4.6"), @r###"
success: true
exit_code: 0
----- stdout -----
Added flask==3.0.0 as regular dependency
Added colorama==0.4.6 as regular dependency
Reusing already existing virtualenv
Generating production lockfile: [TEMP_PATH]/project/requirements.lock
Generating dev lockfile: [TEMP_PATH]/project/requirements-dev.lock
Installing dependencies
Done!
----- stderr -----
Built 1 editable in [EXECUTION_TIME]
Resolved 8 packages in [EXECUTION_TIME]
Downloaded 8 packages in [EXECUTION_TIME]
Installed 9 packages in [EXECUTION_TIME]
+ blinker==1.7.0
+ click==8.1.7
+ colorama==0.4.6
+ flask==3.0.0
+ itsdangerous==2.1.2
+ jinja2==3.1.2
+ markupsafe==2.1.3
+ my-project==0.1.0 (from file:[TEMP_PATH]/project)
+ werkzeug==2.3.8
"###);
}

0 comments on commit 6830807

Please sign in to comment.