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 #[serde(
1126 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1127 serialize_with = "uv_pep508::marker::ser::serialize",
1128 default
1129 )]
1130 marker: MarkerTree,
1131 extra: Option<ExtraName>,
1132 group: Option<GroupName>,
1133 },
1134 Url {
1142 url: DisplaySafeUrl,
1143 subdirectory: Option<PortablePathBuf>,
1146 #[serde(
1147 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1148 serialize_with = "uv_pep508::marker::ser::serialize",
1149 default
1150 )]
1151 marker: MarkerTree,
1152 extra: Option<ExtraName>,
1153 group: Option<GroupName>,
1154 },
1155 Path {
1159 path: PortablePathBuf,
1160 editable: Option<bool>,
1162 package: Option<bool>,
1169 #[serde(
1170 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1171 serialize_with = "uv_pep508::marker::ser::serialize",
1172 default
1173 )]
1174 marker: MarkerTree,
1175 extra: Option<ExtraName>,
1176 group: Option<GroupName>,
1177 },
1178 Registry {
1180 index: IndexName,
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 Workspace {
1192 workspace: bool,
1195 editable: Option<bool>,
1197 #[serde(
1198 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1199 serialize_with = "uv_pep508::marker::ser::serialize",
1200 default
1201 )]
1202 marker: MarkerTree,
1203 extra: Option<ExtraName>,
1204 group: Option<GroupName>,
1205 },
1206}
1207
1208impl<'de> Deserialize<'de> for Source {
1211 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1212 where
1213 D: Deserializer<'de>,
1214 {
1215 #[derive(Deserialize, Debug, Clone)]
1216 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1217 struct CatchAll {
1218 git: Option<DisplaySafeUrl>,
1219 subdirectory: Option<PortablePathBuf>,
1220 rev: Option<String>,
1221 tag: Option<String>,
1222 branch: Option<String>,
1223 url: Option<DisplaySafeUrl>,
1224 path: Option<PortablePathBuf>,
1225 editable: Option<bool>,
1226 package: Option<bool>,
1227 index: Option<IndexName>,
1228 workspace: Option<bool>,
1229 #[serde(
1230 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1231 serialize_with = "uv_pep508::marker::ser::serialize",
1232 default
1233 )]
1234 marker: MarkerTree,
1235 extra: Option<ExtraName>,
1236 group: Option<GroupName>,
1237 }
1238
1239 let CatchAll {
1241 git,
1242 subdirectory,
1243 rev,
1244 tag,
1245 branch,
1246 url,
1247 path,
1248 editable,
1249 package,
1250 index,
1251 workspace,
1252 marker,
1253 extra,
1254 group,
1255 } = CatchAll::deserialize(deserializer)?;
1256
1257 if extra.is_some() && group.is_some() {
1259 return Err(serde::de::Error::custom(
1260 "cannot specify both `extra` and `group`",
1261 ));
1262 }
1263
1264 if let Some(git) = git {
1266 if index.is_some() {
1267 return Err(serde::de::Error::custom(
1268 "cannot specify both `git` and `index`",
1269 ));
1270 }
1271 if workspace.is_some() {
1272 return Err(serde::de::Error::custom(
1273 "cannot specify both `git` and `workspace`",
1274 ));
1275 }
1276 if path.is_some() {
1277 return Err(serde::de::Error::custom(
1278 "cannot specify both `git` and `path`",
1279 ));
1280 }
1281 if url.is_some() {
1282 return Err(serde::de::Error::custom(
1283 "cannot specify both `git` and `url`",
1284 ));
1285 }
1286 if editable.is_some() {
1287 return Err(serde::de::Error::custom(
1288 "cannot specify both `git` and `editable`",
1289 ));
1290 }
1291 if package.is_some() {
1292 return Err(serde::de::Error::custom(
1293 "cannot specify both `git` and `package`",
1294 ));
1295 }
1296
1297 match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1299 (None, None, None) => {}
1300 (Some(_), None, None) => {}
1301 (None, Some(_), None) => {}
1302 (None, None, Some(_)) => {}
1303 _ => {
1304 return Err(serde::de::Error::custom(
1305 "expected at most one of `rev`, `tag`, or `branch`",
1306 ));
1307 }
1308 }
1309
1310 let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1312 DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1313 } else {
1314 git
1315 };
1316
1317 return Ok(Self::Git {
1318 git,
1319 subdirectory,
1320 rev,
1321 tag,
1322 branch,
1323 marker,
1324 extra,
1325 group,
1326 });
1327 }
1328
1329 if let Some(url) = url {
1331 if index.is_some() {
1332 return Err(serde::de::Error::custom(
1333 "cannot specify both `url` and `index`",
1334 ));
1335 }
1336 if workspace.is_some() {
1337 return Err(serde::de::Error::custom(
1338 "cannot specify both `url` and `workspace`",
1339 ));
1340 }
1341 if path.is_some() {
1342 return Err(serde::de::Error::custom(
1343 "cannot specify both `url` and `path`",
1344 ));
1345 }
1346 if git.is_some() {
1347 return Err(serde::de::Error::custom(
1348 "cannot specify both `url` and `git`",
1349 ));
1350 }
1351 if rev.is_some() {
1352 return Err(serde::de::Error::custom(
1353 "cannot specify both `url` and `rev`",
1354 ));
1355 }
1356 if tag.is_some() {
1357 return Err(serde::de::Error::custom(
1358 "cannot specify both `url` and `tag`",
1359 ));
1360 }
1361 if branch.is_some() {
1362 return Err(serde::de::Error::custom(
1363 "cannot specify both `url` and `branch`",
1364 ));
1365 }
1366 if editable.is_some() {
1367 return Err(serde::de::Error::custom(
1368 "cannot specify both `url` and `editable`",
1369 ));
1370 }
1371 if package.is_some() {
1372 return Err(serde::de::Error::custom(
1373 "cannot specify both `url` and `package`",
1374 ));
1375 }
1376
1377 return Ok(Self::Url {
1378 url,
1379 subdirectory,
1380 marker,
1381 extra,
1382 group,
1383 });
1384 }
1385
1386 if let Some(path) = path {
1388 if index.is_some() {
1389 return Err(serde::de::Error::custom(
1390 "cannot specify both `path` and `index`",
1391 ));
1392 }
1393 if workspace.is_some() {
1394 return Err(serde::de::Error::custom(
1395 "cannot specify both `path` and `workspace`",
1396 ));
1397 }
1398 if git.is_some() {
1399 return Err(serde::de::Error::custom(
1400 "cannot specify both `path` and `git`",
1401 ));
1402 }
1403 if url.is_some() {
1404 return Err(serde::de::Error::custom(
1405 "cannot specify both `path` and `url`",
1406 ));
1407 }
1408 if rev.is_some() {
1409 return Err(serde::de::Error::custom(
1410 "cannot specify both `path` and `rev`",
1411 ));
1412 }
1413 if tag.is_some() {
1414 return Err(serde::de::Error::custom(
1415 "cannot specify both `path` and `tag`",
1416 ));
1417 }
1418 if branch.is_some() {
1419 return Err(serde::de::Error::custom(
1420 "cannot specify both `path` and `branch`",
1421 ));
1422 }
1423
1424 if editable == Some(true) && package == Some(false) {
1426 return Err(serde::de::Error::custom(
1427 "cannot specify both `editable = true` and `package = false`",
1428 ));
1429 }
1430
1431 return Ok(Self::Path {
1432 path,
1433 editable,
1434 package,
1435 marker,
1436 extra,
1437 group,
1438 });
1439 }
1440
1441 if let Some(index) = index {
1443 if workspace.is_some() {
1444 return Err(serde::de::Error::custom(
1445 "cannot specify both `index` and `workspace`",
1446 ));
1447 }
1448 if git.is_some() {
1449 return Err(serde::de::Error::custom(
1450 "cannot specify both `index` and `git`",
1451 ));
1452 }
1453 if url.is_some() {
1454 return Err(serde::de::Error::custom(
1455 "cannot specify both `index` and `url`",
1456 ));
1457 }
1458 if path.is_some() {
1459 return Err(serde::de::Error::custom(
1460 "cannot specify both `index` and `path`",
1461 ));
1462 }
1463 if rev.is_some() {
1464 return Err(serde::de::Error::custom(
1465 "cannot specify both `index` and `rev`",
1466 ));
1467 }
1468 if tag.is_some() {
1469 return Err(serde::de::Error::custom(
1470 "cannot specify both `index` and `tag`",
1471 ));
1472 }
1473 if branch.is_some() {
1474 return Err(serde::de::Error::custom(
1475 "cannot specify both `index` and `branch`",
1476 ));
1477 }
1478 if editable.is_some() {
1479 return Err(serde::de::Error::custom(
1480 "cannot specify both `index` and `editable`",
1481 ));
1482 }
1483 if package.is_some() {
1484 return Err(serde::de::Error::custom(
1485 "cannot specify both `index` and `package`",
1486 ));
1487 }
1488
1489 return Ok(Self::Registry {
1490 index,
1491 marker,
1492 extra,
1493 group,
1494 });
1495 }
1496
1497 if let Some(workspace) = workspace {
1499 if index.is_some() {
1500 return Err(serde::de::Error::custom(
1501 "cannot specify both `workspace` and `index`",
1502 ));
1503 }
1504 if git.is_some() {
1505 return Err(serde::de::Error::custom(
1506 "cannot specify both `workspace` and `git`",
1507 ));
1508 }
1509 if url.is_some() {
1510 return Err(serde::de::Error::custom(
1511 "cannot specify both `workspace` and `url`",
1512 ));
1513 }
1514 if path.is_some() {
1515 return Err(serde::de::Error::custom(
1516 "cannot specify both `workspace` and `path`",
1517 ));
1518 }
1519 if rev.is_some() {
1520 return Err(serde::de::Error::custom(
1521 "cannot specify both `workspace` and `rev`",
1522 ));
1523 }
1524 if tag.is_some() {
1525 return Err(serde::de::Error::custom(
1526 "cannot specify both `workspace` and `tag`",
1527 ));
1528 }
1529 if branch.is_some() {
1530 return Err(serde::de::Error::custom(
1531 "cannot specify both `workspace` and `branch`",
1532 ));
1533 }
1534 if package.is_some() {
1535 return Err(serde::de::Error::custom(
1536 "cannot specify both `workspace` and `package`",
1537 ));
1538 }
1539
1540 return Ok(Self::Workspace {
1541 workspace,
1542 editable,
1543 marker,
1544 extra,
1545 group,
1546 });
1547 }
1548
1549 Err(serde::de::Error::custom(
1551 "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1552 ))
1553 }
1554}
1555
1556#[derive(Error, Debug)]
1557pub enum SourceError {
1558 #[error("Failed to resolve Git reference: `{0}`")]
1559 UnresolvedReference(String),
1560 #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1561 WorkspacePackageGit(String),
1562 #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1563 WorkspacePackageUrl(String),
1564 #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1565 WorkspacePackageFile(String),
1566 #[error(
1567 "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1568 )]
1569 UnusedRev(String, String),
1570 #[error(
1571 "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1572 )]
1573 UnusedTag(String, String),
1574 #[error(
1575 "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1576 )]
1577 UnusedBranch(String, String),
1578 #[error(
1579 "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1580 )]
1581 UnusedEditable(String),
1582 #[error("Failed to resolve absolute path")]
1583 Absolute(#[from] std::io::Error),
1584 #[error("Path contains invalid characters: `{}`", _0.display())]
1585 NonUtf8Path(PathBuf),
1586 #[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()
1587 )]
1588 OverlappingMarkers(String, String, String),
1589 #[error(
1590 "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1591 )]
1592 MissingMarkers,
1593 #[error("Must provide at least one source")]
1594 EmptySources,
1595}
1596
1597impl Source {
1598 pub fn from_requirement(
1599 name: &PackageName,
1600 source: RequirementSource,
1601 workspace: bool,
1602 editable: Option<bool>,
1603 index: Option<IndexName>,
1604 rev: Option<String>,
1605 tag: Option<String>,
1606 branch: Option<String>,
1607 root: &Path,
1608 existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1609 ) -> Result<Option<Self>, SourceError> {
1610 if !matches!(source, RequirementSource::Git { .. })
1612 && (branch.is_some() || tag.is_some() || rev.is_some())
1613 {
1614 if let Some(sources) = existing_sources {
1615 if let Some(package_sources) = sources.get(name) {
1616 for existing_source in package_sources.iter() {
1617 if let Self::Git {
1618 git,
1619 subdirectory,
1620 marker,
1621 extra,
1622 group,
1623 ..
1624 } = existing_source
1625 {
1626 return Ok(Some(Self::Git {
1627 git: git.clone(),
1628 subdirectory: subdirectory.clone(),
1629 rev,
1630 tag,
1631 branch,
1632 marker: *marker,
1633 extra: extra.clone(),
1634 group: group.clone(),
1635 }));
1636 }
1637 }
1638 }
1639 }
1640 if let Some(rev) = rev {
1641 return Err(SourceError::UnusedRev(name.to_string(), rev));
1642 }
1643 if let Some(tag) = tag {
1644 return Err(SourceError::UnusedTag(name.to_string(), tag));
1645 }
1646 if let Some(branch) = branch {
1647 return Err(SourceError::UnusedBranch(name.to_string(), branch));
1648 }
1649 }
1650
1651 if !workspace {
1653 if !matches!(source, RequirementSource::Directory { .. }) {
1654 if editable == Some(true) {
1655 return Err(SourceError::UnusedEditable(name.to_string()));
1656 }
1657 }
1658 }
1659
1660 if workspace {
1662 return match source {
1663 RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1664 Ok(Some(Self::Workspace {
1665 workspace: true,
1666 editable,
1667 marker: MarkerTree::TRUE,
1668 extra: None,
1669 group: None,
1670 }))
1671 }
1672 RequirementSource::Url { .. } => {
1673 Err(SourceError::WorkspacePackageUrl(name.to_string()))
1674 }
1675 RequirementSource::Git { .. } => {
1676 Err(SourceError::WorkspacePackageGit(name.to_string()))
1677 }
1678 RequirementSource::Path { .. } => {
1679 Err(SourceError::WorkspacePackageFile(name.to_string()))
1680 }
1681 };
1682 }
1683
1684 let source = match source {
1685 RequirementSource::Registry { index: Some(_), .. } => {
1686 return Ok(None);
1687 }
1688 RequirementSource::Registry { index: None, .. } => {
1689 if let Some(index) = index {
1690 Self::Registry {
1691 index,
1692 marker: MarkerTree::TRUE,
1693 extra: None,
1694 group: None,
1695 }
1696 } else {
1697 return Ok(None);
1698 }
1699 }
1700 RequirementSource::Path { install_path, .. }
1701 | RequirementSource::Directory { install_path, .. } => Self::Path {
1702 editable,
1703 package: None,
1704 path: PortablePathBuf::from(
1705 relative_to(&install_path, root)
1706 .or_else(|_| std::path::absolute(&install_path))
1707 .map_err(SourceError::Absolute)?
1708 .into_boxed_path(),
1709 ),
1710 marker: MarkerTree::TRUE,
1711 extra: None,
1712 group: None,
1713 },
1714 RequirementSource::Url {
1715 location,
1716 subdirectory,
1717 ..
1718 } => Self::Url {
1719 url: location,
1720 subdirectory: subdirectory.map(PortablePathBuf::from),
1721 marker: MarkerTree::TRUE,
1722 extra: None,
1723 group: None,
1724 },
1725 RequirementSource::Git {
1726 git, subdirectory, ..
1727 } => {
1728 if rev.is_none() && tag.is_none() && branch.is_none() {
1729 let rev = match git.reference() {
1730 GitReference::Branch(rev) => Some(rev),
1731 GitReference::Tag(rev) => Some(rev),
1732 GitReference::BranchOrTag(rev) => Some(rev),
1733 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1734 GitReference::NamedRef(rev) => Some(rev),
1735 GitReference::DefaultBranch => None,
1736 };
1737 Self::Git {
1738 rev: rev.cloned(),
1739 tag,
1740 branch,
1741 git: git.repository().clone(),
1742 subdirectory: subdirectory.map(PortablePathBuf::from),
1743 marker: MarkerTree::TRUE,
1744 extra: None,
1745 group: None,
1746 }
1747 } else {
1748 Self::Git {
1749 rev,
1750 tag,
1751 branch,
1752 git: git.repository().clone(),
1753 subdirectory: subdirectory.map(PortablePathBuf::from),
1754 marker: MarkerTree::TRUE,
1755 extra: None,
1756 group: None,
1757 }
1758 }
1759 }
1760 };
1761
1762 Ok(Some(source))
1763 }
1764
1765 pub fn marker(&self) -> MarkerTree {
1767 match self {
1768 Self::Git { marker, .. } => *marker,
1769 Self::Url { marker, .. } => *marker,
1770 Self::Path { marker, .. } => *marker,
1771 Self::Registry { marker, .. } => *marker,
1772 Self::Workspace { marker, .. } => *marker,
1773 }
1774 }
1775
1776 pub fn extra(&self) -> Option<&ExtraName> {
1778 match self {
1779 Self::Git { extra, .. } => extra.as_ref(),
1780 Self::Url { extra, .. } => extra.as_ref(),
1781 Self::Path { extra, .. } => extra.as_ref(),
1782 Self::Registry { extra, .. } => extra.as_ref(),
1783 Self::Workspace { extra, .. } => extra.as_ref(),
1784 }
1785 }
1786
1787 pub fn group(&self) -> Option<&GroupName> {
1789 match self {
1790 Self::Git { group, .. } => group.as_ref(),
1791 Self::Url { group, .. } => group.as_ref(),
1792 Self::Path { group, .. } => group.as_ref(),
1793 Self::Registry { group, .. } => group.as_ref(),
1794 Self::Workspace { group, .. } => group.as_ref(),
1795 }
1796 }
1797}
1798
1799#[derive(Debug, Clone, PartialEq, Eq)]
1801pub enum DependencyType {
1802 Production,
1804 Dev,
1806 Optional(ExtraName),
1808 Group(GroupName),
1810}
1811
1812#[derive(Debug, Clone, PartialEq, Eq)]
1813#[cfg_attr(test, derive(Serialize))]
1814pub struct BuildBackendSettingsSchema;
1815
1816impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1817 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1818 where
1819 D: Deserializer<'de>,
1820 {
1821 Ok(Self)
1822 }
1823}
1824
1825#[cfg(feature = "schemars")]
1826impl schemars::JsonSchema for BuildBackendSettingsSchema {
1827 fn schema_name() -> Cow<'static, str> {
1828 BuildBackendSettings::schema_name()
1829 }
1830
1831 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1832 BuildBackendSettings::json_schema(generator)
1833 }
1834}
1835
1836impl OptionsMetadata for BuildBackendSettingsSchema {
1837 fn record(visit: &mut dyn Visit) {
1838 BuildBackendSettings::record(visit);
1839 }
1840
1841 fn documentation() -> Option<&'static str> {
1842 BuildBackendSettings::documentation()
1843 }
1844
1845 fn metadata() -> OptionSet
1846 where
1847 Self: Sized + 'static,
1848 {
1849 BuildBackendSettings::metadata()
1850 }
1851}