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