Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ pub fn pyclass(attr: TokenStream, item: TokenStream) -> TokenStream {
result_to_tokens(pyclass::impl_pyclass(attr, item))
}

#[proc_macro_attribute]
pub fn pyexception(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr = parse_macro_input!(attr as AttributeArgs);
let item = parse_macro_input!(item as Item);
result_to_tokens(pyclass::impl_pyexception(attr, item))
}

#[proc_macro_attribute]
pub fn pyimpl(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr = parse_macro_input!(attr as AttributeArgs);
Expand Down
51 changes: 51 additions & 0 deletions derive/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,36 @@ pub(crate) fn impl_pyclass(
Ok(ret)
}

/// Special macro to create exception types.
///
/// Why do we need it and why can't we just use `pyclass` macro instead?
/// We generate exception types with a `macro_rules`,
/// similar to how CPython does it.
/// But, inside `macro_rules` we don't have an opportunity
/// to add non-literal attributes to `pyclass`.
/// That's why we have to use this proxy.
pub(crate) fn impl_pyexception(
attr: AttributeArgs,
item: Item,
) -> std::result::Result<TokenStream, Diagnostic> {
let class_name = parse_vec_ident(&attr, &item, 0, "first 'class_name'")?;
let base_class_name = parse_vec_ident(&attr, &item, 1, "second 'base_class_name'")?;

// We also need to strip `Py` prefix from `class_name`,
// due to implementation and Python naming conventions mismatch:
// `PyKeyboardInterrupt` -> `KeyboardInterrupt`
let class_name = class_name.strip_prefix("Py").ok_or_else(|| {
syn::Error::new_spanned(&item, "We require 'class_name' to have 'Py' prefix")
})?;

// We just "proxy" it into `pyclass` macro, because, exception is a class.
let ret = quote! {
#[pyclass(module = false, name = #class_name, base = #base_class_name)]
#item
};
Ok(ret)
}

/// #[pymethod] and #[pyclassmethod]
struct MethodItem {
inner: ContentItemInner,
Expand Down Expand Up @@ -935,3 +965,24 @@ where
}
Ok((result, cfgs))
}

fn parse_vec_ident(
attr: &[NestedMeta],
item: &Item,
index: usize,
message: &str,
) -> std::result::Result<String, Diagnostic> {
Ok(attr
.get(index)
.ok_or_else(|| {
syn::Error::new_spanned(&item, format!("We require {} argument to be set", &message))
})?
.get_ident()
.ok_or_else(|| {
syn::Error::new_spanned(
&item,
format!("We require {} argument to be ident or string", &message),
)
})?
.to_string())
}
1 change: 1 addition & 0 deletions derive/src/pymodule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ fn new_module_item(
inner: ContentItemInner { index, attr_name },
pyattrs: pyattrs.unwrap_or_else(Vec::new),
}),
"pyexception" => unreachable!("#[pyexception] {:?}", pyattrs.unwrap_or_else(Vec::new)),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test that it is not counted in module, I can leave it or remove it.

other => unreachable!("#[pymodule] doesn't accept #[{}]", other),
}
}
Expand Down
5 changes: 3 additions & 2 deletions derive/src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub(crate) const ALL_ALLOWED_NAMES: &[&str] = &[
"pyproperty",
"pyfunction",
"pyclass",
"pyexception",
"pystruct_sequence",
"pyattr",
"pyslot",
Expand Down Expand Up @@ -96,7 +97,7 @@ impl ItemMetaInner {
Err(syn::Error::new_spanned(
ident,
format!(
"#[{}({})] is not one of allowed attributes {}",
"#[{}({})] is not one of allowed attributes [{}]",
meta_ident.to_string(),
name,
allowed_names.join(", ")
Expand Down Expand Up @@ -300,7 +301,7 @@ impl ClassItemMeta {
}.map_err(|span| syn::Error::new(
span,
format!(
"#[{attr_name}(module = ...)] must exist as a string or false. Try #[{attr_name}(module=false)] for built-in types.",
"#[{attr_name}(module = ...)] must exist as a string or false. Try #[{attr_name}(module = false)] for built-in types.",
attr_name=inner.meta_name()
),
))?;
Expand Down
43 changes: 43 additions & 0 deletions extra_tests/snippets/builtin_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,46 @@ class SubError(MyError):
assert isinstance(exc, MyError)
assert exc.__cause__ is None
assert exc.__context__ is e


# Regression to
# https://github.com/RustPython/RustPython/issues/2771

# `BaseException` and `Exception`:
assert BaseException.__new__.__qualname__ == 'BaseException.__new__'
assert BaseException.__init__.__qualname__ == 'BaseException.__init__'
assert BaseException().__dict__ == {}
assert BaseException.__doc__

assert Exception.__new__.__qualname__ == 'Exception.__new__'
assert Exception.__init__.__qualname__ == 'Exception.__init__'
assert Exception().__dict__ == {}
assert Exception.__doc__


# Extends `BaseException`, simple:
assert KeyboardInterrupt.__new__.__qualname__ == 'KeyboardInterrupt.__new__'
assert KeyboardInterrupt.__init__.__qualname__ == 'KeyboardInterrupt.__init__'
assert KeyboardInterrupt().__dict__ == {}
assert KeyboardInterrupt.__doc__


# Extends `Exception`, simple:
assert TypeError.__new__.__qualname__ == 'TypeError.__new__'
assert TypeError.__init__.__qualname__ == 'TypeError.__init__'
assert TypeError().__dict__ == {}
assert TypeError.__doc__


# Extends `Exception`, complex:
assert OSError.__new__.__qualname__ == 'OSError.__new__'
assert OSError.__init__.__qualname__ == 'OSError.__init__'
assert OSError().__dict__ == {}
assert OSError.__doc__
assert OSError.errno
assert OSError.strerror
assert OSError(1, 2).errno
assert OSError(1, 2).strerror

assert ImportError.__init__.__qualname__ == 'ImportError.__init__'
assert ImportError(name='a').name == 'a'
6 changes: 3 additions & 3 deletions vm/src/builtins/dict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ macro_rules! dict_iterator {
$class: ident, $iter_class: ident, $reverse_iter_class: ident,
$class_name: literal, $iter_class_name: literal, $reverse_iter_class_name: literal,
$result_fn: expr) => {
#[pyclass(module=false,name = $class_name)]
#[pyclass(module = false, name = $class_name)]
#[derive(Debug)]
pub(crate) struct $name {
pub dict: PyDictRef,
Expand Down Expand Up @@ -670,7 +670,7 @@ macro_rules! dict_iterator {
}
}

#[pyclass(module=false,name = $iter_class_name)]
#[pyclass(module = false, name = $iter_class_name)]
#[derive(Debug)]
pub(crate) struct $iter_name {
pub dict: PyDictRef,
Expand Down Expand Up @@ -734,7 +734,7 @@ macro_rules! dict_iterator {
}
}

#[pyclass(module=false,name = $reverse_iter_class_name)]
#[pyclass(module = false, name = $reverse_iter_class_name)]
#[derive(Debug)]
pub(crate) struct $reverse_iter_name {
pub dict: PyDictRef,
Expand Down
Loading