1use std::borrow::Cow;
10use std::collections::BTreeMap;
11use std::fmt::Formatter;
12use std::ops::Deref;
13use std::path::{Path, PathBuf};
14use std::str::FromStr;
15
16use glob::Pattern;
17use rustc_hash::{FxBuildHasher, FxHashSet};
18use serde::de::SeqAccess;
19use serde::{Deserialize, Deserializer, Serialize};
20use thiserror::Error;
21use tracing::instrument;
22use uv_build_backend::BuildBackendSettings;
23use uv_configuration::{ExcludeDependency, GitLfsSetting, Override};
24use uv_distribution_types::{Index, IndexName, RequirementSource};
25use uv_fs::{PortablePathBuf, relative_to};
26use uv_git_types::GitReference;
27use uv_macros::OptionsMetadata;
28use uv_normalize::{DefaultGroups, ExtraName, GroupName, PackageName};
29use uv_options_metadata::{OptionSet, OptionsMetadata, Visit};
30use uv_pep440::{Version, VersionSpecifiers};
31use uv_pep508::MarkerTree;
32use uv_pypi_types::{
33 ConflictError, Conflicts, DependencyGroups, SchemaConflicts, SupportedEnvironments,
34 VerbatimParsedUrl,
35};
36use uv_redacted::DisplaySafeUrl;
37use uv_toml::deserialize_unique_map;
38
39#[derive(Error, Debug)]
40pub enum PyprojectTomlError {
41 #[error(transparent)]
42 Toml(#[from] toml::de::Error),
43 #[error("Failed to parse `tool.uv.sources`")]
44 Source(
45 #[from]
46 #[source]
47 SourceError,
48 ),
49 #[error(
50 "`pyproject.toml` is using the `[project]` table, but the required `project.name` field is not set"
51 )]
52 MissingName,
53 #[error(
54 "`pyproject.toml` is using the `[project]` table, but the required `project.version` field is neither set nor present in the `project.dynamic` list"
55 )]
56 MissingVersion,
57}
58
59fn deserialize_optional_dependencies<'de, D, V>(
60 deserializer: D,
61) -> Result<Option<BTreeMap<ExtraName, V>>, D::Error>
62where
63 D: Deserializer<'de>,
64 V: Deserialize<'de>,
65{
66 deserialize_unique_map(deserializer, |key: &ExtraName| {
67 format!("duplicate normalized extra name `{key}`")
68 })
69 .map(Some)
70}
71
72#[derive(Deserialize, Debug, Clone)]
74#[cfg_attr(test, derive(Serialize))]
75#[serde(rename_all = "kebab-case")]
76pub struct PyProjectToml {
77 pub project: Option<Project>,
79 pub tool: Option<Tool>,
81 pub dependency_groups: Option<DependencyGroups>,
83 #[serde(skip)]
85 pub raw: String,
86
87 #[serde(default, skip_serializing)]
89 build_system: Option<serde::de::IgnoredAny>,
90}
91
92impl PyProjectToml {
93 #[instrument("toml::from_str workspace", skip_all, fields(path = %_path.as_ref().display()))]
95 pub fn from_string(raw: String, _path: impl AsRef<Path>) -> Result<Self, PyprojectTomlError> {
96 let pyproject: Self = match toml::from_str(&raw) {
97 Ok(pyproject) => pyproject,
98 Err(error) => {
99 let sources = toml::from_str::<PyProjectTomlSourcesWire>(&raw)
101 .map_err(PyprojectTomlError::Toml)?
102 .tool
103 .and_then(|tool| tool.uv)
104 .and_then(|uv| uv.sources);
105 if let Some(sources) = sources {
106 ToolUvSources::try_from(sources)?;
107 }
108 return Err(PyprojectTomlError::Toml(error));
109 }
110 };
111
112 Ok(Self { raw, ..pyproject })
113 }
114
115 pub fn is_package(&self, require_build_system: bool) -> bool {
118 if let Some(is_package) = self.tool_uv_package() {
120 return is_package;
121 }
122
123 self.build_system.is_some() || !require_build_system
125 }
126
127 fn tool_uv_package(&self) -> Option<bool> {
129 self.tool
130 .as_ref()
131 .and_then(|tool| tool.uv.as_ref())
132 .and_then(|uv| uv.package)
133 }
134
135 pub fn has_scripts(&self) -> bool {
137 if let Some(ref project) = self.project {
138 project.gui_scripts.is_some() || project.scripts.is_some()
139 } else {
140 false
141 }
142 }
143
144 pub(crate) fn conflicts(&self) -> Result<Conflicts, ConflictError> {
146 let empty = Conflicts::empty();
147 let Some(project) = self.project.as_ref() else {
148 return Ok(empty);
149 };
150 let Some(tool) = self.tool.as_ref() else {
151 return Ok(empty);
152 };
153 let Some(tooluv) = tool.uv.as_ref() else {
154 return Ok(empty);
155 };
156 let Some(conflicting) = tooluv.conflicts.as_ref() else {
157 return Ok(empty);
158 };
159 conflicting.to_conflicts_with_package_name(&project.name)
160 }
161}
162
163impl PartialEq for PyProjectToml {
165 fn eq(&self, other: &Self) -> bool {
166 self.project.eq(&other.project) && self.tool.eq(&other.tool)
167 }
168}
169
170impl Eq for PyProjectToml {}
171
172impl AsRef<[u8]> for PyProjectToml {
173 fn as_ref(&self) -> &[u8] {
174 self.raw.as_bytes()
175 }
176}
177
178#[derive(Deserialize, Debug, Clone, PartialEq)]
182#[cfg_attr(test, derive(Serialize))]
183#[serde(rename_all = "kebab-case", try_from = "ProjectWire")]
184pub struct Project {
185 pub name: PackageName,
187 version: Option<Version>,
189 pub(crate) requires_python: Option<VersionSpecifiers>,
191 pub dependencies: Option<Vec<String>>,
193 pub optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
195
196 #[serde(default, skip_serializing)]
198 gui_scripts: Option<serde::de::IgnoredAny>,
199 #[serde(default, skip_serializing)]
201 scripts: Option<serde::de::IgnoredAny>,
202}
203
204#[derive(Deserialize, Debug)]
205#[serde(rename_all = "kebab-case")]
206struct ProjectWire {
207 name: Option<PackageName>,
208 version: Option<Version>,
209 dynamic: Option<Vec<String>>,
210 requires_python: Option<VersionSpecifiers>,
211 dependencies: Option<Vec<String>>,
212 #[serde(default, deserialize_with = "deserialize_optional_dependencies")]
213 optional_dependencies: Option<BTreeMap<ExtraName, Vec<String>>>,
214 gui_scripts: Option<serde::de::IgnoredAny>,
215 scripts: Option<serde::de::IgnoredAny>,
216}
217
218impl TryFrom<ProjectWire> for Project {
219 type Error = PyprojectTomlError;
220
221 fn try_from(value: ProjectWire) -> Result<Self, Self::Error> {
222 let name = value.name.ok_or(PyprojectTomlError::MissingName)?;
224
225 if value.version.is_none()
227 && !value
228 .dynamic
229 .as_ref()
230 .is_some_and(|dynamic| dynamic.iter().any(|field| field == "version"))
231 {
232 return Err(PyprojectTomlError::MissingVersion);
233 }
234
235 Ok(Self {
236 name,
237 version: value.version,
238 requires_python: value.requires_python,
239 dependencies: value.dependencies,
240 optional_dependencies: value.optional_dependencies,
241 gui_scripts: value.gui_scripts,
242 scripts: value.scripts,
243 })
244 }
245}
246
247#[derive(Deserialize, Debug, Clone, PartialEq, Eq)]
248#[cfg_attr(test, derive(Serialize))]
249#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
250pub struct Tool {
251 pub uv: Option<ToolUv>,
252}
253
254fn deserialize_index_vec<'de, D>(deserializer: D) -> Result<Option<Vec<Index>>, D::Error>
260where
261 D: Deserializer<'de>,
262{
263 let indexes = Option::<Vec<Index>>::deserialize(deserializer)?;
264 if let Some(indexes) = indexes.as_ref() {
265 let mut seen_names = FxHashSet::with_capacity_and_hasher(indexes.len(), FxBuildHasher);
266 let mut seen_default = false;
267 for index in indexes {
268 if let Some(name) = index.name.as_ref() {
269 if !seen_names.insert(name) {
270 return Err(serde::de::Error::custom(format!(
271 "duplicate index name `{name}`"
272 )));
273 }
274 }
275 if index.default {
276 if seen_default {
277 return Err(serde::de::Error::custom(
278 "found multiple indexes with `default = true`; only one index may be marked as default",
279 ));
280 }
281 seen_default = true;
282 }
283 }
284 }
285 Ok(indexes)
286}
287
288pub type OverrideDependency = Override<uv_pep508::Requirement<VerbatimParsedUrl>>;
290
291#[derive(Deserialize, OptionsMetadata, Debug, Clone, PartialEq, Eq)]
294#[cfg_attr(test, derive(Serialize))]
295#[serde(rename_all = "kebab-case")]
296#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
297pub struct ToolUv {
298 #[option(
306 default = "{}",
307 value_type = "dict",
308 example = r#"
309 [tool.uv.sources]
310 httpx = { git = "https://github.com/encode/httpx", tag = "0.27.0" }
311 pytest = { url = "https://files.pythonhosted.org/packages/6b/77/7440a06a8ead44c7757a64362dd22df5760f9b12dc5f11b6188cd2fc27a0/pytest-8.3.3-py3-none-any.whl" }
312 pydantic = { path = "/path/to/pydantic", editable = true }
313 "#
314 )]
315 pub sources: Option<ToolUvSources>,
316
317 #[option(
345 default = "[]",
346 value_type = "dict",
347 example = r#"
348 [[tool.uv.index]]
349 name = "pytorch"
350 url = "https://download.pytorch.org/whl/cu130"
351 "#
352 )]
353 #[serde(deserialize_with = "deserialize_index_vec", default)]
354 pub index: Option<Vec<Index>>,
355
356 #[option_group]
358 pub(crate) workspace: Option<ToolUvWorkspace>,
359
360 #[option(
363 default = r#"true"#,
364 value_type = "bool",
365 example = r#"
366 managed = false
367 "#
368 )]
369 pub(crate) managed: Option<bool>,
370
371 #[option(
382 default = r#"true"#,
383 value_type = "bool",
384 example = r#"
385 package = false
386 "#
387 )]
388 package: Option<bool>,
389
390 #[option(
394 default = r#"["dev"]"#,
395 value_type = r#"str | list[str]"#,
396 example = r#"
397 default-groups = ["docs"]
398 "#
399 )]
400 pub default_groups: Option<DefaultGroups>,
401
402 #[option(
411 default = "[]",
412 value_type = "dict",
413 example = r#"
414 [tool.uv.dependency-groups]
415 my-group = {requires-python = ">=3.12"}
416 "#
417 )]
418 pub(crate) dependency_groups: Option<ToolUvDependencyGroups>,
419
420 #[cfg_attr(
430 feature = "schemars",
431 schemars(
432 with = "Option<Vec<String>>",
433 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
434 )
435 )]
436 #[option(
437 default = "[]",
438 value_type = "list[str]",
439 example = r#"
440 dev-dependencies = ["ruff==0.5.0"]
441 "#
442 )]
443 pub dev_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
444
445 #[option(
474 default = "[]",
475 value_type = "list[str | dict]",
476 example = r#"
477 override-dependencies = [
478 # Always install Werkzeug 2.3.0.
479 "werkzeug==2.3.0",
480 # Use itsdangerous 2.1.2 when requested by Flask 3.0.0.
481 { package = { name = "flask", version = "3.0.0" }, dependencies = ["itsdangerous==2.1.2"] },
482 ]
483 "#
484 )]
485 pub(crate) override_dependencies: Option<Vec<OverrideDependency>>,
486
487 #[option(
508 default = "[]",
509 value_type = "list[str | dict]",
510 example = r#"
511 # Exclude Werkzeug from being installed, even if transitive dependencies request it.
512 exclude-dependencies = [
513 "werkzeug",
514 { package = { name = "flask", version = "3.0.0" }, dependencies = ["itsdangerous"] },
515 ]
516 "#
517 )]
518 pub(crate) exclude_dependencies: Option<Vec<ExcludeDependency>>,
519
520 #[cfg_attr(
534 feature = "schemars",
535 schemars(
536 with = "Option<Vec<String>>",
537 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
538 )
539 )]
540 #[option(
541 default = "[]",
542 value_type = "list[str]",
543 example = r#"
544 # Ensure that the grpcio version is always less than 1.65, if it's requested by a
545 # direct or transitive dependency.
546 constraint-dependencies = ["grpcio<1.65"]
547 "#
548 )]
549 pub(crate) constraint_dependencies: Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
550
551 #[cfg_attr(
565 feature = "schemars",
566 schemars(
567 with = "Option<Vec<String>>",
568 description = "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`."
569 )
570 )]
571 #[option(
572 default = "[]",
573 value_type = "list[str]",
574 example = r#"
575 # Ensure that the setuptools v60.0.0 is used whenever a package has a build dependency
576 # on setuptools.
577 build-constraint-dependencies = ["setuptools==60.0.0"]
578 "#
579 )]
580 pub(crate) build_constraint_dependencies:
581 Option<Vec<uv_pep508::Requirement<VerbatimParsedUrl>>>,
582
583 #[cfg_attr(
592 feature = "schemars",
593 schemars(
594 with = "Option<Vec<String>>",
595 description = "A list of environment markers, e.g., `python_version >= '3.6'`."
596 )
597 )]
598 #[option(
599 default = "[]",
600 value_type = "str | list[str]",
601 example = r#"
602 # Resolve for macOS, but not for Linux or Windows.
603 environments = ["sys_platform == 'darwin'"]
604 "#
605 )]
606 pub(crate) environments: Option<SupportedEnvironments>,
607
608 #[cfg_attr(
628 feature = "schemars",
629 schemars(
630 with = "Option<Vec<String>>",
631 description = "A list of environment markers, e.g., `sys_platform == 'darwin'."
632 )
633 )]
634 #[option(
635 default = "[]",
636 value_type = "str | list[str]",
637 example = r#"
638 # Require that the package is available on the following platforms:
639 required-environments = [
640 # macOS on Apple Silicon (ARM)
641 "sys_platform == 'darwin' and platform_machine == 'arm64'",
642 # Linux on x86_64 (Intel/AMD)
643 "sys_platform == 'linux' and platform_machine == 'x86_64'",
644 # Windows on x86_64 (Intel/AMD)
645 "sys_platform == 'win32' and platform_machine == 'AMD64'",
646 ]
647 "#
648 )]
649 pub(crate) required_environments: Option<SupportedEnvironments>,
650
651 #[cfg_attr(
666 feature = "schemars",
667 schemars(description = "A list of sets of conflicting groups or extras.")
668 )]
669 #[option(
670 default = r#"[]"#,
671 value_type = "list[list[dict]]",
672 example = r#"
673 # Require that `package[extra1]` and `package[extra2]` are resolved
674 # in different forks so that they cannot conflict with one another.
675 conflicts = [
676 [
677 { extra = "extra1" },
678 { extra = "extra2" },
679 ]
680 ]
681
682 # Require that the dependency groups `group1` and `group2`
683 # are resolved in different forks so that they cannot conflict
684 # with one another.
685 conflicts = [
686 [
687 { group = "group1" },
688 { group = "group2" },
689 ]
690 ]
691 "#
692 )]
693 pub(crate) conflicts: Option<SchemaConflicts>,
694
695 #[option_group]
702 build_backend: Option<BuildBackendSettingsSchema>,
703}
704
705#[derive(Default, Debug, Clone, PartialEq, Eq)]
706#[cfg_attr(test, derive(Serialize))]
707#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
708pub struct ToolUvSources(BTreeMap<PackageName, Sources>);
709
710#[derive(Deserialize, Debug)]
711#[serde(rename_all = "kebab-case")]
712struct PyProjectTomlSourcesWire {
713 tool: Option<ToolSourcesWire>,
714}
715
716#[derive(Deserialize, Debug)]
717struct ToolSourcesWire {
718 uv: Option<ToolUvSourcesOnlyWire>,
719}
720
721#[derive(Deserialize, Debug)]
722#[serde(rename_all = "kebab-case")]
723struct ToolUvSourcesOnlyWire {
724 sources: Option<ToolUvSourcesWire>,
725}
726
727#[derive(Default, Debug, Clone, PartialEq, Eq)]
728struct ToolUvSourcesWire(BTreeMap<PackageName, SourcesWire>);
729
730impl ToolUvSources {
731 pub fn inner(&self) -> &BTreeMap<PackageName, Sources> {
733 &self.0
734 }
735
736 #[must_use]
738 pub(crate) fn into_inner(self) -> BTreeMap<PackageName, Sources> {
739 self.0
740 }
741}
742
743impl TryFrom<ToolUvSourcesWire> for ToolUvSources {
744 type Error = SourceError;
745
746 fn try_from(wire: ToolUvSourcesWire) -> Result<Self, Self::Error> {
747 wire.0
748 .into_iter()
749 .map(|(name, sources)| Sources::try_from(sources).map(|sources| (name, sources)))
750 .collect::<Result<BTreeMap<_, _>, _>>()
751 .map(Self)
752 }
753}
754
755impl<'de> serde::de::Deserialize<'de> for ToolUvSourcesWire {
757 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
758 where
759 D: Deserializer<'de>,
760 {
761 deserialize_unique_map(deserializer, |key: &PackageName| {
762 format!("duplicate sources for package `{key}`")
763 })
764 .map(ToolUvSourcesWire)
765 }
766}
767
768impl<'de> serde::de::Deserialize<'de> for ToolUvSources {
770 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
771 where
772 D: Deserializer<'de>,
773 {
774 deserialize_unique_map(deserializer, |key: &PackageName| {
775 format!("duplicate sources for package `{key}`")
776 })
777 .map(ToolUvSources)
778 }
779}
780
781#[derive(Default, Debug, Clone, PartialEq, Eq)]
782#[cfg_attr(test, derive(Serialize))]
783#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
784pub(crate) struct ToolUvDependencyGroups(BTreeMap<GroupName, DependencyGroupSettings>);
785
786impl ToolUvDependencyGroups {
787 pub(crate) fn inner(&self) -> &BTreeMap<GroupName, DependencyGroupSettings> {
789 &self.0
790 }
791}
792
793impl<'de> serde::de::Deserialize<'de> for ToolUvDependencyGroups {
795 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
796 where
797 D: Deserializer<'de>,
798 {
799 deserialize_unique_map(deserializer, |key: &GroupName| {
800 format!("duplicate settings for dependency group `{key}`")
801 })
802 .map(ToolUvDependencyGroups)
803 }
804}
805
806#[derive(Deserialize, Default, Debug, Clone, PartialEq, Eq)]
807#[cfg_attr(test, derive(Serialize))]
808#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
809#[serde(rename_all = "kebab-case")]
810pub(crate) struct DependencyGroupSettings {
811 #[cfg_attr(feature = "schemars", schemars(with = "Option<String>"))]
813 pub(crate) requires_python: Option<VersionSpecifiers>,
814}
815
816#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
817#[serde(untagged, rename_all = "kebab-case")]
818#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
819enum ExtraBuildDependencyWire {
820 Unannotated(uv_pep508::Requirement<VerbatimParsedUrl>),
821 #[serde(rename_all = "kebab-case")]
822 Annotated {
823 requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
824 match_runtime: bool,
825 },
826}
827
828#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
829#[serde(
830 deny_unknown_fields,
831 from = "ExtraBuildDependencyWire",
832 into = "ExtraBuildDependencyWire"
833)]
834#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
835pub struct ExtraBuildDependency {
836 pub requirement: uv_pep508::Requirement<VerbatimParsedUrl>,
837 pub match_runtime: bool,
838}
839
840impl From<ExtraBuildDependency> for uv_pep508::Requirement<VerbatimParsedUrl> {
841 fn from(value: ExtraBuildDependency) -> Self {
842 value.requirement
843 }
844}
845
846impl From<ExtraBuildDependencyWire> for ExtraBuildDependency {
847 fn from(wire: ExtraBuildDependencyWire) -> Self {
848 match wire {
849 ExtraBuildDependencyWire::Unannotated(requirement) => Self {
850 requirement,
851 match_runtime: false,
852 },
853 ExtraBuildDependencyWire::Annotated {
854 requirement,
855 match_runtime,
856 } => Self {
857 requirement,
858 match_runtime,
859 },
860 }
861 }
862}
863
864impl From<ExtraBuildDependency> for ExtraBuildDependencyWire {
865 fn from(item: ExtraBuildDependency) -> Self {
866 Self::Annotated {
867 requirement: item.requirement,
868 match_runtime: item.match_runtime,
869 }
870 }
871}
872
873#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize)]
874#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
875pub struct ExtraBuildDependencies(BTreeMap<PackageName, Vec<ExtraBuildDependency>>);
876
877impl std::ops::Deref for ExtraBuildDependencies {
878 type Target = BTreeMap<PackageName, Vec<ExtraBuildDependency>>;
879
880 fn deref(&self) -> &Self::Target {
881 &self.0
882 }
883}
884
885impl std::ops::DerefMut for ExtraBuildDependencies {
886 fn deref_mut(&mut self) -> &mut Self::Target {
887 &mut self.0
888 }
889}
890
891impl IntoIterator for ExtraBuildDependencies {
892 type Item = (PackageName, Vec<ExtraBuildDependency>);
893 type IntoIter = std::collections::btree_map::IntoIter<PackageName, Vec<ExtraBuildDependency>>;
894
895 fn into_iter(self) -> Self::IntoIter {
896 self.0.into_iter()
897 }
898}
899
900impl FromIterator<(PackageName, Vec<ExtraBuildDependency>)> for ExtraBuildDependencies {
901 fn from_iter<T: IntoIterator<Item = (PackageName, Vec<ExtraBuildDependency>)>>(
902 iter: T,
903 ) -> Self {
904 Self(iter.into_iter().collect())
905 }
906}
907
908impl<'de> serde::de::Deserialize<'de> for ExtraBuildDependencies {
910 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
911 where
912 D: Deserializer<'de>,
913 {
914 deserialize_unique_map(deserializer, |key: &PackageName| {
915 format!("duplicate extra-build-dependencies for `{key}`")
916 })
917 .map(ExtraBuildDependencies)
918 }
919}
920
921#[derive(Deserialize, OptionsMetadata, Default, Debug, Clone, PartialEq, Eq)]
922#[cfg_attr(test, derive(Serialize))]
923#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
924#[serde(rename_all = "kebab-case", deny_unknown_fields)]
925pub(crate) struct ToolUvWorkspace {
926 #[option(
932 default = "[]",
933 value_type = "list[str]",
934 example = r#"
935 members = ["member1", "path/to/member2", "libs/*"]
936 "#
937 )]
938 pub(crate) members: Option<Vec<SerdePattern>>,
939 #[option(
946 default = "[]",
947 value_type = "list[str]",
948 example = r#"
949 exclude = ["member1", "path/to/member2", "libs/*"]
950 "#
951 )]
952 pub(crate) exclude: Option<Vec<SerdePattern>>,
953}
954
955#[derive(Debug, Clone, PartialEq, Eq)]
957pub(crate) struct SerdePattern(Pattern);
958
959impl serde::ser::Serialize for SerdePattern {
960 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
961 where
962 S: serde::ser::Serializer,
963 {
964 self.0.as_str().serialize(serializer)
965 }
966}
967
968impl<'de> serde::Deserialize<'de> for SerdePattern {
969 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
970 struct Visitor;
971
972 impl serde::de::Visitor<'_> for Visitor {
973 type Value = SerdePattern;
974
975 fn expecting(&self, f: &mut Formatter) -> std::fmt::Result {
976 f.write_str("a string")
977 }
978
979 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
980 Pattern::from_str(v)
981 .map(SerdePattern)
982 .map_err(serde::de::Error::custom)
983 }
984 }
985
986 deserializer.deserialize_str(Visitor)
987 }
988}
989
990#[cfg(feature = "schemars")]
991impl schemars::JsonSchema for SerdePattern {
992 fn schema_name() -> Cow<'static, str> {
993 Cow::Borrowed("SerdePattern")
994 }
995
996 fn json_schema(generator: &mut schemars::generate::SchemaGenerator) -> schemars::Schema {
997 <String as schemars::JsonSchema>::json_schema(generator)
998 }
999}
1000
1001impl Deref for SerdePattern {
1002 type Target = Pattern;
1003
1004 fn deref(&self) -> &Self::Target {
1005 &self.0
1006 }
1007}
1008
1009#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1010#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1011#[serde(rename_all = "kebab-case", try_from = "SourcesWire")]
1012pub struct Sources(#[cfg_attr(feature = "schemars", schemars(with = "SourcesWire"))] Vec<Source>);
1013
1014impl Sources {
1015 pub fn iter(&self) -> impl Iterator<Item = &Source> {
1021 self.0.iter()
1022 }
1023}
1024
1025impl FromIterator<Source> for Sources {
1026 fn from_iter<T: IntoIterator<Item = Source>>(iter: T) -> Self {
1027 Self(iter.into_iter().collect())
1028 }
1029}
1030
1031impl IntoIterator for Sources {
1032 type Item = Source;
1033 type IntoIter = std::vec::IntoIter<Source>;
1034
1035 fn into_iter(self) -> Self::IntoIter {
1036 self.0.into_iter()
1037 }
1038}
1039
1040#[derive(Debug, Clone, PartialEq, Eq)]
1041#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
1042enum SourcesWire {
1043 One(Source),
1044 Many(Vec<Source>),
1045}
1046
1047impl<'de> serde::de::Deserialize<'de> for SourcesWire {
1048 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1049 where
1050 D: Deserializer<'de>,
1051 {
1052 struct Visitor;
1053
1054 impl<'de> serde::de::Visitor<'de> for Visitor {
1055 type Value = SourcesWire;
1056
1057 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1058 formatter.write_str("a single source (as a map) or list of sources")
1059 }
1060
1061 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
1062 where
1063 A: SeqAccess<'de>,
1064 {
1065 let sources = serde::de::Deserialize::deserialize(
1066 serde::de::value::SeqAccessDeserializer::new(seq),
1067 )?;
1068 Ok(SourcesWire::Many(sources))
1069 }
1070
1071 fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
1072 where
1073 M: serde::de::MapAccess<'de>,
1074 {
1075 let source = serde::de::Deserialize::deserialize(
1076 serde::de::value::MapAccessDeserializer::new(&mut map),
1077 )?;
1078 Ok(SourcesWire::One(source))
1079 }
1080 }
1081
1082 deserializer.deserialize_any(Visitor)
1083 }
1084}
1085
1086impl TryFrom<SourcesWire> for Sources {
1087 type Error = SourceError;
1088
1089 fn try_from(wire: SourcesWire) -> Result<Self, Self::Error> {
1090 match wire {
1091 SourcesWire::One(source) => Ok(Self(vec![source])),
1092 SourcesWire::Many(sources) => {
1093 for [lhs, rhs] in sources.array_windows() {
1094 if lhs.extra() != rhs.extra() {
1095 continue;
1096 }
1097 if lhs.group() != rhs.group() {
1098 continue;
1099 }
1100
1101 let lhs = lhs.marker();
1102 let rhs = rhs.marker();
1103 if !lhs.is_disjoint(rhs) {
1104 let Some(left) = lhs.contents().map(|contents| contents.to_string()) else {
1105 return Err(SourceError::MissingMarkers);
1106 };
1107
1108 let Some(right) = rhs.contents().map(|contents| contents.to_string())
1109 else {
1110 return Err(SourceError::MissingMarkers);
1111 };
1112
1113 let mut hint = lhs.negate();
1114 hint.and(rhs);
1115 let hint = hint
1116 .contents()
1117 .map(|contents| contents.to_string())
1118 .unwrap_or_else(|| "true".to_string());
1119
1120 return Err(SourceError::OverlappingMarkers(left, right, hint));
1121 }
1122 }
1123
1124 if sources.is_empty() {
1126 return Err(SourceError::EmptySources);
1127 }
1128
1129 Ok(Self(sources))
1130 }
1131 }
1132 }
1133}
1134
1135#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
1137#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1138#[serde(rename_all = "kebab-case", untagged, deny_unknown_fields)]
1139pub enum Source {
1140 Git {
1147 git: DisplaySafeUrl,
1149 subdirectory: Option<PortablePathBuf>,
1151 path: Option<PortablePathBuf>,
1153 rev: Option<String>,
1155 tag: Option<String>,
1156 branch: Option<String>,
1157 lfs: Option<bool>,
1159 #[serde(
1160 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1161 serialize_with = "uv_pep508::marker::ser::serialize",
1162 default
1163 )]
1164 marker: MarkerTree,
1165 extra: Option<ExtraName>,
1166 group: Option<GroupName>,
1167 },
1168 Url {
1176 url: DisplaySafeUrl,
1177 subdirectory: Option<PortablePathBuf>,
1180 #[serde(
1181 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1182 serialize_with = "uv_pep508::marker::ser::serialize",
1183 default
1184 )]
1185 marker: MarkerTree,
1186 extra: Option<ExtraName>,
1187 group: Option<GroupName>,
1188 },
1189 Path {
1193 path: PortablePathBuf,
1194 editable: Option<bool>,
1196 package: Option<bool>,
1203 #[serde(
1204 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1205 serialize_with = "uv_pep508::marker::ser::serialize",
1206 default
1207 )]
1208 marker: MarkerTree,
1209 extra: Option<ExtraName>,
1210 group: Option<GroupName>,
1211 },
1212 Registry {
1214 index: IndexName,
1215 #[serde(
1216 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1217 serialize_with = "uv_pep508::marker::ser::serialize",
1218 default
1219 )]
1220 marker: MarkerTree,
1221 extra: Option<ExtraName>,
1222 group: Option<GroupName>,
1223 },
1224 Workspace {
1226 workspace: bool,
1229 editable: Option<bool>,
1231 #[serde(
1232 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1233 serialize_with = "uv_pep508::marker::ser::serialize",
1234 default
1235 )]
1236 marker: MarkerTree,
1237 extra: Option<ExtraName>,
1238 group: Option<GroupName>,
1239 },
1240}
1241
1242impl<'de> Deserialize<'de> for Source {
1245 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1246 where
1247 D: Deserializer<'de>,
1248 {
1249 #[derive(Deserialize, Debug, Clone)]
1250 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1251 struct CatchAll {
1252 git: Option<DisplaySafeUrl>,
1253 subdirectory: Option<PortablePathBuf>,
1254 rev: Option<String>,
1255 tag: Option<String>,
1256 branch: Option<String>,
1257 lfs: Option<bool>,
1258 url: Option<DisplaySafeUrl>,
1259 path: Option<PortablePathBuf>,
1260 editable: Option<bool>,
1261 package: Option<bool>,
1262 index: Option<IndexName>,
1263 workspace: Option<bool>,
1264 #[serde(
1265 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1266 serialize_with = "uv_pep508::marker::ser::serialize",
1267 default
1268 )]
1269 marker: MarkerTree,
1270 extra: Option<ExtraName>,
1271 group: Option<GroupName>,
1272 }
1273
1274 let CatchAll {
1276 git,
1277 subdirectory,
1278 rev,
1279 tag,
1280 branch,
1281 lfs,
1282 url,
1283 path,
1284 editable,
1285 package,
1286 index,
1287 workspace,
1288 marker,
1289 extra,
1290 group,
1291 } = CatchAll::deserialize(deserializer)?;
1292
1293 if extra.is_some() && group.is_some() {
1295 return Err(serde::de::Error::custom(
1296 "cannot specify both `extra` and `group`",
1297 ));
1298 }
1299
1300 if let Some(git) = git {
1302 if index.is_some() {
1303 return Err(serde::de::Error::custom(
1304 "cannot specify both `git` and `index`",
1305 ));
1306 }
1307 if workspace.is_some() {
1308 return Err(serde::de::Error::custom(
1309 "cannot specify both `git` and `workspace`",
1310 ));
1311 }
1312 if url.is_some() {
1313 return Err(serde::de::Error::custom(
1314 "cannot specify both `git` and `url`",
1315 ));
1316 }
1317 if editable.is_some() {
1318 return Err(serde::de::Error::custom(
1319 "cannot specify both `git` and `editable`",
1320 ));
1321 }
1322 if package.is_some() {
1323 return Err(serde::de::Error::custom(
1324 "cannot specify both `git` and `package`",
1325 ));
1326 }
1327 if subdirectory.is_some() && path.is_some() {
1328 return Err(serde::de::Error::custom(
1329 "cannot specify both `subdirectory` and `path`",
1330 ));
1331 }
1332
1333 match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1335 (None, None, None) => {}
1336 (Some(_), None, None) => {}
1337 (None, Some(_), None) => {}
1338 (None, None, Some(_)) => {}
1339 _ => {
1340 return Err(serde::de::Error::custom(
1341 "expected at most one of `rev`, `tag`, or `branch`",
1342 ));
1343 }
1344 }
1345
1346 let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1348 DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1349 } else {
1350 git
1351 };
1352
1353 return Ok(Self::Git {
1354 git,
1355 subdirectory,
1356 path,
1357 rev,
1358 tag,
1359 branch,
1360 lfs,
1361 marker,
1362 extra,
1363 group,
1364 });
1365 }
1366
1367 if let Some(url) = url {
1369 if index.is_some() {
1370 return Err(serde::de::Error::custom(
1371 "cannot specify both `url` and `index`",
1372 ));
1373 }
1374 if workspace.is_some() {
1375 return Err(serde::de::Error::custom(
1376 "cannot specify both `url` and `workspace`",
1377 ));
1378 }
1379 if path.is_some() {
1380 return Err(serde::de::Error::custom(
1381 "cannot specify both `url` and `path`",
1382 ));
1383 }
1384 if git.is_some() {
1385 return Err(serde::de::Error::custom(
1386 "cannot specify both `url` and `git`",
1387 ));
1388 }
1389 if rev.is_some() {
1390 return Err(serde::de::Error::custom(
1391 "cannot specify both `url` and `rev`",
1392 ));
1393 }
1394 if tag.is_some() {
1395 return Err(serde::de::Error::custom(
1396 "cannot specify both `url` and `tag`",
1397 ));
1398 }
1399 if branch.is_some() {
1400 return Err(serde::de::Error::custom(
1401 "cannot specify both `url` and `branch`",
1402 ));
1403 }
1404 if editable.is_some() {
1405 return Err(serde::de::Error::custom(
1406 "cannot specify both `url` and `editable`",
1407 ));
1408 }
1409 if package.is_some() {
1410 return Err(serde::de::Error::custom(
1411 "cannot specify both `url` and `package`",
1412 ));
1413 }
1414
1415 return Ok(Self::Url {
1416 url,
1417 subdirectory,
1418 marker,
1419 extra,
1420 group,
1421 });
1422 }
1423
1424 if let Some(path) = path {
1426 if index.is_some() {
1427 return Err(serde::de::Error::custom(
1428 "cannot specify both `path` and `index`",
1429 ));
1430 }
1431 if workspace.is_some() {
1432 return Err(serde::de::Error::custom(
1433 "cannot specify both `path` and `workspace`",
1434 ));
1435 }
1436 if git.is_some() {
1437 return Err(serde::de::Error::custom(
1438 "cannot specify both `path` and `git`",
1439 ));
1440 }
1441 if url.is_some() {
1442 return Err(serde::de::Error::custom(
1443 "cannot specify both `path` and `url`",
1444 ));
1445 }
1446 if rev.is_some() {
1447 return Err(serde::de::Error::custom(
1448 "cannot specify both `path` and `rev`",
1449 ));
1450 }
1451 if tag.is_some() {
1452 return Err(serde::de::Error::custom(
1453 "cannot specify both `path` and `tag`",
1454 ));
1455 }
1456 if branch.is_some() {
1457 return Err(serde::de::Error::custom(
1458 "cannot specify both `path` and `branch`",
1459 ));
1460 }
1461
1462 if editable == Some(true) && package == Some(false) {
1464 return Err(serde::de::Error::custom(
1465 "cannot specify both `editable = true` and `package = false`",
1466 ));
1467 }
1468
1469 return Ok(Self::Path {
1470 path,
1471 editable,
1472 package,
1473 marker,
1474 extra,
1475 group,
1476 });
1477 }
1478
1479 if let Some(index) = index {
1481 if workspace.is_some() {
1482 return Err(serde::de::Error::custom(
1483 "cannot specify both `index` and `workspace`",
1484 ));
1485 }
1486 if git.is_some() {
1487 return Err(serde::de::Error::custom(
1488 "cannot specify both `index` and `git`",
1489 ));
1490 }
1491 if url.is_some() {
1492 return Err(serde::de::Error::custom(
1493 "cannot specify both `index` and `url`",
1494 ));
1495 }
1496 if path.is_some() {
1497 return Err(serde::de::Error::custom(
1498 "cannot specify both `index` and `path`",
1499 ));
1500 }
1501 if rev.is_some() {
1502 return Err(serde::de::Error::custom(
1503 "cannot specify both `index` and `rev`",
1504 ));
1505 }
1506 if tag.is_some() {
1507 return Err(serde::de::Error::custom(
1508 "cannot specify both `index` and `tag`",
1509 ));
1510 }
1511 if branch.is_some() {
1512 return Err(serde::de::Error::custom(
1513 "cannot specify both `index` and `branch`",
1514 ));
1515 }
1516 if editable.is_some() {
1517 return Err(serde::de::Error::custom(
1518 "cannot specify both `index` and `editable`",
1519 ));
1520 }
1521 if package.is_some() {
1522 return Err(serde::de::Error::custom(
1523 "cannot specify both `index` and `package`",
1524 ));
1525 }
1526
1527 return Ok(Self::Registry {
1528 index,
1529 marker,
1530 extra,
1531 group,
1532 });
1533 }
1534
1535 if let Some(workspace) = workspace {
1537 if index.is_some() {
1538 return Err(serde::de::Error::custom(
1539 "cannot specify both `workspace` and `index`",
1540 ));
1541 }
1542 if git.is_some() {
1543 return Err(serde::de::Error::custom(
1544 "cannot specify both `workspace` and `git`",
1545 ));
1546 }
1547 if url.is_some() {
1548 return Err(serde::de::Error::custom(
1549 "cannot specify both `workspace` and `url`",
1550 ));
1551 }
1552 if path.is_some() {
1553 return Err(serde::de::Error::custom(
1554 "cannot specify both `workspace` and `path`",
1555 ));
1556 }
1557 if rev.is_some() {
1558 return Err(serde::de::Error::custom(
1559 "cannot specify both `workspace` and `rev`",
1560 ));
1561 }
1562 if tag.is_some() {
1563 return Err(serde::de::Error::custom(
1564 "cannot specify both `workspace` and `tag`",
1565 ));
1566 }
1567 if branch.is_some() {
1568 return Err(serde::de::Error::custom(
1569 "cannot specify both `workspace` and `branch`",
1570 ));
1571 }
1572 if package.is_some() {
1573 return Err(serde::de::Error::custom(
1574 "cannot specify both `workspace` and `package`",
1575 ));
1576 }
1577
1578 return Ok(Self::Workspace {
1579 workspace,
1580 editable,
1581 marker,
1582 extra,
1583 group,
1584 });
1585 }
1586
1587 Err(serde::de::Error::custom(
1589 "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1590 ))
1591 }
1592}
1593
1594#[derive(Error, Debug)]
1595pub enum SourceError {
1596 #[error("Failed to resolve Git reference: `{0}`")]
1597 UnresolvedReference(String),
1598 #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1599 WorkspacePackageGit(String),
1600 #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1601 WorkspacePackageUrl(String),
1602 #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1603 WorkspacePackageFile(String),
1604 #[error(
1605 "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1606 )]
1607 UnusedRev(String, String),
1608 #[error(
1609 "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1610 )]
1611 UnusedTag(String, String),
1612 #[error(
1613 "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1614 )]
1615 UnusedBranch(String, String),
1616 #[error(
1617 "`{0}` did not resolve to a Git repository, but a Git extension (`--lfs`) was provided."
1618 )]
1619 UnusedLfs(String),
1620 #[error(
1621 "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1622 )]
1623 UnusedEditable(String),
1624 #[error("Failed to resolve absolute path")]
1625 Absolute(#[from] std::io::Error),
1626 #[error("Path contains invalid characters: `{}`", _0.display())]
1627 NonUtf8Path(PathBuf),
1628 #[error("Source markers must be disjoint, but the following markers overlap: `{0}` and `{1}`.")]
1629 OverlappingMarkers(String, String, String),
1630 #[error(
1631 "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1632 )]
1633 MissingMarkers,
1634 #[error("Must provide at least one source")]
1635 EmptySources,
1636}
1637
1638impl uv_errors::Hint for SourceError {
1639 fn hints(&self) -> uv_errors::Hints<'_> {
1640 match self {
1641 Self::OverlappingMarkers(_, rhs, replacement) => {
1642 uv_errors::Hints::from(format!("replace `{rhs}` with `{replacement}`"))
1643 }
1644 _ => uv_errors::Hints::none(),
1645 }
1646 }
1647}
1648
1649impl Source {
1650 pub fn from_requirement(
1651 name: &PackageName,
1652 source: RequirementSource,
1653 workspace: bool,
1654 editable: Option<bool>,
1655 index: Option<IndexName>,
1656 rev: Option<String>,
1657 tag: Option<String>,
1658 branch: Option<String>,
1659 lfs: GitLfsSetting,
1660 root: &Path,
1661 existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1662 ) -> Result<Option<Self>, SourceError> {
1663 if !matches!(
1665 source,
1666 RequirementSource::GitDirectory { .. } | RequirementSource::GitPath { .. }
1667 ) && (branch.is_some()
1668 || tag.is_some()
1669 || rev.is_some()
1670 || matches!(lfs, GitLfsSetting::Enabled { .. }))
1671 {
1672 if let Some(sources) = existing_sources
1673 && let Some(package_sources) = sources.get(name)
1674 {
1675 for existing_source in package_sources.iter() {
1676 if let Self::Git {
1677 git,
1678 subdirectory,
1679 path,
1680 marker,
1681 extra,
1682 group,
1683 ..
1684 } = existing_source
1685 {
1686 return Ok(Some(Self::Git {
1687 git: git.clone(),
1688 subdirectory: subdirectory.clone(),
1689 rev,
1690 tag,
1691 branch,
1692 lfs: lfs.into(),
1693 marker: *marker,
1694 path: path.clone(),
1695 extra: extra.clone(),
1696 group: group.clone(),
1697 }));
1698 }
1699 }
1700 }
1701 if let Some(rev) = rev {
1702 return Err(SourceError::UnusedRev(name.to_string(), rev));
1703 }
1704 if let Some(tag) = tag {
1705 return Err(SourceError::UnusedTag(name.to_string(), tag));
1706 }
1707 if let Some(branch) = branch {
1708 return Err(SourceError::UnusedBranch(name.to_string(), branch));
1709 }
1710 if matches!(lfs, GitLfsSetting::Enabled { from_env: false }) {
1711 return Err(SourceError::UnusedLfs(name.to_string()));
1712 }
1713 }
1714
1715 if !workspace {
1717 if !matches!(source, RequirementSource::Directory { .. }) {
1718 if editable == Some(true) {
1719 return Err(SourceError::UnusedEditable(name.to_string()));
1720 }
1721 }
1722 }
1723
1724 if workspace {
1726 return match source {
1727 RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1728 Ok(Some(Self::Workspace {
1729 workspace: true,
1730 editable,
1731 marker: MarkerTree::TRUE,
1732 extra: None,
1733 group: None,
1734 }))
1735 }
1736 RequirementSource::Url { .. } => {
1737 Err(SourceError::WorkspacePackageUrl(name.to_string()))
1738 }
1739 RequirementSource::GitDirectory { .. } => {
1740 Err(SourceError::WorkspacePackageGit(name.to_string()))
1741 }
1742 RequirementSource::GitPath { .. } => {
1743 Err(SourceError::WorkspacePackageGit(name.to_string()))
1744 }
1745 RequirementSource::Path { .. } => {
1746 Err(SourceError::WorkspacePackageFile(name.to_string()))
1747 }
1748 };
1749 }
1750
1751 let source = match source {
1752 RequirementSource::Registry { index: Some(_), .. } => {
1753 return Ok(None);
1754 }
1755 RequirementSource::Registry { index: None, .. } => {
1756 if let Some(index) = index {
1757 Self::Registry {
1758 index,
1759 marker: MarkerTree::TRUE,
1760 extra: None,
1761 group: None,
1762 }
1763 } else {
1764 return Ok(None);
1765 }
1766 }
1767 RequirementSource::Path { install_path, .. } => Self::Path {
1768 editable: None,
1769 package: None,
1770 path: PortablePathBuf::from(
1771 relative_to(&install_path, root)
1772 .or_else(|_| std::path::absolute(&install_path))
1773 .map_err(SourceError::Absolute)?
1774 .into_boxed_path(),
1775 ),
1776 marker: MarkerTree::TRUE,
1777 extra: None,
1778 group: None,
1779 },
1780 RequirementSource::Directory {
1781 install_path,
1782 editable: is_editable,
1783 ..
1784 } => Self::Path {
1785 editable: editable.or(is_editable),
1786 package: None,
1787 path: PortablePathBuf::from(
1788 relative_to(&install_path, root)
1789 .or_else(|_| std::path::absolute(&install_path))
1790 .map_err(SourceError::Absolute)?
1791 .into_boxed_path(),
1792 ),
1793 marker: MarkerTree::TRUE,
1794 extra: None,
1795 group: None,
1796 },
1797 RequirementSource::Url {
1798 location,
1799 subdirectory,
1800 ..
1801 } => Self::Url {
1802 url: location,
1803 subdirectory: subdirectory.map(PortablePathBuf::from),
1804 marker: MarkerTree::TRUE,
1805 extra: None,
1806 group: None,
1807 },
1808 RequirementSource::GitDirectory {
1809 git, subdirectory, ..
1810 } => {
1811 if rev.is_none() && tag.is_none() && branch.is_none() {
1812 let rev = match git.reference() {
1813 GitReference::Branch(rev) => Some(rev),
1814 GitReference::Tag(rev) => Some(rev),
1815 GitReference::BranchOrTag(rev) => Some(rev),
1816 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1817 GitReference::NamedRef(rev) => Some(rev),
1818 GitReference::DefaultBranch => None,
1819 };
1820 Self::Git {
1821 rev: rev.cloned(),
1822 tag,
1823 branch,
1824 lfs: lfs.into(),
1825 git: git.url().clone(),
1826 subdirectory: subdirectory.map(PortablePathBuf::from),
1827 path: None,
1828 marker: MarkerTree::TRUE,
1829 extra: None,
1830 group: None,
1831 }
1832 } else {
1833 Self::Git {
1834 rev,
1835 tag,
1836 branch,
1837 lfs: lfs.into(),
1838 git: git.url().clone(),
1839 subdirectory: subdirectory.map(PortablePathBuf::from),
1840 path: None,
1841 marker: MarkerTree::TRUE,
1842 extra: None,
1843 group: None,
1844 }
1845 }
1846 }
1847 RequirementSource::GitPath {
1848 git, install_path, ..
1849 } => {
1850 if rev.is_none() && tag.is_none() && branch.is_none() {
1851 let rev = match git.reference() {
1852 GitReference::Branch(rev) => Some(rev),
1853 GitReference::Tag(rev) => Some(rev),
1854 GitReference::BranchOrTag(rev) => Some(rev),
1855 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1856 GitReference::NamedRef(rev) => Some(rev),
1857 GitReference::DefaultBranch => None,
1858 };
1859 Self::Git {
1860 rev: rev.cloned(),
1861 tag,
1862 branch,
1863 lfs: lfs.into(),
1864 git: git.url().clone(),
1865 subdirectory: None,
1866 path: Some(PortablePathBuf::from(install_path.as_path())),
1867 marker: MarkerTree::TRUE,
1868 extra: None,
1869 group: None,
1870 }
1871 } else {
1872 Self::Git {
1873 rev,
1874 tag,
1875 branch,
1876 lfs: lfs.into(),
1877 git: git.url().clone(),
1878 subdirectory: None,
1879 path: Some(PortablePathBuf::from(install_path.as_path())),
1880 marker: MarkerTree::TRUE,
1881 extra: None,
1882 group: None,
1883 }
1884 }
1885 }
1886 };
1887
1888 Ok(Some(source))
1889 }
1890
1891 pub fn marker(&self) -> MarkerTree {
1893 match self {
1894 Self::Git { marker, .. } => *marker,
1895 Self::Url { marker, .. } => *marker,
1896 Self::Path { marker, .. } => *marker,
1897 Self::Registry { marker, .. } => *marker,
1898 Self::Workspace { marker, .. } => *marker,
1899 }
1900 }
1901
1902 pub fn extra(&self) -> Option<&ExtraName> {
1904 match self {
1905 Self::Git { extra, .. } => extra.as_ref(),
1906 Self::Url { extra, .. } => extra.as_ref(),
1907 Self::Path { extra, .. } => extra.as_ref(),
1908 Self::Registry { extra, .. } => extra.as_ref(),
1909 Self::Workspace { extra, .. } => extra.as_ref(),
1910 }
1911 }
1912
1913 pub fn group(&self) -> Option<&GroupName> {
1915 match self {
1916 Self::Git { group, .. } => group.as_ref(),
1917 Self::Url { group, .. } => group.as_ref(),
1918 Self::Path { group, .. } => group.as_ref(),
1919 Self::Registry { group, .. } => group.as_ref(),
1920 Self::Workspace { group, .. } => group.as_ref(),
1921 }
1922 }
1923}
1924
1925#[derive(Debug, Clone, PartialEq, Eq)]
1927pub enum DependencyType {
1928 Production,
1930 Dev,
1932 Optional(ExtraName),
1934 Group(GroupName),
1936}
1937
1938impl DependencyType {
1939 pub fn toml_table_name(&self) -> Cow<'_, str> {
1941 match self {
1942 Self::Production => Cow::Borrowed("`project.dependencies`"),
1943 Self::Dev => {
1944 Cow::Borrowed("`tool.uv.dev-dependencies` or `tool.uv.dependency-groups.dev`")
1945 }
1946 Self::Optional(extra) => Cow::Owned(format!("`project.optional-dependencies.{extra}`")),
1947 Self::Group(group) => Cow::Owned(format!("`dependency-groups.{group}`")),
1948 }
1949 }
1950}
1951
1952#[derive(Debug, Clone, PartialEq, Eq)]
1953#[cfg_attr(test, derive(Serialize))]
1954pub(crate) struct BuildBackendSettingsSchema;
1955
1956impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1957 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1958 where
1959 D: Deserializer<'de>,
1960 {
1961 Ok(Self)
1962 }
1963}
1964
1965#[cfg(feature = "schemars")]
1966impl schemars::JsonSchema for BuildBackendSettingsSchema {
1967 fn schema_name() -> Cow<'static, str> {
1968 BuildBackendSettings::schema_name()
1969 }
1970
1971 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1972 BuildBackendSettings::json_schema(generator)
1973 }
1974}
1975
1976impl OptionsMetadata for BuildBackendSettingsSchema {
1977 fn record(visit: &mut dyn Visit) {
1978 BuildBackendSettings::record(visit);
1979 }
1980
1981 fn documentation() -> Option<&'static str> {
1982 BuildBackendSettings::documentation()
1983 }
1984
1985 fn metadata() -> OptionSet
1986 where
1987 Self: Sized + 'static,
1988 {
1989 BuildBackendSettings::metadata()
1990 }
1991}