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 owo_colors::OwoColorize;
19use rustc_hash::{FxBuildHasher, FxHashSet};
20use serde::de::{IntoDeserializer, SeqAccess};
21use serde::{Deserialize, Deserializer, Serialize};
22use thiserror::Error;
23
24use uv_build_backend::BuildBackendSettings;
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 Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments, VerbatimParsedUrl,
35};
36use uv_redacted::DisplaySafeUrl;
37
38#[derive(Error, Debug)]
39pub enum PyprojectTomlError {
40 #[error(transparent)]
41 TomlSyntax(#[from] toml_edit::TomlError),
42 #[error(transparent)]
43 TomlSchema(#[from] toml_edit::de::Error),
44 #[error(
45 "`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set"
46 )]
47 MissingName,
48 #[error(
49 "`pyproject.toml` is using the `[project]` table, but the required `project.version` field is neither set nor present in the `project.dynamic` list"
50 )]
51 MissingVersion,
52}
53
54fn deserialize_unique_map<'de, D, K, V, F>(
56 deserializer: D,
57 error_msg: F,
58) -> Result<BTreeMap<K, V>, D::Error>
59where
60 D: Deserializer<'de>,
61 K: Deserialize<'de> + Ord + std::fmt::Display,
62 V: Deserialize<'de>,
63 F: FnOnce(&K) -> String,
64{
65 struct Visitor<K, V, F>(F, std::marker::PhantomData<(K, V)>);
66
67 impl<'de, K, V, F> serde::de::Visitor<'de> for Visitor<K, V, F>
68 where
69 K: Deserialize<'de> + Ord + std::fmt::Display,
70 V: Deserialize<'de>,
71 F: FnOnce(&K) -> String,
72 {
73 type Value = BTreeMap<K, V>;
74
75 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
76 formatter.write_str("a map with unique keys")
77 }
78
79 fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
80 where
81 M: serde::de::MapAccess<'de>,
82 {
83 use std::collections::btree_map::Entry;
84
85 let mut map = BTreeMap::new();
86 while let Some((key, value)) = access.next_entry::<K, V>()? {
87 match map.entry(key) {
88 Entry::Occupied(entry) => {
89 return Err(serde::de::Error::custom((self.0)(entry.key())));
90 }
91 Entry::Vacant(entry) => {
92 entry.insert(value);
93 }
94 }
95 }
96 Ok(map)
97 }
98 }
99
100 deserializer.deserialize_map(Visitor(error_msg, std::marker::PhantomData))
101}
102
103#[derive(Deserialize, Debug, Clone)]
105#[cfg_attr(test, derive(Serialize))]
106#[serde(rename_all = "kebab-case")]
107pub struct PyProjectToml {
108 pub project: Option<Project>,
110 pub tool: Option<Tool>,
112 pub dependency_groups: Option<DependencyGroups>,
114 #[serde(skip)]
116 pub raw: String,
117
118 #[serde(default, skip_serializing)]
120 pub build_system: Option<serde::de::IgnoredAny>,
121}
122
123impl PyProjectToml {
124 pub fn from_string(raw: String) -> Result<Self, PyprojectTomlError> {
126 let pyproject =
127 toml_edit::Document::from_str(&raw).map_err(PyprojectTomlError::TomlSyntax)?;
128 let pyproject = Self::deserialize(pyproject.into_deserializer())
129 .map_err(PyprojectTomlError::TomlSchema)?;
130 Ok(Self { raw, ..pyproject })
131 }
132
133 pub fn is_package(&self, require_build_system: bool) -> bool {
136 if let Some(is_package) = self.tool_uv_package() {
138 return is_package;
139 }
140
141 self.build_system.is_some() || !require_build_system
143 }
144
145 fn tool_uv_package(&self) -> Option<bool> {
147 self.tool
148 .as_ref()
149 .and_then(|tool| tool.uv.as_ref())
150 .and_then(|uv| uv.package)
151 }
152
153 pub fn is_dynamic(&self) -> bool {
155 self.project
156 .as_ref()
157 .is_some_and(|project| project.version.is_none())
158 }
159
160 pub fn has_scripts(&self) -> bool {
162 if let Some(ref project) = self.project {
163 project.gui_scripts.is_some() || project.scripts.is_some()
164 } else {
165 false
166 }
167 }
168
169 pub fn conflicts(&self) -> Conflicts {
171 let empty = Conflicts::empty();
172 let Some(project) = self.project.as_ref() else {
173 return empty;
174 };
175 let Some(tool) = self.tool.as_ref() else {
176 return empty;
177 };
178 let Some(tooluv) = tool.uv.as_ref() else {
179 return empty;
180 };
181 let Some(conflicting) = tooluv.conflicts.as_ref() else {
182 return empty;
183 };
184 conflicting.to_conflicts_with_package_name(&project.name)
185 }
186}
187
188impl PartialEq for PyProjectToml {
190 fn eq(&self, other: &Self) -> bool {
191 self.project.eq(&other.project) && self.tool.eq(&other.tool)
192 }
193}
194
195impl Eq for PyProjectToml {}
196
197impl AsRef<[u8]> for PyProjectToml {
198 fn as_ref(&self) -> &[u8] {
199 self.raw.as_bytes()
200 }
201}
202
203#[derive(Deserialize, Debug, Clone, PartialEq)]
207#[cfg_attr(test, derive(Serialize))]
208#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
209pub struct Project {
210 pub name: PackageName,
212 pub version: Option<Version>,
214 pub requires_python: Option<VersionSpecifiers>,
216 pub dependencies: Option<Vec<String>>,
218 pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
220
221 #[serde(default, skip_serializing)]
223 pub(crate) gui_scripts: Option<serde::de::IgnoredAny>,
224 #[serde(default, skip_serializing)]
226 pub(crate) scripts: Option<serde::de::IgnoredAny>,
227}
228
229#[derive(Deserialize, Debug)]
230#[serde(rename_all = "kebab-case")]
231struct ProjectWire {
232 name: Option<PackageName>,
233 version: Option<Version>,
234 dynamic: Option<Vec<String>>,
235 requires_python: Option<VersionSpecifiers>,
236 dependencies: Option<Vec<String>>,
237 optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
238 gui_scripts: Option<serde::de::IgnoredAny>,
239 scripts: Option<serde::de::IgnoredAny>,
240}
241
242impl TryFrom<ProjectWire> for Project {
243 type Error = PyprojectTomlError;
244
245 fn try_from(value: ProjectWire) -> Result<Self, Self::Error> {
246 let name = value.name.ok_or(PyprojectTomlError::MissingName)?;
248
249 if value.version.is_none()
251 && !value
252 .dynamic
253 .as_ref()
254 .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
255 {
256 return Err(PyprojectTomlError::MissingVersion);
257 }
258
259 Ok(Self {
260 name,
261 version: value.version,
262 requires_python: value.requires_python,
263 dependencies: value.dependencies,
264 optional_dependencies: value.optional_dependencies,
265 gui_scripts: value.gui_scripts,
266 scripts: value.scripts,
267 })
268 }
269}
270
271#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
272#[cfg_attr(test, derive(Serialize))]
273#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
274pub struct Tool {
275 pub uv: Option<ToolUv>,
276}
277
278fn deserialize_index_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Index>>, D::Error>
283where
284 D: Deserializer<'de>,
285{
286 let indexes = Option::<Vec<Index>>::deserialize(deserializer)?;
287 if let Some(indexes) = indexes.as_ref() {
288 let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher);
289 for index in indexes {
290 if let Some(name) = index.name.as_ref() {
291 if !seen_names.insert(name) {
292 return Err(serde::de::Error::custom(format!(
293 "duplicate index name `{name}`"
294 )));
295 }
296 }
297 }
298 }
299 Ok(indexes)
300}
301
302#[derive(Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
305#[cfg_attr(test, derive(Serialize))]
306#[serde(rename_all = "kebab-case")]
307#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
308pub struct ToolUv {
309 #[option(
317 default = "{}",
318 value_type = "dict",
319 example = r#"
320 [tool.uv.sources]
321 httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" }
322 pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" }
323 pydantic = { path = "/path/to/pydantic", editable = true }
324 "#
325 )]
326 pub sources: Option<ToolUvSources>,
327
328 #[option(
356 default = "[]",
357 value_type = "dict",
358 example = r#"
359 [[tool.uv.index]]
360 name = "pytorch"
361 url = "https://download.pytorch.org/whl/cu121"
362 "#
363 )]
364 #[serde(deserialize_with = "deserialize_index_vec", default)]
365 pub index: Option<Vec<Index>>,
366
367 #[option_group]
369 pub workspace: Option<ToolUvWorkspace>,
370
371 #[option(
374 default = r#"true"#,
375 value_type = "bool",
376 example = r#"
377 managed = false
378 "#
379 )]
380 pub managed: Option<bool>,
381
382 #[option(
393 default = r#"true"#,
394 value_type = "bool",
395 example = r#"
396 package = false
397 "#
398 )]
399 pub package: Option<bool>,
400
401 #[option(
405 default = r#"["dev"]"#,
406 value_type = r#"str | list[str]"#,
407 example = r#"
408 default-groups = ["docs"]
409 "#
410 )]
411 pub default_groups: Option<DefaultGroups>,
412
413 #[option(
422 default = "[]",
423 value_type = "dict",
424 example = r#"
425 [tool.uv.dependency-groups]
426 my-group = {requires-python = ">=3.12"}
427 "#
428 )]
429 pub dependency_groups: Option<ToolUvDependencyGroups>,
430
431 #[cfg_attr(
441 feature = "schemars",
442 schemars(
443 with = "Option<Vec<String>>",
444 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
445 )
446 )]
447 #[option(
448 default = "[]",
449 value_type = "list[str]",
450 example = r#"
451 dev-dependencies = ["ruff==0.5.0"]
452 "#
453 )]
454 pub dev_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
455
456 #[cfg_attr(
475 feature = "schemars",
476 schemars(
477 with = "Option<Vec<String>>",
478 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
479 )
480 )]
481 #[option(
482 default = "[]",
483 value_type = "list[str]",
484 example = r#"
485 # Always install Werkzeug 2.3.0, regardless of whether transitive dependencies request
486 # a different version.
487 override-dependencies = ["werkzeug==2.3.0"]
488 "#
489 )]
490 pub override_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
491
492 #[cfg_attr(
507 feature = "schemars",
508 schemars(
509 with = "Option<Vec<String>>",
510 description = "Package names to exclude, e.g., `werkzeug`, `numpy`."
511 )
512 )]
513 #[option(
514 default = "[]",
515 value_type = "list[str]",
516 example = r#"
517 # Exclude Werkzeug from being installed, even if transitive dependencies request it.
518 exclude-dependencies = ["werkzeug"]
519 "#
520 )]
521 pub exclude_dependencies: Option<Vec<PackageName>>,
522
523 #[cfg_attr(
537 feature = "schemars",
538 schemars(
539 with = "Option<Vec<String>>",
540 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
541 )
542 )]
543 #[option(
544 default = "[]",
545 value_type = "list[str]",
546 example = r#"
547 # Ensure that the grpcio version is always less than 1.65, if it's requested by a
548 # direct or transitive dependency.
549 constraint-dependencies = ["grpcio<1.65"]
550 "#
551 )]
552 pub constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
553
554 #[cfg_attr(
568 feature = "schemars",
569 schemars(
570 with = "Option<Vec<String>>",
571 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
572 )
573 )]
574 #[option(
575 default = "[]",
576 value_type = "list[str]",
577 example = r#"
578 # Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency
579 # on setuptools.
580 build-constraint-dependencies = ["setuptools==60.0.0"]
581 "#
582 )]
583 pub build_constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
584
585 #[cfg_attr(
594 feature = "schemars",
595 schemars(
596 with = "Option<Vec<String>>",
597 description = "A list of environment markers, e.g., `python_version >= '3.6'`."
598 )
599 )]
600 #[option(
601 default = "[]",
602 value_type = "str | list[str]",
603 example = r#"
604 # Resolve for macOS, but not for Linux or Windows.
605 environments = ["sys_platform == 'darwin'"]
606 "#
607 )]
608 pub environments: Option<SupportedEnvironments>,
609
610 #[cfg_attr(
630 feature = "schemars",
631 schemars(
632 with = "Option<Vec<String>>",
633 description = "A list of environment markers, e.g., `sys_platform == 'darwin'."
634 )
635 )]
636 #[option(
637 default = "[]",
638 value_type = "str | list[str]",
639 example = r#"
640 # Require that the package is available for macOS ARM and x86 (Intel).
641 required-environments = [
642 "sys_platform == 'darwin' and platform_machine == 'arm64'",
643 "sys_platform == 'darwin' and platform_machine == 'x86_64'",
644 ]
645 "#
646 )]
647 pub required_environments: Option<SupportedEnvironments>,
648
649 #[cfg_attr(
664 feature = "schemars",
665 schemars(description = "A list of sets of conflicting groups or extras.")
666 )]
667 #[option(
668 default = r#"[]"#,
669 value_type = "list[list[dict]]",
670 example = r#"
671 # Require that `package[extra1]` and `package[extra2]` are resolved
672 # in different forks so that they cannot conflict with one another.
673 conflicts = [
674 [
675 { extra = "extra1" },
676 { extra = "extra2" },
677 ]
678 ]
679
680 # Require that the dependency groups `group1` and `group2`
681 # are resolved in different forks so that they cannot conflict
682 # with one another.
683 conflicts = [
684 [
685 { group = "group1" },
686 { group = "group2" },
687 ]
688 ]
689 "#
690 )]
691 pub conflicts: Option<SchemaConflicts>,
692
693 #[option_group]
700 pub build_backend: Option<BuildBackendSettingsSchema>,
701}
702
703#[derive(Default, Debug, Clone, PartialEq, Eq)]
704#[cfg_attr(test, derive(Serialize))]
705#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
706pub struct ToolUvSources(BTreeMap<PackageName, Sources>);
707
708impl ToolUvSources {
709 pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
711 &self.0
712 }
713
714 #[must_use]
716 pub fn into_inner(self) -> BTreeMap<PackageName, Sources> {
717 self.0
718 }
719}
720
721impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
723 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
724 where
725 D: Deserializer<'de>,
726 {
727 deserialize_unique_map(deserializer, |key: &PackageName| {
728 format!("duplicate sources for package `{key}`")
729 })
730 .map(ToolUvSources)
731 }
732}
733
734#[derive(Default, Debug, Clone, PartialEq, Eq)]
735#[cfg_attr(test, derive(Serialize))]
736#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
737pub struct ToolUvDependencyGroups(BTreeMap<GroupName, DependencyGroupSettings>);
738
739impl ToolUvDependencyGroups {
740 pub fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
742 &self.0
743 }
744
745 #[must_use]
747 pub fn into_inner(self) -> BTreeMap<GroupName, DependencyGroupSettings> {
748 self.0
749 }
750}
751
752impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
754 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
755 where
756 D: Deserializer<'de>,
757 {
758 deserialize_unique_map(deserializer, |key: &GroupName| {
759 format!("duplicate settings for dependency group `{key}`")
760 })
761 .map(ToolUvDependencyGroups)
762 }
763}
764
765#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
766#[cfg_attr(test, derive(Serialize))]
767#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
768#[serde(rename_all = "kebab-case")]
769pub struct DependencyGroupSettings {
770 #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
772 pub requires_python: Option<VersionSpecifiers>,
773}
774
775#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
776#[serde(untagged, rename_all = "kebab-case")]
777#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
778pub enum ExtraBuildDependencyWire {
779 Unannotated(uv_pep508::Requirement<VerbatimParsedUrl>),
780 #[serde(rename_all = "kebab-case")]
781 Annotated {
782 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
783 match_runtime: bool,
784 },
785}
786
787#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
788#[serde(
789 deny_unknown_fields,
790 from = "ExtraBuildDependencyWire",
791 into = "ExtraBuildDependencyWire"
792)]
793#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
794pub struct ExtraBuildDependency {
795 pub requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
796 pub match_runtime: bool,
797}
798
799impl From<ExtraBuildDependency> for uv_pep508::Requirement<VerbatimParsedUrl> {
800 fn from(value: ExtraBuildDependency) -> Self {
801 value.requirement
802 }
803}
804
805impl From<ExtraBuildDependencyWire> for ExtraBuildDependency {
806 fn from(wire: ExtraBuildDependencyWire) -> Self {
807 match wire {
808 ExtraBuildDependencyWire::Unannotated(requirement) => Self {
809 requirement,
810 match_runtime: false,
811 },
812 ExtraBuildDependencyWire::Annotated {
813 requirement,
814 match_runtime,
815 } => Self {
816 requirement,
817 match_runtime,
818 },
819 }
820 }
821}
822
823impl From<ExtraBuildDependency> for ExtraBuildDependencyWire {
824 fn from(item: ExtraBuildDependency) -> Self {
825 Self::Annotated {
826 requirement: item.requirement,
827 match_runtime: item.match_runtime,
828 }
829 }
830}
831
832#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
833#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
834pub struct ExtraBuildDependencies(BTreeMap<PackageName, Vec<ExtraBuildDependency>>);
835
836impl std::ops::Deref for ExtraBuildDependencies {
837 type Target = BTreeMap<PackageName, Vec<ExtraBuildDependency>>;
838
839 fn deref(&self) -> &Self::Target {
840 &self.0
841 }
842}
843
844impl std::ops::DerefMut for ExtraBuildDependencies {
845 fn deref_mut(&mut self) -> &mut Self::Target {
846 &mut self.0
847 }
848}
849
850impl IntoIterator for ExtraBuildDependencies {
851 type Item = (PackageName, Vec<ExtraBuildDependency>);
852 type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildDependency>>;
853
854 fn into_iter(self) -> Self::IntoIter {
855 self.0.into_iter()
856 }
857}
858
859impl FromIterator<(PackageName, Vec<ExtraBuildDependency>)> for ExtraBuildDependencies {
860 fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildDependency>)>>(
861 iter: T,
862 ) -> Self {
863 Self(iter.into_iter().collect())
864 }
865}
866
867impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
869 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
870 where
871 D: Deserializer<'de>,
872 {
873 deserialize_unique_map(deserializer, |key: &PackageName| {
874 format!("duplicate extra-build-dependencies for `{key}`")
875 })
876 .map(ExtraBuildDependencies)
877 }
878}
879
880#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
881#[cfg_attr(test, derive(Serialize))]
882#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
883#[serde(rename_all = "kebab-case", deny_unknown_fields)]
884pub struct ToolUvWorkspace {
885 #[option(
891 default = "[]",
892 value_type = "list[str]",
893 example = r#"
894 members = ["member1", "path/to/member2", "libs/*"]
895 "#
896 )]
897 pub members: Option<Vec<SerdePattern>>,
898 #[option(
905 default = "[]",
906 value_type = "list[str]",
907 example = r#"
908 exclude = ["member1", "path/to/member2", "libs/*"]
909 "#
910 )]
911 pub exclude: Option<Vec<SerdePattern>>,
912}
913
914#[derive(Debug, Clone, PartialEq, Eq)]
916pub struct SerdePattern(Pattern);
917
918impl serde::ser::Serialize for SerdePattern {
919 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
920 where
921 S: serde::ser::Serializer,
922 {
923 self.0.as_str().serialize(serializer)
924 }
925}
926
927impl<'de> serde::Deserialize<'de> for SerdePattern {
928 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
929 struct Visitor;
930
931 impl serde::de::Visitor<'_> for Visitor {
932 type Value = SerdePattern;
933
934 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
935 f.write_str("a string")
936 }
937
938 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
939 Pattern::from_str(v)
940 .map(SerdePattern)
941 .map_err(serde::de::Error::custom)
942 }
943 }
944
945 deserializer.deserialize_str(Visitor)
946 }
947}
948
949#[cfg(feature = "schemars")]
950impl schemars::JsonSchema for SerdePattern {
951 fn schema_name() -> Cow<'static, str> {
952 Cow::Borrowed("SerdePattern")
953 }
954
955 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
956 <String as schemars::JsonSchema>::json_schema(generator)
957 }
958}
959
960impl Deref for SerdePattern {
961 type Target = Pattern;
962
963 fn deref(&self) -> &Self::Target {
964 &self.0
965 }
966}
967
968#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
969#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
970#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]
971pub struct Sources(#[cfg_attr(feature = "schemars", schemars(with = "SourcesWire"))] Vec<Source>);
972
973impl Sources {
974 pub fn iter(&self) -> impl Iterator<Item = &Source> {
980 self.0.iter()
981 }
982
983 pub fn is_empty(&self) -> bool {
985 self.0.is_empty()
986 }
987
988 pub fn len(&self) -> usize {
990 self.0.len()
991 }
992}
993
994impl FromIterator<Source> for Sources {
995 fn from_iter<T: IntoIterator<Item = Source>>(iter: T) -> Self {
996 Self(iter.into_iter().collect())
997 }
998}
999
1000impl IntoIterator for Sources {
1001 type Item = Source;
1002 type IntoIter = std::vec::IntoIter<Source>;
1003
1004 fn into_iter(self) -> Self::IntoIter {
1005 self.0.into_iter()
1006 }
1007}
1008
1009#[derive(Debug, Clone, PartialEq, Eq)]
1010#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
1011#[allow(clippy::large_enum_variant)]
1012enum SourcesWire {
1013 One(Source),
1014 Many(Vec<Source>),
1015}
1016
1017impl<'de> serde::de::Deserialize<'de> for SourcesWire {
1018 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1019 where
1020 D: Deserializer<'de>,
1021 {
1022 struct Visitor;
1023
1024 impl<'de> serde::de::Visitor<'de> for Visitor {
1025 type Value = SourcesWire;
1026
1027 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1028 formatter.write_str("a single source (as a map) or list of sources")
1029 }
1030
1031 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
1032 where
1033 A: SeqAccess<'de>,
1034 {
1035 let sources = serde::de::Deserialize::deserialize(
1036 serde::de::value::SeqAccessDeserializer::new(seq),
1037 )?;
1038 Ok(SourcesWire::Many(sources))
1039 }
1040
1041 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
1042 where
1043 M: serde::de::MapAccess<'de>,
1044 {
1045 let source = serde::de::Deserialize::deserialize(
1046 serde::de::value::MapAccessDeserializer::new(&mut map),
1047 )?;
1048 Ok(SourcesWire::One(source))
1049 }
1050 }
1051
1052 deserializer.deserialize_any(Visitor)
1053 }
1054}
1055
1056impl TryFrom<SourcesWire> for Sources {
1057 type Error = SourceError;
1058
1059 fn try_from(wire: SourcesWire) -> Result<Self, Self::Error> {
1060 match wire {
1061 SourcesWire::One(source) => Ok(Self(vec![source])),
1062 SourcesWire::Many(sources) => {
1063 for (lhs, rhs) in sources.iter().zip(sources.iter().skip(1)) {
1064 if lhs.extra() != rhs.extra() {
1065 continue;
1066 }
1067 if lhs.group() != rhs.group() {
1068 continue;
1069 }
1070
1071 let lhs = lhs.marker();
1072 let rhs = rhs.marker();
1073 if !lhs.is_disjoint(rhs) {
1074 let Some(left) = lhs.contents().map(|contents| contents.to_string()) else {
1075 return Err(SourceError::MissingMarkers);
1076 };
1077
1078 let Some(right) = rhs.contents().map(|contents| contents.to_string())
1079 else {
1080 return Err(SourceError::MissingMarkers);
1081 };
1082
1083 let mut hint = lhs.negate();
1084 hint.and(rhs);
1085 let hint = hint
1086 .contents()
1087 .map(|contents| contents.to_string())
1088 .unwrap_or_else(|| "true".to_string());
1089
1090 return Err(SourceError::OverlappingMarkers(left, right, hint));
1091 }
1092 }
1093
1094 if sources.is_empty() {
1096 return Err(SourceError::EmptySources);
1097 }
1098
1099 Ok(Self(sources))
1100 }
1101 }
1102 }
1103}
1104
1105#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
1107#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1108#[serde(rename_all = "kebab-case", untagged, deny_unknown_fields)]
1109pub enum Source {
1110 Git {
1117 git: DisplaySafeUrl,
1119 subdirectory: Option<PortablePathBuf>,
1121 rev: Option<String>,
1123 tag: Option<String>,
1124 branch: Option<String>,
1125 lfs: Option<bool>,
1127 #[serde(
1128 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1129 serialize_with = "uv_pep508::marker::ser::serialize",
1130 default
1131 )]
1132 marker: MarkerTree,
1133 extra: Option<ExtraName>,
1134 group: Option<GroupName>,
1135 },
1136 Url {
1144 url: DisplaySafeUrl,
1145 subdirectory: Option<PortablePathBuf>,
1148 #[serde(
1149 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1150 serialize_with = "uv_pep508::marker::ser::serialize",
1151 default
1152 )]
1153 marker: MarkerTree,
1154 extra: Option<ExtraName>,
1155 group: Option<GroupName>,
1156 },
1157 Path {
1161 path: PortablePathBuf,
1162 editable: Option<bool>,
1164 package: Option<bool>,
1171 #[serde(
1172 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1173 serialize_with = "uv_pep508::marker::ser::serialize",
1174 default
1175 )]
1176 marker: MarkerTree,
1177 extra: Option<ExtraName>,
1178 group: Option<GroupName>,
1179 },
1180 Registry {
1182 index: IndexName,
1183 #[serde(
1184 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1185 serialize_with = "uv_pep508::marker::ser::serialize",
1186 default
1187 )]
1188 marker: MarkerTree,
1189 extra: Option<ExtraName>,
1190 group: Option<GroupName>,
1191 },
1192 Workspace {
1194 workspace: bool,
1197 editable: Option<bool>,
1199 #[serde(
1200 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1201 serialize_with = "uv_pep508::marker::ser::serialize",
1202 default
1203 )]
1204 marker: MarkerTree,
1205 extra: Option<ExtraName>,
1206 group: Option<GroupName>,
1207 },
1208}
1209
1210impl<'de> Deserialize<'de> for Source {
1213 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1214 where
1215 D: Deserializer<'de>,
1216 {
1217 #[derive(Deserialize, Debug, Clone)]
1218 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1219 struct CatchAll {
1220 git: Option<DisplaySafeUrl>,
1221 subdirectory: Option<PortablePathBuf>,
1222 rev: Option<String>,
1223 tag: Option<String>,
1224 branch: Option<String>,
1225 lfs: Option<bool>,
1226 url: Option<DisplaySafeUrl>,
1227 path: Option<PortablePathBuf>,
1228 editable: Option<bool>,
1229 package: Option<bool>,
1230 index: Option<IndexName>,
1231 workspace: 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 let CatchAll {
1244 git,
1245 subdirectory,
1246 rev,
1247 tag,
1248 branch,
1249 lfs,
1250 url,
1251 path,
1252 editable,
1253 package,
1254 index,
1255 workspace,
1256 marker,
1257 extra,
1258 group,
1259 } = CatchAll::deserialize(deserializer)?;
1260
1261 if extra.is_some() && group.is_some() {
1263 return Err(serde::de::Error::custom(
1264 "cannot specify both `extra` and `group`",
1265 ));
1266 }
1267
1268 if let Some(git) = git {
1270 if index.is_some() {
1271 return Err(serde::de::Error::custom(
1272 "cannot specify both `git` and `index`",
1273 ));
1274 }
1275 if workspace.is_some() {
1276 return Err(serde::de::Error::custom(
1277 "cannot specify both `git` and `workspace`",
1278 ));
1279 }
1280 if path.is_some() {
1281 return Err(serde::de::Error::custom(
1282 "cannot specify both `git` and `path`",
1283 ));
1284 }
1285 if url.is_some() {
1286 return Err(serde::de::Error::custom(
1287 "cannot specify both `git` and `url`",
1288 ));
1289 }
1290 if editable.is_some() {
1291 return Err(serde::de::Error::custom(
1292 "cannot specify both `git` and `editable`",
1293 ));
1294 }
1295 if package.is_some() {
1296 return Err(serde::de::Error::custom(
1297 "cannot specify both `git` and `package`",
1298 ));
1299 }
1300
1301 match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1303 (None, None, None) => {}
1304 (Some(_), None, None) => {}
1305 (None, Some(_), None) => {}
1306 (None, None, Some(_)) => {}
1307 _ => {
1308 return Err(serde::de::Error::custom(
1309 "expected at most one of `rev`, `tag`, or `branch`",
1310 ));
1311 }
1312 }
1313
1314 let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1316 DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1317 } else {
1318 git
1319 };
1320
1321 return Ok(Self::Git {
1322 git,
1323 subdirectory,
1324 rev,
1325 tag,
1326 branch,
1327 lfs,
1328 marker,
1329 extra,
1330 group,
1331 });
1332 }
1333
1334 if let Some(url) = url {
1336 if index.is_some() {
1337 return Err(serde::de::Error::custom(
1338 "cannot specify both `url` and `index`",
1339 ));
1340 }
1341 if workspace.is_some() {
1342 return Err(serde::de::Error::custom(
1343 "cannot specify both `url` and `workspace`",
1344 ));
1345 }
1346 if path.is_some() {
1347 return Err(serde::de::Error::custom(
1348 "cannot specify both `url` and `path`",
1349 ));
1350 }
1351 if git.is_some() {
1352 return Err(serde::de::Error::custom(
1353 "cannot specify both `url` and `git`",
1354 ));
1355 }
1356 if rev.is_some() {
1357 return Err(serde::de::Error::custom(
1358 "cannot specify both `url` and `rev`",
1359 ));
1360 }
1361 if tag.is_some() {
1362 return Err(serde::de::Error::custom(
1363 "cannot specify both `url` and `tag`",
1364 ));
1365 }
1366 if branch.is_some() {
1367 return Err(serde::de::Error::custom(
1368 "cannot specify both `url` and `branch`",
1369 ));
1370 }
1371 if editable.is_some() {
1372 return Err(serde::de::Error::custom(
1373 "cannot specify both `url` and `editable`",
1374 ));
1375 }
1376 if package.is_some() {
1377 return Err(serde::de::Error::custom(
1378 "cannot specify both `url` and `package`",
1379 ));
1380 }
1381
1382 return Ok(Self::Url {
1383 url,
1384 subdirectory,
1385 marker,
1386 extra,
1387 group,
1388 });
1389 }
1390
1391 if let Some(path) = path {
1393 if index.is_some() {
1394 return Err(serde::de::Error::custom(
1395 "cannot specify both `path` and `index`",
1396 ));
1397 }
1398 if workspace.is_some() {
1399 return Err(serde::de::Error::custom(
1400 "cannot specify both `path` and `workspace`",
1401 ));
1402 }
1403 if git.is_some() {
1404 return Err(serde::de::Error::custom(
1405 "cannot specify both `path` and `git`",
1406 ));
1407 }
1408 if url.is_some() {
1409 return Err(serde::de::Error::custom(
1410 "cannot specify both `path` and `url`",
1411 ));
1412 }
1413 if rev.is_some() {
1414 return Err(serde::de::Error::custom(
1415 "cannot specify both `path` and `rev`",
1416 ));
1417 }
1418 if tag.is_some() {
1419 return Err(serde::de::Error::custom(
1420 "cannot specify both `path` and `tag`",
1421 ));
1422 }
1423 if branch.is_some() {
1424 return Err(serde::de::Error::custom(
1425 "cannot specify both `path` and `branch`",
1426 ));
1427 }
1428
1429 if editable == Some(true) && package == Some(false) {
1431 return Err(serde::de::Error::custom(
1432 "cannot specify both `editable = true` and `package = false`",
1433 ));
1434 }
1435
1436 return Ok(Self::Path {
1437 path,
1438 editable,
1439 package,
1440 marker,
1441 extra,
1442 group,
1443 });
1444 }
1445
1446 if let Some(index) = index {
1448 if workspace.is_some() {
1449 return Err(serde::de::Error::custom(
1450 "cannot specify both `index` and `workspace`",
1451 ));
1452 }
1453 if git.is_some() {
1454 return Err(serde::de::Error::custom(
1455 "cannot specify both `index` and `git`",
1456 ));
1457 }
1458 if url.is_some() {
1459 return Err(serde::de::Error::custom(
1460 "cannot specify both `index` and `url`",
1461 ));
1462 }
1463 if path.is_some() {
1464 return Err(serde::de::Error::custom(
1465 "cannot specify both `index` and `path`",
1466 ));
1467 }
1468 if rev.is_some() {
1469 return Err(serde::de::Error::custom(
1470 "cannot specify both `index` and `rev`",
1471 ));
1472 }
1473 if tag.is_some() {
1474 return Err(serde::de::Error::custom(
1475 "cannot specify both `index` and `tag`",
1476 ));
1477 }
1478 if branch.is_some() {
1479 return Err(serde::de::Error::custom(
1480 "cannot specify both `index` and `branch`",
1481 ));
1482 }
1483 if editable.is_some() {
1484 return Err(serde::de::Error::custom(
1485 "cannot specify both `index` and `editable`",
1486 ));
1487 }
1488 if package.is_some() {
1489 return Err(serde::de::Error::custom(
1490 "cannot specify both `index` and `package`",
1491 ));
1492 }
1493
1494 return Ok(Self::Registry {
1495 index,
1496 marker,
1497 extra,
1498 group,
1499 });
1500 }
1501
1502 if let Some(workspace) = workspace {
1504 if index.is_some() {
1505 return Err(serde::de::Error::custom(
1506 "cannot specify both `workspace` and `index`",
1507 ));
1508 }
1509 if git.is_some() {
1510 return Err(serde::de::Error::custom(
1511 "cannot specify both `workspace` and `git`",
1512 ));
1513 }
1514 if url.is_some() {
1515 return Err(serde::de::Error::custom(
1516 "cannot specify both `workspace` and `url`",
1517 ));
1518 }
1519 if path.is_some() {
1520 return Err(serde::de::Error::custom(
1521 "cannot specify both `workspace` and `path`",
1522 ));
1523 }
1524 if rev.is_some() {
1525 return Err(serde::de::Error::custom(
1526 "cannot specify both `workspace` and `rev`",
1527 ));
1528 }
1529 if tag.is_some() {
1530 return Err(serde::de::Error::custom(
1531 "cannot specify both `workspace` and `tag`",
1532 ));
1533 }
1534 if branch.is_some() {
1535 return Err(serde::de::Error::custom(
1536 "cannot specify both `workspace` and `branch`",
1537 ));
1538 }
1539 if package.is_some() {
1540 return Err(serde::de::Error::custom(
1541 "cannot specify both `workspace` and `package`",
1542 ));
1543 }
1544
1545 return Ok(Self::Workspace {
1546 workspace,
1547 editable,
1548 marker,
1549 extra,
1550 group,
1551 });
1552 }
1553
1554 Err(serde::de::Error::custom(
1556 "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1557 ))
1558 }
1559}
1560
1561#[derive(Error, Debug)]
1562pub enum SourceError {
1563 #[error("Failed to resolve Git reference: `{0}`")]
1564 UnresolvedReference(String),
1565 #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1566 WorkspacePackageGit(String),
1567 #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1568 WorkspacePackageUrl(String),
1569 #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1570 WorkspacePackageFile(String),
1571 #[error(
1572 "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1573 )]
1574 UnusedRev(String, String),
1575 #[error(
1576 "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1577 )]
1578 UnusedTag(String, String),
1579 #[error(
1580 "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1581 )]
1582 UnusedBranch(String, String),
1583 #[error(
1584 "`{0}` did not resolve to a Git repository, but a Git extension (`--lfs`) was provided."
1585 )]
1586 UnusedLfs(String),
1587 #[error(
1588 "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1589 )]
1590 UnusedEditable(String),
1591 #[error("Failed to resolve absolute path")]
1592 Absolute(#[from] std::io::Error),
1593 #[error("Path contains invalid characters: `{}`", _0.display())]
1594 NonUtf8Path(PathBuf),
1595 #[error("Source markers must be disjoint, but the following markers overlap: `{0}` and `{1}`.\n\n{hint}{colon} replace `{1}` with `{2}`.", hint = "hint".bold().cyan(), colon = ":".bold()
1596 )]
1597 OverlappingMarkers(String, String, String),
1598 #[error(
1599 "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1600 )]
1601 MissingMarkers,
1602 #[error("Must provide at least one source")]
1603 EmptySources,
1604}
1605
1606impl Source {
1607 pub fn from_requirement(
1608 name: &PackageName,
1609 source: RequirementSource,
1610 workspace: bool,
1611 editable: Option<bool>,
1612 index: Option<IndexName>,
1613 rev: Option<String>,
1614 tag: Option<String>,
1615 branch: Option<String>,
1616 lfs: Option<bool>,
1617 root: &Path,
1618 existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1619 ) -> Result<Option<Self>, SourceError> {
1620 if !matches!(source, RequirementSource::Git { .. })
1622 && (branch.is_some() || tag.is_some() || rev.is_some() || lfs.is_some())
1623 {
1624 if let Some(sources) = existing_sources {
1625 if let Some(package_sources) = sources.get(name) {
1626 for existing_source in package_sources.iter() {
1627 if let Self::Git {
1628 git,
1629 subdirectory,
1630 marker,
1631 extra,
1632 group,
1633 ..
1634 } = existing_source
1635 {
1636 return Ok(Some(Self::Git {
1637 git: git.clone(),
1638 subdirectory: subdirectory.clone(),
1639 rev,
1640 tag,
1641 branch,
1642 lfs,
1643 marker: *marker,
1644 extra: extra.clone(),
1645 group: group.clone(),
1646 }));
1647 }
1648 }
1649 }
1650 }
1651 if let Some(rev) = rev {
1652 return Err(SourceError::UnusedRev(name.to_string(), rev));
1653 }
1654 if let Some(tag) = tag {
1655 return Err(SourceError::UnusedTag(name.to_string(), tag));
1656 }
1657 if let Some(branch) = branch {
1658 return Err(SourceError::UnusedBranch(name.to_string(), branch));
1659 }
1660 if let Some(true) = lfs {
1661 return Err(SourceError::UnusedLfs(name.to_string()));
1662 }
1663 }
1664
1665 if !workspace {
1667 if !matches!(source, RequirementSource::Directory { .. }) {
1668 if editable == Some(true) {
1669 return Err(SourceError::UnusedEditable(name.to_string()));
1670 }
1671 }
1672 }
1673
1674 if workspace {
1676 return match source {
1677 RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1678 Ok(Some(Self::Workspace {
1679 workspace: true,
1680 editable,
1681 marker: MarkerTree::TRUE,
1682 extra: None,
1683 group: None,
1684 }))
1685 }
1686 RequirementSource::Url { .. } => {
1687 Err(SourceError::WorkspacePackageUrl(name.to_string()))
1688 }
1689 RequirementSource::Git { .. } => {
1690 Err(SourceError::WorkspacePackageGit(name.to_string()))
1691 }
1692 RequirementSource::Path { .. } => {
1693 Err(SourceError::WorkspacePackageFile(name.to_string()))
1694 }
1695 };
1696 }
1697
1698 let source = match source {
1699 RequirementSource::Registry { index: Some(_), .. } => {
1700 return Ok(None);
1701 }
1702 RequirementSource::Registry { index: None, .. } => {
1703 if let Some(index) = index {
1704 Self::Registry {
1705 index,
1706 marker: MarkerTree::TRUE,
1707 extra: None,
1708 group: None,
1709 }
1710 } else {
1711 return Ok(None);
1712 }
1713 }
1714 RequirementSource::Path { install_path, .. } => Self::Path {
1715 editable: None,
1716 package: None,
1717 path: PortablePathBuf::from(
1718 relative_to(&install_path, root)
1719 .or_else(|_| std::path::absolute(&install_path))
1720 .map_err(SourceError::Absolute)?
1721 .into_boxed_path(),
1722 ),
1723 marker: MarkerTree::TRUE,
1724 extra: None,
1725 group: None,
1726 },
1727 RequirementSource::Directory {
1728 install_path,
1729 editable: is_editable,
1730 ..
1731 } => Self::Path {
1732 editable: editable.or(is_editable),
1733 package: None,
1734 path: PortablePathBuf::from(
1735 relative_to(&install_path, root)
1736 .or_else(|_| std::path::absolute(&install_path))
1737 .map_err(SourceError::Absolute)?
1738 .into_boxed_path(),
1739 ),
1740 marker: MarkerTree::TRUE,
1741 extra: None,
1742 group: None,
1743 },
1744 RequirementSource::Url {
1745 location,
1746 subdirectory,
1747 ..
1748 } => Self::Url {
1749 url: location,
1750 subdirectory: subdirectory.map(PortablePathBuf::from),
1751 marker: MarkerTree::TRUE,
1752 extra: None,
1753 group: None,
1754 },
1755 RequirementSource::Git {
1756 git, subdirectory, ..
1757 } => {
1758 if rev.is_none() && tag.is_none() && branch.is_none() {
1759 let rev = match git.reference() {
1760 GitReference::Branch(rev) => Some(rev),
1761 GitReference::Tag(rev) => Some(rev),
1762 GitReference::BranchOrTag(rev) => Some(rev),
1763 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1764 GitReference::NamedRef(rev) => Some(rev),
1765 GitReference::DefaultBranch => None,
1766 };
1767 Self::Git {
1768 rev: rev.cloned(),
1769 tag,
1770 branch,
1771 lfs,
1772 git: git.repository().clone(),
1773 subdirectory: subdirectory.map(PortablePathBuf::from),
1774 marker: MarkerTree::TRUE,
1775 extra: None,
1776 group: None,
1777 }
1778 } else {
1779 Self::Git {
1780 rev,
1781 tag,
1782 branch,
1783 lfs,
1784 git: git.repository().clone(),
1785 subdirectory: subdirectory.map(PortablePathBuf::from),
1786 marker: MarkerTree::TRUE,
1787 extra: None,
1788 group: None,
1789 }
1790 }
1791 }
1792 };
1793
1794 Ok(Some(source))
1795 }
1796
1797 pub fn marker(&self) -> MarkerTree {
1799 match self {
1800 Self::Git { marker, .. } => *marker,
1801 Self::Url { marker, .. } => *marker,
1802 Self::Path { marker, .. } => *marker,
1803 Self::Registry { marker, .. } => *marker,
1804 Self::Workspace { marker, .. } => *marker,
1805 }
1806 }
1807
1808 pub fn extra(&self) -> Option<&ExtraName> {
1810 match self {
1811 Self::Git { extra, .. } => extra.as_ref(),
1812 Self::Url { extra, .. } => extra.as_ref(),
1813 Self::Path { extra, .. } => extra.as_ref(),
1814 Self::Registry { extra, .. } => extra.as_ref(),
1815 Self::Workspace { extra, .. } => extra.as_ref(),
1816 }
1817 }
1818
1819 pub fn group(&self) -> Option<&GroupName> {
1821 match self {
1822 Self::Git { group, .. } => group.as_ref(),
1823 Self::Url { group, .. } => group.as_ref(),
1824 Self::Path { group, .. } => group.as_ref(),
1825 Self::Registry { group, .. } => group.as_ref(),
1826 Self::Workspace { group, .. } => group.as_ref(),
1827 }
1828 }
1829}
1830
1831#[derive(Debug, Clone, PartialEq, Eq)]
1833pub enum DependencyType {
1834 Production,
1836 Dev,
1838 Optional(ExtraName),
1840 Group(GroupName),
1842}
1843
1844#[derive(Debug, Clone, PartialEq, Eq)]
1845#[cfg_attr(test, derive(Serialize))]
1846pub struct BuildBackendSettingsSchema;
1847
1848impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1849 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1850 where
1851 D: Deserializer<'de>,
1852 {
1853 Ok(Self)
1854 }
1855}
1856
1857#[cfg(feature = "schemars")]
1858impl schemars::JsonSchema for BuildBackendSettingsSchema {
1859 fn schema_name() -> Cow<'static, str> {
1860 BuildBackendSettings::schema_name()
1861 }
1862
1863 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1864 BuildBackendSettings::json_schema(generator)
1865 }
1866}
1867
1868impl OptionsMetadata for BuildBackendSettingsSchema {
1869 fn record(visit: &mut dyn Visit) {
1870 BuildBackendSettings::record(visit);
1871 }
1872
1873 fn documentation() -> Option<&'static str> {
1874 BuildBackendSettings::documentation()
1875 }
1876
1877 fn metadata() -> OptionSet
1878 where
1879 Self: Sized + 'static,
1880 {
1881 BuildBackendSettings::metadata()
1882 }
1883}