Skip to content

Commit 67b3388

Browse files
authored
Merge pull request #2897 from sobolevn/better-exceptions
Adds better exception macro
2 parents aff0e3c + a725e2b commit 67b3388

File tree

8 files changed

+275
-25
lines changed

8 files changed

+275
-25
lines changed

derive/src/lib.rs

+7
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ pub fn pyclass(attr: TokenStream, item: TokenStream) -> TokenStream {
4141
result_to_tokens(pyclass::impl_pyclass(attr, item))
4242
}
4343

44+
#[proc_macro_attribute]
45+
pub fn pyexception(attr: TokenStream, item: TokenStream) -> TokenStream {
46+
let attr = parse_macro_input!(attr as AttributeArgs);
47+
let item = parse_macro_input!(item as Item);
48+
result_to_tokens(pyclass::impl_pyexception(attr, item))
49+
}
50+
4451
#[proc_macro_attribute]
4552
pub fn pyimpl(attr: TokenStream, item: TokenStream) -> TokenStream {
4653
let attr = parse_macro_input!(attr as AttributeArgs);

derive/src/pyclass.rs

+51
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,36 @@ pub(crate) fn impl_pyclass(
258258
Ok(ret)
259259
}
260260

261+
/// Special macro to create exception types.
262+
///
263+
/// Why do we need it and why can't we just use `pyclass` macro instead?
264+
/// We generate exception types with a `macro_rules`,
265+
/// similar to how CPython does it.
266+
/// But, inside `macro_rules` we don't have an opportunity
267+
/// to add non-literal attributes to `pyclass`.
268+
/// That's why we have to use this proxy.
269+
pub(crate) fn impl_pyexception(
270+
attr: AttributeArgs,
271+
item: Item,
272+
) -> std::result::Result<TokenStream, Diagnostic> {
273+
let class_name = parse_vec_ident(&attr, &item, 0, "first 'class_name'")?;
274+
let base_class_name = parse_vec_ident(&attr, &item, 1, "second 'base_class_name'")?;
275+
276+
// We also need to strip `Py` prefix from `class_name`,
277+
// due to implementation and Python naming conventions mismatch:
278+
// `PyKeyboardInterrupt` -> `KeyboardInterrupt`
279+
let class_name = class_name.strip_prefix("Py").ok_or_else(|| {
280+
syn::Error::new_spanned(&item, "We require 'class_name' to have 'Py' prefix")
281+
})?;
282+
283+
// We just "proxy" it into `pyclass` macro, because, exception is a class.
284+
let ret = quote! {
285+
#[pyclass(module = false, name = #class_name, base = #base_class_name)]
286+
#item
287+
};
288+
Ok(ret)
289+
}
290+
261291
/// #[pymethod] and #[pyclassmethod]
262292
struct MethodItem {
263293
inner: ContentItemInner,
@@ -973,3 +1003,24 @@ where
9731003
}
9741004
Ok((result, cfgs))
9751005
}
1006+
1007+
fn parse_vec_ident(
1008+
attr: &[NestedMeta],
1009+
item: &Item,
1010+
index: usize,
1011+
message: &str,
1012+
) -> std::result::Result<String, Diagnostic> {
1013+
Ok(attr
1014+
.get(index)
1015+
.ok_or_else(|| {
1016+
syn::Error::new_spanned(&item, format!("We require {} argument to be set", &message))
1017+
})?
1018+
.get_ident()
1019+
.ok_or_else(|| {
1020+
syn::Error::new_spanned(
1021+
&item,
1022+
format!("We require {} argument to be ident or string", &message),
1023+
)
1024+
})?
1025+
.to_string())
1026+
}

derive/src/pymodule.rs

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ fn new_module_item(
109109
inner: ContentItemInner { index, attr_name },
110110
pyattrs: pyattrs.unwrap_or_else(Vec::new),
111111
}),
112+
"pyexception" => unreachable!("#[pyexception] {:?}", pyattrs.unwrap_or_else(Vec::new)),
112113
other => unreachable!("#[pymodule] doesn't accept #[{}]", other),
113114
}
114115
}

