Skip to content

Commit

Permalink
Add req export
Browse files Browse the repository at this point in the history
  • Loading branch information
charliermarsh committed Aug 29, 2024
1 parent f2ce805 commit 150027f
Show file tree
Hide file tree
Showing 21 changed files with 1,610 additions and 105 deletions.
105 changes: 99 additions & 6 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use pep508_rs::Requirement;
use pypi_types::VerbatimParsedUrl;
use uv_cache::CacheArgs;
use uv_configuration::{
ConfigSettingEntry, IndexStrategy, KeyringProviderType, PackageNameSpecifier, TargetTriple,
TrustedHost,
ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier,
TargetTriple, TrustedHost,
};
use uv_normalize::{ExtraName, PackageName};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
Expand Down Expand Up @@ -654,6 +654,23 @@ pub enum ProjectCommand {
after_long_help = ""
)]
Lock(LockArgs),
/// Export the project's lockfile to an alternate format.
///
/// At present, only `requirements-txt` is supported.
///
/// The project is re-locked before exporting unless the `--locked` or `--frozen` flag is
/// provided.
///
/// uv will search for a project in the current directory or any parent directory. If a project
/// cannot be found, uv will exit with an error.
///
/// If operating in a workspace, the root will be exported by default; however, a specific
/// member can be selected using the `--package` option.
#[command(
after_help = "Use `uv help export` for more details.",
after_long_help = ""
)]
Export(ExportArgs),
/// Display the project's dependency tree.
Tree(TreeArgs),
}
Expand Down Expand Up @@ -2290,8 +2307,7 @@ pub struct RunArgs {

/// Run the command in a specific package in the workspace.
///
/// If not in a workspace, or if the workspace member does not exist, uv
/// will exit with an error.
/// If the workspace member does not exist, uv will exit with an error.
#[arg(long)]
pub package: Option<PackageName>,

Expand Down Expand Up @@ -2422,8 +2438,7 @@ pub struct SyncArgs {
/// The workspace's environment (`.venv`) is updated to reflect the subset
/// of dependencies declared by the specified workspace member package.
///
/// If not in a workspace, or if the workspace member does not exist, uv
/// will exit with an error.
/// If the workspace member does not exist, uv will exit with an error.
#[arg(long)]
pub package: Option<PackageName>,

Expand Down Expand Up @@ -2753,6 +2768,84 @@ pub struct TreeArgs {
pub python: Option<String>,
}

#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct ExportArgs {
/// The format to which `uv.lock` should be exported.
///
/// At present, only `requirements-txt` is supported.
#[arg(long, value_enum, default_value_t = ExportFormat::default())]
pub format: ExportFormat,

/// Export the dependencies for a specific package in the workspace.
///
/// If the workspace member does not exist, uv will exit with an error.
#[arg(long)]
pub package: Option<PackageName>,

/// Include optional dependencies from the extra group name.
///
/// May be provided more than once.
#[arg(long, conflicts_with = "all_extras", value_parser = extra_name_with_clap_error)]
pub extra: Option<Vec<ExtraName>>,

/// Include all optional dependencies.
#[arg(long, conflicts_with = "extra")]
pub all_extras: bool,

#[arg(long, overrides_with("all_extras"), hide = true)]
pub no_all_extras: bool,

/// Include development dependencies.
#[arg(long, overrides_with("no_dev"), hide = true)]
pub dev: bool,

/// Omit development dependencies.
#[arg(long, overrides_with("dev"))]
pub no_dev: bool,

/// Assert that the `uv.lock` will remain unchanged.
///
/// Requires that the lockfile is up-to-date. If the lockfile is missing or
/// needs to be updated, uv will exit with an error.
#[arg(long, conflicts_with = "frozen")]
pub locked: bool,

/// Do not update the `uv.lock` before exporting.
///
/// If a `uv.lock` does not exist, uv will exit with an error.
#[arg(long, conflicts_with = "locked")]
pub frozen: bool,

#[command(flatten)]
pub resolver: ResolverArgs,

#[command(flatten)]
pub build: BuildArgs,

#[command(flatten)]
pub refresh: RefreshArgs,

/// The Python interpreter to use during resolution.
///
/// A Python interpreter is required for building source distributions to
/// determine package metadata when there are not wheels.
///
/// The interpreter is also used as the fallback value for the minimum
/// Python version if `requires-python` is not set.
///
/// See `uv help python` for details on Python discovery and supported
/// request formats.
#[arg(
long,
short,
env = "UV_PYTHON",
verbatim_doc_comment,
help_heading = "Python options"
)]
pub python: Option<String>,
}

