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