1use std::collections::Bound;
2
3use version_ranges::Ranges;
4
5use uv_distribution_filename::WheelFilename;
6use uv_pep440::{
7 LowerBound, UpperBound, Version, VersionSpecifier, VersionSpecifiers,
8 release_specifiers_to_ranges,
9};
10use uv_pep508::{MarkerExpression, MarkerTree, MarkerValueVersion};
11use uv_platform_tags::{AbiTag, CPythonAbiVariants, LanguageTag};
12
13#[derive(Debug, Clone, Eq, PartialEq, Hash)]
17pub struct RequiresPython {
18 specifiers: VersionSpecifiers,
27 range: RequiresPythonRange,
32}
33
34impl RequiresPython {
35 pub fn greater_than_equal_version(version: &Version) -> Self {
37 let version = version.only_release();
38 Self {
39 specifiers: VersionSpecifiers::from(VersionSpecifier::greater_than_equal_version(
40 version.clone(),
41 )),
42 range: RequiresPythonRange(
43 LowerBound::new(Bound::Included(version.clone())),
44 UpperBound::new(Bound::Unbounded),
45 ),
46 }
47 }
48
49 pub fn from_specifiers(specifiers: VersionSpecifiers) -> Self {
51 let (lower_bound, upper_bound) = release_specifiers_to_ranges(specifiers.clone())
52 .bounding_range()
53 .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
54 .unwrap_or((Bound::Unbounded, Bound::Unbounded));
55 Self {
56 specifiers,
57 range: RequiresPythonRange(LowerBound::new(lower_bound), UpperBound::new(upper_bound)),
58 }
59 }
60
61 pub fn intersection<'a>(
65 specifiers: impl Iterator<Item = &'a VersionSpecifiers>,
66 ) -> Option<Self> {
67 let range = specifiers
69 .map(|specs| release_specifiers_to_ranges(specs.clone()))
70 .reduce(|acc, r| acc.intersection(&r))?;
71
72 if range.is_empty() {
74 return None;
75 }
76
77 let specifiers = VersionSpecifiers::from_release_only_bounds(range.iter());
79
80 let range = RequiresPythonRange::from_range(&range);
82
83 Some(Self { specifiers, range })
84 }
85
86 pub fn split(&self, bound: Bound<Version>) -> Option<(Self, Self)> {
91 let RequiresPythonRange(.., upper) = &self.range;
92
93 let upper = Ranges::from_range_bounds((bound, upper.clone().into()));
94 let lower = upper.complement();
95
96 let lower = lower.intersection(&Ranges::from(self.range.clone()));
98 let upper = upper.intersection(&Ranges::from(self.range.clone()));
99
100 if lower.is_empty() || upper.is_empty() {
101 None
102 } else {
103 Some((
104 Self {
105 specifiers: VersionSpecifiers::from_release_only_bounds(lower.iter()),
106 range: RequiresPythonRange::from_range(&lower),
107 },
108 Self {
109 specifiers: VersionSpecifiers::from_release_only_bounds(upper.iter()),
110 range: RequiresPythonRange::from_range(&upper),
111 },
112 ))
113 }
114 }
115
116 pub fn narrow(&self, range: &RequiresPythonRange) -> Option<Self> {
120 if *range == self.range {
121 return None;
122 }
123 let lower = if range.0 >= self.range.0 {
124 Some(&range.0)
125 } else {
126 None
127 };
128 let upper = if range.1 <= self.range.1 {
129 Some(&range.1)
130 } else {
131 None
132 };
133 let range = match (lower, upper) {
134 (Some(lower), Some(upper)) => Some(RequiresPythonRange(lower.clone(), upper.clone())),
135 (Some(lower), None) => Some(RequiresPythonRange(lower.clone(), self.range.1.clone())),
136 (None, Some(upper)) => Some(RequiresPythonRange(self.range.0.clone(), upper.clone())),
137 (None, None) => None,
138 }?;
139 Some(Self {
140 specifiers: range.specifiers(),
141 range,
142 })
143 }
144
145 pub fn to_marker_tree(&self) -> MarkerTree {
158 match (self.range.0.as_ref(), self.range.1.as_ref()) {
159 (Bound::Included(lower), Bound::Included(upper)) => {
160 let mut lower = MarkerTree::expression(MarkerExpression::Version {
161 key: MarkerValueVersion::PythonFullVersion,
162 specifier: VersionSpecifier::greater_than_equal_version(lower.clone()),
163 });
164 let upper = MarkerTree::expression(MarkerExpression::Version {
165 key: MarkerValueVersion::PythonFullVersion,
166 specifier: VersionSpecifier::less_than_equal_version(upper.clone()),
167 });
168 lower = lower.and(upper);
169 lower
170 }
171 (Bound::Included(lower), Bound::Excluded(upper)) => {
172 let mut lower = MarkerTree::expression(MarkerExpression::Version {
173 key: MarkerValueVersion::PythonFullVersion,
174 specifier: VersionSpecifier::greater_than_equal_version(lower.clone()),
175 });
176 let upper = MarkerTree::expression(MarkerExpression::Version {
177 key: MarkerValueVersion::PythonFullVersion,
178 specifier: VersionSpecifier::less_than_version(upper.clone()),
179 });
180 lower = lower.and(upper);
181 lower
182 }
183 (Bound::Excluded(lower), Bound::Included(upper)) => {
184 let mut lower = MarkerTree::expression(MarkerExpression::Version {
185 key: MarkerValueVersion::PythonFullVersion,
186 specifier: VersionSpecifier::greater_than_version(lower.clone()),
187 });
188 let upper = MarkerTree::expression(MarkerExpression::Version {
189 key: MarkerValueVersion::PythonFullVersion,
190 specifier: VersionSpecifier::less_than_equal_version(upper.clone()),
191 });
192 lower = lower.and(upper);
193 lower
194 }
195 (Bound::Excluded(lower), Bound::Excluded(upper)) => {
196 let mut lower = MarkerTree::expression(MarkerExpression::Version {
197 key: MarkerValueVersion::PythonFullVersion,
198 specifier: VersionSpecifier::greater_than_version(lower.clone()),
199 });
200 let upper = MarkerTree::expression(MarkerExpression::Version {
201 key: MarkerValueVersion::PythonFullVersion,
202 specifier: VersionSpecifier::less_than_version(upper.clone()),
203 });
204 lower = lower.and(upper);
205 lower
206 }
207 (Bound::Unbounded, Bound::Unbounded) => MarkerTree::TRUE,
208 (Bound::Unbounded, Bound::Included(upper)) => {
209 MarkerTree::expression(MarkerExpression::Version {
210 key: MarkerValueVersion::PythonFullVersion,
211 specifier: VersionSpecifier::less_than_equal_version(upper.clone()),
212 })
213 }
214 (Bound::Unbounded, Bound::Excluded(upper)) => {
215 MarkerTree::expression(MarkerExpression::Version {
216 key: MarkerValueVersion::PythonFullVersion,
217 specifier: VersionSpecifier::less_than_version(upper.clone()),
218 })
219 }
220 (Bound::Included(lower), Bound::Unbounded) => {
221 MarkerTree::expression(MarkerExpression::Version {
222 key: MarkerValueVersion::PythonFullVersion,
223 specifier: VersionSpecifier::greater_than_equal_version(lower.clone()),
224 })
225 }
226 (Bound::Excluded(lower), Bound::Unbounded) => {
227 MarkerTree::expression(MarkerExpression::Version {
228 key: MarkerValueVersion::PythonFullVersion,
229 specifier: VersionSpecifier::greater_than_version(lower.clone()),
230 })
231 }
232 }
233 }
234
235 pub fn contains(&self, version: &Version) -> bool {
242 let version = version.only_release();
243 self.specifiers.contains(&version)
244 }
245
246 pub fn is_contained_by(&self, target: &VersionSpecifiers) -> bool {
266 let target = release_specifiers_to_ranges(target.clone())
267 .bounding_range()
268 .map(|bounding_range| bounding_range.0.cloned())
269 .unwrap_or(Bound::Unbounded);
270
271 *self.range.lower() >= LowerBound(target.clone())
275 }
276
277 pub fn specifiers(&self) -> &VersionSpecifiers {
279 &self.specifiers
280 }
281
282 pub fn is_unbounded(&self) -> bool {
284 self.range.lower().as_ref() == Bound::Unbounded
285 }
286
287 pub fn is_exact_without_patch(&self) -> bool {
290 match self.range.lower().as_ref() {
291 Bound::Included(version) => {
292 version.release().len() == 2
293 && self.range.upper().as_ref() == Bound::Included(version)
294 }
295 _ => false,
296 }
297 }
298
299 pub fn range(&self) -> &RequiresPythonRange {
301 &self.range
302 }
303
304 pub fn abi_tag(&self) -> Option<AbiTag> {
306 match self.range.lower().as_ref() {
307 Bound::Included(version) | Bound::Excluded(version) => {
308 let major = version.release().first().copied()?;
309 let major = u8::try_from(major).ok()?;
310 let minor = version.release().get(1).copied()?;
311 let minor = u8::try_from(minor).ok()?;
312 Some(AbiTag::CPython {
313 variant: CPythonAbiVariants::default(),
314 python_version: (major, minor),
315 })
316 }
317 Bound::Unbounded => None,
318 }
319 }
320
321 pub fn simplify_markers(&self, marker: MarkerTree) -> MarkerTree {
350 let (lower, upper) = (self.range().lower(), self.range().upper());
351 marker.simplify_python_versions(lower.as_ref(), upper.as_ref())
352 }
353
354 pub fn complexify_markers(&self, marker: MarkerTree) -> MarkerTree {
370 let (lower, upper) = (self.range().lower(), self.range().upper());
371 marker.complexify_python_versions(lower.as_ref(), upper.as_ref())
372 }
373
374 pub fn matches_wheel_tag(&self, wheel: &WheelFilename) -> bool {
380 wheel.abi_tags().iter().any(|abi_tag| {
381 if abi_tag.is_stable_abi() {
382 true
384 } else if *abi_tag == AbiTag::None {
385 wheel.python_tags().iter().any(|python_tag| {
386 if matches!(
388 python_tag,
389 LanguageTag::Python { major: 2, .. }
390 | LanguageTag::CPython {
391 python_version: (2, ..)
392 }
393 | LanguageTag::PyPy {
394 python_version: (2, ..)
395 }
396 | LanguageTag::GraalPy {
397 python_version: (2, ..)
398 }
399 | LanguageTag::Pyston {
400 python_version: (2, ..)
401 }
402 ) {
403 return false;
404 }
405
406 if let LanguageTag::Python {
409 major: 3,
410 minor: Some(minor),
411 } = python_tag
412 {
413 let wheel_bound =
415 UpperBound(Bound::Included(Version::new([3, u64::from(*minor)])));
416 if wheel_bound > self.range.upper().major_minor() {
417 return false;
418 }
419
420 return true;
421 }
422
423 if let LanguageTag::CPython {
426 python_version: (3, minor),
427 }
428 | LanguageTag::PyPy {
429 python_version: (3, minor),
430 }
431 | LanguageTag::GraalPy {
432 python_version: (3, minor),
433 }
434 | LanguageTag::Pyston {
435 python_version: (3, minor),
436 } = python_tag
437 {
438 let wheel_bound =
440 LowerBound(Bound::Included(Version::new([3, u64::from(*minor)])));
441 if wheel_bound < self.range.lower().major_minor() {
442 return false;
443 }
444
445 let wheel_bound =
447 UpperBound(Bound::Included(Version::new([3, u64::from(*minor)])));
448 if wheel_bound > self.range.upper().major_minor() {
449 return false;
450 }
451
452 return true;
453 }
454
455 true
457 })
458 } else if matches!(
459 abi_tag,
460 AbiTag::CPython {
461 python_version: (2, ..),
462 ..
463 } | AbiTag::PyPy {
464 python_version: None | Some((2, ..)),
465 ..
466 } | AbiTag::GraalPy {
467 python_version: (2, ..),
468 ..
469 }
470 ) {
471 false
473 } else if let AbiTag::CPython {
474 python_version: (3, minor),
475 ..
476 }
477 | AbiTag::PyPy {
478 python_version: Some((3, minor)),
479 ..
480 }
481 | AbiTag::GraalPy {
482 python_version: (3, minor),
483 ..
484 } = abi_tag
485 {
486 let wheel_bound = LowerBound(Bound::Included(Version::new([3, u64::from(*minor)])));
488 if wheel_bound < self.range.lower().major_minor() {
489 return false;
490 }
491
492 let wheel_bound = UpperBound(Bound::Included(Version::new([3, u64::from(*minor)])));
494 if wheel_bound > self.range.upper().major_minor() {
495 return false;
496 }
497
498 true
499 } else {
500 true
502 }
503 })
504 }
505}
506
507impl std::fmt::Display for RequiresPython {
508 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
509 std::fmt::Display::fmt(&self.specifiers, f)
510 }
511}
512
513impl serde::Serialize for RequiresPython {
514 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
515 self.specifiers.serialize(serializer)
516 }
517}
518
519impl<'de> serde::Deserialize<'de> for RequiresPython {
520 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
521 let specifiers = VersionSpecifiers::deserialize(deserializer)?;
522 let range = release_specifiers_to_ranges(specifiers.clone());
523 let range = RequiresPythonRange::from_range(&range);
524 Ok(Self { specifiers, range })
525 }
526}
527
528#[derive(Debug, Clone, Eq, PartialEq, Hash)]
529pub struct RequiresPythonRange(LowerBound, UpperBound);
530
531impl RequiresPythonRange {
532 fn from_range(range: &Ranges<Version>) -> Self {
534 let (lower, upper) = range
535 .bounding_range()
536 .map(|(lower_bound, upper_bound)| (lower_bound.cloned(), upper_bound.cloned()))
537 .unwrap_or((Bound::Unbounded, Bound::Unbounded));
538 Self(LowerBound(lower), UpperBound(upper))
539 }
540
541 pub fn new(lower: LowerBound, upper: UpperBound) -> Self {
543 Self(lower, upper)
544 }
545
546 pub fn lower(&self) -> &LowerBound {
548 &self.0
549 }
550
551 pub fn upper(&self) -> &UpperBound {
553 &self.1
554 }
555
556 fn specifiers(&self) -> VersionSpecifiers {
558 [self.0.specifier(), self.1.specifier()]
559 .into_iter()
560 .flatten()
561 .collect()
562 }
563}
564
565impl Default for RequiresPythonRange {
566 fn default() -> Self {
567 Self(LowerBound(Bound::Unbounded), UpperBound(Bound::Unbounded))
568 }
569}
570
571impl From<RequiresPythonRange> for Ranges<Version> {
572 fn from(value: RequiresPythonRange) -> Self {
573 Self::from_range_bounds::<(Bound<Version>, Bound<Version>), _>((
574 value.0.into(),
575 value.1.into(),
576 ))
577 }
578}
579
580#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, PartialOrd, Ord, serde::Deserialize)]
588pub struct SimplifiedMarkerTree(MarkerTree);
589
590impl SimplifiedMarkerTree {
591 pub fn new(requires_python: &RequiresPython, marker: MarkerTree) -> Self {
594 Self(requires_python.simplify_markers(marker))
595 }
596
597 pub fn into_marker(self, requires_python: &RequiresPython) -> MarkerTree {
600 requires_python.complexify_markers(self.0)
601 }
602
603 pub fn try_to_string(self) -> Option<String> {
608 self.0.try_to_string()
609 }
610
611 pub fn as_simplified_marker_tree(self) -> MarkerTree {
613 self.0
614 }
615
616 pub fn and(&mut self, marker: Self) {
618 self.0 = self.0.and(marker.0);
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use std::cmp::Ordering;
625 use std::collections::Bound;
626 use std::str::FromStr;
627
628 use uv_distribution_filename::WheelFilename;
629 use uv_pep440::{LowerBound, UpperBound, Version, VersionSpecifiers};
630
631 use crate::RequiresPython;
632
633 #[test]
634 fn requires_python_included() {
635 let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap();
636 let requires_python = RequiresPython::from_specifiers(version_specifiers);
637 let wheel_names = &[
638 "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl",
639 "black-24.4.2-cp310-cp310-win_amd64.whl",
640 "black-24.4.2-cp310-none-win_amd64.whl",
641 "cbor2-5.6.4-py3-none-any.whl",
642 "solace_pubsubplus-1.8.0-py36-none-manylinux_2_12_x86_64.whl",
643 "torch-1.10.0-py310-none-macosx_10_9_x86_64.whl",
644 "torch-1.10.0-py37-none-macosx_10_9_x86_64.whl",
645 "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",
646 ];
647 for wheel_name in wheel_names {
648 assert!(
649 requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
650 "{wheel_name}"
651 );
652 }
653
654 let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap();
655 let requires_python = RequiresPython::from_specifiers(version_specifiers);
656 let wheel_names = &["dearpygui-1.11.1-cp312-cp312-win_amd64.whl"];
657 for wheel_name in wheel_names {
658 assert!(
659 requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
660 "{wheel_name}"
661 );
662 }
663
664 let version_specifiers = VersionSpecifiers::from_str("==3.12.6").unwrap();
665 let requires_python = RequiresPython::from_specifiers(version_specifiers);
666 let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl"];
667 for wheel_name in wheel_names {
668 assert!(
669 requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
670 "{wheel_name}"
671 );
672 }
673
674 let version_specifiers = VersionSpecifiers::from_str("==3.12").unwrap();
675 let requires_python = RequiresPython::from_specifiers(version_specifiers);
676 let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl"];
677 for wheel_name in wheel_names {
678 assert!(
679 requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
680 "{wheel_name}"
681 );
682 }
683 }
684
685 #[test]
686 fn requires_python_dropped() {
687 let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap();
688 let requires_python = RequiresPython::from_specifiers(version_specifiers);
689 let wheel_names = &[
690 "PySocks-1.7.1-py27-none-any.whl",
691 "black-24.4.2-cp39-cp39-win_amd64.whl",
692 "dearpygui-1.11.1-cp312-cp312-win_amd64.whl",
693 "psutil-6.0.0-cp27-none-win32.whl",
694 "psutil-6.0.0-cp36-cp36m-win32.whl",
695 "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl",
696 "torch-1.10.0-cp311-none-macosx_10_9_x86_64.whl",
697 "torch-1.10.0-cp36-none-macosx_10_9_x86_64.whl",
698 "torch-1.10.0-py311-none-macosx_10_9_x86_64.whl",
699 ];
700 for wheel_name in wheel_names {
701 assert!(
702 !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
703 "{wheel_name}"
704 );
705 }
706
707 let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap();
708 let requires_python = RequiresPython::from_specifiers(version_specifiers);
709 let wheel_names = &["dearpygui-1.11.1-cp310-cp310-win_amd64.whl"];
710 for wheel_name in wheel_names {
711 assert!(
712 !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
713 "{wheel_name}"
714 );
715 }
716 }
717
718 #[test]
719 fn lower_bound_ordering() {
720 let versions = &[
721 LowerBound::new(Bound::Unbounded),
723 LowerBound::new(Bound::Included(Version::new([3, 8]))),
725 LowerBound::new(Bound::Excluded(Version::new([3, 8]))),
727 LowerBound::new(Bound::Included(Version::new([3, 8, 1]))),
729 LowerBound::new(Bound::Excluded(Version::new([3, 8, 1]))),
731 ];
732 for (i, v1) in versions.iter().enumerate() {
733 for v2 in &versions[i + 1..] {
734 assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}");
735 }
736 }
737 }
738
739 #[test]
740 fn upper_bound_ordering() {
741 let versions = &[
742 UpperBound::new(Bound::Excluded(Version::new([3, 8]))),
744 UpperBound::new(Bound::Included(Version::new([3, 8]))),
746 UpperBound::new(Bound::Excluded(Version::new([3, 8, 1]))),
748 UpperBound::new(Bound::Included(Version::new([3, 8, 1]))),
750 UpperBound::new(Bound::Unbounded),
752 ];
753 for (i, v1) in versions.iter().enumerate() {
754 for v2 in &versions[i + 1..] {
755 assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}");
756 }
757 }
758 }
759
760 #[test]
761 fn is_exact_without_patch() {
762 let test_cases = [
763 ("==3.12", true),
764 ("==3.10, <3.11", true),
765 ("==3.10, <=3.11", true),
766 ("==3.12.1", false),
767 ("==3.12.*", false),
768 ("==3.*", false),
769 (">=3.10", false),
770 (">3.9", false),
771 ("<4.0", false),
772 (">=3.10, <3.11", false),
773 ("", false),
774 ];
775 for (version, expected) in test_cases {
776 let version_specifiers = VersionSpecifiers::from_str(version).unwrap();
777 let requires_python = RequiresPython::from_specifiers(version_specifiers);
778 assert_eq!(requires_python.is_exact_without_patch(), expected);
779 }
780 }
781
782 #[test]
783 fn split_version() {
784 let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
786 let requires_python = RequiresPython::from_specifiers(version_specifiers);
787 let (lower, upper) = requires_python
788 .split(Bound::Excluded(Version::new([3, 12])))
789 .unwrap();
790 assert_eq!(
791 lower,
792 RequiresPython::from_specifiers(VersionSpecifiers::from_str(">=3.10, <=3.12").unwrap())
793 );
794 assert_eq!(
795 upper,
796 RequiresPython::from_specifiers(VersionSpecifiers::from_str(">3.12").unwrap())
797 );
798
799 let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
801 let requires_python = RequiresPython::from_specifiers(version_specifiers);
802 let (lower, upper) = requires_python
803 .split(Bound::Included(Version::new([3, 12])))
804 .unwrap();
805 assert_eq!(
806 lower,
807 RequiresPython::from_specifiers(VersionSpecifiers::from_str(">=3.10, <3.12").unwrap())
808 );
809 assert_eq!(
810 upper,
811 RequiresPython::from_specifiers(VersionSpecifiers::from_str(">=3.12").unwrap())
812 );
813
814 let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
816 let requires_python = RequiresPython::from_specifiers(version_specifiers);
817 assert!(
818 requires_python
819 .split(Bound::Included(Version::new([3, 9])))
820 .is_none()
821 );
822
823 let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
825 let requires_python = RequiresPython::from_specifiers(version_specifiers);
826 assert!(
827 requires_python
828 .split(Bound::Included(Version::new([3, 10])))
829 .is_none()
830 );
831
832 let version_specifiers = VersionSpecifiers::from_str(">=3.9, <3.13").unwrap();
834 let requires_python = RequiresPython::from_specifiers(version_specifiers);
835 let (lower, upper) = requires_python
836 .split(Bound::Included(Version::new([3, 11])))
837 .unwrap();
838 assert_eq!(
839 lower,
840 RequiresPython::from_specifiers(VersionSpecifiers::from_str(">=3.9, <3.11").unwrap())
841 );
842 assert_eq!(
843 upper,
844 RequiresPython::from_specifiers(VersionSpecifiers::from_str(">=3.11, <3.13").unwrap())
845 );
846 }
847}