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