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 fmt::Display for PythonVersion {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        let PythonVersion { major, minor } = self;
115        write!(f, "{major}.{minor}")
116    }
117}
118
119#[derive(thiserror::Error, Debug, PartialEq, Eq, Clone)]
120pub enum PythonVersionDeserializationError {
121    #[error("Invalid python version `{0}`: expected `major.minor`")]
122    WrongPeriodNumber(Box<str>),
123    #[error("Invalid major version `{0}`: {1}")]
124    InvalidMajorVersion(Box<str>, #[source] std::num::ParseIntError),
125    #[error("Invalid minor version `{0}`: {1}")]
126    InvalidMinorVersion(Box<str>, #[source] std::num::ParseIntError),
127}
128
129impl TryFrom<(&str, &str)> for PythonVersion {
130    type Error = PythonVersionDeserializationError;
131
132    fn try_from(value: (&str, &str)) -> Result<Self, Self::Error> {
133        let (major, minor) = value;
134        Ok(Self {
135            major: major.parse().map_err(|err| {
136                PythonVersionDeserializationError::InvalidMajorVersion(Box::from(major), err)
137            })?,
138            minor: minor.parse().map_err(|err| {
139                PythonVersionDeserializationError::InvalidMinorVersion(Box::from(minor), err)
140            })?,
141        })
142    }
143}
144
145impl FromStr for PythonVersion {
146    type Err = PythonVersionDeserializationError;
147
148    fn from_str(s: &str) -> Result<Self, Self::Err> {
149        let (major, minor) = s
150            .split_once('.')
151            .ok_or_else(|| PythonVersionDeserializationError::WrongPeriodNumber(Box::from(s)))?;
152
153        Self::try_from((major, minor)).map_err(|err| {
154            // Give a better error message for something like `3.8.5` or `3..8`
155            if matches!(
156                err,
157                PythonVersionDeserializationError::InvalidMinorVersion(_, _)
158            ) && minor.contains('.')
159            {
160                PythonVersionDeserializationError::WrongPeriodNumber(Box::from(s))
161            } else {
162                err
163            }
164        })
165    }
166}
167
168#[cfg(feature = "serde")]
169mod serde {
170    use super::PythonVersion;
171
172    impl<'de> serde::Deserialize<'de> for PythonVersion {
173        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174        where
175            D: serde::Deserializer<'de>,
176        {
177            String::deserialize(deserializer)?
178                .parse()
179                .map_err(serde::de::Error::custom)
180        }
181    }
182
183    impl serde::Serialize for PythonVersion {
184        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
185        where
186            S: serde::Serializer,
187        {
188            serializer.serialize_str(&self.to_string())
189        }
190    }
191}
192
193#[cfg(feature = "schemars")]
194mod schemars {
195    use super::PythonVersion;
196    use schemars::{JsonSchema, Schema, SchemaGenerator};
197    use serde_json::Value;
198
199    impl JsonSchema for PythonVersion {
200        fn schema_name() -> std::borrow::Cow<'static, str> {
201            std::borrow::Cow::Borrowed("PythonVersion")
202        }
203
204        fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
205            let mut any_of: Vec<Value> = vec![
206                schemars::json_schema!({
207                    "type": "string",
208                    "pattern": r"^\d+\.\d+$",
209                })
210                .into(),
211            ];
212
213            for version in Self::iter() {
214                let mut schema = schemars::json_schema!({
215                    "const": version.to_string(),
216                });
217                schema.ensure_object().insert(
218                    "description".to_string(),
219                    Value::String(format!("Python {version}")),
220                );
221                any_of.push(schema.into());
222            }
223
224            let mut schema = Schema::default();
225            schema
226                .ensure_object()
227                .insert("anyOf".to_string(), Value::Array(any_of));
228            schema
229        }
230    }
231}