Skip to main content

use_python_keyword/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Common hard Python keywords.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum PythonKeyword {
10    False,
11    None,
12    True,
13    And,
14    As,
15    Assert,
16    Async,
17    Await,
18    Break,
19    Class,
20    Continue,
21    Def,
22    Del,
23    Elif,
24    Else,
25    Except,
26    Finally,
27    For,
28    From,
29    Global,
30    If,
31    Import,
32    In,
33    Is,
34    Lambda,
35    Nonlocal,
36    Not,
37    Or,
38    Pass,
39    Raise,
40    Return,
41    Try,
42    While,
43    With,
44    Yield,
45}
46
47impl PythonKeyword {
48    /// Returns the Python source spelling for this keyword.
49    #[must_use]
50    pub const fn as_str(self) -> &'static str {
51        match self {
52            Self::False => "False",
53            Self::None => "None",
54            Self::True => "True",
55            Self::And => "and",
56            Self::As => "as",
57            Self::Assert => "assert",
58            Self::Async => "async",
59            Self::Await => "await",
60            Self::Break => "break",
61            Self::Class => "class",
62            Self::Continue => "continue",
63            Self::Def => "def",
64            Self::Del => "del",
65            Self::Elif => "elif",
66            Self::Else => "else",
67            Self::Except => "except",
68            Self::Finally => "finally",
69            Self::For => "for",
70            Self::From => "from",
71            Self::Global => "global",
72            Self::If => "if",
73            Self::Import => "import",
74            Self::In => "in",
75            Self::Is => "is",
76            Self::Lambda => "lambda",
77            Self::Nonlocal => "nonlocal",
78            Self::Not => "not",
79            Self::Or => "or",
80            Self::Pass => "pass",
81            Self::Raise => "raise",
82            Self::Return => "return",
83            Self::Try => "try",
84            Self::While => "while",
85            Self::With => "with",
86            Self::Yield => "yield",
87        }
88    }
89}
90
91impl fmt::Display for PythonKeyword {
92    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
93        formatter.write_str(self.as_str())
94    }
95}
96
97impl FromStr for PythonKeyword {
98    type Err = PythonKeywordParseError;
99
100    fn from_str(input: &str) -> Result<Self, Self::Err> {
101        let trimmed = non_empty(input)?;
102        match trimmed {
103            "False" => Ok(Self::False),
104            "None" => Ok(Self::None),
105            "True" => Ok(Self::True),
106            "and" => Ok(Self::And),
107            "as" => Ok(Self::As),
108            "assert" => Ok(Self::Assert),
109            "async" => Ok(Self::Async),
110            "await" => Ok(Self::Await),
111            "break" => Ok(Self::Break),
112            "class" => Ok(Self::Class),
113            "continue" => Ok(Self::Continue),
114            "def" => Ok(Self::Def),
115            "del" => Ok(Self::Del),
116            "elif" => Ok(Self::Elif),
117            "else" => Ok(Self::Else),
118            "except" => Ok(Self::Except),
119            "finally" => Ok(Self::Finally),
120            "for" => Ok(Self::For),
121            "from" => Ok(Self::From),
122            "global" => Ok(Self::Global),
123            "if" => Ok(Self::If),
124            "import" => Ok(Self::Import),
125            "in" => Ok(Self::In),
126            "is" => Ok(Self::Is),
127            "lambda" => Ok(Self::Lambda),
128            "nonlocal" => Ok(Self::Nonlocal),
129            "not" => Ok(Self::Not),
130            "or" => Ok(Self::Or),
131            "pass" => Ok(Self::Pass),
132            "raise" => Ok(Self::Raise),
133            "return" => Ok(Self::Return),
134            "try" => Ok(Self::Try),
135            "while" => Ok(Self::While),
136            "with" => Ok(Self::With),
137            "yield" => Ok(Self::Yield),
138            _ => Err(PythonKeywordParseError::Unknown),
139        }
140    }
141}
142
143/// Python soft keywords used by pattern matching and newer syntax contexts.
144#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
145pub enum PythonSoftKeyword {
146    Match,
147    Case,
148    Type,
149    Underscore,
150}
151
152impl PythonSoftKeyword {
153    /// Returns the Python source spelling for this soft keyword.
154    #[must_use]
155    pub const fn as_str(self) -> &'static str {
156        match self {
157            Self::Match => "match",
158            Self::Case => "case",
159            Self::Type => "type",
160            Self::Underscore => "_",
161        }
162    }
163}
164
165impl fmt::Display for PythonSoftKeyword {
166    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167        formatter.write_str(self.as_str())
168    }
169}
170
171impl FromStr for PythonSoftKeyword {
172    type Err = PythonKeywordParseError;
173
174    fn from_str(input: &str) -> Result<Self, Self::Err> {
175        match non_empty(input)? {
176            "match" => Ok(Self::Match),
177            "case" => Ok(Self::Case),
178            "type" => Ok(Self::Type),
179            "_" => Ok(Self::Underscore),
180            _ => Err(PythonKeywordParseError::Unknown),
181        }
182    }
183}
184
185/// A hard or soft reserved Python word.
186#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
187pub enum PythonReservedWord {
188    Keyword(PythonKeyword),
189    SoftKeyword(PythonSoftKeyword),
190}
191
192impl PythonReservedWord {
193    /// Returns the Python source spelling for this reserved word.
194    #[must_use]
195    pub const fn as_str(self) -> &'static str {
196        match self {
197            Self::Keyword(keyword) => keyword.as_str(),
198            Self::SoftKeyword(keyword) => keyword.as_str(),
199        }
200    }
201}
202
203impl fmt::Display for PythonReservedWord {
204    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
205        formatter.write_str(self.as_str())
206    }
207}
208
209impl FromStr for PythonReservedWord {
210    type Err = PythonKeywordParseError;
211
212    fn from_str(input: &str) -> Result<Self, Self::Err> {
213        PythonKeyword::from_str(input).map_or_else(
214            |_| PythonSoftKeyword::from_str(input).map(Self::SoftKeyword),
215            |keyword| Ok(Self::Keyword(keyword)),
216        )
217    }
218}
219
220/// Error returned when a Python keyword label is empty or unknown.
221#[derive(Clone, Copy, Debug, Eq, PartialEq)]
222pub enum PythonKeywordParseError {
223    Empty,
224    Unknown,
225}
226
227impl fmt::Display for PythonKeywordParseError {
228    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
229        match self {
230            Self::Empty => formatter.write_str("Python keyword cannot be empty"),
231            Self::Unknown => formatter.write_str("unknown Python keyword"),
232        }
233    }
234}
235
236impl Error for PythonKeywordParseError {}
237
238/// Returns whether `input` is a hard Python keyword.
239#[must_use]
240pub fn is_python_keyword(input: &str) -> bool {
241    input.parse::<PythonKeyword>().is_ok()
242}
243
244/// Returns whether `input` is a Python soft keyword.
245#[must_use]
246pub fn is_python_soft_keyword(input: &str) -> bool {
247    input.parse::<PythonSoftKeyword>().is_ok()
248}
249
250/// Returns whether `input` is either a hard or soft Python reserved word.
251#[must_use]
252pub fn is_python_reserved_word(input: &str) -> bool {
253    input.parse::<PythonReservedWord>().is_ok()
254}
255
256fn non_empty(input: &str) -> Result<&str, PythonKeywordParseError> {
257    let trimmed = input.trim();
258    if trimmed.is_empty() {
259        Err(PythonKeywordParseError::Empty)
260    } else {
261        Ok(trimmed)
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::{
268        PythonKeyword, PythonKeywordParseError, PythonReservedWord, PythonSoftKeyword,
269        is_python_keyword, is_python_reserved_word, is_python_soft_keyword,
270    };
271
272    #[test]
273    fn parses_and_displays_hard_keywords() -> Result<(), PythonKeywordParseError> {
274        assert_eq!("False".parse::<PythonKeyword>()?, PythonKeyword::False);
275        assert_eq!("async".parse::<PythonKeyword>()?, PythonKeyword::Async);
276        assert_eq!(PythonKeyword::Return.to_string(), "return");
277        assert!(is_python_keyword("class"));
278        assert!(!is_python_keyword("match"));
279        Ok(())
280    }
281
282    #[test]
283    fn parses_soft_keywords_and_reserved_words() -> Result<(), PythonKeywordParseError> {
284        assert_eq!(
285            "match".parse::<PythonSoftKeyword>()?,
286            PythonSoftKeyword::Match
287        );
288        assert_eq!(PythonSoftKeyword::Underscore.to_string(), "_");
289        assert_eq!(
290            "case".parse::<PythonReservedWord>()?,
291            PythonReservedWord::SoftKeyword(PythonSoftKeyword::Case)
292        );
293        assert!(is_python_soft_keyword("type"));
294        assert!(is_python_reserved_word("lambda"));
295        Ok(())
296    }
297
298    #[test]
299    fn rejects_empty_and_unknown_labels() {
300        assert_eq!(
301            "".parse::<PythonKeyword>(),
302            Err(PythonKeywordParseError::Empty)
303        );
304        assert_eq!(
305            "FALSE".parse::<PythonKeyword>(),
306            Err(PythonKeywordParseError::Unknown)
307        );
308    }
309}