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