//! 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::ops::Deref; use std::path::{Path, PathBuf}; use std::{collections::BTreeMap, mem}; use glob::Pattern; use serde::{Deserialize, Serialize}; use thiserror::Error; use url::Url; use pep440_rs::VersionSpecifiers; use pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl}; use uv_fs::relative_to; use uv_git::GitReference; use uv_macros::OptionsMetadata; 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 raw: String, /// Used to determine whether a `build-system` is present. #[serde(default, skip_serializing)] build_system: Option, } 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 }) } /// Returns `true` if the project should be considered a Python package, as opposed to a /// non-package ("virtual") project. pub fn is_package(&self) -> bool { // If `tool.uv.package` is set, defer to that explicit setting. if let Some(is_package) = self .tool .as_ref() .and_then(|tool| tool.uv.as_ref()) .and_then(|uv| uv.package) { return is_package; } // Otherwise, a project is assumed to be a package if `build-system` is present. self.build_system.is_some() } } // 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 {} impl AsRef<[u8]> for PyProjectToml { fn as_ref(&self) -> &[u8] { self.raw.as_bytes() } } /// 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, } // NOTE(charlie): When adding fields to this struct, mark them as ignored on `Options` in // `crates/uv-settings/src/settings.rs`. #[derive(Serialize, Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)] #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ToolUv { /// The sources to use (e.g., workspace members, Git repositories, local paths) when resolving /// dependencies. pub sources: Option>, /// The workspace definition for the project, if any. #[option_group] pub workspace: Option, /// Whether the project is managed by uv. If `false`, uv will ignore the project when /// `uv run` is invoked. #[option( default = r#"true"#, value_type = "bool", example = r#" managed = false "# )] pub managed: Option, /// Whether the project should be considered a Python package, or a non-package ("virtual") /// project. /// /// Packages are built and installed into the virtual environment in editable mode and thus /// require a build backend, while virtual projects are _not_ built or installed; instead, only /// their dependencies are included in the virtual environment. /// /// Creating a package requires that a `build-system` is present in the `pyproject.toml`, and /// that the project adheres to a structure that adheres to the build backend's expectations /// (e.g., a `src` layout). #[option( default = r#"true"#, value_type = "bool", example = r#" package = false "# )] pub package: Option, /// The project's development dependencies. Development dependencies will be installed by /// default in `uv run` and `uv sync`, but will not appear in the project's published metadata. #[cfg_attr( feature = "schemars", schemars( with = "Option>", description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`." ) )] #[option( default = r#"[]"#, value_type = "list[str]", example = r#" dev-dependencies = ["ruff==0.5.0"] "# )] pub dev_dependencies: Option>>, /// A list of supported environments against which to resolve dependencies. /// /// By default, uv will resolve for all possible environments during a `uv lock` operation. /// However, you can restrict the set of supported environments to improve performance and avoid /// unsatisfiable branches in the solution space. /// /// These environments will also respected when `uv pip compile` is invoked with the /// `--universal` flag. #[cfg_attr( feature = "schemars", schemars( with = "Option>", description = "A list of environment markers, e.g., `python_version >= '3.6'`." ) )] #[option( default = r#"[]"#, value_type = "str | list[str]", example = r#" # Resolve for macOS, but not for Linux or Windows. environments = ["sys_platform == 'darwin'"] "# )] pub environments: Option, /// Overrides to apply when resolving the project's dependencies. /// /// Overrides are used to force selection of a specific version of a package, regardless of the /// version requested by any other package, and regardless of whether choosing that version /// would typically constitute an invalid resolution. /// /// While constraints are _additive_, in that they're combined with the requirements of the /// constituent packages, overrides are _absolute_, in that they completely replace the /// requirements of any constituent packages. /// /// !!! note /// In `uv lock`, `uv sync`, and `uv run`, uv will only read `override-dependencies` from /// the `pyproject.toml` at the workspace root, and will ignore any declarations in other /// workspace members or `uv.toml` files. #[cfg_attr( feature = "schemars", schemars( with = "Option>", description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`." ) )] #[option( default = r#"[]"#, value_type = "list[str]", example = r#" # Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request # a different version. override-dependencies = ["werkzeug==2.3.0"] "# )] pub override_dependencies: Option>>, /// Constraints to apply when resolving the project's dependencies. /// /// Constraints are used to restrict the versions of dependencies that are selected during /// resolution. /// /// Including a package as a constraint will _not_ trigger installation of the package on its /// own; instead, the package must be requested elsewhere in the project's first-party or /// transitive dependencies. /// /// !!! note /// In `uv lock`, `uv sync`, and `uv run`, uv will only read `constraint-dependencies` from /// the `pyproject.toml` at the workspace root, and will ignore any declarations in other /// workspace members or `uv.toml` files. #[cfg_attr( feature = "schemars", schemars( with = "Option>", description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`." ) )] #[option( default = r#"[]"#, value_type = "list[str]", example = r#" # Ensure that the grpcio version is always less than 1.65, if it's requested by a # transitive dependency. constraint-dependencies = ["grpcio<1.65"] "# )] pub constraint_dependencies: Option>>, } #[derive(Serialize, Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct ToolUvWorkspace { /// Packages to include as workspace members. /// /// Supports both globs and explicit paths. /// /// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html). #[option( default = r#"[]"#, value_type = "list[str]", example = r#" members = ["member1", "path/to/member2", "libs/*"] "# )] pub members: Option>, /// Packages to exclude as workspace members. If a package matches both `members` and /// `exclude`, it will be excluded. /// /// Supports both globs and explicit paths. /// /// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html). #[option( default = r#"[]"#, value_type = "list[str]", example = r#" exclude = ["member1", "path/to/member2", "libs/*"] "# )] 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(rename_all = "kebab-case", 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: PathBuf, /// `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, }, /// 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, path: PathBuf, index: String, workspace: bool, }, } #[derive(Error, Debug)] pub enum SourceError { #[error("Failed to resolve Git reference: `{0}`")] UnresolvedReference(String), #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")] WorkspacePackageGit(String), #[error("Workspace dependency `{0}` must refer to local directory, not a URL")] WorkspacePackageUrl(String), #[error("Workspace dependency `{0}` must refer to local directory, not a file")] WorkspacePackageFile(String), #[error("`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided.")] UnusedRev(String, String), #[error("`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided.")] UnusedTag(String, String), #[error("`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided.")] UnusedBranch(String, String), #[error("Failed to resolve absolute path")] Absolute(#[from] std::io::Error), #[error("Path contains invalid characters: `{}`", _0.display())] NonUtf8Path(PathBuf), } impl Source { pub fn from_requirement( name: &PackageName, source: RequirementSource, workspace: bool, editable: Option, rev: Option, tag: Option, branch: Option, root: &Path, ) -> Result, SourceError> { // If we resolved to a non-Git source, and the user specified a Git reference, error. if !matches!(source, RequirementSource::Git { .. }) { if let Some(rev) = rev { return Err(SourceError::UnusedRev(name.to_string(), rev)); } if let Some(tag) = tag { return Err(SourceError::UnusedTag(name.to_string(), tag)); } if let Some(branch) = branch { return Err(SourceError::UnusedBranch(name.to_string(), branch)); } } // If the source is a workspace package, error if the user tried to specify a source. if workspace { return match source { RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => { Ok(Some(Source::Workspace { workspace: true })) } RequirementSource::Url { .. } => { Err(SourceError::WorkspacePackageUrl(name.to_string())) } RequirementSource::Git { .. } => { Err(SourceError::WorkspacePackageGit(name.to_string())) } RequirementSource::Path { .. } => { Err(SourceError::WorkspacePackageFile(name.to_string())) } }; } let source = match source { RequirementSource::Registry { .. } => return Ok(None), RequirementSource::Path { install_path, .. } | RequirementSource::Directory { install_path, .. } => Source::Path { editable, path: relative_to(&install_path, root) .or_else(|_| std::path::absolute(&install_path)) .map_err(SourceError::Absolute)?, }, RequirementSource::Url { subdirectory, url, .. } => Source::Url { url: url.to_url(), subdirectory, }, RequirementSource::Git { repository, mut reference, subdirectory, .. } => { if rev.is_none() && tag.is_none() && branch.is_none() { let rev = match reference { GitReference::FullCommit(ref mut rev) => Some(mem::take(rev)), GitReference::Branch(ref mut rev) => Some(mem::take(rev)), GitReference::Tag(ref mut rev) => Some(mem::take(rev)), GitReference::ShortCommit(ref mut rev) => Some(mem::take(rev)), GitReference::BranchOrTag(ref mut rev) => Some(mem::take(rev)), GitReference::BranchOrTagOrCommit(ref mut rev) => Some(mem::take(rev)), GitReference::NamedRef(ref mut rev) => Some(mem::take(rev)), GitReference::DefaultBranch => None, }; Source::Git { rev, tag, branch, git: repository, subdirectory, } } else { Source::Git { rev, tag, branch, git: repository, subdirectory, } } } }; Ok(Some(source)) } } /// The type of a dependency in a `pyproject.toml`. #[derive(Debug, Clone, PartialEq, Eq)] pub enum DependencyType { /// A dependency in `project.dependencies`. Production, /// A dependency in `tool.uv.dev-dependencies`. Dev, /// A dependency in `project.optional-dependencies.{0}`. Optional(ExtraName), } /// 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) } }