//! Reads the following fields from `pyproject.toml`: //! //! * `project.{dependencies,optional-dependencies}` //! * `tool.uv.sources` //! * `tool.uv.workspace` //! //! Then lowers them into a dependency specification. use std::collections::BTreeMap; use std::ops::Deref; use glob::Pattern; use serde::{Deserialize, Serialize}; use url::Url; use pep440_rs::VersionSpecifiers; use pypi_types::VerbatimParsedUrl; use uv_normalize::{ExtraName, PackageName}; /// A `pyproject.toml` as specified in PEP 517. #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "kebab-case")] pub struct PyProjectToml { /// PEP 621-compliant project metadata. pub project: Option, /// Tool-specific metadata. pub tool: Option, /// The raw unserialized document. #[serde(skip)] pub(crate) raw: String, } impl PyProjectToml { /// Parse a `PyProjectToml` from a raw TOML string. pub fn from_string(raw: String) -> Result { let pyproject = toml::from_str(&raw)?; Ok(PyProjectToml { raw, ..pyproject }) } } // Ignore raw document in comparison. impl PartialEq for PyProjectToml { fn eq(&self, other: &Self) -> bool { self.project.eq(&other.project) && self.tool.eq(&other.tool) } } impl Eq for PyProjectToml {} /// PEP 621 project metadata (`project`). /// /// See . #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] pub struct Project { /// The name of the project pub name: PackageName, /// The Python versions this project is compatible with. pub requires_python: Option, /// The optional dependencies of the project. pub optional_dependencies: Option>>, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct Tool { pub uv: Option, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ToolUv { pub sources: Option>, pub workspace: Option, #[cfg_attr( feature = "schemars", schemars( with = "Option>", description = "PEP 508-style requirements, e.g., `flask==3.0.0`, or `black @ https://...`." ) )] pub dev_dependencies: Option>>, } #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ToolUvWorkspace { pub members: Option>, pub exclude: Option>, } /// (De)serialize globs as strings. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] pub struct SerdePattern(#[serde(with = "serde_from_and_to_string")] pub Pattern); #[cfg(feature = "schemars")] impl schemars::JsonSchema for SerdePattern { fn schema_name() -> String { ::schema_name() } fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema { ::json_schema(gen) } } impl Deref for SerdePattern { type Target = Pattern; fn deref(&self) -> &Self::Target { &self.0 } } /// A `tool.uv.sources` value. #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(untagged, deny_unknown_fields)] pub enum Source { /// A remote Git repository, available over HTTPS or SSH. /// /// Example: /// ```toml /// flask = { git = "https://github.com/pallets/flask", tag = "3.0.0" } /// ``` Git { /// The repository URL (without the `git+` prefix). git: Url, /// The path to the directory with the `pyproject.toml`, if it's not in the archive root. subdirectory: Option, // Only one of the three may be used; we'll validate this later and emit a custom error. rev: Option, tag: Option, branch: Option, }, /// A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution /// (`.zip`, `.tar.gz`). /// /// Example: /// ```toml /// flask = { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl" } /// ``` Url { url: Url, /// For source distributions, the path to the directory with the `pyproject.toml`, if it's /// not in the archive root. subdirectory: Option, }, /// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or /// `.tar.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or /// `setup.py` file in the root). Path { path: String, /// `false` by default. editable: Option, }, /// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`. Registry { // TODO(konstin): The string is more-or-less a placeholder index: String, }, /// A dependency on another package in the workspace. Workspace { /// When set to `false`, the package will be fetched from the remote index, rather than /// included as a workspace package. workspace: bool, /// `true` by default. editable: Option, }, /// A catch-all variant used to emit precise error messages when deserializing. CatchAll { git: String, subdirectory: Option, rev: Option, tag: Option, branch: Option, url: String, patch: String, index: String, workspace: bool, }, } /// mod serde_from_and_to_string { use std::fmt::Display; use std::str::FromStr; use serde::{de, Deserialize, Deserializer, Serializer}; pub(super) fn serialize(value: &T, serializer: S) -> Result where T: Display, S: Serializer, { serializer.collect_str(value) } pub(super) fn deserialize<'de, T, D>(deserializer: D) -> Result where T: FromStr, T::Err: Display, D: Deserializer<'de>, { String::deserialize(deserializer)? .parse() .map_err(de::Error::custom) } }