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 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 { install_path, .. } => Self::Path {
1776 editable: None,
1777 package: None,
1778 path: PortablePathBuf::from(
1779 relative_to(&install_path, root)
1780 .or_else(|_| std::path::absolute(&install_path))
1781 .map_err(SourceError::Absolute)?
1782 .into_boxed_path(),
1783 ),
1784 marker: MarkerTree::TRUE,
1785 extra: None,
1786 group: None,
1787 },
1788 RequirementSource::Directory {
1789 install_path,
1790 editable: is_editable,
1791 ..
1792 } => Self::Path {
1793 editable: editable.or(is_editable),
1794 package: None,
1795 path: PortablePathBuf::from(
1796 relative_to(&install_path, root)
1797 .or_else(|_| std::path::absolute(&install_path))
1798 .map_err(SourceError::Absolute)?
1799 .into_boxed_path(),
1800 ),
1801 marker: MarkerTree::TRUE,
1802 extra: None,
1803 group: None,
1804 },
1805 RequirementSource::Url {
1806 location,
1807 subdirectory,
1808 ..
1809 } => Self::Url {
1810 url: location,
1811 subdirectory: subdirectory.map(PortablePathBuf::from),
1812 marker: MarkerTree::TRUE,
1813 extra: None,
1814 group: None,
1815 },
1816 RequirementSource::GitDirectory {
1817 git, subdirectory, ..
1818 } => {
1819 if rev.is_none() && tag.is_none() && branch.is_none() {
1820 let rev = match git.reference() {
1821 GitReference::Branch(rev) => Some(rev),
1822 GitReference::Tag(rev) => Some(rev),
1823 GitReference::BranchOrTag(rev) => Some(rev),
1824 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1825 GitReference::NamedRef(rev) => Some(rev),
1826 GitReference::DefaultBranch => None,
1827 };
1828 Self::Git {
1829 rev: rev.cloned(),
1830 tag,
1831 branch,
1832 lfs: lfs.into(),
1833 git: git.url().clone(),
1834 subdirectory: subdirectory.map(PortablePathBuf::from),
1835 path: None,
1836 marker: MarkerTree::TRUE,
1837 extra: None,
1838 group: None,
1839 }
1840 } else {
1841 Self::Git {
1842 rev,
1843 tag,
1844 branch,
1845 lfs: lfs.into(),
1846 git: git.url().clone(),
1847 subdirectory: subdirectory.map(PortablePathBuf::from),
1848 path: None,
1849 marker: MarkerTree::TRUE,
1850 extra: None,
1851 group: None,
1852 }
1853 }
1854 }
1855 RequirementSource::GitPath {
1856 git, install_path, ..
1857 } => {
1858 if rev.is_none() && tag.is_none() && branch.is_none() {
1859 let rev = match git.reference() {
1860 GitReference::Branch(rev) => Some(rev),
1861 GitReference::Tag(rev) => Some(rev),
1862 GitReference::BranchOrTag(rev) => Some(rev),
1863 GitReference::BranchOrTagOrCommit(rev) => Some(rev),
1864 GitReference::NamedRef(rev) => Some(rev),
1865 GitReference::DefaultBranch => None,
1866 };
1867 Self::Git {
1868 rev: rev.cloned(),
1869 tag,
1870 branch,
1871 lfs: lfs.into(),
1872 git: git.url().clone(),
1873 subdirectory: None,
1874 path: Some(PortablePathBuf::from(install_path.as_path())),
1875 marker: MarkerTree::TRUE,
1876 extra: None,
1877 group: None,
1878 }
1879 } else {
1880 Self::Git {
1881 rev,
1882 tag,
1883 branch,
1884 lfs: lfs.into(),
1885 git: git.url().clone(),
1886 subdirectory: None,
1887 path: Some(PortablePathBuf::from(install_path.as_path())),
1888 marker: MarkerTree::TRUE,
1889 extra: None,
1890 group: None,
1891 }
1892 }
1893 }
1894 };
1895
1896 Ok(Some(source))
1897 }
1898
1899 pub fn marker(&self) -> MarkerTree {
1901 match self {
1902 Self::Git { marker, .. } => *marker,
1903 Self::Url { marker, .. } => *marker,
1904 Self::Path { marker, .. } => *marker,
1905 Self::Registry { marker, .. } => *marker,
1906 Self::Workspace { marker, .. } => *marker,
1907 }
1908 }
1909
1910 pub fn extra(&self) -> Option<&ExtraName> {
1912 match self {
1913 Self::Git { extra, .. } => extra.as_ref(),
1914 Self::Url { extra, .. } => extra.as_ref(),
1915 Self::Path { extra, .. } => extra.as_ref(),
1916 Self::Registry { extra, .. } => extra.as_ref(),
1917 Self::Workspace { extra, .. } => extra.as_ref(),
1918 }
1919 }
1920
1921 pub fn group(&self) -> Option<&GroupName> {
1923 match self {
1924 Self::Git { group, .. } => group.as_ref(),
1925 Self::Url { group, .. } => group.as_ref(),
1926 Self::Path { group, .. } => group.as_ref(),
1927 Self::Registry { group, .. } => group.as_ref(),
1928 Self::Workspace { group, .. } => group.as_ref(),
1929 }
1930 }
1931}
1932
1933#[derive(Debug, Clone, PartialEq, Eq)]
1935pub enum DependencyType {
1936 Production,
1938 Dev,
1940 Optional(ExtraName),
1942 Group(GroupName),
1944}
1945
1946impl DependencyType {
1947 pub fn toml_table_name(&self) -> Cow<'_, str> {
1949 match self {
1950 Self::Production => Cow::Borrowed("`project.dependencies`"),
1951 Self::Dev => {
1952 Cow::Borrowed("`tool.uv.dev-dependencies` or `tool.uv.dependency-groups.dev`")
1953 }
1954 Self::Optional(extra) => Cow::Owned(format!("`project.optional-dependencies.{extra}`")),
1955 Self::Group(group) => Cow::Owned(format!("`dependency-groups.{group}`")),
1956 }
1957 }
1958}
1959
1960#[derive(Debug, Clone, PartialEq, Eq)]
1961#[cfg_attr(test, derive(Serialize))]
1962pub(crate) struct BuildBackendSettingsSchema;
1963
1964impl<'de> Deserialize<'de> for BuildBackendSettingsSchema {
1965 fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
1966 where
1967 D: Deserializer<'de>,
1968 {
1969 Ok(Self)
1970 }
1971}
1972
1973#[cfg(feature = "schemars")]
1974impl schemars::JsonSchema for BuildBackendSettingsSchema {
1975 fn schema_name() -> Cow<'static, str> {
1976 BuildBackendSettings::schema_name()
1977 }
1978
1979 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
1980 BuildBackendSettings::json_schema(generator)
1981 }
1982}
1983
1984impl OptionsMetadata for BuildBackendSettingsSchema {
1985 fn record(visit: &mut dyn Visit) {
1986 BuildBackendSettings::record(visit);
1987 }
1988
1989 fn documentation() -> Option<&'static str> {
1990 BuildBackendSettings::documentation()
1991 }
1992
1993 fn metadata() -> OptionSet
1994 where
1995 Self: Sized + 'static,
1996 {
1997 BuildBackendSettings::metadata()
1998 }
1999}