mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-01 14:31:12 +00:00
Write relative paths with unnamed requirement syntax (#3682)
## Summary This PR falls back to writing an unnamed requirement if it appears to be a relative URL. pip is way more flexible when providing an unnamed requirement than when providing a PEP 508 requirement. For example, _only_ this works: ``` black @ file:///Users/crmarsh/workspace/uv/scripts/packages/black_editable ``` Any other form will fail. Meanwhile, _all_ of these work: ``` file:///Users/crmarsh/workspace/uv/scripts/packages/black_editable scripts/packages/black_editable ./scripts/packages/black_editable file:./scripts/packages/black_editable file:scripts/packages/black_editable ``` Closes https://github.com/astral-sh/uv/issues/3180.
This commit is contained in:
parent
0362918196
commit
49f0e84f3d
3 changed files with 99 additions and 41 deletions
|
@ -1,14 +1,11 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
|
|
||||||
use itertools::Itertools;
|
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use petgraph::visit::EdgeRef;
|
use petgraph::visit::EdgeRef;
|
||||||
use petgraph::Direction;
|
use petgraph::Direction;
|
||||||
|
|
||||||
use distribution_types::{
|
use distribution_types::{IndexUrl, LocalEditable, Name, SourceAnnotations, Verbatim};
|
||||||
DistributionMetadata, IndexUrl, LocalEditable, Name, SourceAnnotations, Verbatim,
|
|
||||||
};
|
|
||||||
use pypi_types::HashDigest;
|
use pypi_types::HashDigest;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
|
@ -155,19 +152,7 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> {
|
||||||
let mut line = match node {
|
let mut line = match node {
|
||||||
Node::Editable(editable) => format!("-e {}", editable.verbatim()),
|
Node::Editable(editable) => format!("-e {}", editable.verbatim()),
|
||||||
Node::Distribution(dist) => {
|
Node::Distribution(dist) => {
|
||||||
if self.include_extras && !dist.extras.is_empty() {
|
dist.to_requirements_txt(self.include_extras).to_string()
|
||||||
let mut extras = dist.extras.clone();
|
|
||||||
extras.sort_unstable();
|
|
||||||
extras.dedup();
|
|
||||||
format!(
|
|
||||||
"{}[{}]{}",
|
|
||||||
dist.name(),
|
|
||||||
extras.into_iter().join(", "),
|
|
||||||
dist.version_or_url().verbatim()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
dist.verbatim().to_string()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use distribution_types::{DistributionMetadata, Name, ResolvedDist, VersionOrUrlRef};
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
use distribution_types::{DistributionMetadata, Name, ResolvedDist, Verbatim, VersionOrUrlRef};
|
||||||
|
use pep508_rs::{split_scheme, Scheme};
|
||||||
use pypi_types::{HashDigest, Metadata23};
|
use pypi_types::{HashDigest, Metadata23};
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
|
|
||||||
|
@ -13,7 +18,7 @@ mod graph;
|
||||||
/// A pinned package with its resolved distribution and metadata. The [`ResolvedDist`] refers to a
|
/// A pinned package with its resolved distribution and metadata. The [`ResolvedDist`] refers to a
|
||||||
/// specific distribution (e.g., a specific wheel), while the [`Metadata23`] refers to the metadata
|
/// specific distribution (e.g., a specific wheel), while the [`Metadata23`] refers to the metadata
|
||||||
/// for the package-version pair.
|
/// for the package-version pair.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct AnnotatedDist {
|
pub(crate) struct AnnotatedDist {
|
||||||
pub(crate) dist: ResolvedDist,
|
pub(crate) dist: ResolvedDist,
|
||||||
pub(crate) extras: Vec<ExtraName>,
|
pub(crate) extras: Vec<ExtraName>,
|
||||||
|
@ -21,6 +26,74 @@ pub(crate) struct AnnotatedDist {
|
||||||
pub(crate) metadata: Metadata23,
|
pub(crate) metadata: Metadata23,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AnnotatedDist {
|
||||||
|
/// Convert the [`AnnotatedDist`] to a requirement that adheres to the `requirements.txt`
|
||||||
|
/// format.
|
||||||
|
///
|
||||||
|
/// This typically results in a PEP 508 representation of the requirement, but will write an
|
||||||
|
/// unnamed requirement for relative paths, which can't be represented with PEP 508 (but are
|
||||||
|
/// supported in `requirements.txt`).
|
||||||
|
pub(crate) fn to_requirements_txt(&self, include_extras: bool) -> Cow<str> {
|
||||||
|
// If the URL is not _definitively_ an absolute `file://` URL, write it as a relative path.
|
||||||
|
if let VersionOrUrlRef::Url(url) = self.dist.version_or_url() {
|
||||||
|
let given = url.verbatim();
|
||||||
|
match split_scheme(&given) {
|
||||||
|
Some((scheme, path)) => {
|
||||||
|
match Scheme::parse(scheme) {
|
||||||
|
Some(Scheme::File) => {
|
||||||
|
if path
|
||||||
|
.strip_prefix("//localhost")
|
||||||
|
.filter(|path| path.starts_with('/'))
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
// Always absolute; nothing to do.
|
||||||
|
} else if let Some(path) = path.strip_prefix("//") {
|
||||||
|
// Strip the prefix, to convert, e.g., `file://flask-3.0.3-py3-none-any.whl` to `flask-3.0.3-py3-none-any.whl`.
|
||||||
|
//
|
||||||
|
// However, we should allow any of the following:
|
||||||
|
// - `file://flask-3.0.3-py3-none-any.whl`
|
||||||
|
// - `file://C:\Users\user\flask-3.0.3-py3-none-any.whl`
|
||||||
|
// - `file:///C:\Users\user\flask-3.0.3-py3-none-any.whl`
|
||||||
|
if !path.starts_with("${PROJECT_ROOT}")
|
||||||
|
&& !Path::new(path).has_root()
|
||||||
|
{
|
||||||
|
return Cow::Owned(path.to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Ex) `file:./flask-3.0.3-py3-none-any.whl`
|
||||||
|
return given;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) => {}
|
||||||
|
None => {
|
||||||
|
// Ex) `flask @ C:\Users\user\flask-3.0.3-py3-none-any.whl`
|
||||||
|
return given;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
// Ex) `flask @ flask-3.0.3-py3-none-any.whl`
|
||||||
|
return given;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.extras.is_empty() || !include_extras {
|
||||||
|
self.dist.verbatim()
|
||||||
|
} else {
|
||||||
|
let mut extras = self.extras.clone();
|
||||||
|
extras.sort_unstable();
|
||||||
|
extras.dedup();
|
||||||
|
Cow::Owned(format!(
|
||||||
|
"{}[{}]{}",
|
||||||
|
self.name(),
|
||||||
|
extras.into_iter().join(", "),
|
||||||
|
self.version_or_url().verbatim()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Name for AnnotatedDist {
|
impl Name for AnnotatedDist {
|
||||||
fn name(&self) -> &PackageName {
|
fn name(&self) -> &PackageName {
|
||||||
self.dist.name()
|
self.dist.name()
|
||||||
|
|
|
@ -2065,7 +2065,7 @@ fn allowed_transitive_url_path_dependency() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
||||||
hatchling-editable @ ${HATCH_PATH}
|
${HATCH_PATH}
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
iniconfig @ git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4
|
iniconfig @ git+https://github.com/pytest-dev/iniconfig@9cae43103df70bac6fde7b9f35ad11a9f1be0cb4
|
||||||
# via hatchling-editable
|
# via hatchling-editable
|
||||||
|
@ -2510,7 +2510,7 @@ fn compile_wheel_path_dependency() -> Result<()> {
|
||||||
# via flask
|
# via flask
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
# via flask
|
# via flask
|
||||||
flask @ file:flask-3.0.0-py3-none-any.whl
|
file:flask-3.0.0-py3-none-any.whl
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
# via flask
|
# via flask
|
||||||
|
@ -2543,7 +2543,7 @@ fn compile_wheel_path_dependency() -> Result<()> {
|
||||||
# via flask
|
# via flask
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
# via flask
|
# via flask
|
||||||
flask @ file://flask-3.0.0-py3-none-any.whl
|
flask-3.0.0-py3-none-any.whl
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
# via flask
|
# via flask
|
||||||
|
@ -2576,7 +2576,7 @@ fn compile_wheel_path_dependency() -> Result<()> {
|
||||||
# via flask
|
# via flask
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
# via flask
|
# via flask
|
||||||
flask @ ./flask-3.0.0-py3-none-any.whl
|
./flask-3.0.0-py3-none-any.whl
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
# via flask
|
# via flask
|
||||||
|
@ -2609,7 +2609,7 @@ fn compile_wheel_path_dependency() -> Result<()> {
|
||||||
# via flask
|
# via flask
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
# via flask
|
# via flask
|
||||||
flask @ [TEMP_DIR]/flask-3.0.0-py3-none-any.whl
|
[TEMP_DIR]/flask-3.0.0-py3-none-any.whl
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
# via flask
|
# via flask
|
||||||
|
@ -3192,7 +3192,7 @@ fn respect_http_env_var() -> Result<()> {
|
||||||
# via flask
|
# via flask
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
# via flask
|
# via flask
|
||||||
flask @ ${URL}
|
${URL}
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
# via flask
|
# via flask
|
||||||
|
@ -3233,7 +3233,7 @@ fn respect_unnamed_env_var() -> Result<()> {
|
||||||
# via flask
|
# via flask
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
# via flask
|
# via flask
|
||||||
flask @ ${URL}
|
${URL}
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
# via flask
|
# via flask
|
||||||
|
@ -3305,7 +3305,7 @@ fn respect_file_env_var() -> Result<()> {
|
||||||
# via flask
|
# via flask
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
# via flask
|
# via flask
|
||||||
flask @ ${FILE_PATH}
|
${FILE_PATH}
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
itsdangerous==2.1.2
|
itsdangerous==2.1.2
|
||||||
# via flask
|
# via flask
|
||||||
|
@ -3468,7 +3468,7 @@ fn recursive_extras_direct_url() -> Result<()> {
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
attrs==23.2.0
|
attrs==23.2.0
|
||||||
# via aiohttp
|
# via aiohttp
|
||||||
black @ ../../scripts/packages/black_editable
|
../../scripts/packages/black_editable
|
||||||
# via -r [TEMP_DIR]/requirements.in
|
# via -r [TEMP_DIR]/requirements.in
|
||||||
frozenlist==1.4.1
|
frozenlist==1.4.1
|
||||||
# via
|
# via
|
||||||
|
@ -3964,7 +3964,7 @@ fn generate_hashes_local_directory() -> Result<()> {
|
||||||
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
|
--hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \
|
||||||
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
--hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f
|
||||||
# via anyio
|
# via anyio
|
||||||
poetry-editable @ ../../scripts/packages/poetry_editable
|
../../scripts/packages/poetry_editable
|
||||||
# via -r [TEMP_DIR]/requirements.in
|
# via -r [TEMP_DIR]/requirements.in
|
||||||
sniffio==1.3.1 \
|
sniffio==1.3.1 \
|
||||||
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
|
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
|
||||||
|
@ -6799,11 +6799,11 @@ fn compile_root_uri_non_editable() -> Result<()> {
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
||||||
black @ ${BLACK_PATH}
|
${BLACK_PATH}
|
||||||
# via
|
# via
|
||||||
# -r requirements.in
|
# -r requirements.in
|
||||||
# root-editable
|
# root-editable
|
||||||
root-editable @ ${ROOT_PATH}
|
${ROOT_PATH}
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
|
|
||||||
----- stderr -----
|
----- stderr -----
|
||||||
|
@ -7186,7 +7186,7 @@ fn unnamed_path_requirement() -> Result<()> {
|
||||||
# via
|
# via
|
||||||
# httpx
|
# httpx
|
||||||
# poetry-editable
|
# poetry-editable
|
||||||
black @ ../../scripts/packages/black_editable
|
../../scripts/packages/black_editable
|
||||||
# via -r [TEMP_DIR]/requirements.in
|
# via -r [TEMP_DIR]/requirements.in
|
||||||
certifi==2024.2.2
|
certifi==2024.2.2
|
||||||
# via
|
# via
|
||||||
|
@ -7206,13 +7206,13 @@ fn unnamed_path_requirement() -> Result<()> {
|
||||||
# anyio
|
# anyio
|
||||||
# httpx
|
# httpx
|
||||||
# requests
|
# requests
|
||||||
poetry-editable @ ../../scripts/packages/poetry_editable
|
../../scripts/packages/poetry_editable
|
||||||
# via -r [TEMP_DIR]/requirements.in
|
# via -r [TEMP_DIR]/requirements.in
|
||||||
requests==2.31.0
|
requests==2.31.0
|
||||||
# via setup-cfg-editable
|
# via setup-cfg-editable
|
||||||
setup-cfg-editable @ ../../scripts/packages/setup_cfg_editable
|
../../scripts/packages/setup_cfg_editable
|
||||||
# via -r [TEMP_DIR]/requirements.in
|
# via -r [TEMP_DIR]/requirements.in
|
||||||
setup-py-editable @ ../../scripts/packages/setup_py_editable
|
../../scripts/packages/setup_py_editable
|
||||||
# via -r [TEMP_DIR]/requirements.in
|
# via -r [TEMP_DIR]/requirements.in
|
||||||
sniffio==1.3.1
|
sniffio==1.3.1
|
||||||
# via
|
# via
|
||||||
|
@ -7322,7 +7322,7 @@ fn dynamic_dependencies() -> Result<()> {
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z [TEMP_DIR]/requirements.in
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z [TEMP_DIR]/requirements.in
|
||||||
anyio==4.3.0
|
anyio==4.3.0
|
||||||
# via hatchling-dynamic
|
# via hatchling-dynamic
|
||||||
hatchling-dynamic @ ../../scripts/packages/hatchling_dynamic
|
../../scripts/packages/hatchling_dynamic
|
||||||
# via -r [TEMP_DIR]/requirements.in
|
# via -r [TEMP_DIR]/requirements.in
|
||||||
idna==3.6
|
idna==3.6
|
||||||
# via anyio
|
# via anyio
|
||||||
|
@ -7698,7 +7698,7 @@ requires-python = ">3.8"
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
||||||
anyio @ file://[TEMP_DIR]/anyio/
|
anyio @ file://[TEMP_DIR]/anyio/
|
||||||
# via lib
|
# via lib
|
||||||
example @ ./app
|
./app
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
idna==3.6
|
idna==3.6
|
||||||
# via anyio
|
# via anyio
|
||||||
|
@ -7786,7 +7786,7 @@ requires-python = ">3.8"
|
||||||
# via
|
# via
|
||||||
# --override overrides.txt
|
# --override overrides.txt
|
||||||
# lib
|
# lib
|
||||||
example @ ./app
|
./app
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
idna==3.6
|
idna==3.6
|
||||||
# via anyio
|
# via anyio
|
||||||
|
@ -7896,12 +7896,12 @@ requires-python = ">3.8"
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
# This file was autogenerated by uv via the following command:
|
# This file was autogenerated by uv via the following command:
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --override overrides.txt --constraint constraints.txt
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --override overrides.txt --constraint constraints.txt
|
||||||
anyio @ ./anyio
|
./anyio
|
||||||
# via
|
# via
|
||||||
# -c constraints.txt
|
# -c constraints.txt
|
||||||
# --override overrides.txt
|
# --override overrides.txt
|
||||||
# lib
|
# lib
|
||||||
example @ ./app
|
./app
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
idna==3.6
|
idna==3.6
|
||||||
# via anyio
|
# via anyio
|
||||||
|
@ -7947,7 +7947,7 @@ requires-python = ">3.8"
|
||||||
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
|
||||||
click==8.1.7
|
click==8.1.7
|
||||||
# via flask
|
# via flask
|
||||||
example @ .
|
.
|
||||||
# via -r requirements.in
|
# via -r requirements.in
|
||||||
flask==2.0.0rc1
|
flask==2.0.0rc1
|
||||||
# via example
|
# via example
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue