1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7#[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 #[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#[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 #[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#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
187pub enum PythonReservedWord {
188 Keyword(PythonKeyword),
189 SoftKeyword(PythonSoftKeyword),
190}
191
192impl PythonReservedWord {
193 #[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#[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#[must_use]
240pub fn is_python_keyword(input: &str) -> bool {
241 input.parse::<PythonKeyword>().is_ok()
242}
243
244#[must_use]
246pub fn is_python_soft_keyword(input: &str) -> bool {
247 input.parse::<PythonSoftKeyword>().is_ok()
248}
249
250#[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}