Skip to main content

ruff_python_ast/
python_version.rs

1use std::{fmt, str::FromStr};
2
3/// Representation of a Python version.
4///
5/// N.B. This does not necessarily represent a Python version that we actually support.
6#[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    /// The minimum supported Python version.
59    pub const fn lowest() -> Self {
60        Self::PY37
61    }
62
63    pub const fn latest() -> Self {
64        Self::PY314
65    }
66
67    /// The latest Python version supported in preview
68    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        // Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version.
76        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    /// Return `true` if the current version supports [PEP 701].
88    ///
89    /// [PEP 701]: https://peps.python.org/pep-0701/
90    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            // Give a better error message for something like `3.8.5` or `3..8`
167            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}