Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 1 addition & 12 deletions crates/uv-distribution-types/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ pub struct Resolution {
}

impl Resolution {
/// Create a new resolution from the given pinned packages.
/// Create a [`Resolution`] from the given pinned packages.
pub fn new(graph: petgraph::graph::DiGraph<Node, Edge>) -> Self {
Self {
graph,
Expand Down Expand Up @@ -208,17 +208,6 @@ pub enum Edge {
Dev(GroupName, MarkerTree),
}

impl Edge {
/// Return the [`MarkerTree`] for this edge.
pub fn marker(&self) -> &MarkerTree {
match self {
Self::Prod(marker) => marker,
Self::Optional(_, marker) => marker,
Self::Dev(_, marker) => marker,
}
}
}

impl From<&ResolvedDist> for RequirementSource {
fn from(resolved_dist: &ResolvedDist) -> Self {
match resolved_dist {
Expand Down
62 changes: 50 additions & 12 deletions crates/uv-requirements/src/sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub enum RequirementsSource {
Package(RequirementsTxtRequirement),
/// An editable path was provided on the command line (e.g., `pip install -e ../flask`).
Editable(RequirementsTxtRequirement),
/// Dependencies were provided via a `pylock.toml` file.
PylockToml(PathBuf),
/// Dependencies were provided via a `requirements.txt` file (e.g., `pip install -r requirements.txt`).
RequirementsTxt(PathBuf),
/// Dependencies were provided via a `pyproject.toml` file (e.g., `pip-compile pyproject.toml`).
Expand All @@ -39,19 +41,32 @@ impl RequirementsSource {
Self::SetupCfg(path)
} else if path.ends_with("environment.yml") {
Self::EnvironmentYml(path)
} else if path
.file_name()
.is_some_and(|file_name| file_name.to_str().is_some_and(is_pylock_toml))
{
Self::PylockToml(path)
} else {
Self::RequirementsTxt(path)
}
}

/// Parse a [`RequirementsSource`] from a `requirements.txt` file.
pub fn from_requirements_txt(path: PathBuf) -> Self {
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(filename) {
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(file_name) {
warn_user!(
"The file `{}` appears to be a `{}` file, but requirements must be specified in `requirements.txt` format.",
path.user_display(),
filename
file_name
);
}
}
if let Some(file_name) = path.file_name() {
if file_name.to_str().is_some_and(is_pylock_toml) {
warn_user!(
"The file `{}` appears to be a `pylock.toml` file, but requirements must be specified in `requirements.txt` format.",
path.user_display(),
);
}
}
Expand All @@ -60,12 +75,20 @@ impl RequirementsSource {

/// Parse a [`RequirementsSource`] from a `constraints.txt` file.
pub fn from_constraints_txt(path: PathBuf) -> Self {
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(filename) {
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(file_name) {
warn_user!(
"The file `{}` appears to be a `{}` file, but constraints must be specified in `requirements.txt` format.",
path.user_display(),
filename
file_name
);
}
}
if let Some(file_name) = path.file_name() {
if file_name.to_str().is_some_and(is_pylock_toml) {
warn_user!(
"The file `{}` appears to be a `pylock.toml` file, but constraints must be specified in `requirements.txt` format.",
path.user_display(),
);
}
}
Expand All @@ -74,12 +97,20 @@ impl RequirementsSource {

/// Parse a [`RequirementsSource`] from an `overrides.txt` file.
pub fn from_overrides_txt(path: PathBuf) -> Self {
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(filename) {
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
if path.ends_with(file_name) {
warn_user!(
"The file `{}` appears to be a `{}` file, but overrides must be specified in `requirements.txt` format.",
path.user_display(),
filename
file_name
);
}
}
if let Some(file_name) = path.file_name() {
if file_name.to_str().is_some_and(is_pylock_toml) {
warn_user!(
"The file `{}` appears to be a `pylock.toml` file, but overrides must be specified in `requirements.txt` format.",
path.user_display(),
);
}
}
Expand Down Expand Up @@ -110,7 +141,10 @@ impl RequirementsSource {

// Similarly, if the user provided a `pyproject.toml` file without `-r` (as in
// `uv pip install pyproject.toml`), prompt them to correct it.
if (name == "pyproject.toml" || name == "setup.py" || name == "setup.cfg")
if (name == "pyproject.toml"
|| name == "setup.py"
|| name == "setup.cfg"
|| is_pylock_toml(name))
&& Path::new(&name).is_file()
{
let term = Term::stderr();
Expand Down Expand Up @@ -155,7 +189,10 @@ impl RequirementsSource {

// Similarly, if the user provided a `pyproject.toml` file without `--with-requirements` (as in
// `uvx --with pyproject.toml ruff`), prompt them to correct it.
if (name == "pyproject.toml" || name == "setup.py" || name == "setup.cfg")
if (name == "pyproject.toml"
|| name == "setup.py"
|| name == "setup.cfg"
|| is_pylock_toml(name))
&& Path::new(&name).is_file()
{
let term = Term::stderr();
Expand Down Expand Up @@ -217,7 +254,8 @@ impl std::fmt::Display for RequirementsSource {
match self {
Self::Package(package) => write!(f, "{package:?}"),
Self::Editable(path) => write!(f, "-e {path:?}"),
Self::RequirementsTxt(path)
Self::PylockToml(path)
| Self::RequirementsTxt(path)
| Self::PyprojectToml(path)
| Self::SetupPy(path)
| Self::SetupCfg(path)
Expand Down
85 changes: 84 additions & 1 deletion crates/uv-requirements/src/specification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ pub struct RequirementsSpecification {
pub constraints: Vec<NameRequirementSpecification>,
/// The overrides for the project.
pub overrides: Vec<UnresolvedRequirementSpecification>,
/// The `pylock.toml` file from which to extract the resolution.
pub pylock: Option<PathBuf>,
/// The source trees from which to extract requirements.
pub source_trees: Vec<PathBuf>,
/// The groups to use for `source_trees`
Expand Down Expand Up @@ -190,6 +192,16 @@ impl RequirementsSpecification {
..Self::default()
}
}
RequirementsSource::PylockToml(path) => {
if !path.is_file() {
return Err(anyhow::anyhow!("File not found: `{}`", path.user_display()));
}

Self {
pylock: Some(path.clone()),
..Self::default()
}
}
RequirementsSource::SourceTree(path) => {
if !path.is_dir() {
return Err(anyhow::anyhow!(
Expand Down Expand Up @@ -231,7 +243,66 @@ impl RequirementsSpecification {
) -> Result<Self> {
let mut spec = Self::default();

// Resolve sources into specifications so we know their `source_tree`s∂
// Disallow `pylock.toml` files as constraints.
if let Some(pylock_toml) = constraints.iter().find_map(|source| {
if let RequirementsSource::PylockToml(path) = source {
Some(path)
} else {
None
}
}) {
return Err(anyhow::anyhow!(
"Cannot use `{}` as a constraint file",
pylock_toml.user_display()
));
}

// Disallow `pylock.toml` files as overrides.
if let Some(pylock_toml) = overrides.iter().find_map(|source| {
if let RequirementsSource::PylockToml(path) = source {
Some(path)
} else {
None
}
}) {
return Err(anyhow::anyhow!(
"Cannot use `{}` as an override file",
pylock_toml.user_display()
));
}

// If we have a `pylock.toml`, don't allow additional requirements, constraints, or
// overrides.
if requirements
.iter()
.any(|source| matches!(source, RequirementsSource::PylockToml(..)))
{
if requirements
.iter()
.any(|source| !matches!(source, RequirementsSource::PylockToml(..)))
{
return Err(anyhow::anyhow!(
"Cannot specify additional requirements alongside a `pylock.toml` file",
));
}
if !constraints.is_empty() {
return Err(anyhow::anyhow!(
"Cannot specify additional requirements with a `pylock.toml` file"
));
}
if !overrides.is_empty() {
return Err(anyhow::anyhow!(
"Cannot specify constraints with a `pylock.toml` file"
));
}
if !groups.is_empty() {
return Err(anyhow::anyhow!(
"Cannot specify groups with a `pylock.toml` file"
));
}
}

// Resolve sources into specifications so we know their `source_tree`.
let mut requirement_sources = Vec::new();
for source in requirements {
let source = Self::from_source(source, client_builder).await?;
Expand Down Expand Up @@ -301,6 +372,18 @@ impl RequirementsSpecification {
spec.extras.extend(source.extras);
spec.source_trees.extend(source.source_trees);

// Allow at most one `pylock.toml`.
if let Some(pylock) = source.pylock {
if let Some(existing) = spec.pylock {
return Err(anyhow::anyhow!(
"Multiple `pylock.toml` files specified: `{}` vs. `{}`",
existing.user_display(),
pylock.user_display()
));
}
spec.pylock = Some(pylock);
}

// Use the first project name discovered.
if spec.project.is_none() {
spec.project = source.project;
Expand Down
Loading
Loading