1use std::{fmt, str::FromStr};
2
3#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
7#[cfg_attr(feature = "cache", derive(ruff_macros::CacheKey))]
8#[cfg_attr(feature = "get-size", derive(get_size2::GetSize))]
9pub struct PythonVersion {
10 pub major: u8,
11 pub minor: u8,
12}
13
14impl PythonVersion {
15 pub const PY37: PythonVersion = PythonVersion { major: 3, minor: 7 };
16 pub const PY38: PythonVersion = PythonVersion { major: 3, minor: 8 };
17 pub const PY39: PythonVersion = PythonVersion { major: 3, minor: 9 };
18 pub const PY310: PythonVersion = PythonVersion {
19 major: 3,
20 minor: 10,
21 };
22 pub const PY311: PythonVersion = PythonVersion {
23 major: 3,
24 minor: 11,
25 };
26 pub const PY312: PythonVersion = PythonVersion {
27 major: 3,
28 minor: 12,
29 };
30 pub const PY313: PythonVersion = PythonVersion {
31 major: 3,
32 minor: 13,
33 };
34 pub const PY314: PythonVersion = PythonVersion {
35 major: 3,
36 minor: 14,
37 };
38 pub const PY315: PythonVersion = PythonVersion {
39 major: 3,
40 minor: 15,
41 };
42
43 pub fn iter() -> impl Iterator<Item = PythonVersion> {
44 [
45 PythonVersion::PY37,
46 PythonVersion::PY38,
47 PythonVersion::PY39,
48 PythonVersion::PY310,
49 PythonVersion::PY311,
50 PythonVersion::PY312,
51 PythonVersion::PY313,
52 PythonVersion::PY314,
53 PythonVersion::PY315,
54 ]
55 .into_iter()
56 }
57
58 pub const fn lowest() -> Self {
60 Self::PY37
61 }
62
63 pub const fn latest() -> Self {
64 Self::PY314
65 }
66
67 pub fn latest_preview() -> Self {
69 let latest_preview = Self::PY315;
70 debug_assert!(latest_preview >= Self::latest());
71 latest_preview
72 }
73
74 pub const fn latest_ty() -> Self {
75 Self::PY314
77 }
78
79 pub const fn as_tuple(self) -> (u8, u8) {
80 (self.major, self.minor)
81 }
82
83 pub fn free_threaded_build_available(self) -> bool {
84 self >= PythonVersion::PY313
85 }
86
87 pub fn supports_pep_701(self) -> bool {
91 self >= Self::PY312
92 }
93
94 pub fn defers_annotations(self) -> bool {
95 self >= Self::PY314
96 }
97}
98
99impl Default for PythonVersion {
100 fn default() -> Self {
101 Self::PY310
102 }
103}
104
105impl From<(u8, u8)> for PythonVersion {
106 fn from(value: (u8, u8)) -> Self {
107 let (major, minor) = value;
108 Self { major, minor }
109 }
110}
111
112impl TryFrom<(i64, i64)> for PythonVersion {
113 type Error = std::num::TryFromIntError;
114
115 fn try_from(value: (i64, i64)) -> Result<Self, Self::Error> {
116 let (major, minor) = value;
117 Ok(Self {
118 major: u8::try_from(major)?,
119 minor: u8::try_from(minor)?,
120 })
121 }
122}
123
124impl fmt::Display for PythonVersion {
125 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126 let PythonVersion { major, minor } = self;
127 write!(f, "{major}.{minor}")
128 }
129}
130
131#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)]
132pub enum PythonVersionDeserializationError {
133 #[error("Invalid python version `{0}`: expected `major.minor`")]
134 WrongPeriodNumber(Box<str>),
135 #[error("Invalid major version `{0}`: {1}")]
136 InvalidMajorVersion(Box<str>, #[source] std::num::ParseIntError),
137 #[error("Invalid minor version `{0}`: {1}")]
138 InvalidMinorVersion(Box<str>, #[source] std::num::ParseIntError),
139}
140
141impl TryFrom<(&str, &str)> for PythonVersion {
142 type Error = PythonVersionDeserializationError;
143
144 fn try_from(value: (&str, &str)) -> Result<Self, Self::Error> {
145 let (major, minor) = value;
146 Ok(Self {
147 major: major.parse().map_err(|err| {
148 PythonVersionDeserializationError::InvalidMajorVersion(Box::from(major), err)
149 })?,
150 minor: minor.parse().map_err(|err| {
151 PythonVersionDeserializationError::InvalidMinorVersion(Box::from(minor), err)
152 })?,
153 })
154 }
155}
156
157impl FromStr for PythonVersion {
158 type Err = PythonVersionDeserializationError;
159
160 fn from_str(s: &str) -> Result<Self, Self::Err> {
161 let (major, minor) = s
162 .split_once('.')
163 .ok_or_else(|| PythonVersionDeserializationError::WrongPeriodNumber(Box::from(s)))?;
164
165 Self::try_from((major, minor)).map_err(|err| {
166 if matches!(
168 err,
169 PythonVersionDeserializationError::InvalidMinorVersion(_, _)
170 ) && minor.contains('.')
171 {
172 PythonVersionDeserializationError::WrongPeriodNumber(Box::from(s))
173 } else {
174 err
175 }
176 })
177 }
178}
179
180#[cfg(feature = "serde")]
181mod serde {
182 use super::PythonVersion;
183
184 impl<'de> serde::Deserialize<'de> for PythonVersion {
185 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
186 where
187 D: serde::Deserializer<'de>,
188 {
189 String::deserialize(deserializer)?
190 .parse()
191 .map_err(serde::de::Error::custom)
192 }
193 }
194
195 impl serde::Serialize for PythonVersion {
196 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
197 where
198 S: serde::Serializer,
199 {
200 serializer.serialize_str(&self.to_string())
201 }
202 }
203}
204
205#[cfg(feature = "schemars")]
206mod schemars {
207 use super::PythonVersion;
208 use schemars::{JsonSchema, Schema, SchemaGenerator};
209 use serde_json::Value;
210
211 impl JsonSchema for PythonVersion {
212 fn schema_name() -> std::borrow::Cow<'static, str> {
213 std::borrow::Cow::Borrowed("PythonVersion")
214 }
215
216 fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
217 let mut any_of: Vec<Value> = vec![
218 schemars::json_schema!({
219 "type": "string",
220 "pattern": r"^\d+\.\d+$",
221 })
222 .into(),
223 ];
224
225 for version in Self::iter() {
226 let mut schema = schemars::json_schema!({
227 "const": version.to_string(),
228 });
229 schema.ensure_object().insert(
230 "description".to_string(),
231 Value::String(format!("Python {version}")),
232 );
233 any_of.push(schema.into());
234 }
235
236 let mut schema = Schema::default();
237 schema
238 .ensure_object()
239 .insert("anyOf".to_string(), Value::Array(any_of));
240 schema
241 }
242 }
243}