Skip to content

Commit e089c42

Browse files
Add pylock.toml to uv pip install and uv pip sync (#12992)
## Summary We accept `pylock.toml` as a requirements file (e.g., `uv sync pylock.toml` or `uv pip install -r pylock.toml`). When you provide a `pylock.toml` file, we don't allow you to provide other requirements, or constraints, etc. And you can only provide one `pylock.toml` file, not multiple. We might want to remove this from `uv pip install` for now, since `pip` may end up with a different interface (whereas `uv pip sync` is already specific to uv), and most of the arguments aren't applicable (like `--resolution`, etc.). Regardless, it's behind `--preview` for both commands.
1 parent 05c4092 commit e089c42

File tree

10 files changed

+1572
-148
lines changed

10 files changed

+1572
-148
lines changed

crates/uv-distribution-types/src/resolution.rs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ pub struct Resolution {
1919
}
2020

2121
impl Resolution {
22-
/// Create a new resolution from the given pinned packages.
22+
/// Create a [`Resolution`] from the given pinned packages.
2323
pub fn new(graph: petgraph::graph::DiGraph<Node, Edge>) -> Self {
2424
Self {
2525
graph,
@@ -208,17 +208,6 @@ pub enum Edge {
208208
Dev(GroupName, MarkerTree),
209209
}
210210

211-
impl Edge {
212-
/// Return the [`MarkerTree`] for this edge.
213-
pub fn marker(&self) -> &MarkerTree {
214-
match self {
215-
Self::Prod(marker) => marker,
216-
Self::Optional(_, marker) => marker,
217-
Self::Dev(_, marker) => marker,
218-
}
219-
}
220-
}
221-
222211
impl From<&ResolvedDist> for RequirementSource {
223212
fn from(resolved_dist: &ResolvedDist) -> Self {
224213
match resolved_dist {

crates/uv-requirements/src/sources.rs

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ pub enum RequirementsSource {
1313
Package(RequirementsTxtRequirement),
1414
/// An editable path was provided on the command line (e.g., `pip install -e ../flask`).
1515
Editable(RequirementsTxtRequirement),
16+
/// Dependencies were provided via a `pylock.toml` file.
17+
PylockToml(PathBuf),
1618
/// Dependencies were provided via a `requirements.txt` file (e.g., `pip install -r requirements.txt`).
1719
RequirementsTxt(PathBuf),
1820
/// Dependencies were provided via a `pyproject.toml` file (e.g., `pip-compile pyproject.toml`).
@@ -39,19 +41,32 @@ impl RequirementsSource {
3941
Self::SetupCfg(path)
4042
} else if path.ends_with("environment.yml") {
4143
Self::EnvironmentYml(path)
44+
} else if path
45+
.file_name()
46+
.is_some_and(|file_name| file_name.to_str().is_some_and(is_pylock_toml))
47+
{
48+
Self::PylockToml(path)
4249
} else {
4350
Self::RequirementsTxt(path)
4451
}
4552
}
4653

4754
/// Parse a [`RequirementsSource`] from a `requirements.txt` file.
4855
pub fn from_requirements_txt(path: PathBuf) -> Self {
49-
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
50-
if path.ends_with(filename) {
56+
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
57+
if path.ends_with(file_name) {
5158
warn_user!(
5259
"The file `{}` appears to be a `{}` file, but requirements must be specified in `requirements.txt` format.",
5360
path.user_display(),
54-
filename
61+
file_name
62+
);
63+
}
64+
}
65+
if let Some(file_name) = path.file_name() {
66+
if file_name.to_str().is_some_and(is_pylock_toml) {
67+
warn_user!(
68+
"The file `{}` appears to be a `pylock.toml` file, but requirements must be specified in `requirements.txt` format.",
69+
path.user_display(),
5570
);
5671
}
5772
}
@@ -60,12 +75,20 @@ impl RequirementsSource {
6075

6176
/// Parse a [`RequirementsSource`] from a `constraints.txt` file.
6277
pub fn from_constraints_txt(path: PathBuf) -> Self {
63-
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
64-
if path.ends_with(filename) {
78+
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
79+
if path.ends_with(file_name) {
6580
warn_user!(
6681
"The file `{}` appears to be a `{}` file, but constraints must be specified in `requirements.txt` format.",
6782
path.user_display(),
68-
filename
83+
file_name
84+
);
85+
}
86+
}
87+
if let Some(file_name) = path.file_name() {
88+
if file_name.to_str().is_some_and(is_pylock_toml) {
89+
warn_user!(
90+
"The file `{}` appears to be a `pylock.toml` file, but constraints must be specified in `requirements.txt` format.",
91+
path.user_display(),
6992
);
7093
}
7194
}
@@ -74,12 +97,20 @@ impl RequirementsSource {
7497

7598
/// Parse a [`RequirementsSource`] from an `overrides.txt` file.
7699
pub fn from_overrides_txt(path: PathBuf) -> Self {
77-
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
78-
if path.ends_with(filename) {
100+
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
101+
if path.ends_with(file_name) {
79102
warn_user!(
80103
"The file `{}` appears to be a `{}` file, but overrides must be specified in `requirements.txt` format.",
81104
path.user_display(),
82-
filename
105+
file_name
106+
);
107+
}
108+
}
109+
if let Some(file_name) = path.file_name() {
110+
if file_name.to_str().is_some_and(is_pylock_toml) {
111+
warn_user!(
112+
"The file `{}` appears to be a `pylock.toml` file, but overrides must be specified in `requirements.txt` format.",
113+
path.user_display(),
83114
);
84115
}
85116
}
@@ -110,7 +141,10 @@ impl RequirementsSource {
110141

111142
// Similarly, if the user provided a `pyproject.toml` file without `-r` (as in
112143
// `uv pip install pyproject.toml`), prompt them to correct it.
113-
if (name == "pyproject.toml" || name == "setup.py" || name == "setup.cfg")
144+
if (name == "pyproject.toml"
145+
|| name == "setup.py"
146+
|| name == "setup.cfg"
147+
|| is_pylock_toml(name))
114148
&& Path::new(&name).is_file()
115149
{
116150
let term = Term::stderr();
@@ -155,7 +189,10 @@ impl RequirementsSource {
155189

156190
// Similarly, if the user provided a `pyproject.toml` file without `--with-requirements` (as in
157191
// `uvx --with pyproject.toml ruff`), prompt them to correct it.
158-
if (name == "pyproject.toml" || name == "setup.py" || name == "setup.cfg")
192+
if (name == "pyproject.toml"
193+
|| name == "setup.py"
194+
|| name == "setup.cfg"
195+
|| is_pylock_toml(name))
159196
&& Path::new(&name).is_file()
160197
{
161198
let term = Term::stderr();
@@ -217,7 +254,8 @@ impl std::fmt::Display for RequirementsSource {
217254
match self {
218255
Self::Package(package) => write!(f, "{package:?}"),
219256
Self::Editable(path) => write!(f, "-e {path:?}"),
220-
Self::RequirementsTxt(path)
257+
Self::PylockToml(path)
258+
| Self::RequirementsTxt(path)
221259
| Self::PyprojectToml(path)
222260
| Self::SetupPy(path)
223261
| Self::SetupCfg(path)

crates/uv-requirements/src/specification.rs

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ pub struct RequirementsSpecification {
6161
pub constraints: Vec<NameRequirementSpecification>,
6262
/// The overrides for the project.
6363
pub overrides: Vec<UnresolvedRequirementSpecification>,
64+
/// The `pylock.toml` file from which to extract the resolution.
65+
pub pylock: Option<PathBuf>,
6466
/// The source trees from which to extract requirements.
6567
pub source_trees: Vec<PathBuf>,
6668
/// The groups to use for `source_trees`
@@ -190,6 +192,16 @@ impl RequirementsSpecification {
190192
..Self::default()
191193
}
192194
}
195+
RequirementsSource::PylockToml(path) => {
196+
if !path.is_file() {
197+
return Err(anyhow::anyhow!("File not found: `{}`", path.user_display()));
198+
}
199+
200+
Self {
201+
pylock: Some(path.clone()),
202+
..Self::default()
203+
}
204+
}
193205
RequirementsSource::SourceTree(path) => {
194206
if !path.is_dir() {
195207
return Err(anyhow::anyhow!(
@@ -231,7 +243,66 @@ impl RequirementsSpecification {
231243
) -> Result<Self> {
232244
let mut spec = Self::default();
233245

234-
// Resolve sources into specifications so we know their `source_tree`s∂
246+
// Disallow `pylock.toml` files as constraints.
247+
if let Some(pylock_toml) = constraints.iter().find_map(|source| {
248+
if let RequirementsSource::PylockToml(path) = source {
249+
Some(path)
250+
} else {
251+
None
252+
}
253+
}) {
254+
return Err(anyhow::anyhow!(
255+
"Cannot use `{}` as a constraint file",
256+
pylock_toml.user_display()
257+
));
258+
}
259+
260+
// Disallow `pylock.toml` files as overrides.
261+
if let Some(pylock_toml) = overrides.iter().find_map(|source| {
262+
if let RequirementsSource::PylockToml(path) = source {
263+
Some(path)
264+
} else {
265+
None
266+
}
267+
}) {
268+
return Err(anyhow::anyhow!(
269+
"Cannot use `{}` as an override file",
270+
pylock_toml.user_display()
271+
));
272+
}
273+
274+
// If we have a `pylock.toml`, don't allow additional requirements, constraints, or
275+
// overrides.
276+
if requirements
277+
.iter()
278+
.any(|source| matches!(source, RequirementsSource::PylockToml(..)))
279+
{
280+
if requirements
281+
.iter()
282+
.any(|source| !matches!(source, RequirementsSource::PylockToml(..)))
283+
{
284+
return Err(anyhow::anyhow!(
285+
"Cannot specify additional requirements alongside a `pylock.toml` file",
286+
));
287+
}
288+
if !constraints.is_empty() {
289+
return Err(anyhow::anyhow!(
290+
"Cannot specify additional requirements with a `pylock.toml` file"
291+
));
292+
}
293+
if !overrides.is_empty() {
294+
return Err(anyhow::anyhow!(
295+
"Cannot specify constraints with a `pylock.toml` file"
296+
));
297+
}
298+
if !groups.is_empty() {
299+
return Err(anyhow::anyhow!(
300+
"Cannot specify groups with a `pylock.toml` file"
301+
));
302+
}
303+
}
304+
305+
// Resolve sources into specifications so we know their `source_tree`.
235306
let mut requirement_sources = Vec::new();
236307
for source in requirements {
237308
let source = Self::from_source(source, client_builder).await?;
@@ -301,6 +372,18 @@ impl RequirementsSpecification {
301372
spec.extras.extend(source.extras);
302373
spec.source_trees.extend(source.source_trees);
303374

375+
// Allow at most one `pylock.toml`.
376+
if let Some(pylock) = source.pylock {
377+
if let Some(existing) = spec.pylock {
378+
return Err(anyhow::anyhow!(
379+
"Multiple `pylock.toml` files specified: `{}` vs. `{}`",
380+
existing.user_display(),
381+
pylock.user_display()
382+
));
383+
}
384+
spec.pylock = Some(pylock);
385+
}
386+
304387
// Use the first project name discovered.
305388
if spec.project.is_none() {
306389
spec.project = source.project;

0 commit comments

Comments
 (0)