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, try_relative_to_if};
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 hint = lhs.negate().and(rhs);
1114 let hint = hint
1115 .contents()
1116 .map(|contents| contents.to_string())
1117 .unwrap_or_else(|| "true".to_string());
1118
1119 return Err(SourceError::OverlappingMarkers(left, right, hint));
1120 }
1121 }
1122
1123 if sources.is_empty() {
1125 return Err(SourceError::EmptySources);
1126 }
1127
1128 Ok(Self(sources))
1129 }
1130 }
1131 }
1132}
1133
1134#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
1136#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1137#[serde(rename_all = "kebab-case", untagged, deny_unknown_fields)]
1138pub enum Source {
1139 Git {
1146 git: DisplaySafeUrl,
1148 subdirectory: Option<PortablePathBuf>,
1150 path: Option<PortablePathBuf>,
1152 rev: Option<String>,
1154 tag: Option<String>,
1155 branch: Option<String>,
1156 lfs: Option<bool>,
1158 #[serde(
1159 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1160 serialize_with = "uv_pep508::marker::ser::serialize",
1161 default
1162 )]
1163 marker: MarkerTree,
1164 extra: Option<ExtraName>,
1165 group: Option<GroupName>,
1166 },
1167 Url {
1175 url: DisplaySafeUrl,
1176 subdirectory: Option<PortablePathBuf>,
1179 #[serde(
1180 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1181 serialize_with = "uv_pep508::marker::ser::serialize",
1182 default
1183 )]
1184 marker: MarkerTree,
1185 extra: Option<ExtraName>,
1186 group: Option<GroupName>,
1187 },
1188 Path {
1192 path: PortablePathBuf,
1193 editable: Option<bool>,
1195 package: Option<bool>,
1202 #[serde(
1203 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1204 serialize_with = "uv_pep508::marker::ser::serialize",
1205 default
1206 )]
1207 marker: MarkerTree,
1208 extra: Option<ExtraName>,
1209 group: Option<GroupName>,
1210 },
1211 Registry {
1213 index: IndexName,
1214 #[serde(
1215 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1216 serialize_with = "uv_pep508::marker::ser::serialize",
1217 default
1218 )]
1219 marker: MarkerTree,
1220 extra: Option<ExtraName>,
1221 group: Option<GroupName>,
1222 },
1223 Workspace {
1225 workspace: WorkspaceReference,
1231 editable: Option<bool>,
1233 #[serde(
1234 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1235 serialize_with = "uv_pep508::marker::ser::serialize",
1236 default
1237 )]
1238 marker: MarkerTree,
1239 extra: Option<ExtraName>,
1240 group: Option<GroupName>,
1241 },
1242}
1243
1244#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
1246#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema), schemars(untagged))]
1247#[serde(untagged)]
1248pub enum WorkspaceReference {
1249 Bool(bool),
1250 Path(PortablePathBuf),
1251}
1252
1253impl<'de> Deserialize<'de> for Source {
1256 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
1257 where
1258 D: Deserializer<'de>,
1259 {
1260 #[derive(Deserialize, Debug, Clone)]
1261 #[serde(rename_all = "kebab-case", deny_unknown_fields)]
1262 struct CatchAll {
1263 git: Option<DisplaySafeUrl>,
1264 subdirectory: Option<PortablePathBuf>,
1265 rev: Option<String>,
1266 tag: Option<String>,
1267 branch: Option<String>,
1268 lfs: Option<bool>,
1269 url: Option<DisplaySafeUrl>,
1270 path: Option<PortablePathBuf>,
1271 editable: Option<bool>,
1272 package: Option<bool>,
1273 index: Option<IndexName>,
1274 workspace: Option<WorkspaceReference>,
1275 #[serde(
1276 skip_serializing_if = "uv_pep508::marker::ser::is_empty",
1277 serialize_with = "uv_pep508::marker::ser::serialize",
1278 default
1279 )]
1280 marker: MarkerTree,
1281 extra: Option<ExtraName>,
1282 group: Option<GroupName>,
1283 }
1284
1285 let CatchAll {
1287 git,
1288 subdirectory,
1289 rev,
1290 tag,
1291 branch,
1292 lfs,
1293 url,
1294 path,
1295 editable,
1296 package,
1297 index,
1298 workspace,
1299 marker,
1300 extra,
1301 group,
1302 } = CatchAll::deserialize(deserializer)?;
1303
1304 if extra.is_some() && group.is_some() {
1306 return Err(serde::de::Error::custom(
1307 "cannot specify both `extra` and `group`",
1308 ));
1309 }
1310
1311 if let Some(git) = git {
1313 if index.is_some() {
1314 return Err(serde::de::Error::custom(
1315 "cannot specify both `git` and `index`",
1316 ));
1317 }
1318 if workspace.is_some() {
1319 return Err(serde::de::Error::custom(
1320 "cannot specify both `git` and `workspace`",
1321 ));
1322 }
1323 if url.is_some() {
1324 return Err(serde::de::Error::custom(
1325 "cannot specify both `git` and `url`",
1326 ));
1327 }
1328 if editable.is_some() {
1329 return Err(serde::de::Error::custom(
1330 "cannot specify both `git` and `editable`",
1331 ));
1332 }
1333 if package.is_some() {
1334 return Err(serde::de::Error::custom(
1335 "cannot specify both `git` and `package`",
1336 ));
1337 }
1338 if subdirectory.is_some() && path.is_some() {
1339 return Err(serde::de::Error::custom(
1340 "cannot specify both `subdirectory` and `path`",
1341 ));
1342 }
1343
1344 match (rev.as_ref(), tag.as_ref(), branch.as_ref()) {
1346 (None, None, None) => {}
1347 (Some(_), None, None) => {}
1348 (None, Some(_), None) => {}
1349 (None, None, Some(_)) => {}
1350 _ => {
1351 return Err(serde::de::Error::custom(
1352 "expected at most one of `rev`, `tag`, or `branch`",
1353 ));
1354 }
1355 }
1356
1357 let git = if let Some(git) = git.as_str().strip_prefix("git+") {
1359 DisplaySafeUrl::parse(git).map_err(serde::de::Error::custom)?
1360 } else {
1361 git
1362 };
1363
1364 return Ok(Self::Git {
1365 git,
1366 subdirectory,
1367 path,
1368 rev,
1369 tag,
1370 branch,
1371 lfs,
1372 marker,
1373 extra,
1374 group,
1375 });
1376 }
1377
1378 if let Some(url) = url {
1380 if index.is_some() {
1381 return Err(serde::de::Error::custom(
1382 "cannot specify both `url` and `index`",
1383 ));
1384 }
1385 if workspace.is_some() {
1386 return Err(serde::de::Error::custom(
1387 "cannot specify both `url` and `workspace`",
1388 ));
1389 }
1390 if path.is_some() {
1391 return Err(serde::de::Error::custom(
1392 "cannot specify both `url` and `path`",
1393 ));
1394 }
1395 if git.is_some() {
1396 return Err(serde::de::Error::custom(
1397 "cannot specify both `url` and `git`",
1398 ));
1399 }
1400 if rev.is_some() {
1401 return Err(serde::de::Error::custom(
1402 "cannot specify both `url` and `rev`",
1403 ));
1404 }
1405 if tag.is_some() {
1406 return Err(serde::de::Error::custom(
1407 "cannot specify both `url` and `tag`",
1408 ));
1409 }
1410 if branch.is_some() {
1411 return Err(serde::de::Error::custom(
1412 "cannot specify both `url` and `branch`",
1413 ));
1414 }
1415 if editable.is_some() {
1416 return Err(serde::de::Error::custom(
1417 "cannot specify both `url` and `editable`",
1418 ));
1419 }
1420 if package.is_some() {
1421 return Err(serde::de::Error::custom(
1422 "cannot specify both `url` and `package`",
1423 ));
1424 }
1425
1426 return Ok(Self::Url {
1427 url,
1428 subdirectory,
1429 marker,
1430 extra,
1431 group,
1432 });
1433 }
1434
1435 if let Some(path) = path {
1437 if index.is_some() {
1438 return Err(serde::de::Error::custom(
1439 "cannot specify both `path` and `index`",
1440 ));
1441 }
1442 if workspace.is_some() {
1443 return Err(serde::de::Error::custom(
1444 "cannot specify both `path` and `workspace`",
1445 ));
1446 }
1447 if git.is_some() {
1448 return Err(serde::de::Error::custom(
1449 "cannot specify both `path` and `git`",
1450 ));
1451 }
1452 if url.is_some() {
1453 return Err(serde::de::Error::custom(
1454 "cannot specify both `path` and `url`",
1455 ));
1456 }
1457 if rev.is_some() {
1458 return Err(serde::de::Error::custom(
1459 "cannot specify both `path` and `rev`",
1460 ));
1461 }
1462 if tag.is_some() {
1463 return Err(serde::de::Error::custom(
1464 "cannot specify both `path` and `tag`",
1465 ));
1466 }
1467 if branch.is_some() {
1468 return Err(serde::de::Error::custom(
1469 "cannot specify both `path` and `branch`",
1470 ));
1471 }
1472
1473 if editable == Some(true) && package == Some(false) {
1475 return Err(serde::de::Error::custom(
1476 "cannot specify both `editable = true` and `package = false`",
1477 ));
1478 }
1479
1480 return Ok(Self::Path {
1481 path,
1482 editable,
1483 package,
1484 marker,
1485 extra,
1486 group,
1487 });
1488 }
1489
1490 if let Some(index) = index {
1492 if workspace.is_some() {
1493 return Err(serde::de::Error::custom(
1494 "cannot specify both `index` and `workspace`",
1495 ));
1496 }
1497 if git.is_some() {
1498 return Err(serde::de::Error::custom(
1499 "cannot specify both `index` and `git`",
1500 ));
1501 }
1502 if url.is_some() {
1503 return Err(serde::de::Error::custom(
1504 "cannot specify both `index` and `url`",
1505 ));
1506 }
1507 if path.is_some() {
1508 return Err(serde::de::Error::custom(
1509 "cannot specify both `index` and `path`",
1510 ));
1511 }
1512 if rev.is_some() {
1513 return Err(serde::de::Error::custom(
1514 "cannot specify both `index` and `rev`",
1515 ));
1516 }
1517 if tag.is_some() {
1518 return Err(serde::de::Error::custom(
1519 "cannot specify both `index` and `tag`",
1520 ));
1521 }
1522 if branch.is_some() {
1523 return Err(serde::de::Error::custom(
1524 "cannot specify both `index` and `branch`",
1525 ));
1526 }
1527 if editable.is_some() {
1528 return Err(serde::de::Error::custom(
1529 "cannot specify both `index` and `editable`",
1530 ));
1531 }
1532 if package.is_some() {
1533 return Err(serde::de::Error::custom(
1534 "cannot specify both `index` and `package`",
1535 ));
1536 }
1537
1538 return Ok(Self::Registry {
1539 index,
1540 marker,
1541 extra,
1542 group,
1543 });
1544 }
1545
1546 if let Some(workspace) = workspace {
1548 if index.is_some() {
1549 return Err(serde::de::Error::custom(
1550 "cannot specify both `workspace` and `index`",
1551 ));
1552 }
1553 if git.is_some() {
1554 return Err(serde::de::Error::custom(
1555 "cannot specify both `workspace` and `git`",
1556 ));
1557 }
1558 if url.is_some() {
1559 return Err(serde::de::Error::custom(
1560 "cannot specify both `workspace` and `url`",
1561 ));
1562 }
1563 if path.is_some() {
1564 return Err(serde::de::Error::custom(
1565 "cannot specify both `workspace` and `path`",
1566 ));
1567 }
1568 if rev.is_some() {
1569 return Err(serde::de::Error::custom(
1570 "cannot specify both `workspace` and `rev`",
1571 ));
1572 }
1573 if tag.is_some() {
1574 return Err(serde::de::Error::custom(
1575 "cannot specify both `workspace` and `tag`",
1576 ));
1577 }
1578 if branch.is_some() {
1579 return Err(serde::de::Error::custom(
1580 "cannot specify both `workspace` and `branch`",
1581 ));
1582 }
1583 if package.is_some() {
1584 return Err(serde::de::Error::custom(
1585 "cannot specify both `workspace` and `package`",
1586 ));
1587 }
1588
1589 return Ok(Self::Workspace {
1590 workspace,
1591 editable,
1592 marker,
1593 extra,
1594 group,
1595 });
1596 }
1597
1598 Err(serde::de::Error::custom(
1600 "expected one of `git`, `url`, `path`, `index`, or `workspace`",
1601 ))
1602 }
1603}
1604
1605#[derive(Error, Debug)]
1606pub enum SourceError {
1607 #[error("Failed to resolve Git reference: `{0}`")]
1608 UnresolvedReference(String),
1609 #[error("Workspace dependency `{0}` must refer to local directory, not a Git repository")]
1610 WorkspacePackageGit(String),
1611 #[error("Workspace dependency `{0}` must refer to local directory, not a URL")]
1612 WorkspacePackageUrl(String),
1613 #[error("Workspace dependency `{0}` must refer to local directory, not a file")]
1614 WorkspacePackageFile(String),
1615 #[error(
1616 "`{0}` did not resolve to a Git repository, but a Git reference (`--rev {1}`) was provided."
1617 )]
1618 UnusedRev(String, String),
1619 #[error(
1620 "`{0}` did not resolve to a Git repository, but a Git reference (`--tag {1}`) was provided."
1621 )]
1622 UnusedTag(String, String),
1623 #[error(
1624 "`{0}` did not resolve to a Git repository, but a Git reference (`--branch {1}`) was provided."
1625 )]
1626 UnusedBranch(String, String),
1627 #[error(
1628 "`{0}` did not resolve to a Git repository, but a Git extension (`--lfs`) was provided."
1629 )]
1630 UnusedLfs(String),
1631 #[error(
1632 "`{0}` did not resolve to a local directory, but the `--editable` flag was provided. Editable installs are only supported for local directories."
1633 )]
1634 UnusedEditable(String),
1635 #[error("Failed to resolve absolute path")]
1636 Absolute(#[from] std::io::Error),
1637 #[error("Path contains invalid characters: `{}`", _0.display())]
1638 NonUtf8Path(PathBuf),
1639 #[error("Source markers must be disjoint, but the following markers overlap: `{0}` and `{1}`.")]
1640 OverlappingMarkers(String, String, String),
1641 #[error(
1642 "When multiple sources are provided, each source must include a platform marker (e.g., `marker = \"sys_platform == 'linux'\"`)"
1643 )]
1644 MissingMarkers,
1645 #[error("Must provide at least one source")]
1646 EmptySources,
1647}
1648
1649impl uv_errors::Hint for SourceError {
1650 fn hints(&self) -> uv_errors::Hints<'_> {
1651 match self {
1652 Self::OverlappingMarkers(_, rhs, replacement) => {
1653 uv_errors::Hints::from(format!("replace `{rhs}` with `{replacement}`"))
1654 }
1655 _ => uv_errors::Hints::none(),
1656 }
1657 }
1658}
1659
1660impl Source {
1661 pub fn from_requirement(
1662 name: &PackageName,
1663 source: RequirementSource,
1664 workspace: bool,
1665 editable: Option<bool>,
1666 index: Option<IndexName>,
1667 rev: Option<String>,
1668 tag: Option<String>,
1669 branch: Option<String>,
1670 lfs: GitLfsSetting,
1671 root: &Path,
1672 existing_sources: Option<&BTreeMap<PackageName, Sources>>,
1673 ) -> Result<Option<Self>, SourceError> {
1674 if !matches!(
1676 source,
1677 RequirementSource::GitDirectory { .. } | RequirementSource::GitPath { .. }
1678 ) && (branch.is_some()
1679 || tag.is_some()
1680 || rev.is_some()
1681 || matches!(lfs, GitLfsSetting::Enabled { .. }))
1682 {
1683 if let Some(sources) = existing_sources
1684 && let Some(package_sources) = sources.get(name)
1685 {
1686 for existing_source in package_sources.iter() {
1687 if let Self::Git {
1688 git,
1689 subdirectory,
1690 path,
1691 marker,
1692 extra,
1693 group,
1694 ..
1695 } = existing_source
1696 {
1697 return Ok(Some(Self::Git {
1698 git: git.clone(),
1699 subdirectory: subdirectory.clone(),
1700 rev,
1701 tag,
1702 branch,
1703 lfs: lfs.into(),
1704 marker: *marker,
1705 path: path.clone(),
1706 extra: extra.clone(),
1707 group: group.clone(),
1708 }));
1709 }
1710 }
1711 }
1712 if let Some(rev) = rev {
1713 return Err(SourceError::UnusedRev(name.to_string(), rev));
1714 }
1715 if let Some(tag) = tag {
1716 return Err(SourceError::UnusedTag(name.to_string(), tag));
1717 }
1718 if let Some(branch) = branch {
1719 return Err(SourceError::UnusedBranch(name.to_string(), branch));
1720 }
1721 if matches!(lfs, GitLfsSetting::Enabled { from_env: false }) {
1722 return Err(SourceError::UnusedLfs(name.to_string()));
1723 }
1724 }
1725
1726 if !workspace {
1728 if !matches!(source, RequirementSource::Directory { .. }) {
1729 if editable == Some(true) {
1730 return Err(SourceError::UnusedEditable(name.to_string()));
1731 }
1732 }
1733 }
1734
1735 if workspace {
1737 return match source {
1738 RequirementSource::Registry { .. } | RequirementSource::Directory { .. } => {
1739 Ok(Some(Self::Workspace {
1740 workspace: WorkspaceReference::Bool(true),
1741 editable,
1742 marker: MarkerTree::TRUE,
1743 extra: None,
1744 group: None,
1745 }))
1746 }
1747 RequirementSource::Url { .. } => {
1748 Err(SourceError::WorkspacePackageUrl(name.to_string()))
1749 }
1750 RequirementSource::GitDirectory { .. } => {
1751 Err(SourceError::WorkspacePackageGit(name.to_string()))
1752 }
1753 RequirementSource::GitPath { .. } => {
1754 Err(SourceError::WorkspacePackageGit(name.to_string()))
1755 }
1756 RequirementSource::Path { .. } => {
1757 Err(SourceError::WorkspacePackageFile(name.to_string()))
1758 }
1759 };
1760 }
1761
1762 let source = match source {
1763 RequirementSource::Registry { index: Some(_), .. } => {
1764 return Ok(None);
1765 }
1766 RequirementSource::Registry { index: None, .. } if let Some(index) = index => {
1767 Self::Registry {
1768 index,
1769 marker: MarkerTree::TRUE,
1770 extra: None,
1771 group: None,
1772 }
1773 }
1774 RequirementSource::Registry { index: None, .. } => return Ok(None),
1775 RequirementSource::Path {
1776 install_path, url, ..
1777 } => Self::Path {
1778 editable: None,
1779 package: None,
1780 path: PortablePathBuf::from(
1781 try_relative_to_if(&install_path, root, !url.was_given_absolute())
1782 .map_err(SourceError::Absolute)?
1783 .into_boxed_path(),
1784 ),
1785 marker: MarkerTree::TRUE,
1786 extra: None,
1787 group: None,
1788 },
1789 RequirementSource::Directory {
1790 install_path,
1791 editable: is_editable,
1792 url,
1793 ..
1794 } => Self::Path {
1795 editable: editable.or(is_editable),
1796 package: None,
1797 path: PortablePathBuf::from(
1798 try_relative_to_if(&install_path, root, !url.was_given_absolute())
1799 .map_err(SourceError::Absolute)?
1800 .into_boxed_path(),
1801 ),
1802 marker: MarkerTree::TRUE,
1803 extra: None,
1804 group: None,
1805 },
1806 RequirementSource::Url {
1807 location,
1808 subdirectory,
1809 ..
1810 } => Self::Url {
1811 url: location,
1812 subdirectory: subdirectory.map(PortablePathBuf::from),
1813 marker: MarkerTree::TRUE,
1814 extra: None,
1815 group: None,
1816 },
1817 RequirementSource::GitDirectory {
1818 git, subdirectory, ..
1819 } => {
1820 if rev.is_none() && tag.is_none() && branch.is_none() {
1821 let rev = match git.reference() {
1822 GitReference::Branch(rev) => Some(rev),
1823 GitReference::Tag(rev) => Some(rev),
1824 GitReference::BranchOrTag(rev) => Some(rev),
1825 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1826 GitReference::NamedRef(rev) => Some(rev),
1827 GitReference::DefaultBranch => None,
1828 };
1829 Self::Git {
1830 rev: rev.cloned(),
1831 tag,
1832 branch,
1833 lfs: lfs.into(),
1834 git: git.url().clone(),
1835 subdirectory: subdirectory.map(PortablePathBuf::from),
1836 path: None,
1837 marker: MarkerTree::TRUE,
1838 extra: None,
1839 group: None,
1840 }
1841 } else {
1842 Self::Git {
1843 rev,
1844 tag,
1845 branch,
1846 lfs: lfs.into(),
1847 git: git.url().clone(),
1848 subdirectory: subdirectory.map(PortablePathBuf::from),
1849 path: None,
1850 marker: MarkerTree::TRUE,
1851 extra: None,
1852 group: None,
1853 }
1854 }
1855 }
1856 RequirementSource::GitPath {
1857 git, install_path, ..
1858 } => {
1859 if rev.is_none() && tag.is_none() && branch.is_none() {
1860 let rev = match git.reference() {
1861 GitReference::Branch(rev) => Some(rev),
1862 GitReference::Tag(rev) => Some(rev),
1863 GitReference::BranchOrTag(rev) => Some(rev),
1864 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1865 GitReference::NamedRef(rev) => Some(rev),
1866 GitReference::DefaultBranch => None,
1867 };
1868 Self::Git {
1869 rev: rev.cloned(),
1870 tag,
1871 branch,
1872 lfs: lfs.into(),
1873 git: git.url().clone(),
1874 subdirectory: None,
1875 path: Some(PortablePathBuf::from(install_path.as_path())),
1876 marker: MarkerTree::TRUE,
1877 extra: None,
1878 group: None,
1879 }
1880 } else {
1881 Self::Git {
1882 rev,
1883 tag,
1884 branch,
1885 lfs: lfs.into(),
1886 git: git.url().clone(),
1887 subdirectory: None,
1888 path: Some(PortablePathBuf::from(install_path.as_path())),
1889 marker: MarkerTree::TRUE,
1890 extra: None,
1891 group: None,
1892 }
1893 }
1894 }
1895 };
1896
1897 Ok(Some(source))
1898 }
1899
1900 pub fn marker(&self) -> MarkerTree {
1902 match self {
1903 Self::Git { marker, .. } => *marker,
1904 Self::Url { marker, .. } => *marker,
1905 Self::Path { marker, .. } => *marker,
1906 Self::Registry { marker, .. } => *marker,
1907 Self::Workspace { marker, .. } => *marker,
1908 }
1909 }
1910
1911 pub fn extra(&self) -> Option<&ExtraName> {
1913 match self {
1914 Self::Git { extra, .. } => extra.as_ref(),
1915 Self::Url { extra, .. } => extra.as_ref(),
1916 Self::Path { extra, .. } => extra.as_ref(),
1917 Self::Registry { extra, .. } => extra.as_ref(),
1918 Self::Workspace { extra, .. } => extra.as_ref(),
1919 }
1920 }
1921
1922 pub fn group(&self) -> Option<&GroupName> {
1924 match self {
1925 Self::Git { group, .. } => group.as_ref(),
1926 Self::Url { group, .. } => group.as_ref(),
1927 Self::Path { group, .. } => group.as_ref(),
1928 Self::Registry { group, .. } => group.as_ref(),
1929 Self::Workspace { group, .. } => group.as_ref(),
1930 }
1931 }
1932}
1933
1934#[derive(Debug, Clone, PartialEq, Eq)]
1936pub enum DependencyType {
1937 Production,
1939 Dev,
1941 Optional(ExtraName),
1943 Group(GroupName),
1945}
1946
1947impl DependencyType {
1948 pub fn toml_table_name(&self) -> Cow<'_, str> {
1950 match self {
1951 Self::Production => Cow::Borrowed("`project.dependencies`"),
1952 Self::Dev => {
1953 Cow::Borrowed("`tool.uv.dev-dependencies` or `tool.uv.dependency-groups.dev`")
1954 }
1955 Self::Optional(extra) => Cow::Owned(format!("`project.optional-dependencies.{extra}`")),
1956 Self::Group(group) => Cow::Owned(format!("`dependency-groups.{group}`")),
1957 }
1958 }
1959}
1960
1961#[derive(Debug, Clone, PartialEq, Eq)]
1962#[cfg_attr(test, derive(Serialize))]
1963pub(crate) struct BuildBackendSettingsSchema;
1964
1965impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1966 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1967 where
1968 D: Deserializer<'de>,
1969 {
1970 Ok(Self)
1971 }
1972}
1973
1974#[cfg(feature = "schemars")]
1975impl schemars::JsonSchema for BuildBackendSettingsSchema {
1976 fn schema_name() -> Cow<'static, str> {
1977 BuildBackendSettings::schema_name()
1978 }
1979
1980 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1981 BuildBackendSettings::json_schema(generator)
1982 }
1983}
1984
1985impl OptionsMetadata for BuildBackendSettingsSchema {
1986 fn record(visit: &mut dyn Visit) {
1987 BuildBackendSettings::record(visit);
1988 }
1989
1990 fn documentation() -> Option<&'static str> {
1991 BuildBackendSettings::documentation()
1992 }
1993
1994 fn metadata() -> OptionSet
1995 where
1996 Self: Sized + 'static,
1997 {
1998 BuildBackendSettings::metadata()
1999 }
2000}