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