1use std::fmt::{Display, Formatter};
2use std::io;
3use std::path::{Path, PathBuf};
4use std::str::FromStr;
5
6use thiserror::Error;
7use uv_cache_key::{CacheKey, CacheKeyHasher};
8use uv_distribution_filename::DistExtension;
9use uv_fs::{CWD, PortablePath, PortablePathBuf, normalize_path, try_relative_to_if};
10use uv_git_types::{GitLfs, GitOid, GitReference, GitUrl, GitUrlParseError, OidParseError};
11use uv_normalize::{ExtraName, GroupName, PackageName};
12use uv_pep440::VersionSpecifiers;
13use uv_pep508::{
14 MarkerEnvironment, MarkerTree, RequirementOrigin, VerbatimUrl, VersionOrUrl, marker,
15};
16use uv_redacted::{DisplaySafeUrl, DisplaySafeUrlError};
17
18use crate::{IndexMetadata, IndexUrl};
19
20use uv_pypi_types::{
21 ConflictItem, Hashes, ParsedArchiveUrl, ParsedDirectoryUrl, ParsedGitDirectoryUrl,
22 ParsedGitPathUrl, ParsedPathUrl, ParsedUrl, ParsedUrlError, VerbatimParsedUrl,
23};
24
25#[derive(Debug, Error)]
26enum RequirementError {
27 #[error(transparent)]
28 VerbatimUrlError(#[from] uv_pep508::VerbatimUrlError),
29 #[error(transparent)]
30 ParsedUrlError(#[from] ParsedUrlError),
31 #[error(transparent)]
32 UrlParseError(#[from] DisplaySafeUrlError),
33 #[error(transparent)]
34 OidParseError(#[from] OidParseError),
35 #[error(transparent)]
36 GitUrlParse(#[from] GitUrlParseError),
37}
38
39#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
48pub struct Requirement {
49 pub name: PackageName,
50 #[serde(skip_serializing_if = "<[ExtraName]>::is_empty", default)]
51 pub extras: Box<[ExtraName]>,
52 #[serde(skip_serializing_if = "<[GroupName]>::is_empty", default)]
53 pub groups: Box<[GroupName]>,
54 #[serde(
55 skip_serializing_if = "marker::ser::is_empty",
56 serialize_with = "marker::ser::serialize",
57 default
58 )]
59 pub marker: MarkerTree,
60 #[serde(flatten)]
61 pub source: RequirementSource,
62 #[serde(skip)]
63 pub origin: Option<RequirementOrigin>,
64}
65
66impl Requirement {
67 pub fn evaluate_markers(&self, env: Option<&MarkerEnvironment>, extras: &[ExtraName]) -> bool {
73 self.marker.evaluate_optional_environment(env, extras)
74 }
75
76 pub fn relative_to(self, path: &Path) -> Result<Self, io::Error> {
78 Ok(Self {
79 source: self.source.relative_to(path)?,
80 ..self
81 })
82 }
83
84 #[must_use]
86 pub fn to_absolute(self, path: &Path) -> Self {
87 Self {
88 source: self.source.into_absolute(path),
89 ..self
90 }
91 }
92
93 pub fn hashes(&self) -> Option<Hashes> {
95 let (RequirementSource::Url { ref url, .. } | RequirementSource::Path { ref url, .. }) =
96 self.source
97 else {
98 return None;
99 };
100 let fragment = url.fragment()?;
101 fragment
102 .split('&')
103 .find_map(|fragment| Hashes::parse_fragment(fragment).ok())
104 }
105
106 #[must_use]
108 pub fn with_origin(self, origin: RequirementOrigin) -> Self {
109 Self {
110 origin: Some(origin),
111 ..self
112 }
113 }
114}
115
116impl std::hash::Hash for Requirement {
117 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
118 let Self {
119 name,
120 extras,
121 groups,
122 marker,
123 source,
124 origin: _,
125 } = self;
126 name.hash(state);
127 extras.hash(state);
128 groups.hash(state);
129 marker.hash(state);
130 source.hash(state);
131 }
132}
133
134impl PartialEq for Requirement {
135 fn eq(&self, other: &Self) -> bool {
136 let Self {
137 name,
138 extras,
139 groups,
140 marker,
141 source,
142 origin: _,
143 } = self;
144 let Self {
145 name: other_name,
146 extras: other_extras,
147 groups: other_groups,
148 marker: other_marker,
149 source: other_source,
150 origin: _,
151 } = other;
152 name == other_name
153 && extras == other_extras
154 && groups == other_groups
155 && marker == other_marker
156 && source == other_source
157 }
158}
159
160impl Eq for Requirement {}
161
162impl Ord for Requirement {
163 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
164 let Self {
165 name,
166 extras,
167 groups,
168 marker,
169 source,
170 origin: _,
171 } = self;
172 let Self {
173 name: other_name,
174 extras: other_extras,
175 groups: other_groups,
176 marker: other_marker,
177 source: other_source,
178 origin: _,
179 } = other;
180 name.cmp(other_name)
181 .then_with(|| extras.cmp(other_extras))
182 .then_with(|| groups.cmp(other_groups))
183 .then_with(|| marker.cmp(other_marker))
184 .then_with(|| source.cmp(other_source))
185 }
186}
187
188impl PartialOrd for Requirement {
189 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
190 Some(self.cmp(other))
191 }
192}
193
194impl From<Requirement> for uv_pep508::Requirement<VerbatimUrl> {
195 fn from(requirement: Requirement) -> Self {
197 Self {
198 name: requirement.name,
199 extras: requirement.extras,
200 marker: requirement.marker,
201 origin: requirement.origin,
202 version_or_url: match requirement.source {
203 RequirementSource::Registry { specifier, .. } => {
204 Some(VersionOrUrl::VersionSpecifier(specifier))
205 }
206 RequirementSource::Url { url, .. }
207 | RequirementSource::GitPath { url, .. }
208 | RequirementSource::GitDirectory { url, .. }
209 | RequirementSource::Path { url, .. }
210 | RequirementSource::Directory { url, .. } => Some(VersionOrUrl::Url(url)),
211 },
212 }
213 }
214}
215
216impl From<Requirement> for uv_pep508::Requirement<VerbatimParsedUrl> {
217 fn from(requirement: Requirement) -> Self {
219 Self {
220 name: requirement.name,
221 extras: requirement.extras,
222 marker: requirement.marker,
223 origin: requirement.origin,
224 version_or_url: match requirement.source {
225 RequirementSource::Registry { specifier, .. } => {
226 Some(VersionOrUrl::VersionSpecifier(specifier))
227 }
228 RequirementSource::Url {
229 location,
230 subdirectory,
231 ext,
232 url,
233 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
234 parsed_url: ParsedUrl::Archive(ParsedArchiveUrl {
235 url: location,
236 subdirectory,
237 ext,
238 }),
239 verbatim: url,
240 })),
241 RequirementSource::GitDirectory {
242 git,
243 subdirectory,
244 url,
245 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
246 parsed_url: ParsedUrl::GitDirectory(ParsedGitDirectoryUrl {
247 url: git,
248 subdirectory,
249 }),
250 verbatim: url,
251 })),
252 RequirementSource::GitPath {
253 git,
254 install_path,
255 ext,
256 url,
257 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
258 parsed_url: ParsedUrl::GitPath(ParsedGitPathUrl {
259 url: git,
260 install_path,
261 ext,
262 }),
263 verbatim: url,
264 })),
265 RequirementSource::Path {
266 install_path,
267 ext,
268 url,
269 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
270 parsed_url: ParsedUrl::Path(ParsedPathUrl {
271 url: url.to_url(),
272 install_path,
273 ext,
274 }),
275 verbatim: url,
276 })),
277 RequirementSource::Directory {
278 install_path,
279 editable,
280 r#virtual,
281 url,
282 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
283 parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl {
284 url: url.to_url(),
285 install_path,
286 editable,
287 r#virtual,
288 }),
289 verbatim: url,
290 })),
291 },
292 }
293 }
294}
295
296impl From<uv_pep508::Requirement<VerbatimParsedUrl>> for Requirement {
297 fn from(requirement: uv_pep508::Requirement<VerbatimParsedUrl>) -> Self {
299 let source = match requirement.version_or_url {
300 None => RequirementSource::Registry {
301 specifier: VersionSpecifiers::empty(),
302 index: None,
303 conflict: None,
304 },
305 Some(VersionOrUrl::VersionSpecifier(specifier)) => RequirementSource::Registry {
307 specifier,
308 index: None,
309 conflict: None,
310 },
311 Some(VersionOrUrl::Url(url)) => {
312 RequirementSource::from_parsed_url(url.parsed_url, url.verbatim)
313 }
314 };
315 Self {
316 name: requirement.name,
317 groups: Box::new([]),
318 extras: requirement.extras,
319 marker: requirement.marker,
320 source,
321 origin: requirement.origin,
322 }
323 }
324}
325
326impl Display for Requirement {
327 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
330 write!(f, "{}", self.name)?;
331 if !self.extras.is_empty() {
332 write!(
333 f,
334 "[{}]",
335 self.extras
336 .iter()
337 .map(ToString::to_string)
338 .collect::<Vec<_>>()
339 .join(",")
340 )?;
341 }
342 match &self.source {
343 RequirementSource::Registry {
344 specifier, index, ..
345 } => {
346 write!(f, "{specifier}")?;
347 if let Some(index) = index {
348 write!(f, " (index: {})", index.url)?;
349 }
350 }
351 RequirementSource::Url { url, .. } => {
352 write!(f, " @ {url}")?;
353 }
354 RequirementSource::GitDirectory {
355 url: _,
356 git,
357 subdirectory,
358 } => {
359 write!(f, " @ git+{}", git.url())?;
360 if let Some(reference) = git.reference().as_url_rev() {
361 write!(f, "@{reference}")?;
362 }
363 if let Some(subdirectory) = subdirectory {
364 writeln!(f, "#subdirectory={}", subdirectory.display())?;
365 }
366 if git.lfs().enabled() {
367 writeln!(
368 f,
369 "{}lfs=true",
370 if subdirectory.is_some() { "&" } else { "#" }
371 )?;
372 }
373 }
374 RequirementSource::GitPath {
375 url: _,
376 git,
377 install_path,
378 ext: _,
379 } => {
380 write!(f, " @ git+{}", git.url())?;
381 if let Some(reference) = git.reference().as_url_rev() {
382 write!(f, "@{reference}")?;
383 }
384 write!(f, "#path={}", install_path.display())?;
385 if git.lfs().enabled() {
386 write!(f, "&lfs=true")?;
387 }
388 writeln!(f)?;
389 }
390 RequirementSource::Path { url, .. } => {
391 write!(f, " @ {url}")?;
392 }
393 RequirementSource::Directory { url, .. } => {
394 write!(f, " @ {url}")?;
395 }
396 }
397 if let Some(marker) = self.marker.contents() {
398 write!(f, " ; {marker}")?;
399 }
400 Ok(())
401 }
402}
403
404impl CacheKey for Requirement {
405 fn cache_key(&self, state: &mut CacheKeyHasher) {
406 self.name.as_str().cache_key(state);
407
408 self.groups.len().cache_key(state);
409 for group in &self.groups {
410 group.as_str().cache_key(state);
411 }
412
413 self.extras.len().cache_key(state);
414 for extra in &self.extras {
415 extra.as_str().cache_key(state);
416 }
417
418 if let Some(marker) = self.marker.contents() {
419 1u8.cache_key(state);
420 marker.to_string().cache_key(state);
421 } else {
422 0u8.cache_key(state);
423 }
424
425 match &self.source {
426 RequirementSource::Registry {
427 specifier,
428 index,
429 conflict: _,
430 } => {
431 0u8.cache_key(state);
432 specifier.len().cache_key(state);
433 for spec in specifier.iter() {
434 spec.operator().as_str().cache_key(state);
435 spec.version().cache_key(state);
436 }
437 if let Some(index) = index {
438 1u8.cache_key(state);
439 index.url.cache_key(state);
440 } else {
441 0u8.cache_key(state);
442 }
443 }
445 RequirementSource::Url {
446 location,
447 subdirectory,
448 ext,
449 url,
450 } => {
451 1u8.cache_key(state);
452 location.cache_key(state);
453 if let Some(subdirectory) = subdirectory {
454 1u8.cache_key(state);
455 subdirectory.display().to_string().cache_key(state);
456 } else {
457 0u8.cache_key(state);
458 }
459 ext.name().cache_key(state);
460 url.cache_key(state);
461 }
462 RequirementSource::GitDirectory {
463 git,
464 subdirectory,
465 url,
466 } => {
467 2u8.cache_key(state);
468 git.to_string().cache_key(state);
469 if let Some(subdirectory) = subdirectory {
470 1u8.cache_key(state);
471 subdirectory.display().to_string().cache_key(state);
472 } else {
473 0u8.cache_key(state);
474 }
475 if git.lfs().enabled() {
476 1u8.cache_key(state);
477 }
478 url.cache_key(state);
479 }
480 RequirementSource::GitPath {
481 git,
482 install_path,
483 ext,
484 url,
485 } => {
486 5u8.cache_key(state);
487 git.to_string().cache_key(state);
488 install_path.cache_key(state);
489 ext.name().cache_key(state);
490 if git.lfs().enabled() {
491 1u8.cache_key(state);
492 }
493 url.cache_key(state);
494 }
495 RequirementSource::Path {
496 install_path,
497 ext,
498 url,
499 } => {
500 3u8.cache_key(state);
501 install_path.cache_key(state);
502 ext.name().cache_key(state);
503 url.cache_key(state);
504 }
505 RequirementSource::Directory {
506 install_path,
507 editable,
508 r#virtual,
509 url,
510 } => {
511 4u8.cache_key(state);
512 install_path.cache_key(state);
513 editable.cache_key(state);
514 r#virtual.cache_key(state);
515 url.cache_key(state);
516 }
517 }
518
519 }
521}
522
523#[derive(
530 Hash, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
531)]
532#[serde(try_from = "RequirementSourceWire", into = "RequirementSourceWire")]
533pub enum RequirementSource {
534 Registry {
536 specifier: VersionSpecifiers,
537 index: Option<IndexMetadata>,
539 conflict: Option<ConflictItem>,
541 },
542 Url {
548 location: DisplaySafeUrl,
550 subdirectory: Option<Box<Path>>,
553 ext: DistExtension,
555 url: VerbatimUrl,
558 },
559 GitDirectory {
561 git: GitUrl,
563 subdirectory: Option<Box<Path>>,
565 url: VerbatimUrl,
568 },
569 GitPath {
571 git: GitUrl,
573 install_path: PathBuf,
575 ext: DistExtension,
577 url: VerbatimUrl,
580 },
581 Path {
585 install_path: Box<Path>,
587 ext: DistExtension,
589 url: VerbatimUrl,
592 },
593 Directory {
596 install_path: Box<Path>,
598 editable: Option<bool>,
600 r#virtual: Option<bool>,
602 url: VerbatimUrl,
605 },
606}
607
608impl RequirementSource {
609 pub(crate) fn from_parsed_url(parsed_url: ParsedUrl, url: VerbatimUrl) -> Self {
612 match parsed_url {
613 ParsedUrl::Path(local_file) => Self::Path {
614 install_path: local_file.install_path.clone(),
615 ext: local_file.ext,
616 url,
617 },
618 ParsedUrl::Directory(directory) => Self::Directory {
619 install_path: directory.install_path.clone(),
620 editable: directory.editable,
621 r#virtual: directory.r#virtual,
622 url,
623 },
624 ParsedUrl::GitDirectory(git) => Self::GitDirectory {
625 url,
626 git: git.url,
627 subdirectory: git.subdirectory,
628 },
629 ParsedUrl::GitPath(git) => Self::GitPath {
630 url,
631 git: git.url,
632 install_path: git.install_path.clone(),
633 ext: git.ext,
634 },
635 ParsedUrl::Archive(archive) => Self::Url {
636 url,
637 location: archive.url,
638 subdirectory: archive.subdirectory,
639 ext: archive.ext,
640 },
641 }
642 }
643
644 pub fn to_verbatim_parsed_url(&self) -> Option<VerbatimParsedUrl> {
646 match self {
647 Self::Registry { .. } => None,
648 Self::Url {
649 location,
650 subdirectory,
651 ext,
652 url,
653 } => Some(VerbatimParsedUrl {
654 parsed_url: ParsedUrl::Archive(ParsedArchiveUrl::from_source(
655 location.clone(),
656 subdirectory.clone(),
657 *ext,
658 )),
659 verbatim: url.clone(),
660 }),
661 Self::Path {
662 install_path,
663 ext,
664 url,
665 } => Some(VerbatimParsedUrl {
666 parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source(
667 install_path.clone(),
668 *ext,
669 url.to_url(),
670 )),
671 verbatim: url.clone(),
672 }),
673 Self::Directory {
674 install_path,
675 editable,
676 r#virtual,
677 url,
678 } => Some(VerbatimParsedUrl {
679 parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl::from_source(
680 install_path.clone(),
681 *editable,
682 *r#virtual,
683 url.to_url(),
684 )),
685 verbatim: url.clone(),
686 }),
687 Self::GitDirectory {
688 git,
689 subdirectory,
690 url,
691 } => Some(VerbatimParsedUrl {
692 parsed_url: ParsedUrl::GitDirectory(ParsedGitDirectoryUrl::from_source(
693 git.clone(),
694 subdirectory.clone(),
695 )),
696 verbatim: url.clone(),
697 }),
698 Self::GitPath {
699 git,
700 install_path,
701 ext,
702 url,
703 } => Some(VerbatimParsedUrl {
704 parsed_url: ParsedUrl::GitPath(ParsedGitPathUrl::from_source(
705 git.clone(),
706 install_path.clone(),
707 *ext,
708 )),
709 verbatim: url.clone(),
710 }),
711 }
712 }
713
714 pub fn is_empty(&self) -> bool {
716 match self {
717 Self::Registry { specifier, .. } => specifier.is_empty(),
718 Self::Url { .. }
719 | Self::GitPath { .. }
720 | Self::GitDirectory { .. }
721 | Self::Path { .. }
722 | Self::Directory { .. } => false,
723 }
724 }
725
726 pub fn version_specifiers(&self) -> Option<&VersionSpecifiers> {
728 match self {
729 Self::Registry { specifier, .. } => Some(specifier),
730 Self::Url { .. }
731 | Self::GitPath { .. }
732 | Self::GitDirectory { .. }
733 | Self::Path { .. }
734 | Self::Directory { .. } => None,
735 }
736 }
737
738 fn relative_to(self, path: &Path) -> Result<Self, io::Error> {
740 match self {
741 Self::Registry { .. }
742 | Self::Url { .. }
743 | Self::GitPath { .. }
744 | Self::GitDirectory { .. } => Ok(self),
745 Self::Path {
746 install_path,
747 ext,
748 url,
749 } => Ok(Self::Path {
750 install_path: try_relative_to_if(&install_path, path, !url.was_given_absolute())?
751 .into_boxed_path(),
752 ext,
753 url,
754 }),
755 Self::Directory {
756 install_path,
757 editable,
758 r#virtual,
759 url,
760 ..
761 } => Ok(Self::Directory {
762 install_path: try_relative_to_if(&install_path, path, !url.was_given_absolute())?
763 .into_boxed_path(),
764 editable,
765 r#virtual,
766 url,
767 }),
768 }
769 }
770
771 #[must_use]
773 fn into_absolute(self, root: &Path) -> Self {
774 match self {
775 Self::Registry { .. }
776 | Self::Url { .. }
777 | Self::GitPath { .. }
778 | Self::GitDirectory { .. } => self,
779 Self::Path {
780 install_path,
781 ext,
782 url,
783 } => Self::Path {
784 install_path: normalize_path(root.join(install_path))
785 .into_owned()
786 .into_boxed_path(),
787 ext,
788 url,
789 },
790 Self::Directory {
791 install_path,
792 editable,
793 r#virtual,
794 url,
795 ..
796 } => Self::Directory {
797 install_path: normalize_path(root.join(install_path))
798 .into_owned()
799 .into_boxed_path(),
800 editable,
801 r#virtual,
802 url,
803 },
804 }
805 }
806}
807
808impl Display for RequirementSource {
809 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
812 match self {
813 Self::Registry {
814 specifier, index, ..
815 } => {
816 write!(f, "{specifier}")?;
817 if let Some(index) = index {
818 write!(f, " (index: {})", index.url)?;
819 }
820 }
821 Self::Url { url, .. } => {
822 write!(f, " {url}")?;
823 }
824 Self::GitDirectory {
825 url: _,
826 git,
827 subdirectory,
828 } => {
829 write!(f, " git+{}", git.url())?;
830 if let Some(reference) = git.reference().as_url_rev() {
831 write!(f, "@{reference}")?;
832 }
833 if let Some(subdirectory) = subdirectory {
834 writeln!(f, "#subdirectory={}", subdirectory.display())?;
835 }
836 if git.lfs().enabled() {
837 writeln!(
838 f,
839 "{}lfs=true",
840 if subdirectory.is_some() { "&" } else { "#" }
841 )?;
842 }
843 }
844 Self::GitPath {
845 url: _,
846 git,
847 install_path,
848 ext: _,
849 } => {
850 write!(f, " git+{}", git.url())?;
851 if let Some(reference) = git.reference().as_url_rev() {
852 write!(f, "@{reference}")?;
853 }
854 write!(f, "#path={}", install_path.display())?;
855 if git.lfs().enabled() {
856 write!(f, "&lfs=true")?;
857 }
858 writeln!(f)?;
859 }
860 Self::Path { url, .. } => {
861 write!(f, "{url}")?;
862 }
863 Self::Directory { url, .. } => {
864 write!(f, "{url}")?;
865 }
866 }
867 Ok(())
868 }
869}
870
871#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
872#[serde(untagged)]
873enum RequirementSourceWire {
874 Git { git: String },
876 Direct {
878 url: DisplaySafeUrl,
879 subdirectory: Option<PortablePathBuf>,
880 },
881 Path { path: PortablePathBuf },
883 Directory { directory: PortablePathBuf },
885 Editable { editable: PortablePathBuf },
887 Virtual { r#virtual: PortablePathBuf },
889 Registry {
891 #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)]
892 specifier: VersionSpecifiers,
893 index: Option<DisplaySafeUrl>,
894 conflict: Option<ConflictItem>,
895 },
896}
897
898impl From<RequirementSource> for RequirementSourceWire {
899 fn from(value: RequirementSource) -> Self {
900 match value {
901 RequirementSource::Registry {
902 specifier,
903 index,
904 conflict,
905 } => {
906 let index = index.map(|index| index.url.into_url()).map(|mut index| {
907 index.remove_credentials();
908 index
909 });
910 Self::Registry {
911 specifier,
912 index,
913 conflict,
914 }
915 }
916 RequirementSource::Url {
917 subdirectory,
918 location,
919 ext: _,
920 url: _,
921 } => Self::Direct {
922 url: location,
923 subdirectory: subdirectory.map(PortablePathBuf::from),
924 },
925 RequirementSource::GitDirectory {
926 git,
927 subdirectory,
928 url: _,
929 } => {
930 let mut url = git.url().clone();
931
932 url.remove_credentials();
934
935 url.set_fragment(None);
937 url.set_query(None);
938
939 if let Some(subdirectory) = subdirectory
941 .as_deref()
942 .map(PortablePath::from)
943 .as_ref()
944 .map(PortablePath::to_string)
945 {
946 url.query_pairs_mut()
947 .append_pair("subdirectory", &subdirectory);
948 }
949
950 if git.lfs().enabled() {
952 url.query_pairs_mut().append_pair("lfs", "true");
953 }
954
955 match git.reference() {
957 GitReference::Branch(branch) => {
958 url.query_pairs_mut().append_pair("branch", branch.as_str());
959 }
960 GitReference::Tag(tag) => {
961 url.query_pairs_mut().append_pair("tag", tag.as_str());
962 }
963 GitReference::BranchOrTag(rev)
964 | GitReference::BranchOrTagOrCommit(rev)
965 | GitReference::NamedRef(rev) => {
966 url.query_pairs_mut().append_pair("rev", rev.as_str());
967 }
968 GitReference::DefaultBranch => {}
969 }
970
971 if let Some(precise) = git.precise() {
973 url.set_fragment(Some(&precise.to_string()));
974 }
975
976 Self::Git {
977 git: url.to_string(),
978 }
979 }
980 RequirementSource::GitPath {
981 git,
982 install_path,
983 ext: _,
984 url: _,
985 } => {
986 let mut url = git.url().clone();
987
988 url.remove_credentials();
990
991 url.set_fragment(None);
993 url.set_query(None);
994
995 if let Some(install_path) = install_path.to_str() {
997 url.query_pairs_mut().append_pair("path", install_path);
998 }
999
1000 match git.reference() {
1002 GitReference::Branch(branch) => {
1003 url.query_pairs_mut().append_pair("branch", branch.as_str());
1004 }
1005 GitReference::Tag(tag) => {
1006 url.query_pairs_mut().append_pair("tag", tag.as_str());
1007 }
1008 GitReference::BranchOrTag(rev)
1009 | GitReference::BranchOrTagOrCommit(rev)
1010 | GitReference::NamedRef(rev) => {
1011 url.query_pairs_mut().append_pair("rev", rev.as_str());
1012 }
1013 GitReference::DefaultBranch => {}
1014 }
1015
1016 if git.lfs().enabled() {
1018 url.query_pairs_mut().append_pair("lfs", "true");
1019 }
1020
1021 if let Some(precise) = git.precise() {
1023 url.set_fragment(Some(&precise.to_string()));
1024 }
1025
1026 Self::Git {
1027 git: url.to_string(),
1028 }
1029 }
1030 RequirementSource::Path {
1031 install_path,
1032 ext: _,
1033 url: _,
1034 } => Self::Path {
1035 path: PortablePathBuf::from(install_path),
1036 },
1037 RequirementSource::Directory {
1038 install_path,
1039 editable,
1040 r#virtual,
1041 url: _,
1042 } => {
1043 if editable.unwrap_or(false) {
1044 Self::Editable {
1045 editable: PortablePathBuf::from(install_path),
1046 }
1047 } else if r#virtual.unwrap_or(false) {
1048 Self::Virtual {
1049 r#virtual: PortablePathBuf::from(install_path),
1050 }
1051 } else {
1052 Self::Directory {
1053 directory: PortablePathBuf::from(install_path),
1054 }
1055 }
1056 }
1057 }
1058 }
1059}
1060
1061impl TryFrom<RequirementSourceWire> for RequirementSource {
1062 type Error = RequirementError;
1063
1064 fn try_from(wire: RequirementSourceWire) -> Result<Self, RequirementError> {
1065 match wire {
1066 RequirementSourceWire::Registry {
1067 specifier,
1068 index,
1069 conflict,
1070 } => Ok(Self::Registry {
1071 specifier,
1072 index: index
1073 .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index)))),
1074 conflict,
1075 }),
1076 RequirementSourceWire::Git { git } => {
1077 let mut repository = DisplaySafeUrl::parse(&git)?;
1078
1079 let mut reference = GitReference::DefaultBranch;
1080 let mut subdirectory: Option<PortablePathBuf> = None;
1081 let mut lfs = GitLfs::Disabled;
1082 let mut path: Option<PortablePathBuf> = None;
1083 for (key, val) in repository.query_pairs() {
1084 match &*key {
1085 "tag" => reference = GitReference::Tag(val.into_owned()),
1086 "branch" => reference = GitReference::Branch(val.into_owned()),
1087 "rev" => reference = GitReference::from_rev(val.into_owned()),
1088 "subdirectory" => {
1089 subdirectory = Some(PortablePathBuf::from(val.as_ref()));
1090 }
1091 "lfs" => lfs = GitLfs::from(val.eq_ignore_ascii_case("true")),
1092 "path" => {
1093 path = Some(PortablePathBuf::from(val.as_ref()));
1094 }
1095 _ => {}
1096 }
1097 }
1098
1099 let precise = repository.fragment().map(GitOid::from_str).transpose()?;
1100
1101 repository.set_fragment(None);
1103 repository.set_query(None);
1104
1105 repository.remove_credentials();
1107
1108 let mut url = DisplaySafeUrl::parse(&format!("git+{repository}"))?;
1110 if let Some(rev) = reference.as_url_rev() {
1111 let path = format!("{}@{}", url.path(), rev);
1112 url.set_path(&path);
1113 }
1114 let mut frags: Vec<String> = Vec::new();
1115 if let Some(subdirectory) = subdirectory.as_ref() {
1116 frags.push(format!("subdirectory={subdirectory}"));
1117 }
1118 if lfs.enabled() {
1120 frags.push("lfs=true".to_string());
1121 }
1122 if let Some(path) = path.as_ref() {
1123 frags.push(format!("path={path}"));
1124 }
1125 if !frags.is_empty() {
1126 url.set_fragment(Some(&frags.join("&")));
1127 }
1128 let url = VerbatimUrl::from_url(url);
1129 let git = GitUrl::from_fields(repository, reference, precise, lfs)?;
1130
1131 if let Some(install_path) = path.map(Box::<Path>::from).map(PathBuf::from) {
1132 Ok(Self::GitPath {
1133 git,
1134 ext: DistExtension::from_path(install_path.as_path()).map_err(|err| {
1135 ParsedUrlError::MissingExtensionPath(install_path.clone(), err)
1136 })?,
1137 install_path,
1138 url,
1139 })
1140 } else {
1141 Ok(Self::GitDirectory {
1142 git,
1143 subdirectory: subdirectory.map(Box::<Path>::from),
1144 url,
1145 })
1146 }
1147 }
1148 RequirementSourceWire::Direct { url, subdirectory } => {
1149 let location = url.clone();
1150
1151 let mut url = url;
1153 if let Some(subdirectory) = &subdirectory {
1154 url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
1155 }
1156
1157 Ok(Self::Url {
1158 location,
1159 subdirectory: subdirectory.map(Box::<Path>::from),
1160 ext: DistExtension::from_path(url.path())
1161 .map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?,
1162 url: VerbatimUrl::from_url(url),
1163 })
1164 }
1165 RequirementSourceWire::Path { path } => {
1170 let path = Box::<Path>::from(path);
1171 let url = VerbatimUrl::from_normalized_path(normalize_path(CWD.join(&path)))?;
1172 Ok(Self::Path {
1173 ext: DistExtension::from_path(&path).map_err(|err| {
1174 ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err)
1175 })?,
1176 install_path: path,
1177 url,
1178 })
1179 }
1180 RequirementSourceWire::Directory { directory } => {
1181 let directory = Box::<Path>::from(directory);
1182 let url = VerbatimUrl::from_normalized_path(normalize_path(CWD.join(&directory)))?;
1183 Ok(Self::Directory {
1184 install_path: directory,
1185 editable: Some(false),
1186 r#virtual: Some(false),
1187 url,
1188 })
1189 }
1190 RequirementSourceWire::Editable { editable } => {
1191 let editable = Box::<Path>::from(editable);
1192 let url = VerbatimUrl::from_normalized_path(normalize_path(CWD.join(&editable)))?;
1193 Ok(Self::Directory {
1194 install_path: editable,
1195 editable: Some(true),
1196 r#virtual: Some(false),
1197 url,
1198 })
1199 }
1200 RequirementSourceWire::Virtual { r#virtual } => {
1201 let r#virtual = Box::<Path>::from(r#virtual);
1202 let url = VerbatimUrl::from_normalized_path(normalize_path(CWD.join(&r#virtual)))?;
1203 Ok(Self::Directory {
1204 install_path: r#virtual,
1205 editable: Some(false),
1206 r#virtual: Some(true),
1207 url,
1208 })
1209 }
1210 }
1211 }
1212}
1213
1214#[cfg(test)]
1215mod tests {
1216 use std::path::PathBuf;
1217
1218 use uv_pep508::{MarkerTree, VerbatimUrl};
1219
1220 use crate::{Requirement, RequirementSource};
1221
1222 #[test]
1223 fn roundtrip() {
1224 let requirement = Requirement {
1225 name: "foo".parse().unwrap(),
1226 extras: Box::new([]),
1227 groups: Box::new([]),
1228 marker: MarkerTree::TRUE,
1229 source: RequirementSource::Registry {
1230 specifier: ">1,<2".parse().unwrap(),
1231 index: None,
1232 conflict: None,
1233 },
1234 origin: None,
1235 };
1236
1237 let raw = toml::to_string(&requirement).unwrap();
1238 let deserialized: Requirement = toml::from_str(&raw).unwrap();
1239 assert_eq!(requirement, deserialized);
1240
1241 let path = if cfg!(windows) {
1242 "C:\\home\\ferris\\foo"
1243 } else {
1244 "/home/ferris/foo"
1245 };
1246 let requirement = Requirement {
1247 name: "foo".parse().unwrap(),
1248 extras: Box::new([]),
1249 groups: Box::new([]),
1250 marker: MarkerTree::TRUE,
1251 source: RequirementSource::Directory {
1252 install_path: PathBuf::from(path).into_boxed_path(),
1253 editable: Some(false),
1254 r#virtual: Some(false),
1255 url: VerbatimUrl::from_absolute_path(path).unwrap(),
1256 },
1257 origin: None,
1258 };
1259
1260 let raw = toml::to_string(&requirement).unwrap();
1261 let deserialized: Requirement = toml::from_str(&raw).unwrap();
1262 assert_eq!(requirement, deserialized);
1263 }
1264
1265 #[test]
1266 fn display_git_path_lfs() {
1267 let source: RequirementSource = toml::from_str(
1268 r#"git = "https://github.com/astral-sh/archive-in-git-test?lfs=true&path=archives%2Finiconfig-2.0.0-py3-none-any.whl""#,
1269 )
1270 .unwrap();
1271
1272 assert_eq!(
1273 source.to_string(),
1274 " git+https://github.com/astral-sh/archive-in-git-test#path=archives/iniconfig-2.0.0-py3-none-any.whl&lfs=true\n"
1275 );
1276
1277 let requirement = Requirement {
1278 name: "iniconfig".parse().unwrap(),
1279 extras: Box::new([]),
1280 groups: Box::new([]),
1281 marker: MarkerTree::TRUE,
1282 source,
1283 origin: None,
1284 };
1285 assert_eq!(
1286 requirement.to_string(),
1287 "iniconfig @ git+https://github.com/astral-sh/archive-in-git-test#path=archives/iniconfig-2.0.0-py3-none-any.whl&lfs=true\n"
1288 );
1289 }
1290}