derive/src/util.rs

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ pub(crate) const ALL_ALLOWED_NAMES: &[&str] = &[
1212
"pyproperty",
1313
"pyfunction",
1414
"pyclass",
15+
"pyexception",
1516
"pystruct_sequence",
1617
"pyattr",
1718
"pyslot",
@@ -96,7 +97,7 @@ impl ItemMetaInner {
9697
Err(syn::Error::new_spanned(
9798
ident,
9899
format!(
99-
"#[{}({})] is not one of allowed attributes {}",
100+
"#[{}({})] is not one of allowed attributes [{}]",
100101
meta_ident.to_string(),
101102
name,
102103
allowed_names.join(", ")
@@ -300,7 +301,7 @@ impl ClassItemMeta {
300301
}.map_err(|span| syn::Error::new(
301302
span,
302303
format!(
303-
"#[{attr_name}(module = ...)] must exist as a string or false. Try #[{attr_name}(module=false)] for built-in types.",
304+
"#[{attr_name}(module = ...)] must exist as a string or false. Try #[{attr_name}(module = false)] for built-in types.",
304305
attr_name=inner.meta_name()
305306
),
306307
))?;

extra_tests/snippets/builtin_exceptions.py

+43
Original file line numberDiff line numberDiff line change
@@ -152,3 +152,46 @@ class SubError(MyError):
152152
assert isinstance(exc, MyError)
153153
assert exc.__cause__ is None
154154
assert exc.__context__ is e
155+
156+
157+
# Regression to
158+
# https://github.com/RustPython/RustPython/issues/2771
159+
160+
# `BaseException` and `Exception`:
161+
assert BaseException.__new__.__qualname__ == 'BaseException.__new__'
162+
assert BaseException.__init__.__qualname__ == 'BaseException.__init__'
163+
assert BaseException().__dict__ == {}
164+
assert BaseException.__doc__
165+
166+
assert Exception.__new__.__qualname__ == 'Exception.__new__'
167+
assert Exception.__init__.__qualname__ == 'Exception.__init__'
168+
assert Exception().__dict__ == {}
169+
assert Exception.__doc__
170+
171+
172+
# Extends `BaseException`, simple:
173+
assert KeyboardInterrupt.__new__.__qualname__ == 'KeyboardInterrupt.__new__'
174+
assert KeyboardInterrupt.__init__.__qualname__ == 'KeyboardInterrupt.__init__'
175+
assert KeyboardInterrupt().__dict__ == {}
176+
assert KeyboardInterrupt.__doc__
177+
178+
179+
# Extends `Exception`, simple:
180+
assert TypeError.__new__.__qualname__ == 'TypeError.__new__'
181+
assert TypeError.__init__.__qualname__ == 'TypeError.__init__'
182+
assert TypeError().__dict__ == {}
183+
assert TypeError.__doc__
184+
185+
186+
# Extends `Exception`, complex:
187+
assert OSError.__new__.__qualname__ == 'OSError.__new__'
188+
assert OSError.__init__.__qualname__ == 'OSError.__init__'
189+
assert OSError().__dict__ == {}
190+
assert OSError.__doc__
191+
assert OSError.errno
192+
assert OSError.strerror
193+
assert OSError(1, 2).errno
194+
assert OSError(1, 2).strerror
195+
196+
assert ImportError.__init__.__qualname__ == 'ImportError.__init__'
197+
assert ImportError(name='a').name == 'a'

vm/src/builtins/dict.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -692,7 +692,7 @@ macro_rules! dict_iterator {
692692
}
693693
}
694694

695-
#[pyclass(module=false,name = $iter_class_name)]
695+
#[pyclass(module = false, name = $iter_class_name)]
696696
#[derive(Debug)]
697697
pub(crate) struct $iter_name {
698698
pub dict: PyDictRef,
@@ -752,7 +752,7 @@ macro_rules! dict_iterator {
752752
}
753753
}
754754

755-
#[pyclass(module=false,name = $reverse_iter_class_name)]
755+
#[pyclass(module = false, name = $reverse_iter_class_name)]
756756
#[derive(Debug)]
757757
pub(crate) struct $reverse_iter_name {
758758
pub dict: PyDictRef,

0 commit comments

Comments
 (0)