Skip to content

Commit b723bbf

Browse files
authored
All exceptions are now modified with extend_exception! macro
1 parent 00a86ba commit b723bbf

File tree

7 files changed

+717
-199
lines changed

7 files changed

+717
-199
lines changed

Lib/test/exception_hierarchy.txt

-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ BaseException
4242
| +-- NotImplementedError
4343
| +-- RecursionError
4444
+-- SyntaxError
45-
| +-- TargetScopeError
4645
| +-- IndentationError
4746
| +-- TabError
4847
+-- SystemError

derive/src/lib.rs

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

44+
/// This macro serves a goal of generating multiple
45+
/// `BaseException` / `Exception`
46+
/// subtypes in a uniform and convenient manner.
47+
/// It looks like `SimpleExtendsException` in `CPython`.
48+
/// https://github.com/python/cpython/blob/main/Objects/exceptions.c
49+
///
50+
/// We need `ctx` to be ready to add
51+
/// `properties` / `custom` constructors / slots / methods etc.
52+
/// So, we use `extend_class!` macro as the second
53+
/// step in exception type definition.
54+
#[proc_macro]
55+
pub fn define_exception(input: TokenStream) -> TokenStream {
56+
let exc_def = parse_macro_input!(input as pyclass::PyExceptionDef);
57+
result_to_tokens(pyclass::impl_define_exception(exc_def))
58+
}
59+
60+
/// Helper macro to define `Exception` types.
61+
/// More-or-less is an alias to `pyclass` macro.
4462
#[proc_macro_attribute]
4563
pub fn pyexception(attr: TokenStream, item: TokenStream) -> TokenStream {
4664
let attr = parse_macro_input!(attr as AttributeArgs);

derive/src/pyclass.rs

+112-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ use crate::util::{
66
use proc_macro2::TokenStream;
77
use quote::{quote, quote_spanned, ToTokens};
88
use std::collections::HashMap;
9+
use syn::parse::{Parse, ParseStream, Result as ParsingResult};
910
use syn::{
10-
parse_quote, spanned::Spanned, Attribute, AttributeArgs, Ident, Item, Meta, NestedMeta, Result,
11+
parse_quote, spanned::Spanned, Attribute, AttributeArgs, Ident, Item, LitStr, Meta, NestedMeta,
12+
Result, Token,
1113
};
1214
use syn_ext::ext::*;
1315

@@ -288,6 +290,71 @@ pub(crate) fn impl_pyexception(
288290
Ok(ret)
289291
}
290292

293+
pub(crate) fn impl_define_exception(
294+
exc_def: PyExceptionDef,
295+
) -> std::result::Result<TokenStream, Diagnostic> {
296+
let PyExceptionDef {
297+
class_name,
298+
base_class,
299+
ctx_name,
300+
docs,
301+
tp_new,
302+
init,
303+
} = exc_def;
304+
305+
// We need this method, because of how `CPython` copies `__new__`
306+
// from `BaseException` in `SimpleExtendsException` macro.
307+
// See: `BaseException_new`
308+
let tp_new_slot = match tp_new {
309+
Some(tp_call) => quote! { #tp_call(cls, args, vm) },
310+
None => quote! { #base_class::tp_new(cls, args, vm) },
311+
};
312+
313+
// We need this method, because of how `CPython` copies `__init__`
314+
// from `BaseException` in `SimpleExtendsException` macro.
315+
// See: `(initproc)BaseException_init`
316+
let init_method = match init {
317+
Some(init_def) => quote! { #init_def(zelf, args, vm) },
318+
None => quote! { #base_class::init(zelf, args, vm) },
319+
};
320+
321+
let ret = quote! {
322+
#[pyexception(#class_name, #base_class)]
323+
#[derive(Debug)]
324+
#[doc = #docs]
325+
struct #class_name {}
326+
327+
// We need this to make extend mechanism work:
328+
impl PyValue for #class_name {
329+
fn class(vm: &VirtualMachine) -> &PyTypeRef {
330+
&vm.ctx.exceptions.#ctx_name
331+
}
332+
}
333+
334+
#[pyimpl(flags(BASETYPE, HAS_DICT))]
335+
impl #class_name {
336+
#[pyslot]
337+
pub(crate) fn tp_new(
338+
cls: PyTypeRef,
339+
args: FuncArgs,
340+
vm: &VirtualMachine,
341+
) -> PyResult {
342+
#tp_new_slot
343+
}
344+
345+
#[pymethod(magic)]
346+
pub(crate) fn init(
347+
zelf: PyRef<PyBaseException>,
348+
args: FuncArgs,
349+
vm: &VirtualMachine,
350+
) -> PyResult<()> {
351+
#init_method
352+
}
353+
}
354+
};
355+
Ok(ret)
356+
}
357+
291358
/// #[pymethod] and #[pyclassmethod]
292359
struct MethodItem {
293360
inner: ContentItemInner,
@@ -971,6 +1038,50 @@ where
9711038
Ok((result, cfgs))
9721039
}
9731040

1041+
#[derive(Debug)]
1042+
pub(crate) struct PyExceptionDef {
1043+
pub class_name: Ident,
1044+
pub base_class: Ident,
1045+
pub ctx_name: Ident,
1046+
pub docs: LitStr,
1047+
1048+
/// Holds optional `tp_new` slot to be used instead of a default one:
1049+
pub tp_new: Option<Ident>,
1050+
/// We also store `__init__` magic method, that can
1051+
pub init: Option<Ident>,
1052+
}
1053+
1054+
impl Parse for PyExceptionDef {
1055+
fn parse(input: ParseStream) -> ParsingResult<Self> {
1056+
let class_name: Ident = input.parse()?;
1057+
input.parse::<Token![,]>()?;
1058+
1059+
let base_class: Ident = input.parse()?;
1060+
input.parse::<Token![,]>()?;
1061+
1062+
let ctx_name: Ident = input.parse()?;
1063+
input.parse::<Token![,]>()?;
1064+
1065+
let docs: LitStr = input.parse()?;
1066+
input.parse::<Option<Token![,]>>()?;
1067+
1068+
let tp_new: Option<Ident> = input.parse()?;
1069+
input.parse::<Option<Token![,]>>()?;
1070+
1071+
let init: Option<Ident> = input.parse()?;
1072+
input.parse::<Option<Token![,]>>()?; // leading `,`
1073+
1074+
Ok(PyExceptionDef {
1075+
class_name,
1076+
base_class,
1077+
ctx_name,
1078+
docs,
1079+
tp_new,
1080+
init,
1081+
})
1082+
}
1083+
}
1084+
9741085
fn parse_vec_ident(
9751086
attr: &[NestedMeta],
9761087
item: &Item,

derive/src/pymodule.rs

-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ 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)),
113112
other => unreachable!("#[pymodule] doesn't accept #[{}]", other),
114113
}
115114
}

extra_tests/snippets/builtin_exceptions.py

+14-5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import builtins
12
import platform
23
import sys
34

@@ -161,37 +162,45 @@ class SubError(MyError):
161162
assert BaseException.__new__.__qualname__ == 'BaseException.__new__'
162163
assert BaseException.__init__.__qualname__ == 'BaseException.__init__'
163164
assert BaseException().__dict__ == {}
164-
assert BaseException.__doc__
165165

166166
assert Exception.__new__.__qualname__ == 'Exception.__new__'
167167
assert Exception.__init__.__qualname__ == 'Exception.__init__'
168168
assert Exception().__dict__ == {}
169-
assert Exception.__doc__
170169

171170

172171
# Extends `BaseException`, simple:
173172
assert KeyboardInterrupt.__new__.__qualname__ == 'KeyboardInterrupt.__new__'
174173
assert KeyboardInterrupt.__init__.__qualname__ == 'KeyboardInterrupt.__init__'
175174
assert KeyboardInterrupt().__dict__ == {}
176-
assert KeyboardInterrupt.__doc__
177175

178176

179177
# Extends `Exception`, simple:
180178
assert TypeError.__new__.__qualname__ == 'TypeError.__new__'
181179
assert TypeError.__init__.__qualname__ == 'TypeError.__init__'
182180
assert TypeError().__dict__ == {}
183-
assert TypeError.__doc__
184181

185182

186183
# Extends `Exception`, complex:
187184
assert OSError.__new__.__qualname__ == 'OSError.__new__'
188185
assert OSError.__init__.__qualname__ == 'OSError.__init__'
189186
assert OSError().__dict__ == {}
190-
assert OSError.__doc__
191187
assert OSError.errno
192188
assert OSError.strerror
193189
assert OSError(1, 2).errno
194190
assert OSError(1, 2).strerror
195191

192+
# Custom `__new__` and `__init__`:
196193
assert ImportError.__init__.__qualname__ == 'ImportError.__init__'
197194
assert ImportError(name='a').name == 'a'
195+
assert (
196+
ModuleNotFoundError.__init__.__qualname__ == 'ModuleNotFoundError.__init__'
197+
)
198+
assert ModuleNotFoundError(name='a').name == 'a'
199+
200+
201+
# Check that all exceptions have string `__doc__`:
202+
for exc in filter(
203+
lambda obj: isinstance(obj, BaseException),
204+
vars(builtins).values(),
205+
):
206+
assert isinstance(exc.__doc__, str)

vm/src/builtins/make_module.rs

-1
Original file line numberDiff line numberDiff line change
@@ -988,7 +988,6 @@ pub fn make_module(vm: &VirtualMachine, module: PyObjectRef) {
988988
"NotImplementedError" => ctx.exceptions.not_implemented_error.clone(),
989989
"RecursionError" => ctx.exceptions.recursion_error.clone(),
990990
"SyntaxError" => ctx.exceptions.syntax_error.clone(),
991-
"TargetScopeError" => ctx.exceptions.target_scope_error.clone(),
992991
"IndentationError" => ctx.exceptions.indentation_error.clone(),
993992
"TabError" => ctx.exceptions.tab_error.clone(),
994993
"SystemError" => ctx.exceptions.system_error.clone(),

0 commit comments

Comments
 (0)