1#[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#[derive(Deserialize, Debug, Clone)]
75#[cfg_attr(test, derive(Serialize))]
76#[serde(rename_all = "kebab-case")]
77pub struct PyProjectToml {
78 pub project: Option<Project>,
80 pub tool: Option<Tool>,
82 pub dependency_groups: Option<DependencyGroups>,
84 #[serde(skip)]
86 pub raw: String,
87
88 #[serde(default, skip_serializing)]
90 build_system: Option<serde::de::IgnoredAny>,
91}
92
93impl PyProjectToml {
94 #[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 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 pub fn is_package(&self, require_build_system: bool) -> bool {
119 if let Some(is_package) = self.tool_uv_package() {
121 return is_package;
122 }
123
124 self.build_system.is_some() || !require_build_system
126 }
127
128 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 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 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
164impl 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#[derive(Deserialize, Debug, Clone, PartialEq)]
183#[cfg_attr(test, derive(Serialize))]
184#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
185pub struct Project {
186 pub name: PackageName,
188 version: Option<Version>,
190 pub(crate) requires_python: Option<VersionSpecifiers>,
192 pub dependencies: Option<Vec<String>>,
194 pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
196
197 #[serde(default, skip_serializing)]
199 gui_scripts: Option<serde::de::IgnoredAny>,
200 #[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 let name = value.name.ok_or(PyprojectTomlError::MissingName)?;
225
226 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
255fn 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
289pub type OverrideDependency = Override<uv_pep508::Requirement<VerbatimParsedUrl>>;
291
292#[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 #[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 #[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 #[option_group]
359 pub(crate) workspace: Option<ToolUvWorkspace>,
360
361 #[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 #[option(
383 default = r#"true"#,
384 value_type = "bool",
385 example = r#"
386 package = false
387 "#
388 )]
389 package: Option<bool>,
390
391 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
734 &self.0
735 }
736
737 #[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
756impl<'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
769impl<'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 pub(crate) fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
790 &self.0
791 }
792}
793
794impl<'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 #[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
909impl<'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 #[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 #[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#[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 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 if sources.is_empty() {
1127 return Err(SourceError::EmptySources);
1128 }
1129
1130 Ok(Self(sources))
1131 }
1132 }
1133 }
1134}
1135
1136#[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 Git {
1148 git: DisplaySafeUrl,
1150 subdirectory: Option<PortablePathBuf>,
1152 path: Option<PortablePathBuf>,
1154 rev: Option<String>,
1156 tag: Option<String>,
1157 branch: Option<String>,
1158 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 Url {
1177 url: DisplaySafeUrl,
1178 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 Path {
1194 path: PortablePathBuf,
1195 editable: Option<bool>,
1197 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 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 Workspace {
1227 workspace: bool,
1230 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
1243impl<'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 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 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 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 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 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 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 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 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 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 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 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 !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 !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 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 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 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 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#[derive(Debug, Clone, PartialEq, Eq)]
1928pub enum DependencyType {
1929 Production,
1931 Dev,
1933 Optional(ExtraName),
1935 Group(GroupName),
1937}
1938
1939impl DependencyType {
1940 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}