Skip to content

Commit 84cd136

Browse files
committed
Start to impl
1 parent c4261fe commit 84cd136

File tree

10 files changed

+1517
-143
lines changed

10 files changed

+1517
-143
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: 56 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`).
@@ -27,6 +29,12 @@ pub enum RequirementsSource {
2729
EnvironmentYml(PathBuf),
2830
}
2931

32+
/// Returns `true` if a file name matches the `pylock.toml` pattern defined in PEP 751.
33+
#[allow(clippy::case_sensitive_file_extension_comparisons)]
34+
fn is_pylock_toml(file_name: &str) -> bool {
35+
file_name.starts_with("pylock.") && file_name.ends_with(".toml")
36+
}
37+
3038
impl RequirementsSource {
3139
/// Parse a [`RequirementsSource`] from a [`PathBuf`]. The file type is determined by the file
3240
/// extension.
@@ -39,19 +47,32 @@ impl RequirementsSource {
3947
Self::SetupCfg(path)
4048
} else if path.ends_with("environment.yml") {
4149
Self::EnvironmentYml(path)
50+
} else if path
51+
.file_name()
52+
.is_some_and(|file_name| file_name.to_str().is_some_and(is_pylock_toml))
53+
{
54+
Self::PylockToml(path)
4255
} else {
4356
Self::RequirementsTxt(path)
4457
}
4558
}
4659

4760
/// Parse a [`RequirementsSource`] from a `requirements.txt` file.
4861
pub fn from_requirements_txt(path: PathBuf) -> Self {
49-
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
50-
if path.ends_with(filename) {
62+
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
63+
if path.ends_with(file_name) {
5164
warn_user!(
5265
"The file `{}` appears to be a `{}` file, but requirements must be specified in `requirements.txt` format.",
5366
path.user_display(),
54-
filename
67+
file_name
68+
);
69+
}
70+
}
71+
if let Some(file_name) = path.file_name() {
72+
if file_name.to_str().is_some_and(is_pylock_toml) {
73+
warn_user!(
74+
"The file `{}` appears to be a `pylock.toml` file, but requirements must be specified in `requirements.txt` format.",
75+
path.user_display(),
5576
);
5677
}
5778
}
@@ -60,12 +81,20 @@ impl RequirementsSource {
6081

6182
/// Parse a [`RequirementsSource`] from a `constraints.txt` file.
6283
pub fn from_constraints_txt(path: PathBuf) -> Self {
63-
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
64-
if path.ends_with(filename) {
84+
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
85+
if path.ends_with(file_name) {
6586
warn_user!(
6687
"The file `{}` appears to be a `{}` file, but constraints must be specified in `requirements.txt` format.",
6788
path.user_display(),
68-
filename
89+
file_name
90+
);
91+
}
92+
}
93+
if let Some(file_name) = path.file_name() {
94+
if file_name.to_str().is_some_and(is_pylock_toml) {
95+
warn_user!(
96+
"The file `{}` appears to be a `pylock.toml` file, but constraints must be specified in `requirements.txt` format.",
97+
path.user_display(),
6998
);
7099
}
71100
}
@@ -74,12 +103,20 @@ impl RequirementsSource {
74103

75104
/// Parse a [`RequirementsSource`] from an `overrides.txt` file.
76105
pub fn from_overrides_txt(path: PathBuf) -> Self {
77-
for filename in ["pyproject.toml", "setup.py", "setup.cfg"] {
78-
if path.ends_with(filename) {
106+
for file_name in ["pyproject.toml", "setup.py", "setup.cfg"] {
107+
if path.ends_with(file_name) {
79108
warn_user!(
80109
"The file `{}` appears to be a `{}` file, but overrides must be specified in `requirements.txt` format.",
81110
path.user_display(),
82-
filename
111+
file_name
112+
);
113+
}
114+
}
115+
if let Some(file_name) = path.file_name() {
116+
if file_name.to_str().is_some_and(is_pylock_toml) {
117+
warn_user!(
118+
"The file `{}` appears to be a `pylock.toml` file, but overrides must be specified in `requirements.txt` format.",
119+
path.user_display(),
83120
);
84121
}
85122
}
@@ -110,7 +147,10 @@ impl RequirementsSource {
110147

111148
// Similarly, if the user provided a `pyproject.toml` file without `-r` (as in
112149
// `uv pip install pyproject.toml`), prompt them to correct it.
113-
if (name == "pyproject.toml" || name == "setup.py" || name == "setup.cfg")
150+
if (name == "pyproject.toml"
151+
|| name == "setup.py"
152+
|| name == "setup.cfg"
153+
|| is_pylock_toml(name))
114154
&& Path::new(&name).is_file()
115155
{
116156
let term = Term::stderr();
@@ -155,7 +195,10 @@ impl RequirementsSource {
155195

156196
// Similarly, if the user provided a `pyproject.toml` file without `--with-requirements` (as in
157197
// `uvx --with pyproject.toml ruff`), prompt them to correct it.
158-
if (name == "pyproject.toml" || name == "setup.py" || name == "setup.cfg")
198+
if (name == "pyproject.toml"
199+
|| name == "setup.py"
200+
|| name == "setup.cfg"
201+
|| is_pylock_toml(name))
159202
&& Path::new(&name).is_file()
160203
{
161204
let term = Term::stderr();
@@ -217,7 +260,8 @@ impl std::fmt::Display for RequirementsSource {
217260
match self {
218261
Self::Package(package) => write!(f, "{package:?}"),
219262
Self::Editable(path) => write!(f, "-e {path:?}"),
220-
Self::RequirementsTxt(path)
263+
Self::PylockToml(path)
264+
| Self::RequirementsTxt(path)
221265
| Self::PyprojectToml(path)
222266
| Self::SetupPy(path)
223267
| 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)