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