Skip to content

Commit b5c03c6

Browse files
committed
Adds pyexception macro
1 parent 0ab0c0b commit b5c03c6

File tree

5 files changed

+67
-13
lines changed

5 files changed

+67
-13
lines changed

derive/src/lib.rs

Lines changed: 7 additions & 0 deletions
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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,39 @@ 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'".to_owned())?;
274+
let base_class_name = parse_vec_ident(&attr, &item, 1, "first 'base_class_name'".to_owned())?;
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
280+
.strip_prefix("Py")
281+
.ok_or_else(|| syn::Error::new_spanned(
282+
&item,
283+
"We require 'class_name' to have 'Py' prefix",
284+
))?;
285+
286+
// We just "proxy" it into `pyclass` macro, because, exception is a class.
287+
let ret = quote! {
288+
#[pyclass(module = false, name = #class_name, base = #base_class_name)]
289+
#item
290+
};
291+
Ok(ret)
292+
}
293+
261294
/// #[pymethod] and #[pyclassmethod]
262295
struct MethodItem {
263296
inner: ContentItemInner,
@@ -935,3 +968,23 @@ where
935968
}
936969
Ok((result, cfgs))
937970
}
971+
972+
fn parse_vec_ident(
973+
attr: &[NestedMeta],
974+
item: &Item,
975+
index: usize,
976+
message: String,
977+
) -> std::result::Result<String, Diagnostic> {
978+
Ok(attr
979+
.get(index)
980+
.ok_or_else(|| syn::Error::new_spanned(
981+
&item,
982+
format!("We require {} argument to be set", &message),
983+
))?
984+
.get_ident()
985+
.ok_or_else(|| syn::Error::new_spanned(
986+
&item,
987+
format!("We require {} argument to be ident or string", &message),
988+
))?
989+
.to_string())
990+
}

derive/src/pymodule.rs

Lines changed: 1 addition & 0 deletions
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

Lines changed: 2 additions & 1 deletion
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(", ")

vm/src/exceptions.rs

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -526,16 +526,12 @@ macro_rules! extends_exception {
526526
// This case is like `SimpleExtendsException`:
527527
(
528528
$class_name: ident,
529-
$class_name_literal: literal,
530529
$base_class: ident,
531-
$base_class_literal: literal,
532530
$docs: tt
533531
) => {
534532
extends_exception!(
535533
$class_name,
536-
$class_name_literal,
537534
$base_class,
538-
$base_class_literal,
539535
$docs,
540536
{}
541537
);
@@ -546,13 +542,11 @@ macro_rules! extends_exception {
546542
// This case is like `ComplexExtendsException`:
547543
(
548544
$class_name: ident,
549-
$class_name_literal: literal,
550545
$base_class: ident,
551-
$base_class_literal: literal,
552546
$docs: tt,
553547
{ $($attr_name:expr => $attr_value:expr),* $(,)* }
554548
) => {
555-
#[pyclass(module = false, name = $class_name_literal, base = $base_class_literal)]
549+
#[pyexception($class_name, $base_class)]
556550
#[derive(Debug)]
557551
struct $class_name {}
558552

@@ -585,11 +579,9 @@ macro_rules! extends_exception {
585579
}
586580

587581
extends_exception! {
588-
PyKeyboardInterrupt,
589-
"KeyboardInterrupt",
590-
PyBaseException,
591-
"PyBaseException",
592-
"Program interrupted by user."
582+
PyKeyboardInterrupt,
583+
PyBaseException,
584+
"Program interrupted by user."
593585
}
594586

595587
impl ExceptionZoo {

0 commit comments

Comments
 (0)