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