mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 12:24:15 +00:00
Add uv export --format requirements.txt (#6778)
## Summary The interface here is intentionally a bit more limited than `uv pip compile`, because we don't want `requirements.txt` to be a system of record -- it's just an export format. So, we don't write annotation comments (i.e., which dependency is requested from which), we don't allow writing extras, etc. It's just a flat list of requirements, with their markers and hashes. Closes #6007. Closes #6668. Closes #6670.
This commit is contained in:
parent
670e9603ee
commit
cbfc928a9c
21 changed files with 1610 additions and 105 deletions
81
crates/uv-resolver/src/graph_ops.rs
Normal file
81
crates/uv-resolver/src/graph_ops.rs
Normal file
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
@ -31,6 +33,7 @@ mod exclude_newer;
|
|||
mod exclusions;
|
||||
mod flat_index;
|
||||
mod fork_urls;
|
||||
mod graph_ops;
|
||||
mod lock;
|
||||
mod manifest;
|
||||
mod marker;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
257
crates/uv-resolver/src/lock/requirements_txt.rs
Normal file
257
crates/uv-resolver/src/lock/requirements_txt.rs
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
use std::collections::hash_map::Entry;
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt::Formatter;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use either::Either;
|
||||
use petgraph::{Directed, Graph};
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use url::Url;
|
||||
|
||||
use distribution_filename::{DistExtension, SourceDistExtension};
|
||||
use pep508_rs::MarkerTree;
|
||||
use pypi_types::{ParsedArchiveUrl, ParsedGitUrl};
|
||||
use uv_configuration::ExtrasSpecification;
|
||||
use uv_fs::Simplified;
|
||||
use uv_git::GitReference;
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
|
||||
use crate::graph_ops::{propagate_markers, Markers};
|
||||
use crate::lock::{Package, PackageId, Source};
|
||||
use crate::{Lock, LockError};
|
||||
|
||||
type LockGraph<'lock> = Graph<Node<'lock>, Edge, Directed>;
|
||||
|
||||
/// An export of a [`Lock`] that renders in `requirements.txt` format.
|
||||
#[derive(Debug)]
|
||||
pub struct RequirementsTxtExport<'lock>(LockGraph<'lock>);
|
||||
|
||||
impl<'lock> RequirementsTxtExport<'lock> {
|
||||
pub fn from_lock(
|
||||
lock: &'lock Lock,
|
||||
root_name: &PackageName,
|
||||
extras: &ExtrasSpecification,
|
||||
dev: &[GroupName],
|
||||
) -> Result<Self, LockError> {
|
||||
let size_guess = lock.packages.len();
|
||||
let mut petgraph = LockGraph::with_capacity(size_guess, size_guess);
|
||||
|
||||
let mut queue: VecDeque<(&Package, Option<&ExtraName>)> = VecDeque::new();
|
||||
let mut inverse = FxHashMap::default();
|
||||
|
||||
// Add the workspace package to the queue.
|
||||
let root = lock
|
||||
.find_by_name(root_name)
|
||||
.expect("found too many packages matching root")
|
||||
.expect("could not find root");
|
||||
|
||||
// Add the base package.
|
||||
queue.push_back((root, None));
|
||||
|
||||
// Add any extras.
|
||||
match extras {
|
||||
ExtrasSpecification::None => {}
|
||||
ExtrasSpecification::All => {
|
||||
for extra in root.optional_dependencies.keys() {
|
||||
queue.push_back((root, Some(extra)));
|
||||
}
|
||||
}
|
||||
ExtrasSpecification::Some(extras) => {
|
||||
for extra in extras {
|
||||
queue.push_back((root, Some(extra)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the root package to the graph.
|
||||
inverse.insert(&root.id, petgraph.add_node(Node::from_package(root)));
|
||||
|
||||
// Create all the relevant nodes.
|
||||
let mut seen = FxHashSet::default();
|
||||
|
||||
while let Some((package, extra)) = queue.pop_front() {
|
||||
let index = inverse[&package.id];
|
||||
|
||||
let deps = if let Some(extra) = extra {
|
||||
Either::Left(
|
||||
package
|
||||
.optional_dependencies
|
||||
.get(extra)
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
} else {
|
||||
Either::Right(package.dependencies.iter().chain(
|
||||
dev.iter().flat_map(|group| {
|
||||
package.dev_dependencies.get(group).into_iter().flatten()
|
||||
}),
|
||||
))
|
||||
};
|
||||
|
||||
for dep in deps {
|
||||
let dep_dist = lock.find_by_id(&dep.package_id);
|
||||
|
||||
// Add the dependency to the graph.
|
||||
if let Entry::Vacant(entry) = inverse.entry(&dep.package_id) {
|
||||
entry.insert(petgraph.add_node(Node::from_package(dep_dist)));
|
||||
}
|
||||
|
||||
// Add the edge.
|
||||
let dep_index = inverse[&dep.package_id];
|
||||
petgraph.add_edge(index, dep_index, dep.marker.clone());
|
||||
|
||||
// Push its dependencies on the queue.
|
||||
if seen.insert((&dep.package_id, None)) {
|
||||
queue.push_back((dep_dist, None));
|
||||
}
|
||||
for extra in &dep.extra {
|
||||
if seen.insert((&dep.package_id, Some(extra))) {
|
||||
queue.push_back((dep_dist, Some(extra)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let graph = propagate_markers(petgraph);
|
||||
|
||||
Ok(Self(graph))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RequirementsTxtExport<'_> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
// Collect all packages.
|
||||
let mut nodes = self
|
||||
.0
|
||||
.raw_nodes()
|
||||
.iter()
|
||||
.map(|node| &node.weight)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// Sort the nodes, such that unnamed URLs (editables) appear at the top.
|
||||
nodes.sort_unstable_by(|a, b| {
|
||||
NodeComparator::from(a.package).cmp(&NodeComparator::from(b.package))
|
||||
});
|
||||
|
||||
// Write out each node.
|
||||
for node in nodes {
|
||||
let Node { package, markers } = node;
|
||||
|
||||
match &package.id.source {
|
||||
Source::Registry(_) => {
|
||||
write!(f, "{}=={}", package.id.name, package.id.version)?;
|
||||
}
|
||||
Source::Git(url, git) => {
|
||||
// Remove the fragment and query from the URL; they're already present in the
|
||||
// `GitSource`.
|
||||
let mut url = url.to_url();
|
||||
url.set_fragment(None);
|
||||
url.set_query(None);
|
||||
|
||||
// Reconstruct the `GitUrl` from the `GitSource`.
|
||||
let git_url = uv_git::GitUrl::from_commit(
|
||||
url,
|
||||
GitReference::from(git.kind.clone()),
|
||||
git.precise,
|
||||
);
|
||||
|
||||
// Reconstruct the PEP 508-compatible URL from the `GitSource`.
|
||||
let url = Url::from(ParsedGitUrl {
|
||||
url: git_url.clone(),
|
||||
subdirectory: git.subdirectory.as_ref().map(PathBuf::from),
|
||||
});
|
||||
|
||||
write!(f, "{} @ {}", package.id.name, url)?;
|
||||
}
|
||||
Source::Direct(url, direct) => {
|
||||
let subdirectory = direct.subdirectory.as_ref().map(PathBuf::from);
|
||||
let url = Url::from(ParsedArchiveUrl {
|
||||
url: url.to_url(),
|
||||
subdirectory: subdirectory.clone(),
|
||||
ext: DistExtension::Source(SourceDistExtension::TarGz),
|
||||
});
|
||||
write!(f, "{} @ {}", package.id.name, url)?;
|
||||
}
|
||||
Source::Path(path) | Source::Directory(path) => {
|
||||
if path.as_os_str().is_empty() {
|
||||
write!(f, ".")?;
|
||||
} else if path.is_absolute() {
|
||||
write!(f, "{}", Url::from_file_path(path).unwrap())?;
|
||||
} else {
|
||||
write!(f, "{}", path.portable_display())?;
|
||||
}
|
||||
}
|
||||
Source::Editable(path) => {
|
||||
if path.as_os_str().is_empty() {
|
||||
write!(f, "-e .")?;
|
||||
} else {
|
||||
write!(f, "-e {}", path.portable_display())?;
|
||||
}
|
||||
}
|
||||
Source::Virtual(_) => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(contents) = markers.contents() {
|
||||
write!(f, " ; {contents}")?;
|
||||
}
|
||||
|
||||
let hashes = package.hashes();
|
||||
if !hashes.is_empty() {
|
||||
for hash in &hashes {
|
||||
writeln!(f, " \\")?;
|
||||
write!(f, " --hash=")?;
|
||||
write!(f, "{hash}")?;
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The nodes of the [`LockGraph`].
|
||||
#[derive(Debug)]
|
||||
struct Node<'lock> {
|
||||
package: &'lock Package,
|
||||
markers: MarkerTree,
|
||||
}
|
||||
|
||||
impl<'lock> Node<'lock> {
|
||||
/// Construct a [`Node`] from a [`Package`].
|
||||
fn from_package(package: &'lock Package) -> Self {
|
||||
Self {
|
||||
package,
|
||||
markers: MarkerTree::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Markers for Node<'_> {
|
||||
fn set_markers(&mut self, markers: MarkerTree) {
|
||||
self.markers = markers;
|
||||
}
|
||||
}
|
||||
|
||||
/// The edges of the [`LockGraph`].
|
||||
type Edge = MarkerTree;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
enum NodeComparator<'lock> {
|
||||
Editable(&'lock Path),
|
||||
Path(&'lock Path),
|
||||
Package(&'lock PackageId),
|
||||
}
|
||||
|
||||
impl<'lock> From<&'lock Package> for NodeComparator<'lock> {
|
||||
fn from(value: &'lock Package) -> Self {
|
||||
match &value.id.source {
|
||||
Source::Path(path) | Source::Directory(path) => Self::Path(path),
|
||||
Source::Editable(path) => Self::Editable(path),
|
||||
_ => Self::Package(&value.id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
use std::collections::BTreeSet;
|
||||
|
||||
use owo_colors::OwoColorize;
|
||||
use petgraph::algo::greedy_feedback_arc_set;
|
||||
use petgraph::visit::{EdgeRef, Topo};
|
||||
use petgraph::visit::EdgeRef;
|
||||
use petgraph::Direction;
|
||||
use rustc_hash::{FxBuildHasher, FxHashMap};
|
||||
|
||||
|
|
@ -10,6 +9,7 @@ use distribution_types::{DistributionMetadata, Name, SourceAnnotation, SourceAnn
|
|||
use pep508_rs::MarkerTree;
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
use crate::graph_ops::{propagate_markers, Markers};
|
||||
use crate::resolution::{RequirementsTxtDist, ResolutionGraphNode};
|
||||
use crate::{ResolutionGraph, ResolverMarkers};
|
||||
|
||||
|
|
@ -49,6 +49,14 @@ enum DisplayResolutionGraphNode {
|
|||
Dist(RequirementsTxtDist),
|
||||
}
|
||||
|
||||
impl Markers for DisplayResolutionGraphNode {
|
||||
fn set_markers(&mut self, markers: MarkerTree) {
|
||||
if let DisplayResolutionGraphNode::Dist(node) = self {
|
||||
node.markers = markers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ResolutionGraph> for DisplayResolutionGraph<'a> {
|
||||
fn from(resolution: &'a ResolutionGraph) -> Self {
|
||||
Self::new(
|
||||
|
|
@ -348,77 +356,6 @@ fn to_requirements_txt_graph(graph: &ResolutionPetGraph) -> IntermediatePetGraph
|
|||
next
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn propagate_markers(mut graph: IntermediatePetGraph) -> IntermediatePetGraph {
|
||||
// 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: Option<MarkerTree> = {
|
||||
// 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)
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
// Propagate the marker tree to all downstream nodes.
|
||||
if let Some(marker_tree) = marker_tree.as_ref() {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let DisplayResolutionGraphNode::Dist(node) = &mut graph[index] {
|
||||
node.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
|
||||
}
|
||||
|
||||
/// Reduce the graph, such that all nodes for a single package are combined, regardless of
|
||||
/// the extras.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ pub(crate) type MarkersForDistribution = FxHashMap<(Version, Option<VerbatimUrl>
|
|||
|
||||
/// A complete resolution graph in which every node represents a pinned package and every edge
|
||||
/// represents a dependency between two pinned packages.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ResolutionGraph {
|
||||
/// The underlying graph.
|
||||
pub(crate) petgraph: Graph<ResolutionGraphNode, MarkerTree, Directed>,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pub(crate) struct RequirementsTxtDist {
|
|||
pub(crate) version: Version,
|
||||
pub(crate) extras: Vec<ExtraName>,
|
||||
pub(crate) hashes: Vec<HashDigest>,
|
||||
pub(crate) markers: Option<MarkerTree>,
|
||||
pub(crate) markers: MarkerTree,
|
||||
}
|
||||
|
||||
impl RequirementsTxtDist {
|
||||
|
|
@ -89,11 +89,8 @@ impl RequirementsTxtDist {
|
|||
}
|
||||
};
|
||||
if let Some(given) = given {
|
||||
return if let Some(markers) = self
|
||||
.markers
|
||||
.as_ref()
|
||||
.filter(|_| include_markers)
|
||||
.and_then(MarkerTree::contents)
|
||||
return if let Some(markers) =
|
||||
self.markers.contents().filter(|_| include_markers)
|
||||
{
|
||||
Cow::Owned(format!("{given} ; {markers}"))
|
||||
} else {
|
||||
|
|
@ -104,12 +101,7 @@ impl RequirementsTxtDist {
|
|||
}
|
||||
|
||||
if self.extras.is_empty() || !include_extras {
|
||||
if let Some(markers) = self
|
||||
.markers
|
||||
.as_ref()
|
||||
.filter(|_| include_markers)
|
||||
.and_then(MarkerTree::contents)
|
||||
{
|
||||
if let Some(markers) = self.markers.contents().filter(|_| include_markers) {
|
||||
Cow::Owned(format!("{} ; {}", self.dist.verbatim(), markers))
|
||||
} else {
|
||||
self.dist.verbatim()
|
||||
|
|
@ -118,12 +110,7 @@ impl RequirementsTxtDist {
|
|||
let mut extras = self.extras.clone();
|
||||
extras.sort_unstable();
|
||||
extras.dedup();
|
||||
if let Some(markers) = self
|
||||
.markers
|
||||
.as_ref()
|
||||
.filter(|_| include_markers)
|
||||
.and_then(MarkerTree::contents)
|
||||
{
|
||||
if let Some(markers) = self.markers.contents().filter(|_| include_markers) {
|
||||
Cow::Owned(format!(
|
||||
"{}[{}]{} ; {}",
|
||||
self.name(),
|
||||
|
|
@ -176,7 +163,7 @@ impl From<&AnnotatedDist> for RequirementsTxtDist {
|
|||
vec![]
|
||||
},
|
||||
hashes: annotated.hashes.clone(),
|
||||
markers: None,
|
||||
markers: MarkerTree::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue