1use std::fmt::{Display, Formatter};
2use std::io;
3use std::path::Path;
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, relative_to};
10use uv_git_types::{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, ParsedGitUrl, ParsedPathUrl,
22 ParsedUrl, ParsedUrlError, VerbatimParsedUrl,
23};
24
25#[derive(Debug, Error)]
26pub 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::Git { 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::Git {
242 git,
243 subdirectory,
244 url,
245 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
246 parsed_url: ParsedUrl::Git(ParsedGitUrl {
247 url: git,
248 subdirectory,
249 }),
250 verbatim: url,
251 })),
252 RequirementSource::Path {
253 install_path,
254 ext,
255 url,
256 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
257 parsed_url: ParsedUrl::Path(ParsedPathUrl {
258 url: url.to_url(),
259 install_path,
260 ext,
261 }),
262 verbatim: url,
263 })),
264 RequirementSource::Directory {
265 install_path,
266 editable,
267 r#virtual,
268 url,
269 } => Some(VersionOrUrl::Url(VerbatimParsedUrl {
270 parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl {
271 url: url.to_url(),
272 install_path,
273 editable,
274 r#virtual,
275 }),
276 verbatim: url,
277 })),
278 },
279 }
280 }
281}
282
283impl From<uv_pep508::Requirement<VerbatimParsedUrl>> for Requirement {
284 fn from(requirement: uv_pep508::Requirement<VerbatimParsedUrl>) -> Self {
286 let source = match requirement.version_or_url {
287 None => RequirementSource::Registry {
288 specifier: VersionSpecifiers::empty(),
289 index: None,
290 conflict: None,
291 },
292 Some(VersionOrUrl::VersionSpecifier(specifier)) => RequirementSource::Registry {
294 specifier,
295 index: None,
296 conflict: None,
297 },
298 Some(VersionOrUrl::Url(url)) => {
299 RequirementSource::from_parsed_url(url.parsed_url, url.verbatim)
300 }
301 };
302 Self {
303 name: requirement.name,
304 groups: Box::new([]),
305 extras: requirement.extras,
306 marker: requirement.marker,
307 source,
308 origin: requirement.origin,
309 }
310 }
311}
312
313impl Display for Requirement {
314 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
317 write!(f, "{}", self.name)?;
318 if !self.extras.is_empty() {
319 write!(
320 f,
321 "[{}]",
322 self.extras
323 .iter()
324 .map(ToString::to_string)
325 .collect::<Vec<_>>()
326 .join(",")
327 )?;
328 }
329 match &self.source {
330 RequirementSource::Registry {
331 specifier, index, ..
332 } => {
333 write!(f, "{specifier}")?;
334 if let Some(index) = index {
335 write!(f, " (index: {})", index.url)?;
336 }
337 }
338 RequirementSource::Url { url, .. } => {
339 write!(f, " @ {url}")?;
340 }
341 RequirementSource::Git {
342 url: _,
343 git,
344 subdirectory,
345 } => {
346 write!(f, " @ git+{}", git.repository())?;
347 if let Some(reference) = git.reference().as_str() {
348 write!(f, "@{reference}")?;
349 }
350 if let Some(subdirectory) = subdirectory {
351 writeln!(f, "#subdirectory={}", subdirectory.display())?;
352 }
353 }
354 RequirementSource::Path { url, .. } => {
355 write!(f, " @ {url}")?;
356 }
357 RequirementSource::Directory { url, .. } => {
358 write!(f, " @ {url}")?;
359 }
360 }
361 if let Some(marker) = self.marker.contents() {
362 write!(f, " ; {marker}")?;
363 }
364 Ok(())
365 }
366}
367
368impl CacheKey for Requirement {
369 fn cache_key(&self, state: &mut CacheKeyHasher) {
370 self.name.as_str().cache_key(state);
371
372 self.groups.len().cache_key(state);
373 for group in &self.groups {
374 group.as_str().cache_key(state);
375 }
376
377 self.extras.len().cache_key(state);
378 for extra in &self.extras {
379 extra.as_str().cache_key(state);
380 }
381
382 if let Some(marker) = self.marker.contents() {
383 1u8.cache_key(state);
384 marker.to_string().cache_key(state);
385 } else {
386 0u8.cache_key(state);
387 }
388
389 match &self.source {
390 RequirementSource::Registry {
391 specifier,
392 index,
393 conflict: _,
394 } => {
395 0u8.cache_key(state);
396 specifier.len().cache_key(state);
397 for spec in specifier.iter() {
398 spec.operator().as_str().cache_key(state);
399 spec.version().cache_key(state);
400 }
401 if let Some(index) = index {
402 1u8.cache_key(state);
403 index.url.cache_key(state);
404 } else {
405 0u8.cache_key(state);
406 }
407 }
409 RequirementSource::Url {
410 location,
411 subdirectory,
412 ext,
413 url,
414 } => {
415 1u8.cache_key(state);
416 location.cache_key(state);
417 if let Some(subdirectory) = subdirectory {
418 1u8.cache_key(state);
419 subdirectory.display().to_string().cache_key(state);
420 } else {
421 0u8.cache_key(state);
422 }
423 ext.name().cache_key(state);
424 url.cache_key(state);
425 }
426 RequirementSource::Git {
427 git,
428 subdirectory,
429 url,
430 } => {
431 2u8.cache_key(state);
432 git.to_string().cache_key(state);
433 if let Some(subdirectory) = subdirectory {
434 1u8.cache_key(state);
435 subdirectory.display().to_string().cache_key(state);
436 } else {
437 0u8.cache_key(state);
438 }
439 url.cache_key(state);
440 }
441 RequirementSource::Path {
442 install_path,
443 ext,
444 url,
445 } => {
446 3u8.cache_key(state);
447 install_path.cache_key(state);
448 ext.name().cache_key(state);
449 url.cache_key(state);
450 }
451 RequirementSource::Directory {
452 install_path,
453 editable,
454 r#virtual,
455 url,
456 } => {
457 4u8.cache_key(state);
458 install_path.cache_key(state);
459 editable.cache_key(state);
460 r#virtual.cache_key(state);
461 url.cache_key(state);
462 }
463 }
464
465 }
467}
468
469#[derive(
476 Hash, Debug, Clone, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, serde::Deserialize,
477)]
478#[serde(try_from = "RequirementSourceWire", into = "RequirementSourceWire")]
479pub enum RequirementSource {
480 Registry {
482 specifier: VersionSpecifiers,
483 index: Option<IndexMetadata>,
485 conflict: Option<ConflictItem>,
487 },
488 Url {
494 location: DisplaySafeUrl,
496 subdirectory: Option<Box<Path>>,
499 ext: DistExtension,
501 url: VerbatimUrl,
504 },
505 Git {
507 git: GitUrl,
509 subdirectory: Option<Box<Path>>,
511 url: VerbatimUrl,
514 },
515 Path {
519 install_path: Box<Path>,
521 ext: DistExtension,
523 url: VerbatimUrl,
526 },
527 Directory {
530 install_path: Box<Path>,
532 editable: Option<bool>,
534 r#virtual: Option<bool>,
536 url: VerbatimUrl,
539 },
540}
541
542impl RequirementSource {
543 pub fn from_parsed_url(parsed_url: ParsedUrl, url: VerbatimUrl) -> Self {
546 match parsed_url {
547 ParsedUrl::Path(local_file) => Self::Path {
548 install_path: local_file.install_path.clone(),
549 ext: local_file.ext,
550 url,
551 },
552 ParsedUrl::Directory(directory) => Self::Directory {
553 install_path: directory.install_path.clone(),
554 editable: directory.editable,
555 r#virtual: directory.r#virtual,
556 url,
557 },
558 ParsedUrl::Git(git) => Self::Git {
559 git: git.url.clone(),
560 url,
561 subdirectory: git.subdirectory,
562 },
563 ParsedUrl::Archive(archive) => Self::Url {
564 url,
565 location: archive.url,
566 subdirectory: archive.subdirectory,
567 ext: archive.ext,
568 },
569 }
570 }
571
572 pub fn to_verbatim_parsed_url(&self) -> Option<VerbatimParsedUrl> {
574 match self {
575 Self::Registry { .. } => None,
576 Self::Url {
577 location,
578 subdirectory,
579 ext,
580 url,
581 } => Some(VerbatimParsedUrl {
582 parsed_url: ParsedUrl::Archive(ParsedArchiveUrl::from_source(
583 location.clone(),
584 subdirectory.clone(),
585 *ext,
586 )),
587 verbatim: url.clone(),
588 }),
589 Self::Path {
590 install_path,
591 ext,
592 url,
593 } => Some(VerbatimParsedUrl {
594 parsed_url: ParsedUrl::Path(ParsedPathUrl::from_source(
595 install_path.clone(),
596 *ext,
597 url.to_url(),
598 )),
599 verbatim: url.clone(),
600 }),
601 Self::Directory {
602 install_path,
603 editable,
604 r#virtual,
605 url,
606 } => Some(VerbatimParsedUrl {
607 parsed_url: ParsedUrl::Directory(ParsedDirectoryUrl::from_source(
608 install_path.clone(),
609 *editable,
610 *r#virtual,
611 url.to_url(),
612 )),
613 verbatim: url.clone(),
614 }),
615 Self::Git {
616 git,
617 subdirectory,
618 url,
619 } => Some(VerbatimParsedUrl {
620 parsed_url: ParsedUrl::Git(ParsedGitUrl::from_source(
621 git.clone(),
622 subdirectory.clone(),
623 )),
624 verbatim: url.clone(),
625 }),
626 }
627 }
628
629 pub fn version_or_url(&self) -> Option<VersionOrUrl<VerbatimParsedUrl>> {
633 match self {
634 Self::Registry { specifier, .. } => {
635 if specifier.is_empty() {
636 None
637 } else {
638 Some(VersionOrUrl::VersionSpecifier(specifier.clone()))
639 }
640 }
641 Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
642 Some(VersionOrUrl::Url(self.to_verbatim_parsed_url()?))
643 }
644 }
645 }
646
647 pub fn is_editable(&self) -> bool {
649 matches!(
650 self,
651 Self::Directory {
652 editable: Some(true),
653 ..
654 }
655 )
656 }
657
658 pub fn is_empty(&self) -> bool {
660 match self {
661 Self::Registry { specifier, .. } => specifier.is_empty(),
662 Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
663 false
664 }
665 }
666 }
667
668 pub fn version_specifiers(&self) -> Option<&VersionSpecifiers> {
670 match self {
671 Self::Registry { specifier, .. } => Some(specifier),
672 Self::Url { .. } | Self::Git { .. } | Self::Path { .. } | Self::Directory { .. } => {
673 None
674 }
675 }
676 }
677
678 pub fn relative_to(self, path: &Path) -> Result<Self, io::Error> {
680 match self {
681 Self::Registry { .. } | Self::Url { .. } | Self::Git { .. } => Ok(self),
682 Self::Path {
683 install_path,
684 ext,
685 url,
686 } => Ok(Self::Path {
687 install_path: relative_to(&install_path, path)
688 .or_else(|_| std::path::absolute(install_path))?
689 .into_boxed_path(),
690 ext,
691 url,
692 }),
693 Self::Directory {
694 install_path,
695 editable,
696 r#virtual,
697 url,
698 ..
699 } => Ok(Self::Directory {
700 install_path: relative_to(&install_path, path)
701 .or_else(|_| std::path::absolute(install_path))?
702 .into_boxed_path(),
703 editable,
704 r#virtual,
705 url,
706 }),
707 }
708 }
709
710 #[must_use]
712 pub fn to_absolute(self, root: &Path) -> Self {
713 match self {
714 Self::Registry { .. } | Self::Url { .. } | Self::Git { .. } => self,
715 Self::Path {
716 install_path,
717 ext,
718 url,
719 } => Self::Path {
720 install_path: uv_fs::normalize_path_buf(root.join(install_path)).into_boxed_path(),
721 ext,
722 url,
723 },
724 Self::Directory {
725 install_path,
726 editable,
727 r#virtual,
728 url,
729 ..
730 } => Self::Directory {
731 install_path: uv_fs::normalize_path_buf(root.join(install_path)).into_boxed_path(),
732 editable,
733 r#virtual,
734 url,
735 },
736 }
737 }
738}
739
740impl Display for RequirementSource {
741 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
744 match self {
745 Self::Registry {
746 specifier, index, ..
747 } => {
748 write!(f, "{specifier}")?;
749 if let Some(index) = index {
750 write!(f, " (index: {})", index.url)?;
751 }
752 }
753 Self::Url { url, .. } => {
754 write!(f, " {url}")?;
755 }
756 Self::Git {
757 url: _,
758 git,
759 subdirectory,
760 } => {
761 write!(f, " git+{}", git.repository())?;
762 if let Some(reference) = git.reference().as_str() {
763 write!(f, "@{reference}")?;
764 }
765 if let Some(subdirectory) = subdirectory {
766 writeln!(f, "#subdirectory={}", subdirectory.display())?;
767 }
768 }
769 Self::Path { url, .. } => {
770 write!(f, "{url}")?;
771 }
772 Self::Directory { url, .. } => {
773 write!(f, "{url}")?;
774 }
775 }
776 Ok(())
777 }
778}
779
780#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
781#[serde(untagged)]
782enum RequirementSourceWire {
783 Git { git: String },
785 Direct {
787 url: DisplaySafeUrl,
788 subdirectory: Option<PortablePathBuf>,
789 },
790 Path { path: PortablePathBuf },
792 Directory { directory: PortablePathBuf },
794 Editable { editable: PortablePathBuf },
796 Virtual { r#virtual: PortablePathBuf },
798 Registry {
800 #[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)]
801 specifier: VersionSpecifiers,
802 index: Option<DisplaySafeUrl>,
803 conflict: Option<ConflictItem>,
804 },
805}
806
807impl From<RequirementSource> for RequirementSourceWire {
808 fn from(value: RequirementSource) -> Self {
809 match value {
810 RequirementSource::Registry {
811 specifier,
812 index,
813 conflict,
814 } => {
815 let index = index.map(|index| index.url.into_url()).map(|mut index| {
816 index.remove_credentials();
817 index
818 });
819 Self::Registry {
820 specifier,
821 index,
822 conflict,
823 }
824 }
825 RequirementSource::Url {
826 subdirectory,
827 location,
828 ext: _,
829 url: _,
830 } => Self::Direct {
831 url: location,
832 subdirectory: subdirectory.map(PortablePathBuf::from),
833 },
834 RequirementSource::Git {
835 git,
836 subdirectory,
837 url: _,
838 } => {
839 let mut url = git.repository().clone();
840
841 url.remove_credentials();
843
844 url.set_fragment(None);
846 url.set_query(None);
847
848 if let Some(subdirectory) = subdirectory
850 .as_deref()
851 .map(PortablePath::from)
852 .as_ref()
853 .map(PortablePath::to_string)
854 {
855 url.query_pairs_mut()
856 .append_pair("subdirectory", &subdirectory);
857 }
858
859 match git.reference() {
861 GitReference::Branch(branch) => {
862 url.query_pairs_mut().append_pair("branch", branch.as_str());
863 }
864 GitReference::Tag(tag) => {
865 url.query_pairs_mut().append_pair("tag", tag.as_str());
866 }
867 GitReference::BranchOrTag(rev)
868 | GitReference::BranchOrTagOrCommit(rev)
869 | GitReference::NamedRef(rev) => {
870 url.query_pairs_mut().append_pair("rev", rev.as_str());
871 }
872 GitReference::DefaultBranch => {}
873 }
874
875 if let Some(precise) = git.precise() {
877 url.set_fragment(Some(&precise.to_string()));
878 }
879
880 Self::Git {
881 git: url.to_string(),
882 }
883 }
884 RequirementSource::Path {
885 install_path,
886 ext: _,
887 url: _,
888 } => Self::Path {
889 path: PortablePathBuf::from(install_path),
890 },
891 RequirementSource::Directory {
892 install_path,
893 editable,
894 r#virtual,
895 url: _,
896 } => {
897 if editable.unwrap_or(false) {
898 Self::Editable {
899 editable: PortablePathBuf::from(install_path),
900 }
901 } else if r#virtual.unwrap_or(false) {
902 Self::Virtual {
903 r#virtual: PortablePathBuf::from(install_path),
904 }
905 } else {
906 Self::Directory {
907 directory: PortablePathBuf::from(install_path),
908 }
909 }
910 }
911 }
912 }
913}
914
915impl TryFrom<RequirementSourceWire> for RequirementSource {
916 type Error = RequirementError;
917
918 fn try_from(wire: RequirementSourceWire) -> Result<Self, RequirementError> {
919 match wire {
920 RequirementSourceWire::Registry {
921 specifier,
922 index,
923 conflict,
924 } => Ok(Self::Registry {
925 specifier,
926 index: index
927 .map(|index| IndexMetadata::from(IndexUrl::from(VerbatimUrl::from_url(index)))),
928 conflict,
929 }),
930 RequirementSourceWire::Git { git } => {
931 let mut repository = DisplaySafeUrl::parse(&git)?;
932
933 let mut reference = GitReference::DefaultBranch;
934 let mut subdirectory: Option<PortablePathBuf> = None;
935 for (key, val) in repository.query_pairs() {
936 match &*key {
937 "tag" => reference = GitReference::Tag(val.into_owned()),
938 "branch" => reference = GitReference::Branch(val.into_owned()),
939 "rev" => reference = GitReference::from_rev(val.into_owned()),
940 "subdirectory" => {
941 subdirectory = Some(PortablePathBuf::from(val.as_ref()));
942 }
943 _ => {}
944 }
945 }
946
947 let precise = repository.fragment().map(GitOid::from_str).transpose()?;
948
949 repository.set_fragment(None);
951 repository.set_query(None);
952
953 repository.remove_credentials();
955
956 let mut url = DisplaySafeUrl::parse(&format!("git+{repository}"))?;
958 if let Some(rev) = reference.as_str() {
959 let path = format!("{}@{}", url.path(), rev);
960 url.set_path(&path);
961 }
962 if let Some(subdirectory) = subdirectory.as_ref() {
963 url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
964 }
965 let url = VerbatimUrl::from_url(url);
966
967 Ok(Self::Git {
968 git: GitUrl::from_fields(repository, reference, precise)?,
969 subdirectory: subdirectory.map(Box::<Path>::from),
970 url,
971 })
972 }
973 RequirementSourceWire::Direct { url, subdirectory } => {
974 let location = url.clone();
975
976 let mut url = url.clone();
978 if let Some(subdirectory) = &subdirectory {
979 url.set_fragment(Some(&format!("subdirectory={subdirectory}")));
980 }
981
982 Ok(Self::Url {
983 location,
984 subdirectory: subdirectory.map(Box::<Path>::from),
985 ext: DistExtension::from_path(url.path())
986 .map_err(|err| ParsedUrlError::MissingExtensionUrl(url.to_string(), err))?,
987 url: VerbatimUrl::from_url(url.clone()),
988 })
989 }
990 RequirementSourceWire::Path { path } => {
995 let path = Box::<Path>::from(path);
996 let url =
997 VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(CWD.join(&path)))?;
998 Ok(Self::Path {
999 ext: DistExtension::from_path(&path).map_err(|err| {
1000 ParsedUrlError::MissingExtensionPath(path.to_path_buf(), err)
1001 })?,
1002 install_path: path,
1003 url,
1004 })
1005 }
1006 RequirementSourceWire::Directory { directory } => {
1007 let directory = Box::<Path>::from(directory);
1008 let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1009 CWD.join(&directory),
1010 ))?;
1011 Ok(Self::Directory {
1012 install_path: directory,
1013 editable: Some(false),
1014 r#virtual: Some(false),
1015 url,
1016 })
1017 }
1018 RequirementSourceWire::Editable { editable } => {
1019 let editable = Box::<Path>::from(editable);
1020 let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1021 CWD.join(&editable),
1022 ))?;
1023 Ok(Self::Directory {
1024 install_path: editable,
1025 editable: Some(true),
1026 r#virtual: Some(false),
1027 url,
1028 })
1029 }
1030 RequirementSourceWire::Virtual { r#virtual } => {
1031 let r#virtual = Box::<Path>::from(r#virtual);
1032 let url = VerbatimUrl::from_normalized_path(uv_fs::normalize_path_buf(
1033 CWD.join(&r#virtual),
1034 ))?;
1035 Ok(Self::Directory {
1036 install_path: r#virtual,
1037 editable: Some(false),
1038 r#virtual: Some(true),
1039 url,
1040 })
1041 }
1042 }
1043 }
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use std::path::PathBuf;
1049
1050 use uv_pep508::{MarkerTree, VerbatimUrl};
1051
1052 use crate::{Requirement, RequirementSource};
1053
1054 #[test]
1055 fn roundtrip() {
1056 let requirement = Requirement {
1057 name: "foo".parse().unwrap(),
1058 extras: Box::new([]),
1059 groups: Box::new([]),
1060 marker: MarkerTree::TRUE,
1061 source: RequirementSource::Registry {
1062 specifier: ">1,<2".parse().unwrap(),
1063 index: None,
1064 conflict: None,
1065 },
1066 origin: None,
1067 };
1068
1069 let raw = toml::to_string(&requirement).unwrap();
1070 let deserialized: Requirement = toml::from_str(&raw).unwrap();
1071 assert_eq!(requirement, deserialized);
1072
1073 let path = if cfg!(windows) {
1074 "C:\\home\\ferris\\foo"
1075 } else {
1076 "/home/ferris/foo"
1077 };
1078 let requirement = Requirement {
1079 name: "foo".parse().unwrap(),
1080 extras: Box::new([]),
1081 groups: Box::new([]),
1082 marker: MarkerTree::TRUE,
1083 source: RequirementSource::Directory {
1084 install_path: PathBuf::from(path).into_boxed_path(),
1085 editable: Some(false),
1086 r#virtual: Some(false),
1087 url: VerbatimUrl::from_absolute_path(path).unwrap(),
1088 },
1089 origin: None,
1090 };
1091
1092 let raw = toml::to_string(&requirement).unwrap();
1093 let deserialized: Requirement = toml::from_str(&raw).unwrap();
1094 assert_eq!(requirement, deserialized);
1095 }
1096}