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:
Charlie Marsh 2024-07-31 12:00:37 -04:00 committed by GitHub
parent 8d14a4cb4f
commit dfec262586
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 169 additions and 79 deletions

5
Cargo.lock generated
View file

@ -4826,6 +4826,7 @@ dependencies = [
"junction", "junction",
"path-absolutize", "path-absolutize",
"path-slash", "path-slash",
"serde",
"tempfile", "tempfile",
"tracing", "tracing",
"urlencoding", "urlencoding",
@ -5016,7 +5017,6 @@ dependencies = [
"itertools 0.13.0", "itertools 0.13.0",
"once-map", "once-map",
"owo-colors", "owo-colors",
"path-slash",
"pep440_rs", "pep440_rs",
"pep508_rs", "pep508_rs",
"petgraph", "petgraph",
@ -5040,6 +5040,7 @@ dependencies = [
"uv-client", "uv-client",
"uv-configuration", "uv-configuration",
"uv-distribution", "uv-distribution",
"uv-fs",
"uv-git", "uv-git",
"uv-normalize", "uv-normalize",
"uv-python", "uv-python",
@ -5117,7 +5118,6 @@ dependencies = [
"dirs-sys", "dirs-sys",
"fs-err", "fs-err",
"install-wheel-rs", "install-wheel-rs",
"path-slash",
"pathdiff", "pathdiff",
"pep440_rs", "pep440_rs",
"pep508_rs", "pep508_rs",
@ -5192,7 +5192,6 @@ dependencies = [
"fs-err", "fs-err",
"glob", "glob",
"insta", "insta",
"path-slash",
"pep440_rs", "pep440_rs",
"pep508_rs", "pep508_rs",
"pypi-types", "pypi-types",

View file

@ -24,6 +24,7 @@ fs-err = { workspace = true }
fs2 = { workspace = true } fs2 = { workspace = true }
path-absolutize = { workspace = true } path-absolutize = { workspace = true }
path-slash = { workspace = true } path-slash = { workspace = true }
serde = { workspace = true, optional = true }
tempfile = { workspace = true } tempfile = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
urlencoding = { workspace = true } urlencoding = { workspace = true }

View file

@ -303,6 +303,100 @@ pub fn relative_to(
Ok(up.join(stripped)) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -26,9 +26,10 @@ requirements-txt = { workspace = true }
uv-client = { workspace = true } uv-client = { workspace = true }
uv-configuration = { workspace = true } uv-configuration = { workspace = true }
uv-distribution = { workspace = true } uv-distribution = { workspace = true }
uv-fs = { workspace = true, features = ["serde"] }
uv-git = { workspace = true } uv-git = { workspace = true }
uv-python = { workspace = true }
uv-normalize = { workspace = true } uv-normalize = { workspace = true }
uv-python = { workspace = true }
uv-types = { workspace = true } uv-types = { workspace = true }
uv-warnings = { workspace = true } uv-warnings = { workspace = true }
uv-workspace = { workspace = true } uv-workspace = { workspace = true }
@ -43,7 +44,6 @@ futures = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
itertools = { workspace = true } itertools = { workspace = true }
owo-colors = { workspace = true } owo-colors = { workspace = true }
path-slash = { workspace = true }
petgraph = { workspace = true } petgraph = { workspace = true }
pubgrub = { workspace = true } pubgrub = { workspace = true }
rkyv = { workspace = true } rkyv = { workspace = true }

View file

@ -2,6 +2,7 @@
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::convert::Infallible;
use std::fmt::{Debug, Display}; use std::fmt::{Debug, Display};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::str::FromStr; use std::str::FromStr;
@ -9,10 +10,8 @@ use std::sync::Arc;
use either::Either; use either::Either;
use itertools::Itertools; use itertools::Itertools;
use path_slash::PathExt;
use petgraph::visit::EdgeRef; use petgraph::visit::EdgeRef;
use rustc_hash::{FxHashMap, FxHashSet}; use rustc_hash::{FxHashMap, FxHashSet};
use serde::{Deserialize, Deserializer};
use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value}; use toml_edit::{value, Array, ArrayOfTables, InlineTable, Item, Table, Value};
use url::Url; use url::Url;
@ -35,6 +34,7 @@ use pypi_types::{
}; };
use uv_configuration::{ExtrasSpecification, Upgrade}; use uv_configuration::{ExtrasSpecification, Upgrade};
use uv_distribution::{ArchiveMetadata, Metadata}; use uv_distribution::{ArchiveMetadata, Metadata};
use uv_fs::{PortablePath, PortablePathBuf};
use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference}; use uv_git::{GitReference, GitSha, RepositoryReference, ResolvedRepositoryReference};
use uv_normalize::{ExtraName, GroupName, PackageName}; use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_workspace::VirtualProject; use uv_workspace::VirtualProject;
@ -1370,19 +1370,6 @@ enum Source {
Editable(PathBuf), 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 { impl Source {
fn from_resolved_dist(resolved_dist: &ResolvedDist) -> Source { fn from_resolved_dist(resolved_dist: &ResolvedDist) -> Source {
match *resolved_dist { match *resolved_dist {
@ -1514,21 +1501,18 @@ impl Source {
} }
} }
Source::Path(ref path) => { Source::Path(ref path) => {
source_table.insert( source_table.insert("path", Value::from(PortablePath::from(path).to_string()));
"path",
Value::from(serialize_path_with_dot(path).into_owned()),
);
} }
Source::Directory(ref path) => { Source::Directory(ref path) => {
source_table.insert( source_table.insert(
"directory", "directory",
Value::from(serialize_path_with_dot(path).into_owned()), Value::from(PortablePath::from(path).to_string()),
); );
} }
Source::Editable(ref path) => { Source::Editable(ref path) => {
source_table.insert( source_table.insert(
"editable", "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) write!(f, "{}+{}", self.name(), url)
} }
Source::Path(path) | Source::Directory(path) | Source::Editable(path) => { 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>, subdirectory: Option<String>,
}, },
Path { Path {
#[serde(deserialize_with = "deserialize_path_with_dot")] path: PortablePathBuf,
path: PathBuf,
}, },
Directory { Directory {
#[serde(deserialize_with = "deserialize_path_with_dot")] directory: PortablePathBuf,
directory: PathBuf,
}, },
Editable { Editable {
#[serde(deserialize_with = "deserialize_path_with_dot")] editable: PortablePathBuf,
editable: PathBuf,
}, },
} }
@ -1634,9 +1615,9 @@ impl TryFrom<SourceWire> for Source {
Ok(Source::Git(url, git_source)) Ok(Source::Git(url, git_source))
} }
Direct { url, subdirectory } => Ok(Source::Direct(url, DirectSource { subdirectory })), Direct { url, subdirectory } => Ok(Source::Direct(url, DirectSource { subdirectory })),
Path { path } => Ok(Source::Path(path)), Path { path } => Ok(Source::Path(path.into())),
Directory { directory } => Ok(Source::Directory(directory)), Directory { directory } => Ok(Source::Directory(directory.into())),
Editable { editable } => Ok(Source::Editable(editable)), 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 /// 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. /// and/or recording where the source dist file originally came from.
#[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)] #[derive(Clone, Debug, serde::Deserialize, PartialEq, Eq)]
#[serde(untagged)] #[serde(try_from = "SourceDistWire")]
enum SourceDist { enum SourceDist {
Url { Url {
url: UrlString, url: UrlString,
@ -1727,26 +1708,12 @@ enum SourceDist {
metadata: SourceDistMetadata, metadata: SourceDistMetadata,
}, },
Path { Path {
#[serde(deserialize_with = "deserialize_path_with_dot")]
path: PathBuf, path: PathBuf,
#[serde(flatten)] #[serde(flatten)]
metadata: SourceDistMetadata, 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 { impl SourceDist {
fn filename(&self) -> Option<Cow<str>> { fn filename(&self) -> Option<Cow<str>> {
match self { match self {
@ -1780,26 +1747,6 @@ impl SourceDist {
} }
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( fn from_annotated_dist(
id: &DistributionId, id: &DistributionId,
annotated_dist: &AnnotatedDist, 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 { impl From<GitReference> for GitSourceKind {
fn from(value: GitReference) -> Self { fn from(value: GitReference) -> Self {
match value { match value {

View file

@ -26,7 +26,6 @@ uv-installer = { workspace = true }
dirs-sys = { workspace = true } dirs-sys = { workspace = true }
fs-err = { workspace = true } fs-err = { workspace = true }
path-slash = { workspace = true }
pathdiff = { workspace = true } pathdiff = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }

View file

@ -1,13 +1,14 @@
use std::path::PathBuf; use std::path::PathBuf;
use path_slash::PathBufExt;
use pypi_types::VerbatimParsedUrl;
use serde::Deserialize; use serde::Deserialize;
use toml_edit::value; use toml_edit::value;
use toml_edit::Array; use toml_edit::Array;
use toml_edit::Table; use toml_edit::Table;
use toml_edit::Value; use toml_edit::Value;
use pypi_types::VerbatimParsedUrl;
use uv_fs::PortablePath;
/// A tool entry. /// A tool entry.
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@ -127,7 +128,7 @@ impl ToolEntrypoint {
table.insert( table.insert(
"install-path", "install-path",
// Use cross-platform slashes so the toml string type does not change // 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 table
} }

View file

@ -26,7 +26,6 @@ uv-options-metadata = { workspace = true }
either = { workspace = true } either = { workspace = true }
fs-err = { workspace = true } fs-err = { workspace = true }
glob = { workspace = true } glob = { workspace = true }
path-slash = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
schemars = { workspace = true, optional = true } schemars = { workspace = true, optional = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }

View file

@ -2,11 +2,11 @@ use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use std::{fmt, mem}; use std::{fmt, mem};
use path_slash::PathExt;
use thiserror::Error; use thiserror::Error;
use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value}; use toml_edit::{Array, DocumentMut, Item, RawString, Table, TomlError, Value};
use pep508_rs::{ExtraName, PackageName, Requirement, VersionOrUrl}; use pep508_rs::{ExtraName, PackageName, Requirement, VersionOrUrl};
use uv_fs::PortablePath;
use crate::pyproject::{DependencyType, PyProjectToml, Source}; use crate::pyproject::{DependencyType, PyProjectToml, Source};
@ -65,8 +65,7 @@ impl PyProjectTomlMut {
.ok_or(Error::MalformedWorkspace)?; .ok_or(Error::MalformedWorkspace)?;
// Add the path to the workspace. // Add the path to the workspace.
// Use cross-platform slashes so the toml string type does not change members.push(PortablePath::from(path.as_ref()).to_string());
members.push(path.as_ref().to_slash_lossy().to_string());
Ok(()) Ok(())
} }