Skip to content
Merged
1 change: 0 additions & 1 deletion Lib/test/exception_hierarchy.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ BaseException
| +-- NotImplementedError
| +-- RecursionError
+-- SyntaxError
| +-- TargetScopeError
| +-- IndentationError
| +-- TabError
+-- SystemError
Expand Down
18 changes: 18 additions & 0 deletions derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ pub fn pyclass(attr: TokenStream, item: TokenStream) -> TokenStream {
result_to_tokens(pyclass::impl_pyclass(attr, item))
}

/// This macro serves a goal of generating multiple
/// `BaseException` / `Exception`
/// subtypes in a uniform and convenient manner.
/// It looks like `SimpleExtendsException` in `CPython`.
/// https://github.com/python/cpython/blob/main/Objects/exceptions.c
///
/// We need `ctx` to be ready to add
/// `properties` / `custom` constructors / slots / methods etc.
/// So, we use `extend_class!` macro as the second
/// step in exception type definition.
#[proc_macro]
pub fn define_exception(input: TokenStream) -> TokenStream {
let exc_def = parse_macro_input!(input as pyclass::PyExceptionDef);
result_to_tokens(pyclass::impl_define_exception(exc_def))
}

/// Helper macro to define `Exception` types.
/// More-or-less is an alias to `pyclass` macro.
#[proc_macro_attribute]
pub fn pyexception(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr = parse_macro_input!(attr as AttributeArgs);
Expand Down
113 changes: 112 additions & 1 deletion derive/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ use crate::util::{
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use std::collections::HashMap;
use syn::parse::{Parse, ParseStream, Result as ParsingResult};
use syn::{
parse_quote, spanned::Spanned, Attribute, AttributeArgs, Ident, Item, Meta, NestedMeta, Result,
parse_quote, spanned::Spanned, Attribute, AttributeArgs, Ident, Item, LitStr, Meta, NestedMeta,
Result, Token,
};
use syn_ext::ext::*;

Expand Down Expand Up @@ -288,6 +290,71 @@ pub(crate) fn impl_pyexception(
Ok(ret)
}

pub(crate) fn impl_define_exception(
exc_def: PyExceptionDef,
) -> std::result::Result<TokenStream, Diagnostic> {
let PyExceptionDef {
class_name,
base_class,
ctx_name,
docs,
tp_new,
init,
} = exc_def;

// We need this method, because of how `CPython` copies `__new__`
// from `BaseException` in `SimpleExtendsException` macro.
// See: `BaseException_new`
let tp_new_slot = match tp_new {
Some(tp_call) => quote! { #tp_call(cls, args, vm) },
None => quote! { #base_class::tp_new(cls, args, vm) },
};

// We need this method, because of how `CPython` copies `__init__`
// from `BaseException` in `SimpleExtendsException` macro.
// See: `(initproc)BaseException_init`
let init_method = match init {
Some(init_def) => quote! { #init_def(zelf, args, vm) },
None => quote! { #base_class::init(zelf, args, vm) },
};

let ret = quote! {
#[pyexception(#class_name, #base_class)]
#[derive(Debug)]
#[doc = #docs]
struct #class_name {}

// We need this to make extend mechanism work:
impl PyValue for #class_name {
fn class(vm: &VirtualMachine) -> &PyTypeRef {
&vm.ctx.exceptions.#ctx_name
}
}

#[pyimpl(flags(BASETYPE, HAS_DICT))]
impl #class_name {
#[pyslot]
pub(crate) fn tp_new(
cls: PyTypeRef,
args: FuncArgs,
vm: &VirtualMachine,
) -> PyResult {
#tp_new_slot
}

#[pymethod(magic)]
pub(crate) fn init(
zelf: PyRef<PyBaseException>,
args: FuncArgs,
vm: &VirtualMachine,
) -> PyResult<()> {
#init_method
}
}
};
Ok(ret)
}

/// #[pymethod] and #[pyclassmethod]
struct MethodItem {
inner: ContentItemInner,
Expand Down Expand Up @@ -971,6 +1038,50 @@ where
Ok((result, cfgs))
}

#[derive(Debug)]
pub(crate) struct PyExceptionDef {
pub class_name: Ident,
pub base_class: Ident,
pub ctx_name: Ident,
pub docs: LitStr,

/// Holds optional `tp_new` slot to be used instead of a default one:
pub tp_new: Option<Ident>,
/// We also store `__init__` magic method, that can
pub init: Option<Ident>,
}

impl Parse for PyExceptionDef {
fn parse(input: ParseStream) -> ParsingResult<Self> {
let class_name: Ident = input.parse()?;
input.parse::<Token![,]>()?;

let base_class: Ident = input.parse()?;
input.parse::<Token![,]>()?;

let ctx_name: Ident = input.parse()?;
input.parse::<Token![,]>()?;

let docs: LitStr = input.parse()?;
input.parse::<Option<Token![,]>>()?;

let tp_new: Option<Ident> = input.parse()?;
input.parse::<Option<Token![,]>>()?;

let init: Option<Ident> = input.parse()?;
input.parse::<Option<Token![,]>>()?; // leading `,`

Ok(PyExceptionDef {
class_name,
base_class,
ctx_name,
docs,
tp_new,
init,
})
}
}

fn parse_vec_ident(
attr: &[NestedMeta],
item: &Item,
Expand Down
1 change: 0 additions & 1 deletion derive/src/pymodule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ 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)),
other => unreachable!("#[pymodule] doesn't accept #[{}]", other),
}
}
Expand Down
19 changes: 14 additions & 5 deletions extra_tests/snippets/builtin_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import builtins
import platform
import sys

Expand Down Expand Up @@ -161,37 +162,45 @@ class SubError(MyError):
assert BaseException.__new__.__qualname__ == 'BaseException.__new__'
assert BaseException.__init__.__qualname__ == 'BaseException.__init__'
assert BaseException().__dict__ == {}
assert BaseException.__doc__

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why are these lines deleted?

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.


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

# Custom `__new__` and `__init__`:
assert ImportError.__init__.__qualname__ == 'ImportError.__init__'
assert ImportError(name='a').name == 'a'
assert (
ModuleNotFoundError.__init__.__qualname__ == 'ModuleNotFoundError.__init__'
)
assert ModuleNotFoundError(name='a').name == 'a'


# Check that all exceptions have string `__doc__`:
for exc in filter(
lambda obj: isinstance(obj, BaseException),
vars(builtins).values(),
):
assert isinstance(exc.__doc__, str)
1 change: 0 additions & 1 deletion vm/src/builtins/make_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,6 @@ pub fn make_module(vm: &VirtualMachine, module: PyObjectRef) {
"NotImplementedError" => ctx.exceptions.not_implemented_error.clone(),
"RecursionError" => ctx.exceptions.recursion_error.clone(),
"SyntaxError" => ctx.exceptions.syntax_error.clone(),
"TargetScopeError" => ctx.exceptions.target_scope_error.clone(),
"IndentationError" => ctx.exceptions.indentation_error.clone(),
"TabError" => ctx.exceptions.tab_error.clone(),
"SystemError" => ctx.exceptions.system_error.clone(),
Expand Down
Loading