mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-31 15:57:26 +00:00
Capture portable path serialization in a struct (#5652)
## Summary I need to reuse this in #5494, so want to abstract it out and make it reusable.
This commit is contained in:
parent
8d14a4cb4f
commit
dfec262586
9 changed files with 169 additions and 79 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -4826,6 +4826,7 @@ dependencies = [
|
|||
"junction",
|
||||
"path-absolutize",
|
||||
"path-slash",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
"urlencoding",
|
||||
|
@ -5016,7 +5017,6 @@ dependencies = [
|
|||
"itertools 0.13.0",
|
||||
"once-map",
|
||||
"owo-colors",
|
||||
"path-slash",
|
||||
"pep440_rs",
|
||||
"pep508_rs",
|
||||
"petgraph",
|
||||
|
@ -5040,6 +5040,7 @@ dependencies = [
|
|||
"uv-client",
|
||||
"uv-configuration",
|
||||
"uv-distribution",
|
||||
"uv-fs",
|
||||
"uv-git",
|
||||
"uv-normalize",
|
||||
"uv-python",
|
||||
|
@ -5117,7 +5118,6 @@ dependencies = [
|
|||
"dirs-sys",
|
||||
"fs-err",
|
||||
"install-wheel-rs",
|
||||
"path-slash",
|
||||
"pathdiff",
|
||||
"pep440_rs",
|
||||
"pep508_rs",
|
||||
|
@ -5192,7 +5192,6 @@ dependencies = [
|
|||
"fs-err",
|
||||
"glob",
|
||||
"insta",
|
||||
"path-slash",
|
||||
"pep440_rs",
|
||||
"pep508_rs",
|
||||
"pypi-types",
|
||||
|
|
|
@ -24,6 +24,7 @@ fs-err = { workspace = true }
|
|||
fs2 = { workspace = true }
|
||||
path-absolutize = { workspace = true }
|
||||
path-slash = { workspace = true }
|
||||
serde = { workspace = true, optional = true }
|
||||
tempfile = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
|
|
|
@ -303,6 +303,100 @@ pub fn relative_to(
|
|||
Ok(up.join(stripped))
|
||||
}
|
||||
|
||||
/// A path that can be serialized and deserialized in a portable way by converting Windows-style
|
||||
/// backslashes to forward slashes, and using a `.` for an empty path.
|
||||
///
|
||||
/// This implementation assumes that the path is valid UTF-8; otherwise, it won't roundtrip.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PortablePath<'a>(&'a Path);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PortablePathBuf(PathBuf);
|
||||
|
||||
impl AsRef<Path> for PortablePath<'_> {
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<&'a T> for PortablePath<'a>
|
||||
where
|
||||
T: AsRef<Path> + ?Sized,
|
||||
{
|
||||
fn from(path: &'a T) -> Self {
|
||||
PortablePath(path.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PortablePath<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let path = self.0.to_slash_lossy();
|
||||
if path.is_empty() {
|
||||
write!(f, ".")
|
||||
} else {
|
||||
write!(f, "{path}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PortablePathBuf {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let path = self.0.to_slash_lossy();
|
||||
if path.is_empty() {
|
||||
write!(f, ".")
|
||||
} else {
|
||||
write!(f, "{path}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PortablePathBuf> for PathBuf {
|
||||
fn from(portable: PortablePathBuf) -> Self {
|
||||
portable.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PathBuf> for PortablePathBuf {
|
||||
fn from(path: PathBuf) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for PortablePathBuf {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
self.to_string().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl serde::Serialize for PortablePath<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::ser::Serializer,
|
||||
{
|
||||
self.to_string().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'de> serde::de::Deserialize<'de> for PortablePathBuf {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::de::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
if s == "." {
|
||||
Ok(Self(PathBuf::new()))
|
||||
} else {
|
||||
Ok(Self(PathBuf::from(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -26,9 +26,10 @@ requirements-txt = { workspace = true }
|
|||
uv-client = { workspace = true }
|
||||
uv-configuration = { workspace = true }
|
||||
uv-distribution = { workspace = true }
|
||||
uv-fs = { workspace = true, features = ["serde"] }
|
||||
uv-git = { workspace = true }
|
||||
uv-python = { workspace = true }
|
||||
uv-normalize = { workspace = true }
|
||||
uv-python = { workspace = true }
|
||||
uv-types = { workspace = true }
|
||||
uv-warnings = { workspace = true }
|
||||
uv-workspace = { workspace = true }
|
||||
|
@ -43,7 +44,6 @@ futures = { workspace = true }
|
|||
indexmap = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
owo-colors = { workspace = true }
|
||||
path-slash = { workspace = true }
|
||||
petgraph = { workspace = true }
|
||||
pubgrub = { workspace = true }
|
||||
rkyv = { workspace = true }
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||
use std::convert::Infallible;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
@ -9,10 +10,8 @@ use std::sync::Arc;
|
|||
|
||||
use either::Either;
|
||||
use itertools::Itertools;
|
||||
use path_slash::PathExt;
|
||||
use petgraph::visit::EdgeRef;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
|
||||
use url::Url;
|
||||
|
||||
|
@ -35,6 +34,7 @@ use pypi_types::{
|
|||
};
|
||||
use uv_configuration::{ExtrasSpecification, Upgrade};
|
||||
use uv_distribution::{ArchiveMetadata, Metadata};
|
||||
use uv_fs::{PortablePath, PortablePathBuf};
|
||||
use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference};
|
||||
use uv_normalize::{ExtraName, GroupName, PackageName};
|
||||
use uv_workspace::VirtualProject;
|
||||
|
@ -1370,19 +1370,6 @@ enum Source {
|
|||
Editable(PathBuf),
|
||||
}
|
||||
|
||||
/// A [`PathBuf`], but we show `.` instead of an empty path.
|
||||
///
|
||||
/// We also normalize backslashes to forward slashes on Windows, to ensure
|
||||
/// that the lockfile contains portable paths.
|
||||
fn serialize_path_with_dot(path: &Path) -> Cow<str> {
|
||||
let path = path.to_slash_lossy();
|
||||
if path.is_empty() {
|
||||
Cow::Borrowed(".")
|
||||
} else {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Source {
|
||||
fn from_resolved_dist(resolved_dist: &ResolvedDist) -> Source {
|
||||
match *resolved_dist {
|
||||
|
@ -1514,21 +1501,18 @@ impl Source {
|
|||
}
|
||||
}
|
||||
Source::Path(ref path) => {
|
||||
source_table.insert(
|
||||
"path",
|
||||
Value::from(serialize_path_with_dot(path).into_owned()),
|
||||
);
|
||||
source_table.insert("path", Value::from(PortablePath::from(path).to_string()));
|
||||
}
|
||||
Source::Directory(ref path) => {
|
||||
source_table.insert(
|
||||
"directory",
|
||||
Value::from(serialize_path_with_dot(path).into_owned()),
|
||||
Value::from(PortablePath::from(path).to_string()),
|
||||
);
|
||||
}
|
||||
Source::Editable(ref path) => {
|
||||
source_table.insert(
|
||||
"editable",
|
||||
Value::from(serialize_path_with_dot(path).into_owned()),
|
||||
Value::from(PortablePath::from(path).to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1543,7 +1527,7 @@ impl std::fmt::Display for Source {
|
|||
write!(f, "{}+{}", self.name(), url)
|
||||
}
|
||||
Source::Path(path) | Source::Directory(path) | Source::Editable(path) => {
|
||||
write!(f, "{}+{}", self.name(), serialize_path_with_dot(path))
|
||||
write!(f, "{}+{}", self.name(), PortablePath::from(path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1592,16 +1576,13 @@ enum SourceWire {
|
|||
subdirectory: Option<String>,
|
||||
},
|
||||
Path {
|
||||
#[serde(deserialize_with = "deserialize_path_with_dot")]
|
||||
path: PathBuf,
|
||||
path: PortablePathBuf,
|
||||
},
|
||||
Directory {
|
||||
#[serde(deserialize_with = "deserialize_path_with_dot")]
|
||||
directory: PathBuf,
|
||||
directory: PortablePathBuf,
|
||||
},
|
||||
Editable {
|
||||
#[serde(deserialize_with = "deserialize_path_with_dot")]
|
||||
editable: PathBuf,
|
||||
editable: PortablePathBuf,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1634,9 +1615,9 @@ impl TryFrom<SourceWire> for Source {
|
|||
Ok(Source::Git(url, git_source))
|
||||
}
|
||||
Direct { url, subdirectory } => Ok(Source::Direct(url, DirectSource { subdirectory })),
|
||||
Path { path } => Ok(Source::Path(path)),
|
||||
Directory { directory } => Ok(Source::Directory(directory)),
|
||||
Editable { editable } => Ok(Source::Editable(editable)),
|
||||
Path { path } => Ok(Source::Path(path.into())),
|
||||
Directory { directory } => Ok(Source::Directory(directory.into())),
|
||||
Editable { editable } => Ok(Source::Editable(editable.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1719,7 +1700,7 @@ struct SourceDistMetadata {
|
|||
/// future, so this should be treated as only a hint to where to look
|
||||
/// and/or recording where the source dist file originally came from.
|
||||
#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
#[serde(try_from = "SourceDistWire")]
|
||||
enum SourceDist {
|
||||
Url {
|
||||
url: UrlString,
|
||||
|
@ -1727,26 +1708,12 @@ enum SourceDist {
|
|||
metadata: SourceDistMetadata,
|
||||
},
|
||||
Path {
|
||||
#[serde(deserialize_with = "deserialize_path_with_dot")]
|
||||
path: PathBuf,
|
||||
#[serde(flatten)]
|
||||
metadata: SourceDistMetadata,
|
||||
},
|
||||
}
|
||||
|
||||
/// A [`PathBuf`], but we show `.` instead of an empty path.
|
||||
fn deserialize_path_with_dot<'de, D>(deserializer: D) -> Result<PathBuf, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let path = String::deserialize(deserializer)?;
|
||||
if path == "." {
|
||||
Ok(PathBuf::new())
|
||||
} else {
|
||||
Ok(PathBuf::from(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceDist {
|
||||
fn filename(&self) -> Option<Cow<str>> {
|
||||
match self {
|
||||
|
@ -1780,26 +1747,6 @@ impl SourceDist {
|
|||
}
|
||||
|
||||
impl SourceDist {
|
||||
/// Returns the TOML representation of this source distribution.
|
||||
fn to_toml(&self) -> anyhow::Result<InlineTable> {
|
||||
let mut table = InlineTable::new();
|
||||
match &self {
|
||||
SourceDist::Url { url, .. } => {
|
||||
table.insert("url", Value::from(url.as_ref()));
|
||||
}
|
||||
SourceDist::Path { path, .. } => {
|
||||
table.insert("path", Value::from(serialize_path_with_dot(path).as_ref()));
|
||||
}
|
||||
}
|
||||
if let Some(hash) = self.hash() {
|
||||
table.insert("hash", Value::from(hash.to_string()));
|
||||
}
|
||||
if let Some(size) = self.size() {
|
||||
table.insert("size", Value::from(i64::try_from(size)?));
|
||||
}
|
||||
Ok(table)
|
||||
}
|
||||
|
||||
fn from_annotated_dist(
|
||||
id: &DistributionId,
|
||||
annotated_dist: &AnnotatedDist,
|
||||
|
@ -1890,6 +1837,57 @@ impl SourceDist {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum SourceDistWire {
|
||||
Url {
|
||||
url: UrlString,
|
||||
#[serde(flatten)]
|
||||
metadata: SourceDistMetadata,
|
||||
},
|
||||
Path {
|
||||
path: PortablePathBuf,
|
||||
#[serde(flatten)]
|
||||
metadata: SourceDistMetadata,
|
||||
},
|
||||
}
|
||||
|
||||
impl SourceDist {
|
||||
/// Returns the TOML representation of this source distribution.
|
||||
fn to_toml(&self) -> anyhow::Result<InlineTable> {
|
||||
let mut table = InlineTable::new();
|
||||
match &self {
|
||||
SourceDist::Url { url, .. } => {
|
||||
table.insert("url", Value::from(url.as_ref()));
|
||||
}
|
||||
SourceDist::Path { path, .. } => {
|
||||
table.insert("path", Value::from(PortablePath::from(path).to_string()));
|
||||
}
|
||||
}
|
||||
if let Some(hash) = self.hash() {
|
||||
table.insert("hash", Value::from(hash.to_string()));
|
||||
}
|
||||
if let Some(size) = self.size() {
|
||||
table.insert("size", Value::from(i64::try_from(size)?));
|
||||
}
|
||||
Ok(table)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<SourceDistWire> for SourceDist {
|
||||
type Error = Infallible;
|
||||
|
||||
fn try_from(wire: SourceDistWire) -> Result<SourceDist, Infallible> {
|
||||
match wire {
|
||||
SourceDistWire::Url { url, metadata } => Ok(SourceDist::Url { url, metadata }),
|
||||
SourceDistWire::Path { path, metadata } => Ok(SourceDist::Path {
|
||||
path: path.into(),
|
||||
metadata,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GitReference> for GitSourceKind {
|
||||
fn from(value: GitReference) -> Self {
|
||||
match value {
|
||||
|
|
|
@ -26,7 +26,6 @@ uv-installer = { workspace = true }
|
|||
|
||||
dirs-sys = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
path-slash = { workspace = true }
|
||||
pathdiff = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use path_slash::PathBufExt;
|
||||
use pypi_types::VerbatimParsedUrl;
|
||||
use serde::Deserialize;
|
||||
use toml_edit::value;
|
||||
use toml_edit::Array;
|
||||
use toml_edit::Table;
|
||||
use toml_edit::Value;
|
||||
|
||||
use pypi_types::VerbatimParsedUrl;
|
||||
use uv_fs::PortablePath;
|
||||
|
||||
/// A tool entry.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
|
@ -127,7 +128,7 @@ impl ToolEntrypoint {
|
|||
table.insert(
|
||||
"install-path",
|
||||
// Use cross-platform slashes so the toml string type does not change
|
||||
value(self.install_path.to_slash_lossy().to_string()),
|
||||
value(PortablePath::from(&self.install_path).to_string()),
|
||||
);
|
||||
table
|
||||
}
|
||||
|
|
|
@ -26,7 +26,6 @@ uv-options-metadata = { workspace = true }
|
|||
either = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
glob = { workspace = true }
|
||||
path-slash = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
|
|
@ -2,11 +2,11 @@ use std::path::Path;
|
|||
use std::str::FromStr;
|
||||
use std::{fmt, mem};
|
||||
|
||||
use path_slash::PathExt;
|
||||
use thiserror::Error;
|
||||
use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value};
|
||||
|
||||
use pep508_rs::{ExtraName, PackageName, Requirement, VersionOrUrl};
|
||||
use uv_fs::PortablePath;
|
||||
|
||||
use crate::pyproject::{DependencyType, PyProjectToml, Source};
|
||||
|
||||
|
@ -65,8 +65,7 @@ impl PyProjectTomlMut {
|
|||
.ok_or(Error::MalformedWorkspace)?;
|
||||
|
||||
// Add the path to the workspace.
|
||||
// Use cross-platform slashes so the toml string type does not change
|
||||
members.push(path.as_ref().to_slash_lossy().to_string());
|
||||
members.push(PortablePath::from(path.as_ref()).to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue