Skip to content

Commit

Permalink
Allow user to constrain supported lock environments
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Aug 19, 2024
1 parent baf17be commit c4267c9
Show file tree
Hide file tree
Showing 14 changed files with 662 additions and 40 deletions.
32 changes: 32 additions & 0 deletions crates/uv-resolver/src/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ pub struct Lock {
/// If this lockfile was built from a forking resolution with non-identical forks, store the
/// forks in the lockfile so we can recreate them in subsequent resolutions.
fork_markers: Vec<MarkerTree>,
/// The list of supported environments specified by the user.
supported_environments: Vec<MarkerTree>,
/// The range of supported Python versions.
requires_python: Option<RequiresPython>,
/// We discard the lockfile if these options don't match.
Expand Down Expand Up @@ -161,6 +163,7 @@ impl Lock {
requires_python,
options,
ResolverManifest::default(),
vec![],
graph.fork_markers.clone(),
)?;
Ok(lock)
Expand All @@ -173,6 +176,7 @@ impl Lock {
requires_python: Option<RequiresPython>,
options: ResolverOptions,
manifest: ResolverManifest,
supported_environments: Vec<MarkerTree>,
fork_markers: Vec<MarkerTree>,
) -> Result<Self, LockError> {
// Put all dependencies for each package in a canonical order and
Expand Down Expand Up @@ -329,6 +333,7 @@ impl Lock {
Ok(Self {
version,
fork_markers,
supported_environments,
requires_python,
options,
packages,
Expand All @@ -344,6 +349,13 @@ impl Lock {
self
}

/// Record the supported environments that were used to generate this lock.
#[must_use]
pub fn with_supported_environments(mut self, supported_environments: Vec<MarkerTree>) -> Self {
self.supported_environments = supported_environments;
self
}

/// Returns the number of packages in the lockfile.
pub fn len(&self) -> usize {
self.packages.len()
Expand Down Expand Up @@ -384,6 +396,11 @@ impl Lock {
self.options.exclude_newer
}

/// Returns the supported environments that were used to generate this lock.
pub fn supported_environments(&self) -> &[MarkerTree] {
&self.supported_environments
}

/// If this lockfile was built from a forking resolution with non-identical forks, return the
/// markers of those forks, otherwise `None`.
pub fn fork_markers(&self) -> &[MarkerTree] {
Expand Down Expand Up @@ -486,6 +503,7 @@ impl Lock {
if let Some(ref requires_python) = self.requires_python {
doc.insert("requires-python", value(requires_python.to_string()));
}

if !self.fork_markers.is_empty() {
let fork_markers = each_element_on_its_line_array(
self.fork_markers
Expand All @@ -496,6 +514,16 @@ impl Lock {
doc.insert("environment-markers", value(fork_markers));
}

if !self.supported_environments.is_empty() {
let supported_environments = each_element_on_its_line_array(
self.supported_environments
.iter()
.filter_map(MarkerTree::contents)
.map(|marker| marker.to_string()),
);
doc.insert("supported-markers", value(supported_environments));
}

// Write the settings that were used to generate the resolution.
// This enables us to invalidate the lockfile if the user changes
// their settings.
Expand Down Expand Up @@ -951,6 +979,8 @@ struct LockWire {
/// forks in the lockfile so we can recreate them in subsequent resolutions.
#[serde(rename = "environment-markers", default)]
fork_markers: Vec<MarkerTree>,
#[serde(rename = "supported-markers", default)]
supported_environments: Vec<MarkerTree>,
/// We discard the lockfile if these options match.
#[serde(default)]
options: ResolverOptions,
Expand All @@ -966,6 +996,7 @@ impl From<Lock> for LockWire {
version: lock.version,
requires_python: lock.requires_python,
fork_markers: lock.fork_markers,
supported_environments: lock.supported_environments,
options: lock.options,
manifest: lock.manifest,
packages: lock.packages.into_iter().map(PackageWire::from).collect(),
Expand Down Expand Up @@ -1005,6 +1036,7 @@ impl TryFrom<LockWire> for Lock {
wire.requires_python,
wire.options,
wire.manifest,
wire.supported_environments,
wire.fork_markers,
)
}
Expand Down
13 changes: 12 additions & 1 deletion crates/uv-resolver/src/resolution/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,18 @@ impl ResolutionGraph {
vec![]
}
ResolverMarkers::Fork(_) => {
panic!("A single fork must be universal");
resolutions
.iter()
.map(|resolution| {
resolution
.markers
.fork_markers()
.expect("A non-forking resolution exists in forking mode")
.clone()
})
// Any unsatisfiable forks were skipped.
.filter(|fork| !fork.is_false())
.collect()
}
}
} else {
Expand Down
4 changes: 4 additions & 0 deletions crates/uv-settings/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ pub struct Options {
#[cfg_attr(feature = "schemars", schemars(skip))]
dev_dependencies: serde::de::IgnoredAny,

#[serde(default, skip_serializing)]
#[cfg_attr(feature = "schemars", schemars(skip))]
environments: serde::de::IgnoredAny,

#[serde(default, skip_serializing)]
#[cfg_attr(feature = "schemars", schemars(skip))]
managed: serde::de::IgnoredAny,
Expand Down
87 changes: 87 additions & 0 deletions crates/uv-workspace/src/environments.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::str::FromStr;

use serde::ser::SerializeSeq;

use pep508_rs::{MarkerTree, Pep508Error};

#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct SupportedEnvironments(Vec<MarkerTree>);

impl SupportedEnvironments {
/// Return the list of marker trees.
pub fn as_markers(&self) -> &[MarkerTree] {
&self.0
}

/// Convert the [`SupportedEnvironments`] struct into a list of marker trees.
pub fn into_markers(self) -> Vec<MarkerTree> {
self.0
}
}

/// Serialize a [`SupportedEnvironments`] struct into a list of marker strings.
impl serde::Serialize for SupportedEnvironments {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.0.len()))?;
for element in &self.0 {
if let Some(contents) = element.contents() {
seq.serialize_element(&contents)?;
}
}
seq.end()
}
}

/// Deserialize a marker string or list of marker strings into a [`SupportedEnvironments`] struct.
impl<'de> serde::Deserialize<'de> for SupportedEnvironments {
fn deserialize<D>(deserializer: D) -> Result<SupportedEnvironments, D::Error>
where
D: serde::Deserializer<'de>,
{
struct StringOrVecVisitor;

impl<'de> serde::de::Visitor<'de> for StringOrVecVisitor {
type Value = SupportedEnvironments;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or a list of strings")
}

fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
let marker = MarkerTree::from_str(value).map_err(serde::de::Error::custom)?;
Ok(SupportedEnvironments(vec![marker]))
}

fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut markers = Vec::new();

while let Some(elem) = seq.next_element::<String>()? {
let marker = MarkerTree::from_str(&elem).map_err(serde::de::Error::custom)?;
markers.push(marker);
}

Ok(SupportedEnvironments(markers))
}
}

deserializer.deserialize_any(StringOrVecVisitor)
}
}

/// Parse a marker string into a [`SupportedEnvironments`] struct.
impl FromStr for SupportedEnvironments {
type Err = Pep508Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
MarkerTree::parse_str(s).map(|markers| SupportedEnvironments(vec![markers]))
}
}
2 changes: 2 additions & 0 deletions crates/uv-workspace/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
pub use environments::SupportedEnvironments;
pub use workspace::{
check_nested_workspaces, DiscoveryOptions, ProjectWorkspace, VirtualProject, Workspace,
WorkspaceError, WorkspaceMember,
};

mod environments;
pub mod pyproject;
pub mod pyproject_mut;
mod workspace;
33 changes: 32 additions & 1 deletion crates/uv-workspace/src/pyproject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize};
use thiserror::Error;
use url::Url;

use crate::environments::SupportedEnvironments;
use pep440_rs::VersionSpecifiers;
use pypi_types::{RequirementSource, VerbatimParsedUrl};
use uv_git::GitReference;
Expand Down Expand Up @@ -98,19 +99,49 @@ pub struct ToolUv {
"#
)]
pub managed: Option<bool>,
/// The project's development dependencies. Development dependencies will be installed by
/// default in `uv run` and `uv sync`, but will not appear in the project's published metadata.
#[cfg_attr(
feature = "schemars",
schemars(
with = "Option<Vec<String>>",
description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
)
)]
#[option(
default = r#"[]"#,
value_type = "list[str]",
example = r#"
dev_dependencies = ["ruff==0.5.0"]
"#
)]
pub dev_dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
/// A list of supported environments against which to resolve dependencies.
///
/// By default, uv will resolve for all possible environments during a `uv lock` operation.
/// However, you can restrict the set of supported environments to improve performance and avoid
/// unsatisfiable branches in the solution space.
#[cfg_attr(
feature = "schemars",
schemars(
with = "Option<Vec<String>>",
description = "A list of environment markers, e.g. `python_version >= '3.6'`."
)
)]
#[option(
default = r#"[]"#,
value_type = "str | list[str]",
example = r#"
# Resolve for macOS, but not for Linux or Windows.
environments = ["sys_platform == 'darwin'"]
"#
)]
pub environments: Option<SupportedEnvironments>,
#[cfg_attr(
feature = "schemars",
schemars(
with = "Option<Vec<String>>",
description = "PEP 508 style requirements, e.g. `ruff==0.5.0`, or `ruff @ https://...`."
description = "PEP 508-style requirements, e.g. `ruff==0.5.0`, or `ruff @ https://...`."
)
)]
pub override_dependencies: Option<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>>,
Expand Down
18 changes: 17 additions & 1 deletion crates/uv-workspace/src/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ use tracing::{debug, trace, warn};
use pep508_rs::{MarkerTree, RequirementOrigin, VerbatimUrl};
use pypi_types::{Requirement, RequirementSource};
use uv_fs::{absolutize_path, normalize_path, relative_to, Simplified};
use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES};
use uv_normalize::{DEV_DEPENDENCIES, GroupName, PackageName};
use uv_warnings::warn_user;

use crate::environments::SupportedEnvironments;
use crate::pyproject::{Project, PyProjectToml, Source, ToolUvWorkspace};

#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -367,6 +368,21 @@ impl Workspace {
.collect()
}

/// Returns the set of supported environments for the workspace.
pub fn environments(&self) -> Option<&SupportedEnvironments> {
let workspace_package = self
.packages
.values()
.find(|workspace_package| workspace_package.root() == self.install_path())?;

workspace_package
.pyproject_toml()
.tool
.as_ref()
.and_then(|tool| tool.uv.as_ref())
.and_then(|uv| uv.environments.as_ref())
}

/// Returns the set of constraints for the workspace.
pub fn constraints(&self) -> Vec<Requirement> {
let Some(workspace_package) = self
Expand Down
Loading

0 comments on commit c4267c9

Please sign in to comment.