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