Skip to main content

uv_workspace/
pyproject.rs

1//! Reads the following fields from `pyproject.toml`:
2//!
3//! * `project.{dependencies,optional-dependencies}`
4//! * `tool.uv.sources`
5//! * `tool.uv.workspace`
6//!
7//! Then lowers them into a dependency specification.
8
9#[cfg(feature = "schemars")]
10use std::borrow::Cow;
11use std::collections::BTreeMap;
12use std::fmt::Formatter;
13use std::ops::Deref;
14use std::path::{Path, PathBuf};
15use std::str::FromStr;
16
17use glob::Pattern;
18use rustc_hash::{FxBuildHasher, FxHashSet};
19use serde::de::SeqAccess;
20use serde::{Deserialize, Deserializer, Serialize};
21use thiserror::Error;
22use tracing::instrument;
23use uv_build_backend::BuildBackendSettings;
24use uv_configuration::{ExcludeDependency, GitLfsSetting, Override};
25use uv_distribution_types::{Index, IndexName, RequirementSource};
26use uv_fs::{PortablePathBuf, relative_to};
27use uv_git_types::GitReference;
28use uv_macros::OptionsMetadata;
29use uv_normalize::{DefaultGroups, ExtraName, GroupName, PackageName};
30use uv_options_metadata::{OptionSet, OptionsMetadata, Visit};
31use uv_pep440::{Version, VersionSpecifiers};
32use uv_pep508::MarkerTree;
33use uv_pypi_types::{
34    ConflictError, Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments,
35    VerbatimParsedUrl,
36};
37use uv_redacted::DisplaySafeUrl;
38use uv_toml::deserialize_unique_map;
39
40#[derive(Error, Debug)]
41pub enum PyprojectTomlError {
42    #[error(transparent)]
43    Toml(#[from] toml::de::Error),
44    #[error("Failed to parse `tool.uv.sources`")]
45    Source(
46        #[from]
47        #[source]
48        SourceError,
49    ),
50    #[error(
51        "`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set"
52    )]
53    MissingName,
54    #[error(
55        "`pyproject.toml` is using the `[project]` table, but the required `project.version` field is neither set nor present in the `project.dynamic` list"
56    )]
57    MissingVersion,
58}
59
60fn deserialize_optional_dependencies<'de, D, V>(
61    deserializer: D,
62) -> Result<Option<BTreeMap<ExtraName, V>>, D::Error>
63where
64    D: Deserializer<'de>,
65    V: Deserialize<'de>,
66{
67    deserialize_unique_map(deserializer, |key: &ExtraName| {
68        format!("duplicate normalized extra name `{key}`")
69    })
70    .map(Some)
71}
72
73/// A `pyproject.toml` as specified in PEP 517.
74#[derive(Deserialize, Debug, Clone)]
75#[cfg_attr(test, derive(Serialize))]
76#[serde(rename_all = "kebab-case")]
77pub struct PyProjectToml {
78    /// PEP 621-compliant project metadata.
79    pub project: Option<Project>,
80    /// Tool-specific metadata.
81    pub tool: Option<Tool>,
82    /// Non-project dependency groups, as defined in PEP 735.
83    pub dependency_groups: Option<DependencyGroups>,
84    /// The raw unserialized document.
85    #[serde(skip)]
86    pub raw: String,
87
88    /// Used to determine whether a `build-system` section is present.
89    #[serde(default, skip_serializing)]
90    build_system: Option<serde::de::IgnoredAny>,
91}
92
93impl PyProjectToml {
94    /// Parse a `PyProjectToml` from a raw TOML string.
95    #[instrument("toml::from_str workspace", skip_all, fields(path = %_path.as_ref().display()))]
96    pub fn from_string(raw: String, _path: impl AsRef<Path>) -> Result<Self, PyprojectTomlError> {
97        let pyproject: Self = match toml::from_str(&raw) {
98            Ok(pyproject) => pyproject,
99            Err(error) => {
100                // Preserve the more specific source error if both parses would fail.
101                let sources = toml::from_str::<PyProjectTomlSourcesWire>(&raw)
102                    .map_err(PyprojectTomlError::Toml)?
103                    .tool
104                    .and_then(|tool| tool.uv)
105                    .and_then(|uv| uv.sources);
106                if let Some(sources) = sources {
107                    ToolUvSources::try_from(sources)?;
108                }
109                return Err(PyprojectTomlError::Toml(error));
110            }
111        };
112
113        Ok(Self { raw, ..pyproject })
114    }
115
116    /// Returns `true` if the project should be considered a Python package, as opposed to a
117    /// non-package ("virtual") project.
118    pub fn is_package(&self, require_build_system: bool) -> bool {
119        // If `tool.uv.package` is set, defer to that explicit setting.
120        if let Some(is_package) = self.tool_uv_package() {
121            return is_package;
122        }
123
124        // Otherwise, a project is assumed to be a package if `build-system` is present.
125        self.build_system.is_some() || !require_build_system
126    }
127
128    /// Returns the value of `tool.uv.package` if set.
129    fn tool_uv_package(&self) -> Option<bool> {
130        self.tool
131            .as_ref()
132            .and_then(|tool| tool.uv.as_ref())
133            .and_then(|uv| uv.package)
134    }
135
136    /// Returns whether the project manifest contains any script table.
137    pub fn has_scripts(&self) -> bool {
138        if let Some(ref project) = self.project {
139            project.gui_scripts.is_some() || project.scripts.is_some()
140        } else {
141            false
142        }
143    }
144
145    /// Returns the set of conflicts for the project.
146    pub(crate) fn conflicts(&self) -> Result<Conflicts, ConflictError> {
147        let empty = Conflicts::empty();
148        let Some(project) = self.project.as_ref() else {
149            return Ok(empty);
150        };
151        let Some(tool) = self.tool.as_ref() else {
152            return Ok(empty);
153        };
154        let Some(tooluv) = tool.uv.as_ref() else {
155            return Ok(empty);
156        };
157        let Some(conflicting) = tooluv.conflicts.as_ref() else {
158            return Ok(empty);
159        };
160        conflicting.to_conflicts_with_package_name(&project.name)
161    }
162}
163
164// Ignore raw document in comparison.
165impl PartialEq for PyProjectToml {
166    fn eq(&self, other: &Self) -> bool {
167        self.project.eq(&other.project) && self.tool.eq(&other.tool)
168    }
169}
170
171impl Eq for PyProjectToml {}
172
173impl AsRef<[u8]> for PyProjectToml {
174    fn as_ref(&self) -> &[u8] {
175        self.raw.as_bytes()
176    }
177}
178
179/// PEP 621 project metadata (`project`).
180///
181/// See <https://packaging.python.org/en/latest/specifications/pyproject-toml>.
182#[derive(Deserialize, Debug, Clone, PartialEq)]
183#[cfg_attr(test, derive(Serialize))]
184#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
185pub struct Project {
186    /// The name of the project
187    pub name: PackageName,
188    /// The version of the project
189    version: Option<Version>,
190    /// The Python versions this project is compatible with.
191    pub(crate) requires_python: Option<VersionSpecifiers>,
192    /// The dependencies of the project.
193    pub dependencies: Option<Vec<String>>,
194    /// The optional dependencies of the project.
195    pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
196
197    /// Used to determine whether a `gui-scripts` section is present.
198    #[serde(default, skip_serializing)]
199    gui_scripts: Option<serde::de::IgnoredAny>,
200    /// Used to determine whether a `scripts` section is present.
201    #[serde(default, skip_serializing)]
202    scripts: Option<serde::de::IgnoredAny>,
203}
204
205#[derive(Deserialize, Debug)]
206#[serde(rename_all = "kebab-case")]
207struct ProjectWire {
208    name: Option<PackageName>,
209    version: Option<Version>,
210    dynamic: Option<Vec<String>>,
211    requires_python: Option<VersionSpecifiers>,
212    dependencies: Option<Vec<String>>,
213    #[serde(default, deserialize_with = "deserialize_optional_dependencies")]
214    optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
215    gui_scripts: Option<serde::de::IgnoredAny>,
216    scripts: Option<serde::de::IgnoredAny>,
217}
218
219impl TryFrom<ProjectWire> for Project {
220    type Error = PyprojectTomlError;
221
222    fn try_from(value: ProjectWire) -> Result<Self, Self::Error> {
223        // If `[project.name]` is not present, show a dedicated error message.
224        let name = value.name.ok_or(PyprojectTomlError::MissingName)?;
225
226        // If `[project.version]` is not present (or listed in `[project.dynamic]`), show a dedicated error message.
227        if value.version.is_none()
228            && !value
229                .dynamic
230                .as_ref()
231                .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
232        {
233            return Err(PyprojectTomlError::MissingVersion);
234        }
235
236        Ok(Self {
237            name,
238            version: value.version,
239            requires_python: value.requires_python,
240            dependencies: value.dependencies,
241            optional_dependencies: value.optional_dependencies,
242            gui_scripts: value.gui_scripts,
243            scripts: value.scripts,
244        })
245    }
246}
247
248#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
249#[cfg_attr(test, derive(Serialize))]
250#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
251pub struct Tool {
252    pub uv: Option<ToolUv>,
253}
254
255/// Validates the `tool.uv.index` field.
256///
257/// This custom deserializer function checks for:
258/// - Duplicate index names
259/// - Multiple indexes marked as default
260fn deserialize_index_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Index>>, D::Error>
261where
262    D: Deserializer<'de>,
263{
264    let indexes = Option::<Vec<Index>>::deserialize(deserializer)?;
265    if let Some(indexes) = indexes.as_ref() {
266        let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher);
267        let mut seen_default = false;
268        for index in indexes {
269            if let Some(name) = index.name.as_ref() {
270                if !seen_names.insert(name) {
271                    return Err(serde::de::Error::custom(format!(
272                        "duplicate index name `{name}`"
273                    )));
274                }
275            }
276            if index.default {
277                if seen_default {
278                    return Err(serde::de::Error::custom(
279                        "found multiple indexes with `default = true`; only one index may be marked as default",
280                    ));
281                }
282                seen_default = true;
283            }
284        }
285    }
286    Ok(indexes)
287}
288
289/// An override dependency before source lowering.
290pub type OverrideDependency = Override<uv_pep508::Requirement<VerbatimParsedUrl>>;
291
292// NOTE(charlie): When adding fields to this struct, mark them as ignored on `Options` in
293// `crates/uv-settings/src/settings.rs`.
294#[derive(Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
295#[cfg_attr(test, derive(Serialize))]
296#[serde(rename_all = "kebab-case")]
297#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
298pub struct ToolUv {
299    /// The sources to use when resolving dependencies.
300    ///
301    /// `tool.uv.sources` enriches the dependency metadata with additional sources, incorporated
302    /// during development. A dependency source can be a Git repository, a URL, a local path, or an
303    /// alternative registry.
304    ///
305    /// See [Dependencies](../concepts/projects/dependencies.md) for more.
306    #[option(
307        default = "{}",
308        value_type = "dict",
309        example = r#"
310            [tool.uv.sources]
311            httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" }
312            pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" }
313            pydantic = { path = "/path/to/pydantic", editable = true }
314        "#
315    )]
316    pub sources: Option<ToolUvSources>,
317
318    /// The indexes to use when resolving dependencies.
319    ///
320    /// Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)
321    /// (the simple repository API), or a local directory laid out in the same format.
322    ///
323    /// Indexes are considered in the order in which they're defined, such that the first-defined
324    /// index has the highest priority. Further, the indexes provided by this setting are given
325    /// higher priority than any indexes specified via [`index_url`](#index-url) or
326    /// [`extra_index_url`](#extra-index-url). uv will only consider the first index that contains
327    /// a given package, unless an alternative [index strategy](#index-strategy) is specified.
328    ///
329    /// If an index is marked as `explicit = true`, it will be used exclusively for the
330    /// dependencies that select it explicitly via `[tool.uv.sources]`, as in:
331    ///
332    /// ```toml
333    /// [[tool.uv.index]]
334    /// name = "pytorch"
335    /// url = "https://download.pytorch.org/whl/cu130"
336    /// explicit = true
337    ///
338    /// [tool.uv.sources]
339    /// torch = { index = "pytorch" }
340    /// ```
341    ///
342    /// If an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is
343    /// given the lowest priority when resolving packages. Additionally, marking an index as default will disable the
344    /// PyPI default index.
345    #[option(
346        default = "[]",
347        value_type = "dict",
348        example = r#"
349            [[tool.uv.index]]
350            name = "pytorch"
351            url = "https://download.pytorch.org/whl/cu130"
352        "#
353    )]
354    #[serde(deserialize_with = "deserialize_index_vec", default)]
355    pub index: Option<Vec<Index>>,
356
357    /// The workspace definition for the project, if any.
358    #[option_group]
359    pub(crate) workspace: Option<ToolUvWorkspace>,
360
361    /// Whether the project is managed by uv. If `false`, uv will ignore the project when
362    /// `uv run` is invoked.
363    #[option(
364        default = r#"true"#,
365        value_type = "bool",
366        example = r#"
367            managed = false
368        "#
369    )]
370    pub(crate) managed: Option<bool>,
371
372    /// Whether the project should be considered a Python package, or a non-package ("virtual")
373    /// project.
374    ///
375    /// Packages are built and installed into the virtual environment in editable mode and thus
376    /// require a build backend, while virtual projects are _not_ built or installed; instead, only
377    /// their dependencies are included in the virtual environment.
378    ///
379    /// Creating a package requires that a `build-system` is present in the `pyproject.toml`, and
380    /// that the project adheres to a structure that adheres to the build backend's expectations
381    /// (e.g., a `src` layout).
382    #[option(
383        default = r#"true"#,
384        value_type = "bool",
385        example = r#"
386            package = false
387        "#
388    )]
389    package: Option<bool>,
390
391    /// The list of `dependency-groups` to install by default.
392    ///
393    /// Can also be the literal `"all"` to default enable all groups.
394    #[option(
395        default = r#"["dev"]"#,
396        value_type = r#"str | list[str]"#,
397        example = r#"
398            default-groups = ["docs"]
399        "#
400    )]
401    pub default_groups: Option<DefaultGroups>,
402
403    /// Additional settings for `dependency-groups`.
404    ///
405    /// Currently this can only be used to add `requires-python` constraints
406    /// to dependency groups (typically to inform uv that your dev tooling
407    /// has a higher python requirement than your actual project).
408    ///
409    /// This cannot be used to define dependency groups, use the top-level
410    /// `[dependency-groups]` table for that.
411    #[option(
412        default = "[]",
413        value_type = "dict",
414        example = r#"
415            [tool.uv.dependency-groups]
416            my-group = {requires-python = ">=3.12"}
417        "#
418    )]
419    pub(crate) dependency_groups: Option<ToolUvDependencyGroups>,
420
421    /// The project's development dependencies.
422    ///
423    /// Development dependencies will be installed by default in `uv run` and `uv sync`, but will
424    /// not appear in the project's published metadata.
425    ///
426    /// Use of this field is not recommend anymore. Instead, use the `dependency-groups.dev` field
427    /// which is a standardized way to declare development dependencies. The contents of
428    /// `tool.uv.dev-dependencies` and `dependency-groups.dev` are combined to determine the final
429    /// requirements of the `dev` dependency group.
430    #[cfg_attr(
431        feature = "schemars",
432        schemars(
433            with = "Option<Vec<String>>",
434            description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
435        )
436    )]
437    #[option(
438        default = "[]",
439        value_type = "list[str]",
440        example = r#"
441            dev-dependencies = ["ruff==0.5.0"]
442        "#
443    )]
444    pub dev_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
445
446    /// Overrides to apply when resolving the project's dependencies.
447    ///
448    /// Overrides are used to force selection of a specific version of a package, regardless of the
449    /// version requested by any other package, and regardless of whether choosing that version
450    /// would typically constitute an invalid resolution.
451    ///
452    /// While constraints are _additive_, in that they're combined with the requirements of the
453    /// constituent packages, overrides are _absolute_, in that they completely replace the
454    /// requirements of any constituent packages.
455    ///
456    /// Including a package as an override will _not_ trigger installation of the package on its
457    /// own; instead, the package must be requested elsewhere in the project's first-party or
458    /// transitive dependencies.
459    ///
460    /// Overrides can be limited to the dependencies declared by a specific package version by
461    /// using a table with `package` and `dependencies`. The `package` table identifies the package
462    /// whose dependencies will be overridden by `name` and, optionally, `version`. If `version` is
463    /// omitted, the overrides apply to all versions of that package. Requirements in `dependencies`
464    /// replace dependencies with the same name and add dependencies that are not declared by the
465    /// package. Dependencies not listed in `dependencies` are left unchanged.
466    ///
467    /// Scoped overrides currently support registry version specifiers only. Direct URL and path
468    /// sources, including Git sources, and explicit indexes are not supported.
469    ///
470    /// !!! note
471    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `override-dependencies` from
472    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
473    ///     workspace members or `uv.toml` files.
474    #[option(
475        default = "[]",
476        value_type = "list[str | dict]",
477        example = r#"
478            override-dependencies = [
479                # Always install Werkzeug 2.3.0.
480                "werkzeug==2.3.0",
481                # Use itsdangerous 2.1.2 when requested by Flask 3.0.0.
482                { package = { name = "flask", version = "3.0.0" }, dependencies = ["itsdangerous==2.1.2"] },
483            ]
484        "#
485    )]
486    pub(crate) override_dependencies: Option<Vec<OverrideDependency>>,
487
488    /// Dependencies to exclude when resolving the project's dependencies.
489    ///
490    /// Excludes are used to prevent a package from being selected during resolution,
491    /// regardless of whether it's requested by any other package. When a package is excluded,
492    /// it will be omitted from the dependency list entirely.
493    ///
494    /// Including a package as an exclusion will prevent it from being installed, even if
495    /// it's requested by transitive dependencies. This can be useful for removing optional
496    /// dependencies or working around packages with broken dependencies.
497    ///
498    /// Exclusions can be limited to the dependencies declared by a specific package version by
499    /// using a table with `package` and `dependencies`. The `package` table identifies the package
500    /// whose dependencies will be excluded by `name` and, optionally, `version`. If `version` is
501    /// omitted, the exclusions apply to all versions of that package. A version-specific entry
502    /// takes precedence over an all-versions entry.
503    ///
504    /// !!! note
505    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `exclude-dependencies` from
506    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
507    ///     workspace members or `uv.toml` files.
508    #[option(
509        default = "[]",
510        value_type = "list[str | dict]",
511        example = r#"
512            # Exclude Werkzeug from being installed, even if transitive dependencies request it.
513            exclude-dependencies = [
514                "werkzeug",
515                { package = { name = "flask", version = "3.0.0" }, dependencies = ["itsdangerous"] },
516            ]
517        "#
518    )]
519    pub(crate) exclude_dependencies: Option<Vec<ExcludeDependency>>,
520
521    /// Constraints to apply when resolving the project's dependencies.
522    ///
523    /// Constraints are used to restrict the versions of dependencies that are selected during
524    /// resolution.
525    ///
526    /// Including a package as a constraint will _not_ trigger installation of the package on its
527    /// own; instead, the package must be requested elsewhere in the project's first-party or
528    /// transitive dependencies.
529    ///
530    /// !!! note
531    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `constraint-dependencies` from
532    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
533    ///     workspace members or `uv.toml` files.
534    #[cfg_attr(
535        feature = "schemars",
536        schemars(
537            with = "Option<Vec<String>>",
538            description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
539        )
540    )]
541    #[option(
542        default = "[]",
543        value_type = "list[str]",
544        example = r#"
545            # Ensure that the grpcio version is always less than 1.65, if it's requested by a
546            # direct or transitive dependency.
547            constraint-dependencies = ["grpcio<1.65"]
548        "#
549    )]
550    pub(crate) constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
551
552    /// Constraints to apply when solving build dependencies.
553    ///
554    /// Build constraints are used to restrict the versions of build dependencies that are selected
555    /// when building a package during resolution or installation.
556    ///
557    /// Including a package as a constraint will _not_ trigger installation of the package during
558    /// a build; instead, the package must be requested elsewhere in the project's build dependency
559    /// graph.
560    ///
561    /// !!! note
562    ///     In `uv lock`, `uv sync`, and `uv run`, uv will only read `build-constraint-dependencies` from
563    ///     the `pyproject.toml` at the workspace root, and will ignore any declarations in other
564    ///     workspace members or `uv.toml` files.
565    #[cfg_attr(
566        feature = "schemars",
567        schemars(
568            with = "Option<Vec<String>>",
569            description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
570        )
571    )]
572    #[option(
573        default = "[]",
574        value_type = "list[str]",
575        example = r#"
576            # Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency
577            # on setuptools.
578            build-constraint-dependencies = ["setuptools==60.0.0"]
579        "#
580    )]
581    pub(crate) build_constraint_dependencies:
582        Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
583
584    /// A list of supported environments against which to resolve dependencies.
585    ///
586    /// By default, uv will resolve for all possible environments during a `uv lock` operation.
587    /// However, you can restrict the set of supported environments to improve performance and avoid
588    /// unsatisfiable branches in the solution space.
589    ///
590    /// These environments will also be respected when `uv pip compile` is invoked with the
591    /// `--universal` flag.
592    #[cfg_attr(
593        feature = "schemars",
594        schemars(
595            with = "Option<Vec<String>>",
596            description = "A list of environment markers, e.g., `python_version >= '3.6'`."
597        )
598    )]
599    #[option(
600        default = "[]",
601        value_type = "str | list[str]",
602        example = r#"
603            # Resolve for macOS, but not for Linux or Windows.
604            environments = ["sys_platform == 'darwin'"]
605        "#
606    )]
607    pub(crate) environments: Option<SupportedEnvironments>,
608
609    /// A list of required platforms, for packages that lack source distributions.
610    ///
611    /// When a package does not have a source distribution, it's availability will be limited to
612    /// the platforms supported by its built distributions (wheels). For example, if a package only
613    /// publishes wheels for Linux, then it won't be installable on macOS or Windows.
614    ///
615    /// By default, uv requires each package to include at least one wheel that is compatible with
616    /// the designated Python version. The `required-environments` setting can be used to ensure that
617    /// the resulting resolution contains wheels for specific platforms, or fails if no such wheels
618    /// are available.
619    ///
620    /// While the `environments` setting _limits_ the set of environments that uv will consider when
621    /// resolving dependencies, `required-environments` _expands_ the set of platforms that uv _must_
622    /// support when resolving dependencies.
623    ///
624    /// For example, `environments = ["sys_platform == 'darwin'"]` would limit uv to solving for
625    /// macOS (and ignoring Linux and Windows). On the other hand, `required-environments = ["sys_platform == 'darwin'"]`
626    /// would _require_ that any package without a source distribution include a wheel for macOS in
627    /// order to be installable.
628    #[cfg_attr(
629        feature = "schemars",
630        schemars(
631            with = "Option<Vec<String>>",
632            description = "A list of environment markers, e.g., `sys_platform == 'darwin'."
633        )
634    )]
635    #[option(
636        default = "[]",
637        value_type = "str | list[str]",
638        example = r#"
639            # Require that the package is available on the following platforms:
640            required-environments = [
641                # macOS on Apple Silicon (ARM)
642                "sys_platform == 'darwin' and platform_machine == 'arm64'",
643                # Linux on x86_64 (Intel/AMD)
644                "sys_platform == 'linux' and platform_machine == 'x86_64'",
645                # Windows on x86_64 (Intel/AMD)
646                "sys_platform == 'win32' and platform_machine == 'AMD64'",
647            ]
648        "#
649    )]
650    pub(crate) required_environments: Option<SupportedEnvironments>,
651
652    /// Declare collections of extras or dependency groups that are conflicting
653    /// (i.e., mutually exclusive).
654    ///
655    /// It's useful to declare conflicts when two or more extras have mutually
656    /// incompatible dependencies. For example, extra `foo` might depend
657    /// on `numpy==2.0.0` while extra `bar` depends on `numpy==2.1.0`. While these
658    /// dependencies conflict, it may be the case that users are not expected to
659    /// activate both `foo` and `bar` at the same time, making it possible to
660    /// generate a universal resolution for the project despite the incompatibility.
661    ///
662    /// By making such conflicts explicit, uv can generate a universal resolution
663    /// for a project, taking into account that certain combinations of extras and
664    /// groups are mutually exclusive. In exchange, installation will fail if a
665    /// user attempts to activate both conflicting extras.
666    #[cfg_attr(
667        feature = "schemars",
668        schemars(description = "A list of sets of conflicting groups or extras.")
669    )]
670    #[option(
671        default = r#"[]"#,
672        value_type = "list[list[dict]]",
673        example = r#"
674            # Require that `package[extra1]` and `package[extra2]` are resolved
675            # in different forks so that they cannot conflict with one another.
676            conflicts = [
677                [
678                    { extra = "extra1" },
679                    { extra = "extra2" },
680                ]
681            ]
682
683            # Require that the dependency groups `group1` and `group2`
684            # are resolved in different forks so that they cannot conflict
685            # with one another.
686            conflicts = [
687                [
688                    { group = "group1" },
689                    { group = "group2" },
690                ]
691            ]
692        "#
693    )]
694    pub(crate) conflicts: Option<SchemaConflicts>,
695
696    // Only exists on this type for schema and docs generation, the build backend settings are
697    // never merged in a workspace and read separately by the backend code.
698    /// Configuration for the uv build backend.
699    ///
700    /// Note that those settings only apply when using the `uv_build` backend, other build backends
701    /// (such as hatchling) have their own configuration.
702    #[option_group]
703    build_backend: Option<BuildBackendSettingsSchema>,
704}
705
706#[derive(Default, Debug, Clone, PartialEq, Eq)]
707#[cfg_attr(test, derive(Serialize))]
708#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
709pub struct ToolUvSources(BTreeMap<PackageName, Sources>);
710
711#[derive(Deserialize, Debug)]
712#[serde(rename_all = "kebab-case")]
713struct PyProjectTomlSourcesWire {
714    tool: Option<ToolSourcesWire>,
715}
716
717#[derive(Deserialize, Debug)]
718struct ToolSourcesWire {
719    uv: Option<ToolUvSourcesOnlyWire>,
720}
721
722#[derive(Deserialize, Debug)]
723#[serde(rename_all = "kebab-case")]
724struct ToolUvSourcesOnlyWire {
725    sources: Option<ToolUvSourcesWire>,
726}
727
728#[derive(Default, Debug, Clone, PartialEq, Eq)]
729struct ToolUvSourcesWire(BTreeMap<PackageName, SourcesWire>);
730
731impl ToolUvSources {
732    /// Returns the underlying `BTreeMap` of package names to sources.
733    pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
734        &self.0
735    }
736
737    /// Convert the [`ToolUvSources`] into its inner `BTreeMap`.
738    #[must_use]
739    pub(crate) fn into_inner(self) -> BTreeMap<PackageName, Sources> {
740        self.0
741    }
742}
743
744impl TryFrom<ToolUvSourcesWire> for ToolUvSources {
745    type Error = SourceError;
746
747    fn try_from(wire: ToolUvSourcesWire) -> Result<Self, Self::Error> {
748        wire.0
749            .into_iter()
750            .map(|(name, sources)| Sources::try_from(sources).map(|sources| (name, sources)))
751            .collect::<Result<BTreeMap<_, _>, _>>()
752            .map(Self)
753    }
754}
755
756/// Ensure that all keys in the TOML table are unique.
757impl<'de> serde::de::Deserialize<'de> for ToolUvSourcesWire {
758    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
759    where
760        D: Deserializer<'de>,
761    {
762        deserialize_unique_map(deserializer, |key: &PackageName| {
763            format!("duplicate sources for package `{key}`")
764        })
765        .map(ToolUvSourcesWire)
766    }
767}
768
769/// Ensure that all keys in the TOML table are unique.
770impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
771    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
772    where
773        D: Deserializer<'de>,
774    {
775        deserialize_unique_map(deserializer, |key: &PackageName| {
776            format!("duplicate sources for package `{key}`")
777        })
778        .map(ToolUvSources)
779    }
780}
781
782#[derive(Default, Debug, Clone, PartialEq, Eq)]
783#[cfg_attr(test, derive(Serialize))]
784#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
785pub(crate) struct ToolUvDependencyGroups(BTreeMap<GroupName, DependencyGroupSettings>);
786
787impl ToolUvDependencyGroups {
788    /// Returns the underlying `BTreeMap` of group names to settings.
789    pub(crate) fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
790        &self.0
791    }
792}
793
794/// Ensure that all keys in the TOML table are unique.
795impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
796    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
797    where
798        D: Deserializer<'de>,
799    {
800        deserialize_unique_map(deserializer, |key: &GroupName| {
801            format!("duplicate settings for dependency group `{key}`")
802        })
803        .map(ToolUvDependencyGroups)
804    }
805}
806
807#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
808#[cfg_attr(test, derive(Serialize))]
809#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
810#[serde(rename_all = "kebab-case")]
811pub(crate) struct DependencyGroupSettings {
812    /// Version of python to require when installing this group
813    #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
814    pub(crate) requires_python: Option<VersionSpecifiers>,
815}
816
817#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
818#[serde(untagged, rename_all = "kebab-case")]
819#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
820enum ExtraBuildDependencyWire {
821    Unannotated(uv_pep508::Requirement<VerbatimParsedUrl>),
822    #[serde(rename_all = "kebab-case")]
823    Annotated {
824        requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
825        match_runtime: bool,
826    },
827}
828
829#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
830#[serde(
831    deny_unknown_fields,
832    from = "ExtraBuildDependencyWire",
833    into = "ExtraBuildDependencyWire"
834)]
835#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
836pub struct ExtraBuildDependency {
837    pub requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
838    pub match_runtime: bool,
839}
840
841impl From<ExtraBuildDependency> for uv_pep508::Requirement<VerbatimParsedUrl> {
842    fn from(value: ExtraBuildDependency) -> Self {
843        value.requirement
844    }
845}
846
847impl From<ExtraBuildDependencyWire> for ExtraBuildDependency {
848    fn from(wire: ExtraBuildDependencyWire) -> Self {
849        match wire {
850            ExtraBuildDependencyWire::Unannotated(requirement) => Self {
851                requirement,
852                match_runtime: false,
853            },
854            ExtraBuildDependencyWire::Annotated {
855                requirement,
856                match_runtime,
857            } => Self {
858                requirement,
859                match_runtime,
860            },
861        }
862    }
863}
864
865impl From<ExtraBuildDependency> for ExtraBuildDependencyWire {
866    fn from(item: ExtraBuildDependency) -> Self {
867        Self::Annotated {
868            requirement: item.requirement,
869            match_runtime: item.match_runtime,
870        }
871    }
872}
873
874#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
875#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
876pub struct ExtraBuildDependencies(BTreeMap<PackageName, Vec<ExtraBuildDependency>>);
877
878impl std::ops::Deref for ExtraBuildDependencies {
879    type Target = BTreeMap<PackageName, Vec<ExtraBuildDependency>>;
880
881    fn deref(&self) -> &Self::Target {
882        &self.0
883    }
884}
885
886impl std::ops::DerefMut for ExtraBuildDependencies {
887    fn deref_mut(&mut self) -> &mut Self::Target {
888        &mut self.0
889    }
890}
891
892impl IntoIterator for ExtraBuildDependencies {
893    type Item = (PackageName, Vec<ExtraBuildDependency>);
894    type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildDependency>>;
895
896    fn into_iter(self) -> Self::IntoIter {
897        self.0.into_iter()
898    }
899}
900
901impl FromIterator<(PackageName, Vec<ExtraBuildDependency>)> for ExtraBuildDependencies {
902    fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildDependency>)>>(
903        iter: T,
904    ) -> Self {
905        Self(iter.into_iter().collect())
906    }
907}
908
909/// Ensure that all keys in the TOML table are unique.
910impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
911    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
912    where
913        D: Deserializer<'de>,
914    {
915        deserialize_unique_map(deserializer, |key: &PackageName| {
916            format!("duplicate extra-build-dependencies for `{key}`")
917        })
918        .map(ExtraBuildDependencies)
919    }
920}
921
922#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
923#[cfg_attr(test, derive(Serialize))]
924#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
925#[serde(rename_all = "kebab-case", deny_unknown_fields)]
926pub(crate) struct ToolUvWorkspace {
927    /// Packages to include as workspace members.
928    ///
929    /// Supports both globs and explicit paths.
930    ///
931    /// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
932    #[option(
933        default = "[]",
934        value_type = "list[str]",
935        example = r#"
936            members = ["member1", "path/to/member2", "libs/*"]
937        "#
938    )]
939    pub(crate) members: Option<Vec<SerdePattern>>,
940    /// Packages to exclude as workspace members. If a package matches both `members` and
941    /// `exclude`, it will be excluded.
942    ///
943    /// Supports both globs and explicit paths.
944    ///
945    /// For more information on the glob syntax, refer to the [`glob` documentation](https://docs.rs/glob/latest/glob/struct.Pattern.html).
946    #[option(
947        default = "[]",
948        value_type = "list[str]",
949        example = r#"
950            exclude = ["member1", "path/to/member2", "libs/*"]
951        "#
952    )]
953    pub(crate) exclude: Option<Vec<SerdePattern>>,
954}
955
956/// (De)serialize globs as strings.
957#[derive(Debug, Clone, PartialEq, Eq)]
958pub(crate) struct SerdePattern(Pattern);
959
960impl serde::ser::Serialize for SerdePattern {
961    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
962    where
963        S: serde::ser::Serializer,
964    {
965        self.0.as_str().serialize(serializer)
966    }
967}
968
969impl<'de> serde::Deserialize<'de> for SerdePattern {
970    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
971        struct Visitor;
972
973        impl serde::de::Visitor<'_> for Visitor {
974            type Value = SerdePattern;
975
976            fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
977                f.write_str("a string")
978            }
979
980            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
981                Pattern::from_str(v)
982                    .map(SerdePattern)
983                    .map_err(serde::de::Error::custom)
984            }
985        }
986
987        deserializer.deserialize_str(Visitor)
988    }
989}
990
991#[cfg(feature = "schemars")]
992impl schemars::JsonSchema for SerdePattern {
993    fn schema_name() -> Cow<'static, str> {
994        Cow::Borrowed("SerdePattern")
995    }
996
997    fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
998        <String as schemars::JsonSchema>::json_schema(generator)
999    }
1000}
1001
1002impl Deref for SerdePattern {
1003    type Target = Pattern;
1004
1005    fn deref(&self) -> &Self::Target {
1006        &self.0
1007    }
1008}
1009
1010#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1011#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1012#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]
1013pub struct Sources(#[cfg_attr(feature = "schemars", schemars(with = "SourcesWire"))] Vec<Source>);
1014
1015impl Sources {
1016    /// Return an [`Iterator`] over the sources.
1017    ///
1018    /// If the iterator contains multiple entries, they will always use disjoint markers.
1019    ///
1020    /// The iterator will contain at most one registry source.
1021    pub fn iter(&self) -> impl Iterator<Item = &Source> {
1022        self.0.iter()
1023    }
1024}
1025
1026impl FromIterator<Source> for Sources {
1027    fn from_iter<T: IntoIterator<Item = Source>>(iter: T) -> Self {
1028        Self(iter.into_iter().collect())
1029    }
1030}
1031
1032impl IntoIterator for Sources {
1033    type Item = Source;
1034    type IntoIter = std::vec::IntoIter<Source>;
1035
1036    fn into_iter(self) -> Self::IntoIter {
1037        self.0.into_iter()
1038    }
1039}
1040
1041#[derive(Debug, Clone, PartialEq, Eq)]
1042#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
1043enum SourcesWire {
1044    One(Source),
1045    Many(Vec<Source>),
1046}
1047
1048impl<'de> serde::de::Deserialize<'de> for SourcesWire {
1049    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1050    where
1051        D: Deserializer<'de>,
1052    {
1053        struct Visitor;
1054
1055        impl<'de> serde::de::Visitor<'de> for Visitor {
1056            type Value = SourcesWire;
1057
1058            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1059                formatter.write_str("a single source (as a map) or list of sources")
1060            }
1061
1062            fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
1063            where
1064                A: SeqAccess<'de>,
1065            {
1066                let sources = serde::de::Deserialize::deserialize(
1067                    serde::de::value::SeqAccessDeserializer::new(seq),
1068                )?;
1069                Ok(SourcesWire::Many(sources))
1070            }
1071
1072            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
1073            where
1074                M: serde::de::MapAccess<'de>,
1075            {
1076                let source = serde::de::Deserialize::deserialize(
1077                    serde::de::value::MapAccessDeserializer::new(&mut map),
1078                )?;
1079                Ok(SourcesWire::One(source))
1080            }
1081        }
1082
1083        deserializer.deserialize_any(Visitor)
1084    }
1085}
1086
1087impl TryFrom<SourcesWire> for Sources {
1088    type Error = SourceError;
1089
1090    fn try_from(wire: SourcesWire) -> Result<Self, Self::Error> {
1091        match wire {
1092            SourcesWire::One(source) => Ok(Self(vec![source])),
1093            SourcesWire::Many(sources) => {
1094                for [lhs, rhs] in sources.array_windows() {
1095                    if lhs.extra() != rhs.extra() {
1096                        continue;
1097                    }
1098                    if lhs.group() != rhs.group() {
1099                        continue;
1100                    }
1101
1102                    let lhs = lhs.marker();
1103                    let rhs = rhs.marker();
1104                    if !lhs.is_disjoint(rhs) {
1105                        let Some(left) = lhs.contents().map(|contents| contents.to_string()) else {
1106                            return Err(SourceError::MissingMarkers);
1107                        };
1108
1109                        let Some(right) = rhs.contents().map(|contents| contents.to_string())
1110                        else {
1111                            return Err(SourceError::MissingMarkers);
1112                        };
1113
1114                        let mut hint = lhs.negate();
1115                        hint.and(rhs);
1116                        let hint = hint
1117                            .contents()
1118                            .map(|contents| contents.to_string())
1119                            .unwrap_or_else(|| "true".to_string());
1120
1121                        return Err(SourceError::OverlappingMarkers(left, right, hint));
1122                    }
1123                }
1124
1125                // Ensure that there is at least one source.
1126                if sources.is_empty() {
1127                    return Err(SourceError::EmptySources);
1128                }
1129
1130                Ok(Self(sources))
1131            }
1132        }
1133    }
1134}
1135
1136/// A `tool.uv.sources` value.
1137#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
1138#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1139#[serde(rename_all = "kebab-case", untagged, deny_unknown_fields)]
1140pub enum Source {
1141    /// A remote Git repository, available over HTTPS or SSH.
1142    ///
1143    /// Example:
1144    /// ```toml
1145    /// flask = { git = "https://github.com/pallets/flask", tag = "3.0.0" }
1146    /// ```
1147    Git {
1148        /// The repository URL (without the `git+` prefix).
1149        git: DisplaySafeUrl,
1150        /// The path to the directory with the `pyproject.toml`, if it's not in the repository root.
1151        subdirectory: Option<PortablePathBuf>,
1152        /// The path to the archive within the repository.
1153        path: Option<PortablePathBuf>,
1154        // Only one of the three may be used; we'll validate this later and emit a custom error.
1155        rev: Option<String>,
1156        tag: Option<String>,
1157        branch: Option<String>,
1158        /// Whether to use Git LFS when cloning the repository.
1159        lfs: Option<bool>,
1160        #[serde(
1161            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1162            serialize_with = "uv_pep508::marker::ser::serialize",
1163            default
1164        )]
1165        marker: MarkerTree,
1166        extra: Option<ExtraName>,
1167        group: Option<GroupName>,
1168    },
1169    /// A remote `http://` or `https://` URL, either a wheel (`.whl`) or a source distribution
1170    /// (`.zip`, `.tar.gz`).
1171    ///
1172    /// Example:
1173    /// ```toml
1174    /// flask = { url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl" }
1175    /// ```
1176    Url {
1177        url: DisplaySafeUrl,
1178        /// For source distributions, the path to the directory with the `pyproject.toml`, if it's
1179        /// not in the archive root.
1180        subdirectory: Option<PortablePathBuf>,
1181        #[serde(
1182            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1183            serialize_with = "uv_pep508::marker::ser::serialize",
1184            default
1185        )]
1186        marker: MarkerTree,
1187        extra: Option<ExtraName>,
1188        group: Option<GroupName>,
1189    },
1190    /// The path to a dependency, either a wheel (a `.whl` file), source distribution (a `.zip` or
1191    /// `.tar.gz` file), or source tree (i.e., a directory containing a `pyproject.toml` or
1192    /// `setup.py` file in the root).
1193    Path {
1194        path: PortablePathBuf,
1195        /// `false` by default.
1196        editable: Option<bool>,
1197        /// Whether to treat the dependency as a buildable Python package (`true`) or as a virtual
1198        /// package (`false`). If `false`, the package will not be built or installed, but its
1199        /// dependencies will be included in the virtual environment.
1200        ///
1201        /// When omitted, the package status is inferred based on the presence of a `[build-system]`
1202        /// in the project's `pyproject.toml`.
1203        package: Option<bool>,
1204        #[serde(
1205            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1206            serialize_with = "uv_pep508::marker::ser::serialize",
1207            default
1208        )]
1209        marker: MarkerTree,
1210        extra: Option<ExtraName>,
1211        group: Option<GroupName>,
1212    },
1213    /// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`.
1214    Registry {
1215        index: IndexName,
1216        #[serde(
1217            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1218            serialize_with = "uv_pep508::marker::ser::serialize",
1219            default
1220        )]
1221        marker: MarkerTree,
1222        extra: Option<ExtraName>,
1223        group: Option<GroupName>,
1224    },
1225    /// A dependency on another package in the workspace.
1226    Workspace {
1227        /// When set to `false`, the package will be fetched from the remote index, rather than
1228        /// included as a workspace package.
1229        workspace: bool,
1230        /// Whether the package should be installed as editable. Defaults to `true`.
1231        editable: Option<bool>,
1232        #[serde(
1233            skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1234            serialize_with = "uv_pep508::marker::ser::serialize",
1235            default
1236        )]
1237        marker: MarkerTree,
1238        extra: Option<ExtraName>,
1239        group: Option<GroupName>,
1240    },
1241}
1242
1243/// A custom deserialization implementation for [`Source`]. This is roughly equivalent to
1244/// `#[serde(untagged)]`, but provides more detailed error messages.
1245impl<'de> Deserialize<'de> for Source {
1246    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1247    where
1248        D: Deserializer<'de>,
1249    {
1250        #[derive(Deserialize, Debug, Clone)]
1251        #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1252        struct CatchAll {
1253            git: Option<DisplaySafeUrl>,
1254            subdirectory: Option<PortablePathBuf>,
1255            rev: Option<String>,
1256            tag: Option<String>,
1257            branch: Option<String>,
1258            lfs: Option<bool>,
1259            url: Option<DisplaySafeUrl>,
1260            path: Option<PortablePathBuf>,
1261            editable: Option<bool>,
1262            package: Option<bool>,
1263            index: Option<IndexName>,
1264            workspace: Option<bool>,
1265            #[serde(
1266                skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1267                serialize_with = "uv_pep508::marker::ser::serialize",
1268                default
1269            )]
1270            marker: MarkerTree,
1271            extra: Option<ExtraName>,
1272            group: Option<GroupName>,
1273        }
1274
1275        // Attempt to deserialize as `CatchAll`.
1276        let CatchAll {
1277            git,
1278            subdirectory,
1279            rev,
1280            tag,
1281            branch,
1282            lfs,
1283            url,
1284            path,
1285            editable,
1286            package,
1287            index,
1288            workspace,
1289            marker,
1290            extra,
1291            group,
1292        } = CatchAll::deserialize(deserializer)?;
1293
1294        // If both `extra` and `group` are set, return an error.
1295        if extra.is_some() && group.is_some() {
1296            return Err(serde::de::Error::custom(
1297                "cannot specify both `extra` and `group`",
1298            ));
1299        }
1300
1301        // If the `git` field is set, we're dealing with a Git source.
1302        if let Some(git) = git {
1303            if index.is_some() {
1304                return Err(serde::de::Error::custom(
1305                    "cannot specify both `git` and `index`",
1306                ));
1307            }
1308            if workspace.is_some() {
1309                return Err(serde::de::Error::custom(
1310                    "cannot specify both `git` and `workspace`",
1311                ));
1312            }
1313            if url.is_some() {
1314                return Err(serde::de::Error::custom(
1315                    "cannot specify both `git` and `url`",
1316                ));
1317            }
1318            if editable.is_some() {
1319                return Err(serde::de::Error::custom(
1320                    "cannot specify both `git` and `editable`",
1321                ));
1322            }
1323            if package.is_some() {
1324                return Err(serde::de::Error::custom(
1325                    "cannot specify both `git` and `package`",
1326                ));
1327            }
1328            if subdirectory.is_some() && path.is_some() {
1329                return Err(serde::de::Error::custom(
1330                    "cannot specify both `subdirectory` and `path`",
1331                ));
1332            }
1333
1334            // At most one of `rev`, `tag`, or `branch` may be set.
1335            match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1336                (None, None, None) => {}
1337                (Some(_), None, None) => {}
1338                (None, Some(_), None) => {}
1339                (None, None, Some(_)) => {}
1340                _ => {
1341                    return Err(serde::de::Error::custom(
1342                        "expected at most one of `rev`, `tag`, or `branch`",
1343                    ));
1344                }
1345            }
1346
1347            // If the user prefixed the URL with `git+`, strip it.
1348            let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1349                DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1350            } else {
1351                git
1352            };
1353
1354            return Ok(Self::Git {
1355                git,
1356                subdirectory,
1357                path,
1358                rev,
1359                tag,
1360                branch,
1361                lfs,
1362                marker,
1363                extra,
1364                group,
1365            });
1366        }
1367
1368        // If the `url` field is set, we're dealing with a URL source.
1369        if let Some(url) = url {
1370            if index.is_some() {
1371                return Err(serde::de::Error::custom(
1372                    "cannot specify both `url` and `index`",
1373                ));
1374            }
1375            if workspace.is_some() {
1376                return Err(serde::de::Error::custom(
1377                    "cannot specify both `url` and `workspace`",
1378                ));
1379            }
1380            if path.is_some() {
1381                return Err(serde::de::Error::custom(
1382                    "cannot specify both `url` and `path`",
1383                ));
1384            }
1385            if git.is_some() {
1386                return Err(serde::de::Error::custom(
1387                    "cannot specify both `url` and `git`",
1388                ));
1389            }
1390            if rev.is_some() {
1391                return Err(serde::de::Error::custom(
1392                    "cannot specify both `url` and `rev`",
1393                ));
1394            }
1395            if tag.is_some() {
1396                return Err(serde::de::Error::custom(
1397                    "cannot specify both `url` and `tag`",
1398                ));
1399            }
1400            if branch.is_some() {
1401                return Err(serde::de::Error::custom(
1402                    "cannot specify both `url` and `branch`",
1403                ));
1404            }
1405            if editable.is_some() {
1406                return Err(serde::de::Error::custom(
1407                    "cannot specify both `url` and `editable`",
1408                ));
1409            }
1410            if package.is_some() {
1411                return Err(serde::de::Error::custom(
1412                    "cannot specify both `url` and `package`",
1413                ));
1414            }
1415
1416            return Ok(Self::Url {
1417                url,
1418                subdirectory,
1419                marker,
1420                extra,
1421                group,
1422            });
1423        }
1424
1425        // If the `path` field is set, we're dealing with a path source.
1426        if let Some(path) = path {
1427            if index.is_some() {
1428                return Err(serde::de::Error::custom(
1429                    "cannot specify both `path` and `index`",
1430                ));
1431            }
1432            if workspace.is_some() {
1433                return Err(serde::de::Error::custom(
1434                    "cannot specify both `path` and `workspace`",
1435                ));
1436            }
1437            if git.is_some() {
1438                return Err(serde::de::Error::custom(
1439                    "cannot specify both `path` and `git`",
1440                ));
1441            }
1442            if url.is_some() {
1443                return Err(serde::de::Error::custom(
1444                    "cannot specify both `path` and `url`",
1445                ));
1446            }
1447            if rev.is_some() {
1448                return Err(serde::de::Error::custom(
1449                    "cannot specify both `path` and `rev`",
1450                ));
1451            }
1452            if tag.is_some() {
1453                return Err(serde::de::Error::custom(
1454                    "cannot specify both `path` and `tag`",
1455                ));
1456            }
1457            if branch.is_some() {
1458                return Err(serde::de::Error::custom(
1459                    "cannot specify both `path` and `branch`",
1460                ));
1461            }
1462
1463            // A project must be packaged in order to be installed as editable.
1464            if editable == Some(true) && package == Some(false) {
1465                return Err(serde::de::Error::custom(
1466                    "cannot specify both `editable = true` and `package = false`",
1467                ));
1468            }
1469
1470            return Ok(Self::Path {
1471                path,
1472                editable,
1473                package,
1474                marker,
1475                extra,
1476                group,
1477            });
1478        }
1479
1480        // If the `index` field is set, we're dealing with a registry source.
1481        if let Some(index) = index {
1482            if workspace.is_some() {
1483                return Err(serde::de::Error::custom(
1484                    "cannot specify both `index` and `workspace`",
1485                ));
1486            }
1487            if git.is_some() {
1488                return Err(serde::de::Error::custom(
1489                    "cannot specify both `index` and `git`",
1490                ));
1491            }
1492            if url.is_some() {
1493                return Err(serde::de::Error::custom(
1494                    "cannot specify both `index` and `url`",
1495                ));
1496            }
1497            if path.is_some() {
1498                return Err(serde::de::Error::custom(
1499                    "cannot specify both `index` and `path`",
1500                ));
1501            }
1502            if rev.is_some() {
1503                return Err(serde::de::Error::custom(
1504                    "cannot specify both `index` and `rev`",
1505                ));
1506            }
1507            if tag.is_some() {
1508                return Err(serde::de::Error::custom(
1509                    "cannot specify both `index` and `tag`",
1510                ));
1511            }
1512            if branch.is_some() {
1513                return Err(serde::de::Error::custom(
1514                    "cannot specify both `index` and `branch`",
1515                ));
1516            }
1517            if editable.is_some() {
1518                return Err(serde::de::Error::custom(
1519                    "cannot specify both `index` and `editable`",
1520                ));
1521            }
1522            if package.is_some() {
1523                return Err(serde::de::Error::custom(
1524                    "cannot specify both `index` and `package`",
1525                ));
1526            }
1527
1528            return Ok(Self::Registry {
1529                index,
1530                marker,
1531                extra,
1532                group,
1533            });
1534        }
1535
1536        // If the `workspace` field is set, we're dealing with a workspace source.
1537        if let Some(workspace) = workspace {
1538            if index.is_some() {
1539                return Err(serde::de::Error::custom(
1540                    "cannot specify both `workspace` and `index`",
1541                ));
1542            }
1543            if git.is_some() {
1544                return Err(serde::de::Error::custom(
1545                    "cannot specify both `workspace` and `git`",
1546                ));
1547            }
1548            if url.is_some() {
1549                return Err(serde::de::Error::custom(
1550                    "cannot specify both `workspace` and `url`",
1551                ));
1552            }
1553            if path.is_some() {
1554                return Err(serde::de::Error::custom(
1555                    "cannot specify both `workspace` and `path`",
1556                ));
1557            }
1558            if rev.is_some() {
1559                return Err(serde::de::Error::custom(
1560                    "cannot specify both `workspace` and `rev`",
1561                ));
1562            }
1563            if tag.is_some() {
1564                return Err(serde::de::Error::custom(
1565                    "cannot specify both `workspace` and `tag`",
1566                ));
1567            }
1568            if branch.is_some() {
1569                return Err(serde::de::Error::custom(
1570                    "cannot specify both `workspace` and `branch`",
1571                ));
1572            }
1573            if package.is_some() {
1574                return Err(serde::de::Error::custom(
1575                    "cannot specify both `workspace` and `package`",
1576                ));
1577            }
1578
1579            return Ok(Self::Workspace {
1580                workspace,
1581                editable,
1582                marker,
1583                extra,
1584                group,
1585            });
1586        }
1587
1588        // If none of the fields are set, we're dealing with an error.
1589        Err(serde::de::Error::custom(
1590            "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1591        ))
1592    }
1593}
1594
1595#[derive(Error, Debug)]
1596pub enum SourceError {
1597    #[error("Failed to resolve Git reference: `{0}`")]
1598    UnresolvedReference(String),
1599    #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1600    WorkspacePackageGit(String),
1601    #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1602    WorkspacePackageUrl(String),
1603    #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1604    WorkspacePackageFile(String),
1605    #[error(
1606        "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1607    )]
1608    UnusedRev(String, String),
1609    #[error(
1610        "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1611    )]
1612    UnusedTag(String, String),
1613    #[error(
1614        "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1615    )]
1616    UnusedBranch(String, String),
1617    #[error(
1618        "`{0}` did not resolve to a Git repository, but a Git extension (`--lfs`) was provided."
1619    )]
1620    UnusedLfs(String),
1621    #[error(
1622        "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1623    )]
1624    UnusedEditable(String),
1625    #[error("Failed to resolve absolute path")]
1626    Absolute(#[from] std::io::Error),
1627    #[error("Path contains invalid characters: `{}`", _0.display())]
1628    NonUtf8Path(PathBuf),
1629    #[error("Source markers must be disjoint, but the following markers overlap: `{0}` and `{1}`.")]
1630    OverlappingMarkers(String, String, String),
1631    #[error(
1632        "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1633    )]
1634    MissingMarkers,
1635    #[error("Must provide at least one source")]
1636    EmptySources,
1637}
1638
1639impl uv_errors::Hint for SourceError {
1640    fn hints(&self) -> uv_errors::Hints<'_> {
1641        match self {
1642            Self::OverlappingMarkers(_, rhs, replacement) => {
1643                uv_errors::Hints::from(format!("replace `{rhs}` with `{replacement}`"))
1644            }
1645            _ => uv_errors::Hints::none(),
1646        }
1647    }
1648}
1649
1650impl Source {
1651    pub fn from_requirement(
1652        name: &PackageName,
1653        source: RequirementSource,
1654        workspace: bool,
1655        editable: Option<bool>,
1656        index: Option<IndexName>,
1657        rev: Option<String>,
1658        tag: Option<String>,
1659        branch: Option<String>,
1660        lfs: GitLfsSetting,
1661        root: &Path,
1662        existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1663    ) -> Result<Option<Self>, SourceError> {
1664        // If the user specified a Git reference for a non-Git source, try existing Git sources before erroring.
1665        if !matches!(
1666            source,
1667            RequirementSource::GitDirectory { .. } | RequirementSource::GitPath { .. }
1668        ) && (branch.is_some()
1669            || tag.is_some()
1670            || rev.is_some()
1671            || matches!(lfs, GitLfsSetting::Enabled { .. }))
1672        {
1673            if let Some(sources) = existing_sources
1674                && let Some(package_sources) = sources.get(name)
1675            {
1676                for existing_source in package_sources.iter() {
1677                    if let Self::Git {
1678                        git,
1679                        subdirectory,
1680                        path,
1681                        marker,
1682                        extra,
1683                        group,
1684                        ..
1685                    } = existing_source
1686                    {
1687                        return Ok(Some(Self::Git {
1688                            git: git.clone(),
1689                            subdirectory: subdirectory.clone(),
1690                            rev,
1691                            tag,
1692                            branch,
1693                            lfs: lfs.into(),
1694                            marker: *marker,
1695                            path: path.clone(),
1696                            extra: extra.clone(),
1697                            group: group.clone(),
1698                        }));
1699                    }
1700                }
1701            }
1702            if let Some(rev) = rev {
1703                return Err(SourceError::UnusedRev(name.to_string(), rev));
1704            }
1705            if let Some(tag) = tag {
1706                return Err(SourceError::UnusedTag(name.to_string(), tag));
1707            }
1708            if let Some(branch) = branch {
1709                return Err(SourceError::UnusedBranch(name.to_string(), branch));
1710            }
1711            if matches!(lfs, GitLfsSetting::Enabled { from_env: false }) {
1712                return Err(SourceError::UnusedLfs(name.to_string()));
1713            }
1714        }
1715
1716        // If we resolved a non-path source, and user specified an `--editable` flag, error.
1717        if !workspace {
1718            if !matches!(source, RequirementSource::Directory { .. }) {
1719                if editable == Some(true) {
1720                    return Err(SourceError::UnusedEditable(name.to_string()));
1721                }
1722            }
1723        }
1724
1725        // If the source is a workspace package, error if the user tried to specify a source.
1726        if workspace {
1727            return match source {
1728                RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1729                    Ok(Some(Self::Workspace {
1730                        workspace: true,
1731                        editable,
1732                        marker: MarkerTree::TRUE,
1733                        extra: None,
1734                        group: None,
1735                    }))
1736                }
1737                RequirementSource::Url { .. } => {
1738                    Err(SourceError::WorkspacePackageUrl(name.to_string()))
1739                }
1740                RequirementSource::GitDirectory { .. } => {
1741                    Err(SourceError::WorkspacePackageGit(name.to_string()))
1742                }
1743                RequirementSource::GitPath { .. } => {
1744                    Err(SourceError::WorkspacePackageGit(name.to_string()))
1745                }
1746                RequirementSource::Path { .. } => {
1747                    Err(SourceError::WorkspacePackageFile(name.to_string()))
1748                }
1749            };
1750        }
1751
1752        let source = match source {
1753            RequirementSource::Registry { index: Some(_), .. } => {
1754                return Ok(None);
1755            }
1756            RequirementSource::Registry { index: None, .. } => {
1757                if let Some(index) = index {
1758                    Self::Registry {
1759                        index,
1760                        marker: MarkerTree::TRUE,
1761                        extra: None,
1762                        group: None,
1763                    }
1764                } else {
1765                    return Ok(None);
1766                }
1767            }
1768            RequirementSource::Path { install_path, .. } => Self::Path {
1769                editable: None,
1770                package: None,
1771                path: PortablePathBuf::from(
1772                    relative_to(&install_path, root)
1773                        .or_else(|_| std::path::absolute(&install_path))
1774                        .map_err(SourceError::Absolute)?
1775                        .into_boxed_path(),
1776                ),
1777                marker: MarkerTree::TRUE,
1778                extra: None,
1779                group: None,
1780            },
1781            RequirementSource::Directory {
1782                install_path,
1783                editable: is_editable,
1784                ..
1785            } => Self::Path {
1786                editable: editable.or(is_editable),
1787                package: None,
1788                path: PortablePathBuf::from(
1789                    relative_to(&install_path, root)
1790                        .or_else(|_| std::path::absolute(&install_path))
1791                        .map_err(SourceError::Absolute)?
1792                        .into_boxed_path(),
1793                ),
1794                marker: MarkerTree::TRUE,
1795                extra: None,
1796                group: None,
1797            },
1798            RequirementSource::Url {
1799                location,
1800                subdirectory,
1801                ..
1802            } => Self::Url {
1803                url: location,
1804                subdirectory: subdirectory.map(PortablePathBuf::from),
1805                marker: MarkerTree::TRUE,
1806                extra: None,
1807                group: None,
1808            },
1809            RequirementSource::GitDirectory {
1810                git, subdirectory, ..
1811            } => {
1812                if rev.is_none() && tag.is_none() && branch.is_none() {
1813                    let rev = match git.reference() {
1814                        GitReference::Branch(rev) => Some(rev),
1815                        GitReference::Tag(rev) => Some(rev),
1816                        GitReference::BranchOrTag(rev) => Some(rev),
1817                        GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1818                        GitReference::NamedRef(rev) => Some(rev),
1819                        GitReference::DefaultBranch => None,
1820                    };
1821                    Self::Git {
1822                        rev: rev.cloned(),
1823                        tag,
1824                        branch,
1825                        lfs: lfs.into(),
1826                        git: git.url().clone(),
1827                        subdirectory: subdirectory.map(PortablePathBuf::from),
1828                        path: None,
1829                        marker: MarkerTree::TRUE,
1830                        extra: None,
1831                        group: None,
1832                    }
1833                } else {
1834                    Self::Git {
1835                        rev,
1836                        tag,
1837                        branch,
1838                        lfs: lfs.into(),
1839                        git: git.url().clone(),
1840                        subdirectory: subdirectory.map(PortablePathBuf::from),
1841                        path: None,
1842                        marker: MarkerTree::TRUE,
1843                        extra: None,
1844                        group: None,
1845                    }
1846                }
1847            }
1848            RequirementSource::GitPath {
1849                git, install_path, ..
1850            } => {
1851                if rev.is_none() && tag.is_none() && branch.is_none() {
1852                    let rev = match git.reference() {
1853                        GitReference::Branch(rev) => Some(rev),
1854                        GitReference::Tag(rev) => Some(rev),
1855                        GitReference::BranchOrTag(rev) => Some(rev),
1856                        GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1857                        GitReference::NamedRef(rev) => Some(rev),
1858                        GitReference::DefaultBranch => None,
1859                    };
1860                    Self::Git {
1861                        rev: rev.cloned(),
1862                        tag,
1863                        branch,
1864                        lfs: lfs.into(),
1865                        git: git.url().clone(),
1866                        subdirectory: None,
1867                        path: Some(PortablePathBuf::from(install_path.as_path())),
1868                        marker: MarkerTree::TRUE,
1869                        extra: None,
1870                        group: None,
1871                    }
1872                } else {
1873                    Self::Git {
1874                        rev,
1875                        tag,
1876                        branch,
1877                        lfs: lfs.into(),
1878                        git: git.url().clone(),
1879                        subdirectory: None,
1880                        path: Some(PortablePathBuf::from(install_path.as_path())),
1881                        marker: MarkerTree::TRUE,
1882                        extra: None,
1883                        group: None,
1884                    }
1885                }
1886            }
1887        };
1888
1889        Ok(Some(source))
1890    }
1891
1892    /// Return the [`MarkerTree`] for the source.
1893    pub fn marker(&self) -> MarkerTree {
1894        match self {
1895            Self::Git { marker, .. } => *marker,
1896            Self::Url { marker, .. } => *marker,
1897            Self::Path { marker, .. } => *marker,
1898            Self::Registry { marker, .. } => *marker,
1899            Self::Workspace { marker, .. } => *marker,
1900        }
1901    }
1902
1903    /// Return the extra name for the source.
1904    pub fn extra(&self) -> Option<&ExtraName> {
1905        match self {
1906            Self::Git { extra, .. } => extra.as_ref(),
1907            Self::Url { extra, .. } => extra.as_ref(),
1908            Self::Path { extra, .. } => extra.as_ref(),
1909            Self::Registry { extra, .. } => extra.as_ref(),
1910            Self::Workspace { extra, .. } => extra.as_ref(),
1911        }
1912    }
1913
1914    /// Return the dependency group name for the source.
1915    pub fn group(&self) -> Option<&GroupName> {
1916        match self {
1917            Self::Git { group, .. } => group.as_ref(),
1918            Self::Url { group, .. } => group.as_ref(),
1919            Self::Path { group, .. } => group.as_ref(),
1920            Self::Registry { group, .. } => group.as_ref(),
1921            Self::Workspace { group, .. } => group.as_ref(),
1922        }
1923    }
1924}
1925
1926/// The type of a dependency in a `pyproject.toml`.
1927#[derive(Debug, Clone, PartialEq, Eq)]
1928pub enum DependencyType {
1929    /// A dependency in `project.dependencies`.
1930    Production,
1931    /// A dependency in `tool.uv.dev-dependencies`.
1932    Dev,
1933    /// A dependency in `project.optional-dependencies.{0}`.
1934    Optional(ExtraName),
1935    /// A dependency in `dependency-groups.{0}`.
1936    Group(GroupName),
1937}
1938
1939impl DependencyType {
1940    /// Return the TOML table name(s) for this dependency type.
1941    pub fn toml_table_name(&self) -> String {
1942        match self {
1943            Self::Production => "`project.dependencies`".to_string(),
1944            Self::Dev => {
1945                "`tool.uv.dev-dependencies` or `tool.uv.dependency-groups.dev`".to_string()
1946            }
1947            Self::Optional(extra) => format!("`project.optional-dependencies.{extra}`"),
1948            Self::Group(group) => format!("`dependency-groups.{group}`"),
1949        }
1950    }
1951}
1952
1953#[derive(Debug, Clone, PartialEq, Eq)]
1954#[cfg_attr(test, derive(Serialize))]
1955pub(crate) struct BuildBackendSettingsSchema;
1956
1957impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1958    fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1959    where
1960        D: Deserializer<'de>,
1961    {
1962        Ok(Self)
1963    }
1964}
1965
1966#[cfg(feature = "schemars")]
1967impl schemars::JsonSchema for BuildBackendSettingsSchema {
1968    fn schema_name() -> Cow<'static, str> {
1969        BuildBackendSettings::schema_name()
1970    }
1971
1972    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1973        BuildBackendSettings::json_schema(generator)
1974    }
1975}
1976
1977impl OptionsMetadata for BuildBackendSettingsSchema {
1978    fn record(visit: &mut dyn Visit) {
1979        BuildBackendSettings::record(visit);
1980    }
1981
1982    fn documentation() -> Option<&'static str> {
1983        BuildBackendSettings::documentation()
1984    }
1985
1986    fn metadata() -> OptionSet
1987    where
1988        Self: Sized + 'static,
1989    {
1990        BuildBackendSettings::metadata()
1991    }
1992}