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, 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: specifiers.clone(),
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.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.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.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.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 gil_disabled: false,
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 == AbiTag::Abi3 {
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 pub 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 pub 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
617#[cfg(test)]
618mod tests {
619 use std::cmp::Ordering;
620 use std::collections::Bound;
621 use std::str::FromStr;
622
623 use uv_distribution_filename::WheelFilename;
624 use uv_pep440::{LowerBound, UpperBound, Version, VersionSpecifiers};
625
626 use crate::RequiresPython;
627
628 #[test]
629 fn requires_python_included() {
630 let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap();
631 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
632 let wheel_names = &[
633 "bcrypt-4.1.3-cp37-abi3-macosx_10_12_universal2.whl",
634 "black-24.4.2-cp310-cp310-win_amd64.whl",
635 "black-24.4.2-cp310-none-win_amd64.whl",
636 "cbor2-5.6.4-py3-none-any.whl",
637 "solace_pubsubplus-1.8.0-py36-none-manylinux_2_12_x86_64.whl",
638 "torch-1.10.0-py310-none-macosx_10_9_x86_64.whl",
639 "torch-1.10.0-py37-none-macosx_10_9_x86_64.whl",
640 "watchfiles-0.22.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl",
641 ];
642 for wheel_name in wheel_names {
643 assert!(
644 requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
645 "{wheel_name}"
646 );
647 }
648
649 let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap();
650 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
651 let wheel_names = &["dearpygui-1.11.1-cp312-cp312-win_amd64.whl"];
652 for wheel_name in wheel_names {
653 assert!(
654 requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
655 "{wheel_name}"
656 );
657 }
658
659 let version_specifiers = VersionSpecifiers::from_str("==3.12.6").unwrap();
660 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
661 let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl"];
662 for wheel_name in wheel_names {
663 assert!(
664 requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
665 "{wheel_name}"
666 );
667 }
668
669 let version_specifiers = VersionSpecifiers::from_str("==3.12").unwrap();
670 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
671 let wheel_names = &["lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl"];
672 for wheel_name in wheel_names {
673 assert!(
674 requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
675 "{wheel_name}"
676 );
677 }
678 }
679
680 #[test]
681 fn requires_python_dropped() {
682 let version_specifiers = VersionSpecifiers::from_str("==3.10.*").unwrap();
683 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
684 let wheel_names = &[
685 "PySocks-1.7.1-py27-none-any.whl",
686 "black-24.4.2-cp39-cp39-win_amd64.whl",
687 "dearpygui-1.11.1-cp312-cp312-win_amd64.whl",
688 "psutil-6.0.0-cp27-none-win32.whl",
689 "psutil-6.0.0-cp36-cp36m-win32.whl",
690 "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl",
691 "torch-1.10.0-cp311-none-macosx_10_9_x86_64.whl",
692 "torch-1.10.0-cp36-none-macosx_10_9_x86_64.whl",
693 "torch-1.10.0-py311-none-macosx_10_9_x86_64.whl",
694 ];
695 for wheel_name in wheel_names {
696 assert!(
697 !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
698 "{wheel_name}"
699 );
700 }
701
702 let version_specifiers = VersionSpecifiers::from_str(">=3.12.3").unwrap();
703 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
704 let wheel_names = &["dearpygui-1.11.1-cp310-cp310-win_amd64.whl"];
705 for wheel_name in wheel_names {
706 assert!(
707 !requires_python.matches_wheel_tag(&WheelFilename::from_str(wheel_name).unwrap()),
708 "{wheel_name}"
709 );
710 }
711 }
712
713 #[test]
714 fn lower_bound_ordering() {
715 let versions = &[
716 LowerBound::new(Bound::Unbounded),
718 LowerBound::new(Bound::Included(Version::new([3, 8]))),
720 LowerBound::new(Bound::Excluded(Version::new([3, 8]))),
722 LowerBound::new(Bound::Included(Version::new([3, 8, 1]))),
724 LowerBound::new(Bound::Excluded(Version::new([3, 8, 1]))),
726 ];
727 for (i, v1) in versions.iter().enumerate() {
728 for v2 in &versions[i + 1..] {
729 assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}");
730 }
731 }
732 }
733
734 #[test]
735 fn upper_bound_ordering() {
736 let versions = &[
737 UpperBound::new(Bound::Excluded(Version::new([3, 8]))),
739 UpperBound::new(Bound::Included(Version::new([3, 8]))),
741 UpperBound::new(Bound::Excluded(Version::new([3, 8, 1]))),
743 UpperBound::new(Bound::Included(Version::new([3, 8, 1]))),
745 UpperBound::new(Bound::Unbounded),
747 ];
748 for (i, v1) in versions.iter().enumerate() {
749 for v2 in &versions[i + 1..] {
750 assert_eq!(v1.cmp(v2), Ordering::Less, "less: {v1:?}\ngreater: {v2:?}");
751 }
752 }
753 }
754
755 #[test]
756 fn is_exact_without_patch() {
757 let test_cases = [
758 ("==3.12", true),
759 ("==3.10, <3.11", true),
760 ("==3.10, <=3.11", true),
761 ("==3.12.1", false),
762 ("==3.12.*", false),
763 ("==3.*", false),
764 (">=3.10", false),
765 (">3.9", false),
766 ("<4.0", false),
767 (">=3.10, <3.11", false),
768 ("", false),
769 ];
770 for (version, expected) in test_cases {
771 let version_specifiers = VersionSpecifiers::from_str(version).unwrap();
772 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
773 assert_eq!(requires_python.is_exact_without_patch(), expected);
774 }
775 }
776
777 #[test]
778 fn split_version() {
779 let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
781 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
782 let (lower, upper) = requires_python
783 .split(Bound::Excluded(Version::new([3, 12])))
784 .unwrap();
785 assert_eq!(
786 lower,
787 RequiresPython::from_specifiers(
788 &VersionSpecifiers::from_str(">=3.10, <=3.12").unwrap()
789 )
790 );
791 assert_eq!(
792 upper,
793 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">3.12").unwrap())
794 );
795
796 let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
798 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
799 let (lower, upper) = requires_python
800 .split(Bound::Included(Version::new([3, 12])))
801 .unwrap();
802 assert_eq!(
803 lower,
804 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.10, <3.12").unwrap())
805 );
806 assert_eq!(
807 upper,
808 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.12").unwrap())
809 );
810
811 let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
813 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
814 assert!(
815 requires_python
816 .split(Bound::Included(Version::new([3, 9])))
817 .is_none()
818 );
819
820 let version_specifiers = VersionSpecifiers::from_str(">=3.10").unwrap();
822 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
823 assert!(
824 requires_python
825 .split(Bound::Included(Version::new([3, 10])))
826 .is_none()
827 );
828
829 let version_specifiers = VersionSpecifiers::from_str(">=3.9, <3.13").unwrap();
831 let requires_python = RequiresPython::from_specifiers(&version_specifiers);
832 let (lower, upper) = requires_python
833 .split(Bound::Included(Version::new([3, 11])))
834 .unwrap();
835 assert_eq!(
836 lower,
837 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.9, <3.11").unwrap())
838 );
839 assert_eq!(
840 upper,
841 RequiresPython::from_specifiers(&VersionSpecifiers::from_str(">=3.11, <3.13").unwrap())
842 );
843 }
844}