#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct ToolNamespace {
Expand Down
10 changes: 10 additions & 0 deletions crates/uv-configuration/src/export_format.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// The format to use when exporting a `uv.lock` file.
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum ExportFormat {
/// Export in `requirements.txt` format.
#[default]
RequirementsTxt,
}
2 changes: 2 additions & 0 deletions crates/uv-configuration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub use build_options::*;
pub use concurrency::*;
pub use config_settings::*;
pub use constraints::*;
pub use export_format::*;
pub use extras::*;
pub use hash::*;
pub use install_options::*;
Expand All @@ -19,6 +20,7 @@ mod build_options;
mod concurrency;
mod config_settings;
mod constraints;
mod export_format;
mod extras;
mod hash;
mod install_options;
Expand Down
81 changes: 81 additions & 0 deletions crates/uv-resolver/src/graph_ops.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
use pep508_rs::MarkerTree;
use petgraph::algo::greedy_feedback_arc_set;
use petgraph::visit::{EdgeRef, Topo};
use petgraph::{Directed, Direction, Graph};

/// A trait for a graph node that can be annotated with a [`MarkerTree`].
pub(crate) trait Markers {
fn set_markers(&mut self, markers: MarkerTree);
}

/// Propagate the [`MarkerTree`] qualifiers across the graph.
///
/// The graph is directed, so if any edge contains a marker, we need to propagate it to all
/// downstream nodes.
pub(crate) fn propagate_markers<T: Markers>(
mut graph: Graph<T, MarkerTree, Directed>,
) -> Graph<T, MarkerTree, Directed> {
// Remove any cycles. By absorption, it should be fine to ignore cycles.
//
// Imagine a graph: `A -> B -> C -> A`. Assume that `A` has weight `1`, `B` has weight `2`,
// and `C` has weight `3`. The weights are the marker trees.
//
// When propagating, we'd return to `A` when we hit the cycle, to create `1 or (1 and 2 and 3)`,
// which resolves to `1`.
//
// TODO(charlie): The above reasoning could be incorrect. Consider using a graph algorithm that
// can handle weight propagation with cycles.
let edges = {
let mut fas = greedy_feedback_arc_set(&graph)
.map(|edge| edge.id())
.collect::<Vec<_>>();
fas.sort_unstable();
let mut edges = Vec::with_capacity(fas.len());
for edge_id in fas.into_iter().rev() {
edges.push(graph.edge_endpoints(edge_id).unwrap());
graph.remove_edge(edge_id);
}
edges
};

let mut topo = Topo::new(&graph);
while let Some(index) = topo.next(&graph) {
let marker_tree = {
// Fold over the edges to combine the marker trees. If any edge is `None`, then
// the combined marker tree is `None`.
let mut edges = graph.edges_directed(index, Direction::Incoming);

edges
.next()
.and_then(|edge| graph.edge_weight(edge.id()).cloned())
.and_then(|initial| {
edges.try_fold(initial, |mut acc, edge| {
acc.or(graph.edge_weight(edge.id())?.clone());
Some(acc)
})
})
.unwrap_or_default()
};

// Propagate the marker tree to all downstream nodes.
let mut walker = graph
.neighbors_directed(index, Direction::Outgoing)
.detach();
while let Some((outgoing, _)) = walker.next(&graph) {
if let Some(weight) = graph.edge_weight_mut(outgoing) {
weight.and(marker_tree.clone());
}
}

let node = &mut graph[index];
node.set_markers(marker_tree);
}

// Re-add the removed edges. We no longer care about the edge _weights_, but we do want the
// edges to be present, to power the `# via` annotations.
for (source, target) in edges {
graph.add_edge(source, target, MarkerTree::TRUE);
}

graph
}
5 changes: 4 additions & 1 deletion crates/uv-resolver/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ pub use error::{NoSolutionError, NoSolutionHeader, ResolveError};
pub use exclude_newer::ExcludeNewer;
pub use exclusions::Exclusions;
pub use flat_index::FlatIndex;
pub use lock::{Lock, LockError, ResolverManifest, SatisfiesResult, TreeDisplay};
pub use lock::{
Lock, LockError, RequirementsTxtExport, ResolverManifest, SatisfiesResult, TreeDisplay,
};
pub use manifest::Manifest;
pub use options::{Options, OptionsBuilder};
pub use preferences::{Preference, PreferenceError, Preferences};
Expand Down Expand Up @@ -31,6 +33,7 @@ mod exclude_newer;
mod exclusions;
mod flat_index;
mod fork_urls;
mod graph_ops;
mod lock;
mod manifest;
mod marker;
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-resolver/src/lock/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_types::BuildContext;
use uv_workspace::{VirtualProject, Workspace};

pub use crate::lock::requirements_txt::RequirementsTxtExport;
pub use crate::lock::tree::TreeDisplay;
use crate::resolution::{AnnotatedDist, ResolutionGraphNode};
use crate::{ExcludeNewer, PrereleaseMode, RequiresPython, ResolutionGraph, ResolutionMode};

mod requirements_txt;
mod tree;

/// The current version of the lockfile format.
Expand Down
Loading

0 comments on commit 150027f

Please sign in to comment.