Skip to main content

rustpython_vm/
exceptions.rs

1use self::types::{PyBaseException, PyBaseExceptionRef};
2use crate::common::lock::PyRwLock;
3use crate::object::{Traverse, TraverseFn};
4use crate::{
5    AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine,
6    builtins::{
7        PyList, PyNone, PyStr, PyStrRef, PyTuple, PyTupleRef, PyType, PyTypeRef,
8        traceback::{PyTraceback, PyTracebackRef},
9    },
10    class::{PyClassImpl, StaticType},
11    convert::{IntoPyException, ToPyException, ToPyObject},
12    function::{ArgIterable, FuncArgs, IntoFuncArgs, PySetterValue},
13    py_io::{self, Write},
14    stdlib::sys,
15    suggestion::offer_suggestions,
16    types::{Callable, Constructor, Initializer, Representable},
17};
18use crossbeam_utils::atomic::AtomicCell;
19use itertools::Itertools;
20use std::{
21    collections::HashSet,
22    io::{self, BufRead, BufReader},
23};
24
25pub use super::exception_group::exception_group;
26
27unsafe impl Traverse for PyBaseException {
28    fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
29        self.traceback.traverse(tracer_fn);
30        self.cause.traverse(tracer_fn);
31        self.context.traverse(tracer_fn);
32        self.args.traverse(tracer_fn);
33    }
34}
35
36impl core::fmt::Debug for PyBaseException {
37    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
38        // TODO: implement more detailed, non-recursive Debug formatter
39        f.write_str("PyBaseException")
40    }
41}
42
43impl PyPayload for PyBaseException {
44    #[inline]
45    fn class(ctx: &Context) -> &'static Py<PyType> {
46        ctx.exceptions.base_exception_type
47    }
48}
49
50impl VirtualMachine {
51    // Why `impl VirtualMachine`?
52    // These functions are natively free function in CPython - not methods of PyException
53
54    /// Print exception chain by calling sys.excepthook
55    pub fn print_exception(&self, exc: PyBaseExceptionRef) {
56        let vm = self;
57        let write_fallback = |exc, errstr| {
58            if let Ok(stderr) = sys::get_stderr(vm) {
59                let mut stderr = py_io::PyWriter(stderr, vm);
60                // if this fails stderr might be closed -- ignore it
61                let _ = writeln!(stderr, "{errstr}");
62                let _ = self.write_exception(&mut stderr, exc);
63            } else {
64                eprintln!("{errstr}\nlost sys.stderr");
65                let _ = self.write_exception(&mut py_io::IoWriter(io::stderr()), exc);
66            }
67        };
68        if let Ok(excepthook) = vm.sys_module.get_attr("excepthook", vm) {
69            let (exc_type, exc_val, exc_tb) = vm.split_exception(exc.clone());
70            if let Err(eh_exc) = excepthook.call((exc_type, exc_val, exc_tb), vm) {
71                write_fallback(&eh_exc, "Error in sys.excepthook:");
72                write_fallback(&exc, "Original exception was:");
73            }
74        } else {
75            write_fallback(&exc, "missing sys.excepthook");
76        }
77    }
78
79    pub fn write_exception<W: Write>(
80        &self,
81        output: &mut W,
82        exc: &Py<PyBaseException>,
83    ) -> Result<(), W::Error> {
84        let seen = &mut HashSet::<usize>::new();
85        self.write_exception_recursive(output, exc, seen)
86    }
87
88    fn write_exception_recursive<W: Write>(
89        &self,
90        output: &mut W,
91        exc: &Py<PyBaseException>,
92        seen: &mut HashSet<usize>,
93    ) -> Result<(), W::Error> {
94        // This function should not be called directly,
95        // use `wite_exception` as a public interface.
96        // It is similar to `print_exception_recursive` from `CPython`.
97        seen.insert(exc.get_id());
98
99        #[allow(clippy::manual_map)]
100        if let Some((cause_or_context, msg)) = if let Some(cause) = exc.__cause__() {
101            // This can be a special case: `raise e from e`,
102            // we just ignore it and treat like `raise e` without any extra steps.
103            Some((
104                cause,
105                "\nThe above exception was the direct cause of the following exception:\n",
106            ))
107        } else if let Some(context) = exc.__context__() {
108            // This can be a special case:
109            //   e = ValueError('e')
110            //   e.__context__ = e
111            // In this case, we just ignore
112            // `__context__` part from going into recursion.
113            Some((
114                context,
115                "\nDuring handling of the above exception, another exception occurred:\n",
116            ))
117        } else {
118            None
119        } {
120            if !seen.contains(&cause_or_context.get_id()) {
121                self.write_exception_recursive(output, &cause_or_context, seen)?;
122                writeln!(output, "{msg}")?;
123            } else {
124                seen.insert(cause_or_context.get_id());
125            }
126        }
127
128        self.write_exception_inner(output, exc)
129    }
130
131    /// Print exception with traceback
132    pub fn write_exception_inner<W: Write>(
133        &self,
134        output: &mut W,
135        exc: &Py<PyBaseException>,
136    ) -> Result<(), W::Error> {
137        let vm = self;
138        if let Some(tb) = exc.traceback.read().clone() {
139            writeln!(output, "Traceback (most recent call last):")?;
140            for tb in tb.iter() {
141                write_traceback_entry(output, &tb)?;
142            }
143        }
144
145        let varargs = exc.args();
146        let args_repr = vm.exception_args_as_string(varargs, true);
147
148        let exc_class = exc.class();
149
150        if exc_class.fast_issubclass(vm.ctx.exceptions.syntax_error) {
151            return self.write_syntaxerror(output, exc, exc_class, &args_repr);
152        }
153
154        let exc_name = exc_class.name();
155        match args_repr.len() {
156            0 => write!(output, "{exc_name}"),
157            1 => write!(output, "{}: {}", exc_name, args_repr[0]),
158            _ => write!(
159                output,
160                "{}: ({})",
161                exc_name,
162                args_repr.into_iter().format(", "),
163            ),
164        }?;
165
166        match offer_suggestions(exc, vm) {
167            Some(suggestions) => writeln!(output, ". Did you mean: '{suggestions}'?"),
168            None => writeln!(output),
169        }
170    }
171
172    /// Format and write a SyntaxError
173    /// This logic is derived from TracebackException._format_syntax_error
174    ///
175    /// The logic has support for `end_offset` to highlight a range in the source code,
176    /// but it looks like `end_offset` is not used yet when SyntaxErrors are created.
177    fn write_syntaxerror<W: Write>(
178        &self,
179        output: &mut W,
180        exc: &Py<PyBaseException>,
181        exc_type: &Py<PyType>,
182        args_repr: &[PyRef<PyStr>],
183    ) -> Result<(), W::Error> {
184        let vm = self;
185        debug_assert!(exc_type.fast_issubclass(vm.ctx.exceptions.syntax_error));
186
187        let getattr = |attr: &'static str| exc.as_object().get_attr(attr, vm).ok();
188
189        let maybe_lineno = getattr("lineno").map(|obj| {
190            obj.str(vm)
191                .unwrap_or_else(|_| vm.ctx.new_str("<lineno str() failed>"))
192        });
193        let maybe_filename = getattr("filename").and_then(|obj| obj.str(vm).ok());
194
195        let maybe_text = getattr("text").map(|obj| {
196            obj.str(vm)
197                .unwrap_or_else(|_| vm.ctx.new_str("<text str() failed>"))
198        });
199
200        let mut filename_suffix = String::new();
201
202        if let Some(lineno) = maybe_lineno {
203            let filename = match maybe_filename {
204                Some(filename) => filename,
205                None => vm.ctx.new_str("<string>"),
206            };
207            writeln!(output, r##"  File "{filename}", line {lineno}"##,)?;
208        } else if let Some(filename) = maybe_filename {
209            filename_suffix = format!(" ({filename})");
210        }
211
212        if let Some(text) = maybe_text {
213            // if text ends with \n or \r\n, remove it
214            use rustpython_common::wtf8::CodePoint;
215            let text_wtf8 = text.as_wtf8();
216            let r_text = text_wtf8.trim_end_matches(|cp: CodePoint| {
217                cp == CodePoint::from_char('\n') || cp == CodePoint::from_char('\r')
218            });
219            let l_text = r_text.trim_start_matches(|cp: CodePoint| {
220                cp == CodePoint::from_char(' ')
221                    || cp == CodePoint::from_char('\n')
222                    || cp == CodePoint::from_char('\x0c') // \f
223            });
224            let spaces = (r_text.len() - l_text.len()) as isize;
225
226            writeln!(output, "    {l_text}")?;
227
228            let maybe_offset: Option<isize> =
229                getattr("offset").and_then(|obj| obj.try_to_value::<isize>(vm).ok());
230
231            if let Some(offset) = maybe_offset {
232                let maybe_end_offset: Option<isize> =
233                    getattr("end_offset").and_then(|obj| obj.try_to_value::<isize>(vm).ok());
234                let maybe_end_lineno: Option<isize> =
235                    getattr("end_lineno").and_then(|obj| obj.try_to_value::<isize>(vm).ok());
236                let maybe_lineno_int: Option<isize> =
237                    getattr("lineno").and_then(|obj| obj.try_to_value::<isize>(vm).ok());
238
239                // Only show caret if end_lineno is same as lineno (or not set)
240                let same_line = match (maybe_lineno_int, maybe_end_lineno) {
241                    (Some(lineno), Some(end_lineno)) => lineno == end_lineno,
242                    _ => true,
243                };
244
245                if same_line {
246                    let mut end_offset = match maybe_end_offset {
247                        Some(0) | None => offset,
248                        Some(end_offset) => end_offset,
249                    };
250
251                    if offset == end_offset || end_offset == -1 {
252                        end_offset = offset + 1;
253                    }
254
255                    // Convert 1-based column offset to 0-based index into stripped text
256                    let colno = offset - 1 - spaces;
257                    let end_colno = end_offset - 1 - spaces;
258                    if colno >= 0 {
259                        let caret_space = l_text
260                            .code_points()
261                            .take(colno as usize)
262                            .map(|cp| cp.to_char().filter(|c| c.is_whitespace()).unwrap_or(' '))
263                            .collect::<String>();
264
265                        let mut error_width = end_colno - colno;
266                        if error_width < 1 {
267                            error_width = 1;
268                        }
269
270                        writeln!(
271                            output,
272                            "    {}{}",
273                            caret_space,
274                            "^".repeat(error_width as usize)
275                        )?;
276                    }
277                }
278            }
279        }
280
281        let exc_name = exc_type.name();
282
283        match args_repr.len() {
284            0 => write!(output, "{exc_name}{filename_suffix}"),
285            1 => write!(output, "{}: {}{}", exc_name, args_repr[0], filename_suffix),
286            _ => write!(
287                output,
288                "{}: ({}){}",
289                exc_name,
290                args_repr.iter().format(", "),
291                filename_suffix
292            ),
293        }?;
294
295        match offer_suggestions(exc, vm) {
296            Some(suggestions) => writeln!(output, ". Did you mean: '{suggestions}'?"),
297            None => writeln!(output),
298        }
299    }
300
301    fn exception_args_as_string(&self, varargs: PyTupleRef, str_single: bool) -> Vec<PyStrRef> {
302        let vm = self;
303        match varargs.len() {
304            0 => vec![],
305            1 => {
306                let args0_repr = if str_single {
307                    varargs[0]
308                        .str(vm)
309                        .unwrap_or_else(|_| PyStr::from("<element str() failed>").into_ref(&vm.ctx))
310                } else {
311                    varargs[0].repr(vm).unwrap_or_else(|_| {
312                        PyStr::from("<element repr() failed>").into_ref(&vm.ctx)
313                    })
314                };
315                vec![args0_repr]
316            }
317            _ => varargs
318                .iter()
319                .map(|vararg| {
320                    vararg.repr(vm).unwrap_or_else(|_| {
321                        PyStr::from("<element repr() failed>").into_ref(&vm.ctx)
322                    })
323                })
324                .collect(),
325        }
326    }
327
328    pub fn split_exception(
329        &self,
330        exc: PyBaseExceptionRef,
331    ) -> (PyObjectRef, PyObjectRef, PyObjectRef) {
332        let tb = exc.__traceback__().to_pyobject(self);
333        let class = exc.class().to_owned();
334        (class.into(), exc.into(), tb)
335    }
336
337    /// Similar to PyErr_NormalizeException in CPython
338    pub fn normalize_exception(
339        &self,
340        exc_type: PyObjectRef,
341        exc_val: PyObjectRef,
342        exc_tb: PyObjectRef,
343    ) -> PyResult<PyBaseExceptionRef> {
344        let ctor = ExceptionCtor::try_from_object(self, exc_type)?;
345        let exc = ctor.instantiate_value(exc_val, self)?;
346        if let Some(tb) = Option::<PyTracebackRef>::try_from_object(self, exc_tb)? {
347            exc.set_traceback_typed(Some(tb));
348        }
349        Ok(exc)
350    }
351
352    pub fn invoke_exception(
353        &self,
354        cls: PyTypeRef,
355        args: Vec<PyObjectRef>,
356    ) -> PyResult<PyBaseExceptionRef> {
357        // TODO: fast-path built-in exceptions by directly instantiating them? Is that really worth it?
358        let res = PyType::call(&cls, args.into_args(self), self)?;
359        res.downcast::<PyBaseException>().map_err(|obj| {
360            self.new_type_error(format!(
361                "calling {} should have returned an instance of BaseException, not {}",
362                cls,
363                obj.class()
364            ))
365        })
366    }
367}
368
369fn print_source_line<W: Write>(
370    output: &mut W,
371    filename: &str,
372    lineno: usize,
373) -> Result<(), W::Error> {
374    // TODO: use io.open() method instead, when available, according to https://github.com/python/cpython/blob/main/Python/traceback.c#L393
375    // TODO: support different encodings
376    let file = match std::fs::File::open(filename) {
377        Ok(file) => file,
378        Err(_) => return Ok(()),
379    };
380    let file = BufReader::new(file);
381
382    for (i, line) in file.lines().enumerate() {
383        if i + 1 == lineno {
384            if let Ok(line) = line {
385                // Indented with 4 spaces
386                writeln!(output, "    {}", line.trim_start())?;
387            }
388            return Ok(());
389        }
390    }
391
392    Ok(())
393}
394
395/// Print exception occurrence location from traceback element
396fn write_traceback_entry<W: Write>(
397    output: &mut W,
398    tb_entry: &Py<PyTraceback>,
399) -> Result<(), W::Error> {
400    let filename = tb_entry.frame.code.source_path().as_str();
401    writeln!(
402        output,
403        r##"  File "{}", line {}, in {}"##,
404        filename.trim_start_matches(r"\\?\"),
405        tb_entry.lineno,
406        tb_entry.frame.code.obj_name
407    )?;
408    print_source_line(output, filename, tb_entry.lineno.get())?;
409
410    Ok(())
411}
412
413#[derive(Clone)]
414pub enum ExceptionCtor {
415    Class(PyTypeRef),
416    Instance(PyBaseExceptionRef),
417}
418
419impl TryFromObject for ExceptionCtor {
420    fn try_from_object(vm: &VirtualMachine, obj: PyObjectRef) -> PyResult<Self> {
421        obj.downcast::<PyType>()
422            .and_then(|cls| {
423                if cls.fast_issubclass(vm.ctx.exceptions.base_exception_type) {
424                    Ok(Self::Class(cls))
425                } else {
426                    Err(cls.into())
427                }
428            })
429            .or_else(|obj| obj.downcast::<PyBaseException>().map(Self::Instance))
430            .map_err(|obj| {
431                vm.new_type_error(format!(
432                    "exceptions must be classes or instances deriving from BaseException, not {}",
433                    obj.class().name()
434                ))
435            })
436    }
437}
438
439impl ExceptionCtor {
440    pub fn instantiate(self, vm: &VirtualMachine) -> PyResult<PyBaseExceptionRef> {
441        match self {
442            Self::Class(cls) => vm.invoke_exception(cls, vec![]),
443            Self::Instance(exc) => Ok(exc),
444        }
445    }
446
447    pub fn instantiate_value(
448        self,
449        value: PyObjectRef,
450        vm: &VirtualMachine,
451    ) -> PyResult<PyBaseExceptionRef> {
452        let exc_inst = value.clone().downcast::<PyBaseException>().ok();
453        match (self, exc_inst) {
454            // both are instances; which would we choose?
455            (Self::Instance(_exc_a), Some(_exc_b)) => {
456                Err(vm.new_type_error("instance exception may not have a separate value"))
457            }
458            // if the "type" is an instance and the value isn't, use the "type"
459            (Self::Instance(exc), None) => Ok(exc),
460            // if the value is an instance of the type, use the instance value
461            (Self::Class(cls), Some(exc)) if exc.fast_isinstance(&cls) => Ok(exc),
462            // otherwise; construct an exception of the type using the value as args
463            (Self::Class(cls), _) => {
464                let args = match_class!(match value {
465                    PyNone => vec![],
466                    tup @ PyTuple => tup.to_vec(),
467                    exc @ PyBaseException => exc.args().to_vec(),
468                    obj => vec![obj],
469                });
470                vm.invoke_exception(cls, args)
471            }
472        }
473    }
474}
475
476#[derive(Debug)]
477pub struct ExceptionZoo {
478    pub base_exception_type: &'static Py<PyType>,
479    pub base_exception_group: &'static Py<PyType>,
480    pub system_exit: &'static Py<PyType>,
481    pub keyboard_interrupt: &'static Py<PyType>,
482    pub generator_exit: &'static Py<PyType>,
483    pub exception_type: &'static Py<PyType>,
484    pub stop_iteration: &'static Py<PyType>,
485    pub stop_async_iteration: &'static Py<PyType>,
486    pub arithmetic_error: &'static Py<PyType>,
487    pub floating_point_error: &'static Py<PyType>,
488    pub overflow_error: &'static Py<PyType>,
489    pub zero_division_error: &'static Py<PyType>,
490    pub assertion_error: &'static Py<PyType>,
491    pub attribute_error: &'static Py<PyType>,
492    pub buffer_error: &'static Py<PyType>,
493    pub eof_error: &'static Py<PyType>,
494    pub import_error: &'static Py<PyType>,
495    pub module_not_found_error: &'static Py<PyType>,
496    pub lookup_error: &'static Py<PyType>,
497    pub index_error: &'static Py<PyType>,
498    pub key_error: &'static Py<PyType>,
499    pub memory_error: &'static Py<PyType>,
500    pub name_error: &'static Py<PyType>,
501    pub unbound_local_error: &'static Py<PyType>,
502    pub os_error: &'static Py<PyType>,
503    pub blocking_io_error: &'static Py<PyType>,
504    pub child_process_error: &'static Py<PyType>,
505    pub connection_error: &'static Py<PyType>,
506    pub broken_pipe_error: &'static Py<PyType>,
507    pub connection_aborted_error: &'static Py<PyType>,
508    pub connection_refused_error: &'static Py<PyType>,
509    pub connection_reset_error: &'static Py<PyType>,
510    pub file_exists_error: &'static Py<PyType>,
511    pub file_not_found_error: &'static Py<PyType>,
512    pub interrupted_error: &'static Py<PyType>,
513    pub is_a_directory_error: &'static Py<PyType>,
514    pub not_a_directory_error: &'static Py<PyType>,
515    pub permission_error: &'static Py<PyType>,
516    pub process_lookup_error: &'static Py<PyType>,
517    pub timeout_error: &'static Py<PyType>,
518    pub reference_error: &'static Py<PyType>,
519    pub runtime_error: &'static Py<PyType>,
520    pub not_implemented_error: &'static Py<PyType>,
521    pub recursion_error: &'static Py<PyType>,
522    pub python_finalization_error: &'static Py<PyType>,
523    pub syntax_error: &'static Py<PyType>,
524    pub incomplete_input_error: &'static Py<PyType>,
525    pub indentation_error: &'static Py<PyType>,
526    pub tab_error: &'static Py<PyType>,
527    pub system_error: &'static Py<PyType>,
528    pub type_error: &'static Py<PyType>,
529    pub value_error: &'static Py<PyType>,
530    pub unicode_error: &'static Py<PyType>,
531    pub unicode_decode_error: &'static Py<PyType>,
532    pub unicode_encode_error: &'static Py<PyType>,
533    pub unicode_translate_error: &'static Py<PyType>,
534
535    #[cfg(feature = "jit")]
536    pub jit_error: &'static Py<PyType>,
537
538    pub warning: &'static Py<PyType>,
539    pub deprecation_warning: &'static Py<PyType>,
540    pub pending_deprecation_warning: &'static Py<PyType>,
541    pub runtime_warning: &'static Py<PyType>,
542    pub syntax_warning: &'static Py<PyType>,
543    pub user_warning: &'static Py<PyType>,
544    pub future_warning: &'static Py<PyType>,
545    pub import_warning: &'static Py<PyType>,
546    pub unicode_warning: &'static Py<PyType>,
547    pub bytes_warning: &'static Py<PyType>,
548    pub resource_warning: &'static Py<PyType>,
549    pub encoding_warning: &'static Py<PyType>,
550}
551
552macro_rules! extend_exception {
553    (
554        $exc_struct:ident,
555        $ctx:expr,
556        $class:expr
557    ) => {
558        extend_exception!($exc_struct, $ctx, $class, {});
559    };
560    (
561        $exc_struct:ident,
562        $ctx:expr,
563        $class:expr,
564        { $($name:expr => $value:expr),* $(,)* }
565    ) => {
566        $exc_struct::extend_class($ctx, $class);
567        extend_class!($ctx, $class, {
568            $($name => $value,)*
569        });
570    };
571}
572
573impl PyBaseException {
574    pub(crate) fn new(args: Vec<PyObjectRef>, vm: &VirtualMachine) -> Self {
575        Self {
576            traceback: PyRwLock::new(None),
577            cause: PyRwLock::new(None),
578            context: PyRwLock::new(None),
579            suppress_context: AtomicCell::new(false),
580            args: PyRwLock::new(PyTuple::new_ref(args, &vm.ctx)),
581        }
582    }
583
584    pub fn get_arg(&self, idx: usize) -> Option<PyObjectRef> {
585        self.args.read().get(idx).cloned()
586    }
587}
588
589#[pyclass(
590    with(Py, PyRef, Constructor, Initializer, Representable),
591    flags(BASETYPE, HAS_DICT)
592)]
593impl PyBaseException {
594    #[pygetset]
595    pub fn args(&self) -> PyTupleRef {
596        self.args.read().clone()
597    }
598
599    #[pygetset(setter)]
600    fn set_args(&self, args: ArgIterable, vm: &VirtualMachine) -> PyResult<()> {
601        let args = args.iter(vm)?.collect::<PyResult<Vec<_>>>()?;
602        *self.args.write() = PyTuple::new_ref(args, &vm.ctx);
603        Ok(())
604    }
605
606    #[pygetset]
607    pub fn __traceback__(&self) -> Option<PyTracebackRef> {
608        self.traceback.read().clone()
609    }
610
611    #[pygetset(setter)]
612    pub fn set___traceback__(&self, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
613        let traceback = if vm.is_none(&value) {
614            None
615        } else {
616            match value.downcast::<PyTraceback>() {
617                Ok(tb) => Some(tb),
618                Err(_) => {
619                    return Err(vm.new_type_error("__traceback__ must be a traceback or None"));
620                }
621            }
622        };
623        self.set_traceback_typed(traceback);
624        Ok(())
625    }
626
627    // Helper method for internal use that doesn't require PyObjectRef
628    pub(crate) fn set_traceback_typed(&self, traceback: Option<PyTracebackRef>) {
629        *self.traceback.write() = traceback;
630    }
631
632    #[pygetset]
633    pub fn __cause__(&self) -> Option<PyRef<Self>> {
634        self.cause.read().clone()
635    }
636
637    #[pygetset(setter)]
638    pub fn set___cause__(&self, cause: Option<PyRef<Self>>) {
639        let mut c = self.cause.write();
640        self.set_suppress_context(true);
641        *c = cause;
642    }
643
644    #[pygetset]
645    pub fn __context__(&self) -> Option<PyRef<Self>> {
646        self.context.read().clone()
647    }
648
649    #[pygetset(setter)]
650    pub fn set___context__(&self, context: Option<PyRef<Self>>) {
651        *self.context.write() = context;
652    }
653
654    #[pygetset]
655    pub(super) fn __suppress_context__(&self) -> bool {
656        self.suppress_context.load()
657    }
658
659    #[pygetset(name = "__suppress_context__", setter)]
660    fn set_suppress_context(&self, suppress_context: bool) {
661        self.suppress_context.store(suppress_context);
662    }
663}
664
665#[pyclass]
666impl Py<PyBaseException> {
667    #[pymethod]
668    pub(super) fn __str__(&self, vm: &VirtualMachine) -> PyResult<PyStrRef> {
669        let str_args = vm.exception_args_as_string(self.args(), true);
670        Ok(match str_args.into_iter().exactly_one() {
671            Err(i) if i.len() == 0 => vm.ctx.empty_str.to_owned(),
672            Ok(s) => s,
673            Err(i) => PyStr::from(format!("({})", i.format(", "))).into_ref(&vm.ctx),
674        })
675    }
676}
677
678#[pyclass]
679impl PyRef<PyBaseException> {
680    #[pymethod]
681    fn with_traceback(self, tb: Option<PyTracebackRef>) -> PyResult<Self> {
682        *self.traceback.write() = tb;
683        Ok(self)
684    }
685
686    #[pymethod]
687    fn add_note(self, note: PyStrRef, vm: &VirtualMachine) -> PyResult<()> {
688        let dict = self
689            .as_object()
690            .dict()
691            .ok_or_else(|| vm.new_attribute_error("Exception object has no __dict__"))?;
692
693        let notes = if let Ok(notes) = dict.get_item("__notes__", vm) {
694            notes
695        } else {
696            let new_notes = vm.ctx.new_list(vec![]);
697            dict.set_item("__notes__", new_notes.clone().into(), vm)?;
698            new_notes.into()
699        };
700
701        let notes = notes
702            .downcast::<PyList>()
703            .map_err(|_| vm.new_type_error("__notes__ must be a list"))?;
704
705        notes.borrow_vec_mut().push(note.into());
706        Ok(())
707    }
708
709    #[pymethod]
710    fn __reduce__(self, vm: &VirtualMachine) -> PyTupleRef {
711        if let Some(dict) = self.as_object().dict().filter(|x| !x.is_empty()) {
712            vm.new_tuple((self.class().to_owned(), self.args(), dict))
713        } else {
714            vm.new_tuple((self.class().to_owned(), self.args()))
715        }
716    }
717
718    #[pymethod]
719    fn __setstate__(self, state: PyObjectRef, vm: &VirtualMachine) -> PyResult<PyObjectRef> {
720        if !vm.is_none(&state) {
721            let dict = state
722                .downcast::<crate::builtins::PyDict>()
723                .map_err(|_| vm.new_type_error("state is not a dictionary"))?;
724
725            for (key, value) in &dict {
726                let key_str = key.str(vm)?;
727                if key_str.as_bytes().starts_with(b"__") {
728                    continue;
729                }
730                self.as_object().set_attr(&key_str, value.clone(), vm)?;
731            }
732        }
733        Ok(vm.ctx.none())
734    }
735}
736
737impl Constructor for PyBaseException {
738    type Args = FuncArgs;
739
740    fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
741        if cls.is(Self::class(&vm.ctx)) && !args.kwargs.is_empty() {
742            return Err(vm.new_type_error("BaseException() takes no keyword arguments"));
743        }
744        Self::new(args.args, vm)
745            .into_ref_with_type(vm, cls)
746            .map(Into::into)
747    }
748
749    fn py_new(_cls: &Py<PyType>, _args: FuncArgs, _vm: &VirtualMachine) -> PyResult<Self> {
750        unimplemented!("use slot_new")
751    }
752}
753
754impl Initializer for PyBaseException {
755    type Args = FuncArgs;
756
757    fn init(zelf: PyRef<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
758        *zelf.args.write() = PyTuple::new_ref(args.args, &vm.ctx);
759        Ok(())
760    }
761}
762
763impl Representable for PyBaseException {
764    #[inline]
765    fn repr_str(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<String> {
766        let repr_args = vm.exception_args_as_string(zelf.args(), false);
767        let cls = zelf.class();
768        Ok(format!("{}({})", cls.name(), repr_args.iter().format(", ")))
769    }
770}
771
772impl ExceptionZoo {
773    pub(crate) fn init() -> Self {
774        use self::types::*;
775
776        let base_exception_type = PyBaseException::init_builtin_type();
777
778        // Sorted By Hierarchy then alphabetized.
779        let base_exception_group = PyBaseExceptionGroup::init_builtin_type();
780        let system_exit = PySystemExit::init_builtin_type();
781        let keyboard_interrupt = PyKeyboardInterrupt::init_builtin_type();
782        let generator_exit = PyGeneratorExit::init_builtin_type();
783
784        let exception_type = PyException::init_builtin_type();
785        let stop_iteration = PyStopIteration::init_builtin_type();
786        let stop_async_iteration = PyStopAsyncIteration::init_builtin_type();
787        let arithmetic_error = PyArithmeticError::init_builtin_type();
788        let floating_point_error = PyFloatingPointError::init_builtin_type();
789        let overflow_error = PyOverflowError::init_builtin_type();
790        let zero_division_error = PyZeroDivisionError::init_builtin_type();
791
792        let assertion_error = PyAssertionError::init_builtin_type();
793        let attribute_error = PyAttributeError::init_builtin_type();
794        let buffer_error = PyBufferError::init_builtin_type();
795        let eof_error = PyEOFError::init_builtin_type();
796
797        let import_error = PyImportError::init_builtin_type();
798        let module_not_found_error = PyModuleNotFoundError::init_builtin_type();
799
800        let lookup_error = PyLookupError::init_builtin_type();
801        let index_error = PyIndexError::init_builtin_type();
802        let key_error = PyKeyError::init_builtin_type();
803
804        let memory_error = PyMemoryError::init_builtin_type();
805
806        let name_error = PyNameError::init_builtin_type();
807        let unbound_local_error = PyUnboundLocalError::init_builtin_type();
808
809        // os errors
810        let os_error = PyOSError::init_builtin_type();
811        let blocking_io_error = PyBlockingIOError::init_builtin_type();
812        let child_process_error = PyChildProcessError::init_builtin_type();
813
814        let connection_error = PyConnectionError::init_builtin_type();
815        let broken_pipe_error = PyBrokenPipeError::init_builtin_type();
816        let connection_aborted_error = PyConnectionAbortedError::init_builtin_type();
817        let connection_refused_error = PyConnectionRefusedError::init_builtin_type();
818        let connection_reset_error = PyConnectionResetError::init_builtin_type();
819
820        let file_exists_error = PyFileExistsError::init_builtin_type();
821        let file_not_found_error = PyFileNotFoundError::init_builtin_type();
822        let interrupted_error = PyInterruptedError::init_builtin_type();
823        let is_a_directory_error = PyIsADirectoryError::init_builtin_type();
824        let not_a_directory_error = PyNotADirectoryError::init_builtin_type();
825        let permission_error = PyPermissionError::init_builtin_type();
826        let process_lookup_error = PyProcessLookupError::init_builtin_type();
827        let timeout_error = PyTimeoutError::init_builtin_type();
828
829        let reference_error = PyReferenceError::init_builtin_type();
830
831        let runtime_error = PyRuntimeError::init_builtin_type();
832        let not_implemented_error = PyNotImplementedError::init_builtin_type();
833        let recursion_error = PyRecursionError::init_builtin_type();
834        let python_finalization_error = PyPythonFinalizationError::init_builtin_type();
835
836        let syntax_error = PySyntaxError::init_builtin_type();
837        let incomplete_input_error = PyIncompleteInputError::init_builtin_type();
838        let indentation_error = PyIndentationError::init_builtin_type();
839        let tab_error = PyTabError::init_builtin_type();
840
841        let system_error = PySystemError::init_builtin_type();
842        let type_error = PyTypeError::init_builtin_type();
843        let value_error = PyValueError::init_builtin_type();
844        let unicode_error = PyUnicodeError::init_builtin_type();
845        let unicode_decode_error = PyUnicodeDecodeError::init_builtin_type();
846        let unicode_encode_error = PyUnicodeEncodeError::init_builtin_type();
847        let unicode_translate_error = PyUnicodeTranslateError::init_builtin_type();
848
849        #[cfg(feature = "jit")]
850        let jit_error = PyJitError::init_builtin_type();
851
852        let warning = PyWarning::init_builtin_type();
853        let deprecation_warning = PyDeprecationWarning::init_builtin_type();
854        let pending_deprecation_warning = PyPendingDeprecationWarning::init_builtin_type();
855        let runtime_warning = PyRuntimeWarning::init_builtin_type();
856        let syntax_warning = PySyntaxWarning::init_builtin_type();
857        let user_warning = PyUserWarning::init_builtin_type();
858        let future_warning = PyFutureWarning::init_builtin_type();
859        let import_warning = PyImportWarning::init_builtin_type();
860        let unicode_warning = PyUnicodeWarning::init_builtin_type();
861        let bytes_warning = PyBytesWarning::init_builtin_type();
862        let resource_warning = PyResourceWarning::init_builtin_type();
863        let encoding_warning = PyEncodingWarning::init_builtin_type();
864
865        Self {
866            base_exception_type,
867            base_exception_group,
868            system_exit,
869            keyboard_interrupt,
870            generator_exit,
871            exception_type,
872            stop_iteration,
873            stop_async_iteration,
874            arithmetic_error,
875            floating_point_error,
876            overflow_error,
877            zero_division_error,
878            assertion_error,
879            attribute_error,
880            buffer_error,
881            eof_error,
882            import_error,
883            module_not_found_error,
884            lookup_error,
885            index_error,
886            key_error,
887            memory_error,
888            name_error,
889            unbound_local_error,
890            os_error,
891            blocking_io_error,
892            child_process_error,
893            connection_error,
894            broken_pipe_error,
895            connection_aborted_error,
896            connection_refused_error,
897            connection_reset_error,
898            file_exists_error,
899            file_not_found_error,
900            interrupted_error,
901            is_a_directory_error,
902            not_a_directory_error,
903            permission_error,
904            process_lookup_error,
905            timeout_error,
906            reference_error,
907            runtime_error,
908            not_implemented_error,
909            recursion_error,
910            python_finalization_error,
911            syntax_error,
912            incomplete_input_error,
913            indentation_error,
914            tab_error,
915            system_error,
916            type_error,
917            value_error,
918            unicode_error,
919            unicode_decode_error,
920            unicode_encode_error,
921            unicode_translate_error,
922
923            #[cfg(feature = "jit")]
924            jit_error,
925
926            warning,
927            deprecation_warning,
928            pending_deprecation_warning,
929            runtime_warning,
930            syntax_warning,
931            user_warning,
932            future_warning,
933            import_warning,
934            unicode_warning,
935            bytes_warning,
936            resource_warning,
937            encoding_warning,
938        }
939    }
940
941    // TODO: remove it after fixing `errno` / `winerror` problem
942    #[allow(
943        clippy::redundant_clone,
944        reason = "temporary workaround until errno/winerror handling is fixed"
945    )]
946    pub fn extend(ctx: &'static Context) {
947        use self::types::*;
948
949        let excs = &ctx.exceptions;
950
951        PyBaseException::extend_class(ctx, excs.base_exception_type);
952
953        // Sorted By Hierarchy then alphabetized.
954        extend_exception!(PyBaseExceptionGroup, ctx, excs.base_exception_group, {
955            "message" => ctx.new_readonly_getset("message", excs.base_exception_group, make_arg_getter(0)),
956            "exceptions" => ctx.new_readonly_getset("exceptions", excs.base_exception_group, make_arg_getter(1)),
957        });
958
959        extend_exception!(PySystemExit, ctx, excs.system_exit, {
960            "code" => ctx.new_readonly_getset("code", excs.system_exit, system_exit_code),
961        });
962        extend_exception!(PyKeyboardInterrupt, ctx, excs.keyboard_interrupt);
963        extend_exception!(PyGeneratorExit, ctx, excs.generator_exit);
964
965        extend_exception!(PyException, ctx, excs.exception_type);
966
967        extend_exception!(PyStopIteration, ctx, excs.stop_iteration, {
968            "value" => ctx.none(),
969        });
970        extend_exception!(PyStopAsyncIteration, ctx, excs.stop_async_iteration);
971
972        extend_exception!(PyArithmeticError, ctx, excs.arithmetic_error);
973        extend_exception!(PyFloatingPointError, ctx, excs.floating_point_error);
974        extend_exception!(PyOverflowError, ctx, excs.overflow_error);
975        extend_exception!(PyZeroDivisionError, ctx, excs.zero_division_error);
976
977        extend_exception!(PyAssertionError, ctx, excs.assertion_error);
978        extend_exception!(PyAttributeError, ctx, excs.attribute_error, {
979            "name" => ctx.none(),
980            "obj" => ctx.none(),
981        });
982        extend_exception!(PyBufferError, ctx, excs.buffer_error);
983        extend_exception!(PyEOFError, ctx, excs.eof_error);
984
985        extend_exception!(PyImportError, ctx, excs.import_error, {
986            "msg" => ctx.new_readonly_getset("msg", excs.import_error, make_arg_getter(0)),
987        });
988        extend_exception!(PyModuleNotFoundError, ctx, excs.module_not_found_error);
989
990        extend_exception!(PyLookupError, ctx, excs.lookup_error);
991        extend_exception!(PyIndexError, ctx, excs.index_error);
992
993        extend_exception!(PyKeyError, ctx, excs.key_error);
994
995        extend_exception!(PyMemoryError, ctx, excs.memory_error);
996        extend_exception!(PyNameError, ctx, excs.name_error, {
997            "name" => ctx.none(),
998        });
999        extend_exception!(PyUnboundLocalError, ctx, excs.unbound_local_error);
1000
1001        // os errors:
1002        // PyOSError now uses struct fields with pygetset, no need for dynamic attributes
1003        extend_exception!(PyOSError, ctx, excs.os_error);
1004
1005        extend_exception!(PyBlockingIOError, ctx, excs.blocking_io_error);
1006        extend_exception!(PyChildProcessError, ctx, excs.child_process_error);
1007
1008        extend_exception!(PyConnectionError, ctx, excs.connection_error);
1009        extend_exception!(PyBrokenPipeError, ctx, excs.broken_pipe_error);
1010        extend_exception!(PyConnectionAbortedError, ctx, excs.connection_aborted_error);
1011        extend_exception!(PyConnectionRefusedError, ctx, excs.connection_refused_error);
1012        extend_exception!(PyConnectionResetError, ctx, excs.connection_reset_error);
1013
1014        extend_exception!(PyFileExistsError, ctx, excs.file_exists_error);
1015        extend_exception!(PyFileNotFoundError, ctx, excs.file_not_found_error);
1016        extend_exception!(PyInterruptedError, ctx, excs.interrupted_error);
1017        extend_exception!(PyIsADirectoryError, ctx, excs.is_a_directory_error);
1018        extend_exception!(PyNotADirectoryError, ctx, excs.not_a_directory_error);
1019        extend_exception!(PyPermissionError, ctx, excs.permission_error);
1020        extend_exception!(PyProcessLookupError, ctx, excs.process_lookup_error);
1021        extend_exception!(PyTimeoutError, ctx, excs.timeout_error);
1022
1023        extend_exception!(PyReferenceError, ctx, excs.reference_error);
1024        extend_exception!(PyRuntimeError, ctx, excs.runtime_error);
1025        extend_exception!(PyNotImplementedError, ctx, excs.not_implemented_error);
1026        extend_exception!(PyRecursionError, ctx, excs.recursion_error);
1027        extend_exception!(
1028            PyPythonFinalizationError,
1029            ctx,
1030            excs.python_finalization_error
1031        );
1032
1033        extend_exception!(PySyntaxError, ctx, excs.syntax_error, {
1034            "msg" => ctx.new_static_getset(
1035                "msg",
1036                excs.syntax_error,
1037                make_arg_getter(0),
1038                syntax_error_set_msg,
1039            ),
1040            // TODO: members
1041            "filename" => ctx.none(),
1042            "lineno" => ctx.none(),
1043            "end_lineno" => ctx.none(),
1044            "offset" => ctx.none(),
1045            "end_offset" => ctx.none(),
1046            "text" => ctx.none(),
1047        });
1048        extend_exception!(PyIncompleteInputError, ctx, excs.incomplete_input_error);
1049        extend_exception!(PyIndentationError, ctx, excs.indentation_error);
1050        extend_exception!(PyTabError, ctx, excs.tab_error);
1051
1052        extend_exception!(PySystemError, ctx, excs.system_error);
1053        extend_exception!(PyTypeError, ctx, excs.type_error);
1054        extend_exception!(PyValueError, ctx, excs.value_error);
1055        extend_exception!(PyUnicodeError, ctx, excs.unicode_error);
1056        extend_exception!(PyUnicodeDecodeError, ctx, excs.unicode_decode_error);
1057        extend_exception!(PyUnicodeEncodeError, ctx, excs.unicode_encode_error);
1058        extend_exception!(PyUnicodeTranslateError, ctx, excs.unicode_translate_error);
1059
1060        #[cfg(feature = "jit")]
1061        extend_exception!(PyJitError, ctx, excs.jit_error);
1062
1063        extend_exception!(PyWarning, ctx, excs.warning);
1064        extend_exception!(PyDeprecationWarning, ctx, excs.deprecation_warning);
1065        extend_exception!(
1066            PyPendingDeprecationWarning,
1067            ctx,
1068            excs.pending_deprecation_warning
1069        );
1070        extend_exception!(PyRuntimeWarning, ctx, excs.runtime_warning);
1071        extend_exception!(PySyntaxWarning, ctx, excs.syntax_warning);
1072        extend_exception!(PyUserWarning, ctx, excs.user_warning);
1073        extend_exception!(PyFutureWarning, ctx, excs.future_warning);
1074        extend_exception!(PyImportWarning, ctx, excs.import_warning);
1075        extend_exception!(PyUnicodeWarning, ctx, excs.unicode_warning);
1076        extend_exception!(PyBytesWarning, ctx, excs.bytes_warning);
1077        extend_exception!(PyResourceWarning, ctx, excs.resource_warning);
1078        extend_exception!(PyEncodingWarning, ctx, excs.encoding_warning);
1079    }
1080}
1081
1082fn make_arg_getter(idx: usize) -> impl Fn(PyBaseExceptionRef) -> Option<PyObjectRef> {
1083    move |exc| exc.get_arg(idx)
1084}
1085
1086fn syntax_error_set_msg(
1087    exc: PyBaseExceptionRef,
1088    value: PySetterValue,
1089    vm: &VirtualMachine,
1090) -> PyResult<()> {
1091    let mut args = exc.args.write();
1092    let mut new_args = args.as_slice().to_vec();
1093    // Ensure the message slot at index 0 always exists for SyntaxError.args.
1094    if new_args.is_empty() {
1095        new_args.push(vm.ctx.none());
1096    }
1097    match value {
1098        PySetterValue::Assign(value) => new_args[0] = value,
1099        PySetterValue::Delete => new_args[0] = vm.ctx.none(),
1100    }
1101    *args = PyTuple::new_ref(new_args, &vm.ctx);
1102    Ok(())
1103}
1104
1105fn system_exit_code(exc: PyBaseExceptionRef) -> Option<PyObjectRef> {
1106    // SystemExit.code based on args length:
1107    // - size == 0: code is None
1108    // - size == 1: code is args[0]
1109    // - size > 1: code is args (the whole tuple)
1110    let args = exc.args.read();
1111    match args.len() {
1112        0 => None,
1113        1 => Some(args.first().unwrap().clone()),
1114        _ => Some(args.as_object().to_owned()),
1115    }
1116}
1117
1118#[cfg(feature = "serde")]
1119pub struct SerializeException<'vm, 's> {
1120    vm: &'vm VirtualMachine,
1121    exc: &'s Py<PyBaseException>,
1122}
1123
1124#[cfg(feature = "serde")]
1125impl<'vm, 's> SerializeException<'vm, 's> {
1126    pub fn new(vm: &'vm VirtualMachine, exc: &'s Py<PyBaseException>) -> Self {
1127        SerializeException { vm, exc }
1128    }
1129}
1130
1131#[cfg(feature = "serde")]
1132pub struct SerializeExceptionOwned<'vm> {
1133    vm: &'vm VirtualMachine,
1134    exc: PyBaseExceptionRef,
1135}
1136
1137#[cfg(feature = "serde")]
1138impl serde::Serialize for SerializeExceptionOwned<'_> {
1139    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1140        let Self { vm, exc } = self;
1141        SerializeException::new(vm, exc).serialize(s)
1142    }
1143}
1144
1145#[cfg(feature = "serde")]
1146impl serde::Serialize for SerializeException<'_, '_> {
1147    fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1148        use serde::ser::*;
1149
1150        let mut struc = s.serialize_struct("PyBaseException", 7)?;
1151        struc.serialize_field("exc_type", &*self.exc.class().name())?;
1152        let tbs = {
1153            struct Tracebacks(PyTracebackRef);
1154            impl serde::Serialize for Tracebacks {
1155                fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1156                    let mut s = s.serialize_seq(None)?;
1157                    for tb in self.0.iter() {
1158                        s.serialize_element(&**tb)?;
1159                    }
1160                    s.end()
1161                }
1162            }
1163            self.exc.__traceback__().map(Tracebacks)
1164        };
1165        struc.serialize_field("traceback", &tbs)?;
1166        struc.serialize_field(
1167            "cause",
1168            &self
1169                .exc
1170                .__cause__()
1171                .map(|exc| SerializeExceptionOwned { vm: self.vm, exc }),
1172        )?;
1173        struc.serialize_field(
1174            "context",
1175            &self
1176                .exc
1177                .__context__()
1178                .map(|exc| SerializeExceptionOwned { vm: self.vm, exc }),
1179        )?;
1180        struc.serialize_field("suppress_context", &self.exc.__suppress_context__())?;
1181
1182        let args = {
1183            struct Args<'vm>(&'vm VirtualMachine, PyTupleRef);
1184            impl serde::Serialize for Args<'_> {
1185                fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
1186                    s.collect_seq(
1187                        self.1
1188                            .iter()
1189                            .map(|arg| crate::py_serde::PyObjectSerializer::new(self.0, arg)),
1190                    )
1191                }
1192            }
1193            Args(self.vm, self.exc.args())
1194        };
1195        struc.serialize_field("args", &args)?;
1196
1197        let rendered = {
1198            let mut rendered = String::new();
1199            self.vm
1200                .write_exception(&mut rendered, self.exc)
1201                .map_err(S::Error::custom)?;
1202            rendered
1203        };
1204        struc.serialize_field("rendered", &rendered)?;
1205
1206        struc.end()
1207    }
1208}
1209
1210pub fn cstring_error(vm: &VirtualMachine) -> PyBaseExceptionRef {
1211    vm.new_value_error("embedded null character")
1212}
1213
1214impl ToPyException for alloc::ffi::NulError {
1215    fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef {
1216        cstring_error(vm)
1217    }
1218}
1219
1220#[cfg(windows)]
1221impl<C> ToPyException for widestring::error::ContainsNul<C> {
1222    fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef {
1223        cstring_error(vm)
1224    }
1225}
1226
1227#[cfg(any(unix, windows, target_os = "wasi"))]
1228pub(crate) fn errno_to_exc_type(errno: i32, vm: &VirtualMachine) -> Option<&'static Py<PyType>> {
1229    use crate::stdlib::errno::errors;
1230    let excs = &vm.ctx.exceptions;
1231    match errno {
1232        #[allow(unreachable_patterns)] // EAGAIN is sometimes the same as EWOULDBLOCK
1233        errors::EWOULDBLOCK | errors::EAGAIN => Some(excs.blocking_io_error),
1234        errors::EALREADY => Some(excs.blocking_io_error),
1235        errors::EINPROGRESS => Some(excs.blocking_io_error),
1236        errors::EPIPE => Some(excs.broken_pipe_error),
1237        #[cfg(not(target_os = "wasi"))]
1238        errors::ESHUTDOWN => Some(excs.broken_pipe_error),
1239        errors::ECHILD => Some(excs.child_process_error),
1240        errors::ECONNABORTED => Some(excs.connection_aborted_error),
1241        errors::ECONNREFUSED => Some(excs.connection_refused_error),
1242        errors::ECONNRESET => Some(excs.connection_reset_error),
1243        errors::EEXIST => Some(excs.file_exists_error),
1244        errors::ENOENT => Some(excs.file_not_found_error),
1245        errors::EISDIR => Some(excs.is_a_directory_error),
1246        errors::ENOTDIR => Some(excs.not_a_directory_error),
1247        errors::EINTR => Some(excs.interrupted_error),
1248        errors::EACCES => Some(excs.permission_error),
1249        errors::EPERM => Some(excs.permission_error),
1250        errors::ESRCH => Some(excs.process_lookup_error),
1251        errors::ETIMEDOUT => Some(excs.timeout_error),
1252        _ => None,
1253    }
1254}
1255
1256#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
1257pub(crate) fn errno_to_exc_type(_errno: i32, _vm: &VirtualMachine) -> Option<&'static Py<PyType>> {
1258    None
1259}
1260
1261pub(crate) trait ToOSErrorBuilder {
1262    fn to_os_error_builder(&self, vm: &VirtualMachine) -> OSErrorBuilder;
1263}
1264
1265pub(crate) struct OSErrorBuilder {
1266    exc_type: PyTypeRef,
1267    errno: Option<i32>,
1268    strerror: Option<PyObjectRef>,
1269    filename: Option<PyObjectRef>,
1270    #[cfg(windows)]
1271    winerror: Option<PyObjectRef>,
1272    filename2: Option<PyObjectRef>,
1273}
1274
1275impl OSErrorBuilder {
1276    #[must_use]
1277    pub fn with_subtype(
1278        exc_type: PyTypeRef,
1279        errno: Option<i32>,
1280        strerror: impl ToPyObject,
1281        vm: &VirtualMachine,
1282    ) -> Self {
1283        let strerror = strerror.to_pyobject(vm);
1284        Self {
1285            exc_type,
1286            errno,
1287            strerror: Some(strerror),
1288            filename: None,
1289            #[cfg(windows)]
1290            winerror: None,
1291            filename2: None,
1292        }
1293    }
1294
1295    #[must_use]
1296    pub fn with_errno(errno: i32, strerror: impl ToPyObject, vm: &VirtualMachine) -> Self {
1297        let exc_type = errno_to_exc_type(errno, vm)
1298            .unwrap_or(vm.ctx.exceptions.os_error)
1299            .to_owned();
1300        Self::with_subtype(exc_type, Some(errno), strerror, vm)
1301    }
1302
1303    #[must_use]
1304    #[allow(dead_code)]
1305    pub(crate) fn filename(mut self, filename: PyObjectRef) -> Self {
1306        self.filename.replace(filename);
1307        self
1308    }
1309
1310    #[must_use]
1311    #[allow(dead_code)]
1312    pub(crate) fn filename2(mut self, filename: PyObjectRef) -> Self {
1313        self.filename2.replace(filename);
1314        self
1315    }
1316
1317    #[must_use]
1318    #[cfg(windows)]
1319    pub(crate) fn winerror(mut self, winerror: PyObjectRef) -> Self {
1320        self.winerror.replace(winerror);
1321        self
1322    }
1323
1324    /// Strip winerror from the builder. Used for C runtime errors
1325    /// (e.g. `_wopen`, `open`) that should produce `[Errno X]` format
1326    /// instead of `[WinError X]`.
1327    #[must_use]
1328    #[cfg(windows)]
1329    pub(crate) fn without_winerror(mut self) -> Self {
1330        self.winerror = None;
1331        self
1332    }
1333
1334    pub fn build(self, vm: &VirtualMachine) -> PyRef<types::PyOSError> {
1335        use types::PyOSError;
1336
1337        let OSErrorBuilder {
1338            exc_type,
1339            errno,
1340            strerror,
1341            filename,
1342            #[cfg(windows)]
1343            winerror,
1344            filename2,
1345        } = self;
1346
1347        let args = if let Some(errno) = errno {
1348            #[cfg(windows)]
1349            let winerror = winerror.to_pyobject(vm);
1350            #[cfg(not(windows))]
1351            let winerror = vm.ctx.none();
1352
1353            vec![
1354                errno.to_pyobject(vm),
1355                strerror.to_pyobject(vm),
1356                filename.to_pyobject(vm),
1357                winerror,
1358                filename2.to_pyobject(vm),
1359            ]
1360        } else {
1361            vec![strerror.to_pyobject(vm)]
1362        };
1363
1364        let payload = PyOSError::py_new(&exc_type, args.clone().into(), vm)
1365            .expect("new_os_error usage error");
1366        let os_error = payload
1367            .into_ref_with_type(vm, exc_type)
1368            .expect("new_os_error usage error");
1369        PyOSError::slot_init(os_error.as_object().to_owned(), args.into(), vm)
1370            .expect("new_os_error usage error");
1371        os_error
1372    }
1373}
1374
1375impl IntoPyException for OSErrorBuilder {
1376    fn into_pyexception(self, vm: &VirtualMachine) -> PyBaseExceptionRef {
1377        self.build(vm).upcast()
1378    }
1379}
1380
1381impl ToOSErrorBuilder for std::io::Error {
1382    fn to_os_error_builder(&self, vm: &VirtualMachine) -> OSErrorBuilder {
1383        use crate::common::os::ErrorExt;
1384
1385        let errno = self.posix_errno();
1386        #[cfg(windows)]
1387        let msg = 'msg: {
1388            // Use C runtime's strerror for POSIX errno values.
1389            // For Windows-specific error codes, fall back to FormatMessage.
1390            const MAX_POSIX_ERRNO: i32 = 127;
1391            if errno > 0 && errno <= MAX_POSIX_ERRNO {
1392                let ptr = unsafe { libc::strerror(errno) };
1393                if !ptr.is_null() {
1394                    let s = unsafe { core::ffi::CStr::from_ptr(ptr) }.to_string_lossy();
1395                    if !s.starts_with("Unknown error") {
1396                        break 'msg s.into_owned();
1397                    }
1398                }
1399            }
1400            self.to_string()
1401        };
1402        #[cfg(unix)]
1403        let msg = {
1404            let ptr = unsafe { libc::strerror(errno) };
1405            if !ptr.is_null() {
1406                unsafe { core::ffi::CStr::from_ptr(ptr) }
1407                    .to_string_lossy()
1408                    .into_owned()
1409            } else {
1410                self.to_string()
1411            }
1412        };
1413        #[cfg(not(any(windows, unix)))]
1414        let msg = self.to_string();
1415
1416        #[allow(unused_mut)]
1417        let mut builder = OSErrorBuilder::with_errno(errno, msg, vm);
1418        #[cfg(windows)]
1419        if let Some(winerror) = self.raw_os_error() {
1420            builder = builder.winerror(winerror.to_pyobject(vm));
1421        }
1422        builder
1423    }
1424}
1425
1426impl ToPyException for std::io::Error {
1427    fn to_pyexception(&self, vm: &VirtualMachine) -> PyBaseExceptionRef {
1428        let builder = self.to_os_error_builder(vm);
1429        builder.into_pyexception(vm)
1430    }
1431}
1432
1433impl IntoPyException for std::io::Error {
1434    fn into_pyexception(self, vm: &VirtualMachine) -> PyBaseExceptionRef {
1435        self.to_pyexception(vm)
1436    }
1437}
1438
1439#[cfg(unix)]
1440impl IntoPyException for nix::Error {
1441    fn into_pyexception(self, vm: &VirtualMachine) -> PyBaseExceptionRef {
1442        std::io::Error::from(self).into_pyexception(vm)
1443    }
1444}
1445
1446#[cfg(unix)]
1447impl IntoPyException for rustix::io::Errno {
1448    fn into_pyexception(self, vm: &VirtualMachine) -> PyBaseExceptionRef {
1449        std::io::Error::from(self).into_pyexception(vm)
1450    }
1451}
1452
1453pub(super) mod types {
1454    use crate::common::lock::PyRwLock;
1455    use crate::object::{MaybeTraverse, Traverse, TraverseFn};
1456    #[cfg_attr(target_arch = "wasm32", allow(unused_imports))]
1457    use crate::{
1458        AsObject, Py, PyAtomicRef, PyObject, PyObjectRef, PyPayload, PyRef, PyResult,
1459        VirtualMachine,
1460        builtins::{
1461            PyInt, PyStrRef, PyTupleRef, PyType, PyTypeRef, traceback::PyTracebackRef,
1462            tuple::IntoPyTuple,
1463        },
1464        convert::ToPyResult,
1465        function::{ArgBytesLike, FuncArgs, KwArgs},
1466        types::{Constructor, Initializer},
1467    };
1468    use crossbeam_utils::atomic::AtomicCell;
1469    use itertools::Itertools;
1470    use rustpython_common::{
1471        str::UnicodeEscapeCodepoint,
1472        wtf8::{Wtf8, Wtf8Buf, wtf8_concat},
1473    };
1474
1475    // Re-export exception group types from dedicated module
1476    pub use crate::exception_group::types::PyBaseExceptionGroup;
1477
1478    // This module is designed to be used as `use builtins::*;`.
1479    // Do not add any pub symbols not included in builtins module.
1480    // `PyBaseExceptionRef` is the only exception.
1481
1482    pub type PyBaseExceptionRef = PyRef<PyBaseException>;
1483
1484    // Sorted By Hierarchy then alphabetized.
1485
1486    #[pyclass(module = false, name = "BaseException", traverse = "manual")]
1487    pub struct PyBaseException {
1488        pub(super) traceback: PyRwLock<Option<PyTracebackRef>>,
1489        pub(super) cause: PyRwLock<Option<PyRef<Self>>>,
1490        pub(super) context: PyRwLock<Option<PyRef<Self>>>,
1491        pub(super) suppress_context: AtomicCell<bool>,
1492        pub(super) args: PyRwLock<PyTupleRef>,
1493    }
1494
1495    #[pyexception(name, base = PyBaseException, ctx = "system_exit")]
1496    #[derive(Debug)]
1497    #[repr(transparent)]
1498    pub struct PySystemExit(PyBaseException);
1499
1500    // SystemExit_init: has its own __init__ that sets the code attribute
1501    #[pyexception(with(Initializer))]
1502    impl PySystemExit {}
1503
1504    impl Initializer for PySystemExit {
1505        type Args = FuncArgs;
1506        fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
1507            // Call BaseException_init first (handles args)
1508            PyBaseException::slot_init(zelf, args, vm)
1509            // Note: code is computed dynamically via system_exit_code getter
1510            // so we don't need to set it here explicitly
1511        }
1512
1513        fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
1514            unreachable!("slot_init is defined")
1515        }
1516    }
1517
1518    #[pyexception(name, base = PyBaseException, ctx = "generator_exit", impl)]
1519    #[derive(Debug)]
1520    #[repr(transparent)]
1521    pub struct PyGeneratorExit(PyBaseException);
1522
1523    #[pyexception(name, base = PyBaseException, ctx = "keyboard_interrupt", impl)]
1524    #[derive(Debug)]
1525    #[repr(transparent)]
1526    pub struct PyKeyboardInterrupt(PyBaseException);
1527
1528    #[pyexception(name, base = PyBaseException, ctx = "exception_type", impl)]
1529    #[derive(Debug)]
1530    #[repr(transparent)]
1531    pub struct PyException(PyBaseException);
1532
1533    #[pyexception(name, base = PyException, ctx = "stop_iteration")]
1534    #[derive(Debug)]
1535    #[repr(transparent)]
1536    pub struct PyStopIteration(PyException);
1537
1538    #[pyexception(with(Initializer))]
1539    impl PyStopIteration {}
1540
1541    impl Initializer for PyStopIteration {
1542        type Args = FuncArgs;
1543        fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
1544            zelf.set_attr("value", vm.unwrap_or_none(args.args.first().cloned()), vm)?;
1545            Ok(())
1546        }
1547
1548        fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
1549            unreachable!("slot_init is defined")
1550        }
1551    }
1552
1553    #[pyexception(name, base = PyException, ctx = "stop_async_iteration", impl)]
1554    #[derive(Debug)]
1555    #[repr(transparent)]
1556    pub struct PyStopAsyncIteration(PyException);
1557
1558    #[pyexception(name, base = PyException, ctx = "arithmetic_error", impl)]
1559    #[derive(Debug)]
1560    #[repr(transparent)]
1561    pub struct PyArithmeticError(PyException);
1562
1563    #[pyexception(name, base = PyArithmeticError, ctx = "floating_point_error", impl)]
1564    #[derive(Debug)]
1565    #[repr(transparent)]
1566    pub struct PyFloatingPointError(PyArithmeticError);
1567    #[pyexception(name, base = PyArithmeticError, ctx = "overflow_error", impl)]
1568    #[derive(Debug)]
1569    #[repr(transparent)]
1570    pub struct PyOverflowError(PyArithmeticError);
1571
1572    #[pyexception(name, base = PyArithmeticError, ctx = "zero_division_error", impl)]
1573    #[derive(Debug)]
1574    #[repr(transparent)]
1575    pub struct PyZeroDivisionError(PyArithmeticError);
1576
1577    #[pyexception(name, base = PyException, ctx = "assertion_error", impl)]
1578    #[derive(Debug)]
1579    #[repr(transparent)]
1580    pub struct PyAssertionError(PyException);
1581
1582    #[pyexception(name, base = PyException, ctx = "attribute_error")]
1583    #[derive(Debug)]
1584    #[repr(transparent)]
1585    pub struct PyAttributeError(PyException);
1586
1587    #[pyexception(with(Initializer))]
1588    impl PyAttributeError {}
1589
1590    impl Initializer for PyAttributeError {
1591        type Args = FuncArgs;
1592
1593        fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
1594            // Only 'name' and 'obj' kwargs are allowed
1595            let mut kwargs = args.kwargs.clone();
1596            let name = kwargs.swap_remove("name");
1597            let obj = kwargs.swap_remove("obj");
1598
1599            // Reject unknown kwargs
1600            if let Some(invalid_key) = kwargs.keys().next() {
1601                return Err(vm.new_type_error(format!(
1602                    "AttributeError() got an unexpected keyword argument '{invalid_key}'"
1603                )));
1604            }
1605
1606            // Pass args without kwargs to BaseException_init
1607            let base_args = FuncArgs::new(args.args.clone(), KwArgs::default());
1608            PyBaseException::slot_init(zelf.clone(), base_args, vm)?;
1609
1610            // Set attributes
1611            zelf.set_attr("name", vm.unwrap_or_none(name), vm)?;
1612            zelf.set_attr("obj", vm.unwrap_or_none(obj), vm)?;
1613            Ok(())
1614        }
1615
1616        fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
1617            unreachable!("slot_init is defined")
1618        }
1619    }
1620
1621    #[pyexception(name, base = PyException, ctx = "buffer_error", impl)]
1622    #[derive(Debug)]
1623    #[repr(transparent)]
1624    pub struct PyBufferError(PyException);
1625
1626    #[pyexception(name, base = PyException, ctx = "eof_error", impl)]
1627    #[derive(Debug)]
1628    #[repr(transparent)]
1629    pub struct PyEOFError(PyException);
1630
1631    #[pyexception(name, base = PyException, ctx = "import_error")]
1632    #[derive(Debug)]
1633    #[repr(transparent)]
1634    pub struct PyImportError(PyException);
1635
1636    #[pyexception(with(Initializer))]
1637    impl PyImportError {
1638        #[pymethod]
1639        fn __reduce__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef {
1640            let obj = exc.as_object().to_owned();
1641            let mut result: Vec<PyObjectRef> = vec![
1642                obj.class().to_owned().into(),
1643                vm.new_tuple((exc.get_arg(0).unwrap(),)).into(),
1644            ];
1645
1646            if let Some(dict) = obj.dict().filter(|x| !x.is_empty()) {
1647                result.push(dict.into());
1648            }
1649
1650            result.into_pytuple(vm)
1651        }
1652    }
1653
1654    impl Initializer for PyImportError {
1655        type Args = FuncArgs;
1656
1657        fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
1658            // Only 'name', 'path', 'name_from' kwargs are allowed
1659            let mut kwargs = args.kwargs.clone();
1660            let name = kwargs.swap_remove("name");
1661            let path = kwargs.swap_remove("path");
1662            let name_from = kwargs.swap_remove("name_from");
1663
1664            // Check for any remaining invalid keyword arguments
1665            if let Some(invalid_key) = kwargs.keys().next() {
1666                return Err(vm.new_type_error(format!(
1667                    "'{invalid_key}' is an invalid keyword argument for ImportError"
1668                )));
1669            }
1670
1671            let dict = zelf.dict().unwrap();
1672            dict.set_item("name", vm.unwrap_or_none(name), vm)?;
1673            dict.set_item("path", vm.unwrap_or_none(path), vm)?;
1674            dict.set_item("name_from", vm.unwrap_or_none(name_from), vm)?;
1675            PyBaseException::slot_init(zelf, args, vm)
1676        }
1677
1678        fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
1679            unreachable!("slot_init is defined")
1680        }
1681    }
1682
1683    #[pyexception(name, base = PyImportError, ctx = "module_not_found_error", impl)]
1684    #[derive(Debug)]
1685    #[repr(transparent)]
1686    pub struct PyModuleNotFoundError(PyImportError);
1687
1688    #[pyexception(name, base = PyException, ctx = "lookup_error", impl)]
1689    #[derive(Debug)]
1690    #[repr(transparent)]
1691    pub struct PyLookupError(PyException);
1692
1693    #[pyexception(name, base = PyLookupError, ctx = "index_error", impl)]
1694    #[derive(Debug)]
1695    #[repr(transparent)]
1696    pub struct PyIndexError(PyLookupError);
1697
1698    #[pyexception(name, base = PyLookupError, ctx = "key_error")]
1699    #[derive(Debug)]
1700    #[repr(transparent)]
1701    pub struct PyKeyError(PyLookupError);
1702
1703    #[pyexception]
1704    impl PyKeyError {
1705        #[pymethod]
1706        fn __str__(zelf: &Py<PyBaseException>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
1707            let args = zelf.args();
1708            Ok(if args.len() == 1 {
1709                vm.exception_args_as_string(args, false)
1710                    .into_iter()
1711                    .exactly_one()
1712                    .unwrap()
1713            } else {
1714                zelf.__str__(vm)?
1715            })
1716        }
1717    }
1718
1719    #[pyexception(name, base = PyException, ctx = "memory_error", impl)]
1720    #[derive(Debug)]
1721    #[repr(transparent)]
1722    pub struct PyMemoryError(PyException);
1723
1724    #[pyexception(name, base = PyException, ctx = "name_error")]
1725    #[derive(Debug)]
1726    #[repr(transparent)]
1727    pub struct PyNameError(PyException);
1728
1729    // NameError_init: handles the .name. kwarg
1730    #[pyexception(with(Initializer))]
1731    impl PyNameError {}
1732
1733    impl Initializer for PyNameError {
1734        type Args = FuncArgs;
1735        fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
1736            // Only 'name' kwarg is allowed
1737            let mut kwargs = args.kwargs.clone();
1738            let name = kwargs.swap_remove("name");
1739
1740            // Reject unknown kwargs
1741            if let Some(invalid_key) = kwargs.keys().next() {
1742                return Err(vm.new_type_error(format!(
1743                    "NameError() got an unexpected keyword argument '{invalid_key}'"
1744                )));
1745            }
1746
1747            // Pass args without kwargs to BaseException_init
1748            let base_args = FuncArgs::new(args.args.clone(), KwArgs::default());
1749            PyBaseException::slot_init(zelf.clone(), base_args, vm)?;
1750
1751            // Set name attribute if provided
1752            if let Some(name) = name {
1753                zelf.set_attr("name", name, vm)?;
1754            }
1755            Ok(())
1756        }
1757
1758        fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
1759            unreachable!("slot_init is defined")
1760        }
1761    }
1762
1763    #[pyexception(name, base = PyNameError, ctx = "unbound_local_error", impl)]
1764    #[derive(Debug)]
1765    #[repr(transparent)]
1766    pub struct PyUnboundLocalError(PyNameError);
1767
1768    #[pyexception(name, base = PyException, ctx = "os_error")]
1769    #[repr(C)]
1770    pub struct PyOSError {
1771        base: PyException,
1772        errno: PyAtomicRef<Option<PyObject>>,
1773        strerror: PyAtomicRef<Option<PyObject>>,
1774        filename: PyAtomicRef<Option<PyObject>>,
1775        filename2: PyAtomicRef<Option<PyObject>>,
1776        #[cfg(windows)]
1777        winerror: PyAtomicRef<Option<PyObject>>,
1778        // For BlockingIOError: characters written before blocking occurred
1779        // -1 means not set (AttributeError when accessed)
1780        written: AtomicCell<isize>,
1781    }
1782
1783    impl crate::class::PySubclass for PyOSError {
1784        type Base = PyException;
1785        fn as_base(&self) -> &Self::Base {
1786            &self.base
1787        }
1788    }
1789
1790    impl core::fmt::Debug for PyOSError {
1791        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1792            f.debug_struct("PyOSError").finish_non_exhaustive()
1793        }
1794    }
1795
1796    unsafe impl Traverse for PyOSError {
1797        fn traverse(&self, tracer_fn: &mut TraverseFn<'_>) {
1798            self.base.try_traverse(tracer_fn);
1799            if let Some(obj) = self.errno.deref() {
1800                tracer_fn(obj);
1801            }
1802            if let Some(obj) = self.strerror.deref() {
1803                tracer_fn(obj);
1804            }
1805            if let Some(obj) = self.filename.deref() {
1806                tracer_fn(obj);
1807            }
1808            if let Some(obj) = self.filename2.deref() {
1809                tracer_fn(obj);
1810            }
1811            #[cfg(windows)]
1812            if let Some(obj) = self.winerror.deref() {
1813                tracer_fn(obj);
1814            }
1815        }
1816    }
1817
1818    // OS Errors:
1819    impl Constructor for PyOSError {
1820        type Args = FuncArgs;
1821
1822        fn py_new(_cls: &Py<PyType>, args: FuncArgs, vm: &VirtualMachine) -> PyResult<Self> {
1823            let len = args.args.len();
1824            // CPython only sets errno/strerror when args len is 2-5
1825            let (errno, strerror) = if (2..=5).contains(&len) {
1826                (Some(args.args[0].clone()), Some(args.args[1].clone()))
1827            } else {
1828                (None, None)
1829            };
1830            let filename = if (3..=5).contains(&len) {
1831                Some(args.args[2].clone())
1832            } else {
1833                None
1834            };
1835            let filename2 = if len == 5 {
1836                args.args.get(4).cloned()
1837            } else {
1838                None
1839            };
1840            // Truncate args for base exception when 3-5 args
1841            let base_args = if (3..=5).contains(&len) {
1842                args.args[..2].to_vec()
1843            } else {
1844                args.args.to_vec()
1845            };
1846            let base_exception = PyBaseException::new(base_args, vm);
1847            Ok(Self {
1848                base: PyException(base_exception),
1849                errno: errno.into(),
1850                strerror: strerror.into(),
1851                filename: filename.into(),
1852                filename2: filename2.into(),
1853                #[cfg(windows)]
1854                winerror: None.into(),
1855                written: AtomicCell::new(-1),
1856            })
1857        }
1858
1859        fn slot_new(cls: PyTypeRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult {
1860            // We need this method, because of how `CPython` copies `init`
1861            // from `BaseException` in `SimpleExtendsException` macro.
1862            // See: `BaseException_new`
1863            if *cls.name() == *vm.ctx.exceptions.os_error.name() {
1864                let args_vec = args.args.to_vec();
1865                let len = args_vec.len();
1866                if (2..=5).contains(&len) {
1867                    let errno = &args_vec[0];
1868                    if let Some(error) = errno
1869                        .downcast_ref::<PyInt>()
1870                        .and_then(|errno| errno.try_to_primitive::<i32>(vm).ok())
1871                        .and_then(|errno| super::errno_to_exc_type(errno, vm))
1872                        .and_then(|typ| vm.invoke_exception(typ.to_owned(), args_vec).ok())
1873                    {
1874                        return error.to_pyresult(vm);
1875                    }
1876                }
1877            }
1878            let payload = Self::py_new(&cls, args, vm)?;
1879            payload.into_ref_with_type(vm, cls).map(Into::into)
1880        }
1881    }
1882
1883    impl Initializer for PyOSError {
1884        type Args = FuncArgs;
1885
1886        fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
1887            let len = args.args.len();
1888            let mut new_args = args;
1889
1890            // All OSError subclasses use #[repr(transparent)] wrapping PyOSError,
1891            // so we can safely access the PyOSError fields through pointer cast
1892            // SAFETY: All OSError subclasses (FileNotFoundError, etc.) are
1893            // #[repr(transparent)] wrappers around PyOSError with identical memory layout
1894            #[allow(deprecated)]
1895            let exc: &Py<PyOSError> = zelf.downcast_ref::<PyOSError>().unwrap();
1896
1897            // Check if this is BlockingIOError - need to handle characters_written
1898            let is_blocking_io_error =
1899                zelf.class()
1900                    .is(vm.ctx.exceptions.blocking_io_error.as_ref());
1901
1902            // SAFETY: slot_init is called during object initialization,
1903            // so fields are None and swap result can be safely ignored
1904            let mut set_filename = true;
1905            if len <= 5 {
1906                // Only set errno/strerror when args len is 2-5
1907                if 2 <= len {
1908                    let _ = unsafe { exc.errno.swap(Some(new_args.args[0].clone())) };
1909                    let _ = unsafe { exc.strerror.swap(Some(new_args.args[1].clone())) };
1910                }
1911                if 3 <= len {
1912                    let third_arg = &new_args.args[2];
1913                    // BlockingIOError's 3rd argument can be the number of characters written
1914                    if is_blocking_io_error
1915                        && !vm.is_none(third_arg)
1916                        && crate::protocol::PyNumber::check(third_arg)
1917                        && let Ok(written) = third_arg.try_index(vm)
1918                        && let Ok(n) = written.try_to_primitive::<isize>(vm)
1919                    {
1920                        exc.written.store(n);
1921                        set_filename = false;
1922                        // Clear filename that was set in py_new
1923                        let _ = unsafe { exc.filename.swap(None) };
1924                    }
1925                    if set_filename {
1926                        let _ = unsafe { exc.filename.swap(Some(third_arg.clone())) };
1927                    }
1928                }
1929                #[cfg(windows)]
1930                if 4 <= len {
1931                    let winerror = new_args.args.get(3).cloned();
1932                    // Store original winerror
1933                    let _ = unsafe { exc.winerror.swap(winerror.clone()) };
1934
1935                    // Convert winerror to errno and update errno + args[0]
1936                    if let Some(errno) = winerror
1937                        .as_ref()
1938                        .and_then(|w| w.downcast_ref::<crate::builtins::PyInt>())
1939                        .and_then(|w| w.try_to_primitive::<i32>(vm).ok())
1940                        .map(crate::common::os::winerror_to_errno)
1941                    {
1942                        let errno_obj = vm.new_pyobj(errno);
1943                        let _ = unsafe { exc.errno.swap(Some(errno_obj.clone())) };
1944                        new_args.args[0] = errno_obj;
1945                    }
1946                }
1947                if len == 5 {
1948                    let _ = unsafe { exc.filename2.swap(new_args.args.get(4).cloned()) };
1949                }
1950            }
1951
1952            // args are truncated to 2 for compatibility (only when 2-5 args and filename is not None)
1953            // truncation happens inside "if (filename && filename != Py_None)" block
1954            let has_filename = exc.filename.to_owned().filter(|f| !vm.is_none(f)).is_some();
1955            if (3..=5).contains(&len) && has_filename {
1956                new_args.args.truncate(2);
1957            }
1958            PyBaseException::slot_init(zelf, new_args, vm)
1959        }
1960
1961        fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
1962            unreachable!("slot_init is defined")
1963        }
1964    }
1965
1966    #[pyexception(with(Constructor, Initializer))]
1967    impl PyOSError {
1968        #[pymethod]
1969        fn __str__(zelf: &Py<PyBaseException>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
1970            let obj = zelf.as_object();
1971
1972            // Get OSError fields directly
1973            let errno_field = obj.get_attr("errno", vm).ok().filter(|v| !vm.is_none(v));
1974            let strerror = obj.get_attr("strerror", vm).ok().filter(|v| !vm.is_none(v));
1975            let filename = obj.get_attr("filename", vm).ok().filter(|v| !vm.is_none(v));
1976            let filename2 = obj
1977                .get_attr("filename2", vm)
1978                .ok()
1979                .filter(|v| !vm.is_none(v));
1980            #[cfg(windows)]
1981            let winerror = obj.get_attr("winerror", vm).ok().filter(|v| !vm.is_none(v));
1982
1983            // Windows: winerror takes priority over errno
1984            #[cfg(windows)]
1985            if let Some(ref win_err) = winerror {
1986                let code = win_err.str(vm)?;
1987                if let Some(ref f) = filename {
1988                    let msg = strerror
1989                        .as_ref()
1990                        .map(|s| s.str(vm))
1991                        .transpose()?
1992                        .map(|s| s.to_string())
1993                        .unwrap_or_else(|| "None".to_owned());
1994                    if let Some(ref f2) = filename2 {
1995                        return Ok(vm.ctx.new_str(format!(
1996                            "[WinError {}] {}: {} -> {}",
1997                            code,
1998                            msg,
1999                            f.repr(vm)?,
2000                            f2.repr(vm)?
2001                        )));
2002                    }
2003                    return Ok(vm.ctx.new_str(format!(
2004                        "[WinError {}] {}: {}",
2005                        code,
2006                        msg,
2007                        f.repr(vm)?
2008                    )));
2009                }
2010                // winerror && strerror (no filename)
2011                if let Some(ref s) = strerror {
2012                    return Ok(vm
2013                        .ctx
2014                        .new_str(format!("[WinError {}] {}", code, s.str(vm)?)));
2015                }
2016            }
2017
2018            // Non-Windows or fallback: use errno
2019            if let Some(ref f) = filename {
2020                let errno_str = errno_field
2021                    .as_ref()
2022                    .map(|e| e.str(vm))
2023                    .transpose()?
2024                    .map(|s| s.to_string())
2025                    .unwrap_or_else(|| "None".to_owned());
2026                let msg = strerror
2027                    .as_ref()
2028                    .map(|s| s.str(vm))
2029                    .transpose()?
2030                    .map(|s| s.to_string())
2031                    .unwrap_or_else(|| "None".to_owned());
2032                if let Some(ref f2) = filename2 {
2033                    return Ok(vm.ctx.new_str(format!(
2034                        "[Errno {}] {}: {} -> {}",
2035                        errno_str,
2036                        msg,
2037                        f.repr(vm)?,
2038                        f2.repr(vm)?
2039                    )));
2040                }
2041                return Ok(vm.ctx.new_str(format!(
2042                    "[Errno {}] {}: {}",
2043                    errno_str,
2044                    msg,
2045                    f.repr(vm)?
2046                )));
2047            }
2048
2049            // errno && strerror (no filename)
2050            if let (Some(e), Some(s)) = (&errno_field, &strerror) {
2051                return Ok(vm
2052                    .ctx
2053                    .new_str(format!("[Errno {}] {}", e.str(vm)?, s.str(vm)?)));
2054            }
2055
2056            // fallback to BaseException.__str__
2057            zelf.__str__(vm)
2058        }
2059
2060        #[pymethod]
2061        fn __reduce__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyTupleRef {
2062            let args = exc.args();
2063            let obj = exc.as_object().to_owned();
2064            let mut result: Vec<PyObjectRef> = vec![obj.class().to_owned().into()];
2065
2066            if args.len() >= 2 && args.len() <= 5 {
2067                // SAFETY: len() == 2 is checked so get_arg 1 or 2 won't panic
2068                let errno = exc.get_arg(0).unwrap();
2069                let msg = exc.get_arg(1).unwrap();
2070
2071                if let Ok(filename) = obj.get_attr("filename", vm) {
2072                    if !vm.is_none(&filename) {
2073                        let mut args_reduced: Vec<PyObjectRef> = vec![errno, msg, filename];
2074
2075                        let filename2 = obj
2076                            .get_attr("filename2", vm)
2077                            .ok()
2078                            .filter(|f| !vm.is_none(f));
2079                        #[cfg(windows)]
2080                        let winerror = obj.get_attr("winerror", vm).ok().filter(|w| !vm.is_none(w));
2081
2082                        if let Some(filename2) = filename2 {
2083                            #[cfg(windows)]
2084                            {
2085                                args_reduced.push(winerror.unwrap_or_else(|| vm.ctx.none()));
2086                            }
2087                            #[cfg(not(windows))]
2088                            args_reduced.push(vm.ctx.none());
2089                            args_reduced.push(filename2);
2090                        } else {
2091                            // Diverges from CPython: include winerror even without
2092                            // filename2 so it survives pickle round-trips.
2093                            #[cfg(windows)]
2094                            if let Some(winerror) = winerror {
2095                                args_reduced.push(winerror);
2096                            }
2097                        }
2098                        result.push(args_reduced.into_pytuple(vm).into());
2099                    } else {
2100                        // filename is None - use original args as-is
2101                        // (may contain winerror at position 3)
2102                        result.push(args.into());
2103                    }
2104                } else {
2105                    result.push(args.into());
2106                }
2107            } else {
2108                result.push(args.into());
2109            }
2110
2111            if let Some(dict) = obj.dict().filter(|x| !x.is_empty()) {
2112                result.push(dict.into());
2113            }
2114            result.into_pytuple(vm)
2115        }
2116
2117        // Getters and setters for OSError fields
2118        #[pygetset]
2119        fn errno(&self) -> Option<PyObjectRef> {
2120            self.errno.to_owned()
2121        }
2122
2123        #[pygetset(setter)]
2124        fn set_errno(&self, value: Option<PyObjectRef>, vm: &VirtualMachine) {
2125            self.errno.swap_to_temporary_refs(value, vm);
2126        }
2127
2128        #[pygetset]
2129        fn strerror(&self) -> Option<PyObjectRef> {
2130            self.strerror.to_owned()
2131        }
2132
2133        #[pygetset(setter, name = "strerror")]
2134        fn set_strerror(&self, value: Option<PyObjectRef>, vm: &VirtualMachine) {
2135            self.strerror.swap_to_temporary_refs(value, vm);
2136        }
2137
2138        #[pygetset]
2139        fn filename(&self) -> Option<PyObjectRef> {
2140            self.filename.to_owned()
2141        }
2142
2143        #[pygetset(setter)]
2144        fn set_filename(&self, value: Option<PyObjectRef>, vm: &VirtualMachine) {
2145            self.filename.swap_to_temporary_refs(value, vm);
2146        }
2147
2148        #[pygetset]
2149        fn filename2(&self) -> Option<PyObjectRef> {
2150            self.filename2.to_owned()
2151        }
2152
2153        #[pygetset(setter)]
2154        fn set_filename2(&self, value: Option<PyObjectRef>, vm: &VirtualMachine) {
2155            self.filename2.swap_to_temporary_refs(value, vm);
2156        }
2157
2158        #[cfg(windows)]
2159        #[pygetset]
2160        fn winerror(&self) -> Option<PyObjectRef> {
2161            self.winerror.to_owned()
2162        }
2163
2164        #[cfg(windows)]
2165        #[pygetset(setter)]
2166        fn set_winerror(&self, value: Option<PyObjectRef>, vm: &VirtualMachine) {
2167            self.winerror.swap_to_temporary_refs(value, vm);
2168        }
2169
2170        #[pygetset]
2171        fn characters_written(&self, vm: &VirtualMachine) -> PyResult<isize> {
2172            let written = self.written.load();
2173            if written == -1 {
2174                Err(vm.new_attribute_error("characters_written"))
2175            } else {
2176                Ok(written)
2177            }
2178        }
2179
2180        #[pygetset(setter)]
2181        fn set_characters_written(
2182            &self,
2183            value: Option<PyObjectRef>,
2184            vm: &VirtualMachine,
2185        ) -> PyResult<()> {
2186            match value {
2187                None => {
2188                    // Deleting the attribute
2189                    if self.written.load() == -1 {
2190                        Err(vm.new_attribute_error("characters_written"))
2191                    } else {
2192                        self.written.store(-1);
2193                        Ok(())
2194                    }
2195                }
2196                Some(v) => {
2197                    let n = v
2198                        .try_index(vm)?
2199                        .try_to_primitive::<isize>(vm)
2200                        .map_err(|_| {
2201                            vm.new_value_error("cannot convert characters_written value to isize")
2202                        })?;
2203                    self.written.store(n);
2204                    Ok(())
2205                }
2206            }
2207        }
2208    }
2209
2210    #[pyexception(name, base = PyOSError, ctx = "blocking_io_error", impl)]
2211    #[repr(transparent)]
2212    #[derive(Debug)]
2213    pub struct PyBlockingIOError(PyOSError);
2214
2215    #[pyexception(name, base = PyOSError, ctx = "child_process_error", impl)]
2216    #[repr(transparent)]
2217    #[derive(Debug)]
2218    pub struct PyChildProcessError(PyOSError);
2219
2220    #[pyexception(name, base = PyOSError, ctx = "connection_error", impl)]
2221    #[repr(transparent)]
2222    #[derive(Debug)]
2223    pub struct PyConnectionError(PyOSError);
2224
2225    #[pyexception(name, base = PyConnectionError, ctx = "broken_pipe_error", impl)]
2226    #[repr(transparent)]
2227    #[derive(Debug)]
2228    pub struct PyBrokenPipeError(PyConnectionError);
2229
2230    #[pyexception(
2231        name,
2232        base = PyConnectionError,
2233        ctx = "connection_aborted_error",
2234        impl
2235    )]
2236    #[repr(transparent)]
2237    #[derive(Debug)]
2238    pub struct PyConnectionAbortedError(PyConnectionError);
2239
2240    #[pyexception(
2241        name,
2242        base = PyConnectionError,
2243        ctx = "connection_refused_error",
2244        impl
2245    )]
2246    #[repr(transparent)]
2247    #[derive(Debug)]
2248    pub struct PyConnectionRefusedError(PyConnectionError);
2249
2250    #[pyexception(name, base = PyConnectionError, ctx = "connection_reset_error", impl)]
2251    #[repr(transparent)]
2252    #[derive(Debug)]
2253    pub struct PyConnectionResetError(PyConnectionError);
2254
2255    #[pyexception(name, base = PyOSError, ctx = "file_exists_error", impl)]
2256    #[repr(transparent)]
2257    #[derive(Debug)]
2258    pub struct PyFileExistsError(PyOSError);
2259
2260    #[pyexception(name, base = PyOSError, ctx = "file_not_found_error", impl)]
2261    #[repr(transparent)]
2262    #[derive(Debug)]
2263    pub struct PyFileNotFoundError(PyOSError);
2264
2265    #[pyexception(name, base = PyOSError, ctx = "interrupted_error", impl)]
2266    #[repr(transparent)]
2267    #[derive(Debug)]
2268    pub struct PyInterruptedError(PyOSError);
2269
2270    #[pyexception(name, base = PyOSError, ctx = "is_a_directory_error", impl)]
2271    #[repr(transparent)]
2272    #[derive(Debug)]
2273    pub struct PyIsADirectoryError(PyOSError);
2274
2275    #[pyexception(name, base = PyOSError, ctx = "not_a_directory_error", impl)]
2276    #[repr(transparent)]
2277    #[derive(Debug)]
2278    pub struct PyNotADirectoryError(PyOSError);
2279
2280    #[pyexception(name, base = PyOSError, ctx = "permission_error", impl)]
2281    #[repr(transparent)]
2282    #[derive(Debug)]
2283    pub struct PyPermissionError(PyOSError);
2284
2285    #[pyexception(name, base = PyOSError, ctx = "process_lookup_error", impl)]
2286    #[repr(transparent)]
2287    #[derive(Debug)]
2288    pub struct PyProcessLookupError(PyOSError);
2289
2290    #[pyexception(name, base = PyOSError, ctx = "timeout_error", impl)]
2291    #[derive(Debug)]
2292    #[repr(transparent)]
2293    pub struct PyTimeoutError(PyOSError);
2294
2295    #[pyexception(name, base = PyException, ctx = "reference_error", impl)]
2296    #[derive(Debug)]
2297    #[repr(transparent)]
2298    pub struct PyReferenceError(PyException);
2299
2300    #[pyexception(name, base = PyException, ctx = "runtime_error", impl)]
2301    #[derive(Debug)]
2302    #[repr(transparent)]
2303    pub struct PyRuntimeError(PyException);
2304
2305    #[pyexception(name, base = PyRuntimeError, ctx = "not_implemented_error", impl)]
2306    #[derive(Debug)]
2307    #[repr(transparent)]
2308    pub struct PyNotImplementedError(PyRuntimeError);
2309
2310    #[pyexception(name, base = PyRuntimeError, ctx = "recursion_error", impl)]
2311    #[derive(Debug)]
2312    #[repr(transparent)]
2313    pub struct PyRecursionError(PyRuntimeError);
2314
2315    #[pyexception(name, base = PyRuntimeError, ctx = "python_finalization_error", impl)]
2316    #[derive(Debug)]
2317    #[repr(transparent)]
2318    pub struct PyPythonFinalizationError(PyRuntimeError);
2319
2320    #[pyexception(name, base = PyException, ctx = "syntax_error")]
2321    #[derive(Debug)]
2322    #[repr(transparent)]
2323    pub struct PySyntaxError(PyException);
2324
2325    #[pyexception(with(Initializer))]
2326    impl PySyntaxError {
2327        #[pymethod]
2328        fn __str__(zelf: &Py<PyBaseException>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
2329            fn basename(filename: &Wtf8) -> &Wtf8 {
2330                let bytes = filename.as_bytes();
2331                let pos = if cfg!(windows) {
2332                    bytes.iter().rposition(|&b| b == b'/' || b == b'\\')
2333                } else {
2334                    bytes.iter().rposition(|&b| b == b'/')
2335                };
2336                match pos {
2337                    // SAFETY: splitting at ASCII byte boundary preserves WTF-8 validity
2338                    Some(pos) => unsafe { Wtf8::from_bytes_unchecked(&bytes[pos + 1..]) },
2339                    None => filename,
2340                }
2341            }
2342
2343            let maybe_lineno = zelf
2344                .as_object()
2345                .get_attr("lineno", vm)
2346                .and_then(|obj| obj.str_utf8(vm))
2347                .ok();
2348            let maybe_filename = zelf.as_object().get_attr("filename", vm).ok().map(|obj| {
2349                obj.str(vm)
2350                    .unwrap_or_else(|_| vm.ctx.new_str("<filename str() failed>"))
2351            });
2352
2353            let msg = match zelf.as_object().get_attr("msg", vm) {
2354                Ok(obj) => obj
2355                    .str(vm)
2356                    .unwrap_or_else(|_| vm.ctx.new_str("<msg str() failed>")),
2357                Err(_) => {
2358                    // Fallback to the base formatting if the msg attribute was deleted or attribute lookup fails for any reason.
2359                    return Py::<PyBaseException>::__str__(zelf, vm);
2360                }
2361            };
2362
2363            let msg_with_location_info: Wtf8Buf = match (maybe_lineno, maybe_filename) {
2364                (Some(lineno), Some(filename)) => wtf8_concat!(
2365                    msg.as_wtf8(),
2366                    " (",
2367                    basename(filename.as_wtf8()),
2368                    ", line ",
2369                    lineno.as_str(),
2370                    ")"
2371                ),
2372                (Some(lineno), None) => {
2373                    wtf8_concat!(msg.as_wtf8(), " (line ", lineno.as_str(), ")")
2374                }
2375                (None, Some(filename)) => {
2376                    wtf8_concat!(msg.as_wtf8(), " (", basename(filename.as_wtf8()), ")")
2377                }
2378                (None, None) => msg.as_wtf8().to_owned(),
2379            };
2380
2381            Ok(vm.ctx.new_str(msg_with_location_info))
2382        }
2383    }
2384
2385    impl Initializer for PySyntaxError {
2386        type Args = FuncArgs;
2387
2388        fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
2389            let len = args.args.len();
2390            let new_args = args;
2391
2392            zelf.set_attr("print_file_and_line", vm.ctx.none(), vm)?;
2393
2394            if len == 2
2395                && let Ok(location_tuple) = new_args.args[1]
2396                    .clone()
2397                    .downcast::<crate::builtins::PyTuple>()
2398            {
2399                let location_tup_len = location_tuple.len();
2400
2401                match location_tup_len {
2402                    4 | 6 => {}
2403                    5 => {
2404                        return Err(vm.new_type_error(
2405                            "end_offset must be provided when end_lineno is provided".to_owned(),
2406                        ));
2407                    }
2408                    _ => {
2409                        return Err(vm.new_type_error(format!(
2410                            "function takes exactly 4 or 6 arguments ({} given)",
2411                            location_tup_len
2412                        )));
2413                    }
2414                }
2415
2416                for (i, &attr) in [
2417                    "filename",
2418                    "lineno",
2419                    "offset",
2420                    "text",
2421                    "end_lineno",
2422                    "end_offset",
2423                ]
2424                .iter()
2425                .enumerate()
2426                {
2427                    if location_tup_len > i {
2428                        zelf.set_attr(attr, location_tuple[i].to_owned(), vm)?;
2429                    } else {
2430                        break;
2431                    }
2432                }
2433            }
2434
2435            PyBaseException::slot_init(zelf, new_args, vm)
2436        }
2437
2438        fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
2439            unreachable!("slot_init is defined")
2440        }
2441    }
2442
2443    // MiddlingExtendsException: inherits __init__ from SyntaxError via MRO
2444    #[pyexception(
2445        name = "_IncompleteInputError",
2446        base = PySyntaxError,
2447        ctx = "incomplete_input_error",
2448        impl
2449    )]
2450    #[derive(Debug)]
2451    #[repr(transparent)]
2452    pub struct PyIncompleteInputError(PySyntaxError);
2453
2454    #[pyexception(name, base = PySyntaxError, ctx = "indentation_error", impl)]
2455    #[derive(Debug)]
2456    #[repr(transparent)]
2457    pub struct PyIndentationError(PySyntaxError);
2458
2459    #[pyexception(name, base = PyIndentationError, ctx = "tab_error", impl)]
2460    #[derive(Debug)]
2461    #[repr(transparent)]
2462    pub struct PyTabError(PyIndentationError);
2463
2464    #[pyexception(name, base = PyException, ctx = "system_error", impl)]
2465    #[derive(Debug)]
2466    #[repr(transparent)]
2467    pub struct PySystemError(PyException);
2468
2469    #[pyexception(name, base = PyException, ctx = "type_error", impl)]
2470    #[derive(Debug)]
2471    #[repr(transparent)]
2472    pub struct PyTypeError(PyException);
2473
2474    #[pyexception(name, base = PyException, ctx = "value_error", impl)]
2475    #[derive(Debug)]
2476    #[repr(transparent)]
2477    pub struct PyValueError(PyException);
2478
2479    #[pyexception(name, base = PyValueError, ctx = "unicode_error", impl)]
2480    #[derive(Debug)]
2481    #[repr(transparent)]
2482    pub struct PyUnicodeError(PyValueError);
2483
2484    #[pyexception(name, base = PyUnicodeError, ctx = "unicode_decode_error")]
2485    #[derive(Debug)]
2486    #[repr(transparent)]
2487    pub struct PyUnicodeDecodeError(PyUnicodeError);
2488
2489    #[pyexception(with(Initializer))]
2490    impl PyUnicodeDecodeError {
2491        #[pymethod]
2492        fn __str__(zelf: &Py<PyBaseException>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
2493            let Ok(object) = zelf.as_object().get_attr("object", vm) else {
2494                return Ok(vm.ctx.empty_str.to_owned());
2495            };
2496            let object: ArgBytesLike = object.try_into_value(vm)?;
2497            let encoding: PyStrRef = zelf
2498                .as_object()
2499                .get_attr("encoding", vm)?
2500                .try_into_value(vm)?;
2501            let start: usize = zelf.as_object().get_attr("start", vm)?.try_into_value(vm)?;
2502            let end: usize = zelf.as_object().get_attr("end", vm)?.try_into_value(vm)?;
2503            let reason: PyStrRef = zelf
2504                .as_object()
2505                .get_attr("reason", vm)?
2506                .try_into_value(vm)?;
2507            Ok(vm.ctx.new_str(if start < object.len() && end <= object.len() && end == start + 1 {
2508                let b = object.borrow_buf()[start];
2509                format!(
2510                    "'{encoding}' codec can't decode byte {b:#02x} in position {start}: {reason}"
2511                )
2512            } else {
2513                format!(
2514                    "'{encoding}' codec can't decode bytes in position {start}-{}: {reason}",
2515                    end - 1,
2516                )
2517            }))
2518        }
2519    }
2520
2521    impl Initializer for PyUnicodeDecodeError {
2522        type Args = FuncArgs;
2523
2524        fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
2525            type Args = (PyStrRef, ArgBytesLike, isize, isize, PyStrRef);
2526            let (encoding, object, start, end, reason): Args = args.bind(vm)?;
2527            zelf.set_attr("encoding", encoding, vm)?;
2528            let object_as_bytes = vm.ctx.new_bytes(object.borrow_buf().to_vec());
2529            zelf.set_attr("object", object_as_bytes, vm)?;
2530            zelf.set_attr("start", vm.ctx.new_int(start), vm)?;
2531            zelf.set_attr("end", vm.ctx.new_int(end), vm)?;
2532            zelf.set_attr("reason", reason, vm)?;
2533            Ok(())
2534        }
2535
2536        fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
2537            unreachable!("slot_init is defined")
2538        }
2539    }
2540
2541    #[pyexception(name, base = PyUnicodeError, ctx = "unicode_encode_error")]
2542    #[derive(Debug)]
2543    #[repr(transparent)]
2544    pub struct PyUnicodeEncodeError(PyUnicodeError);
2545
2546    #[pyexception(with(Initializer))]
2547    impl PyUnicodeEncodeError {
2548        #[pymethod]
2549        fn __str__(zelf: &Py<PyBaseException>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
2550            let Ok(object) = zelf.as_object().get_attr("object", vm) else {
2551                return Ok(vm.ctx.empty_str.to_owned());
2552            };
2553            let object: PyStrRef = object.try_into_value(vm)?;
2554            let encoding: PyStrRef = zelf
2555                .as_object()
2556                .get_attr("encoding", vm)?
2557                .try_into_value(vm)?;
2558            let start: usize = zelf.as_object().get_attr("start", vm)?.try_into_value(vm)?;
2559            let end: usize = zelf.as_object().get_attr("end", vm)?.try_into_value(vm)?;
2560            let reason: PyStrRef = zelf
2561                .as_object()
2562                .get_attr("reason", vm)?
2563                .try_into_value(vm)?;
2564            Ok(vm.ctx.new_str(if start < object.char_len() && end <= object.char_len() && end == start + 1 {
2565                let ch = object.as_wtf8().code_points().nth(start).unwrap();
2566                format!(
2567                    "'{encoding}' codec can't encode character '{}' in position {start}: {reason}",
2568                    UnicodeEscapeCodepoint(ch)
2569                )
2570            } else {
2571                format!(
2572                    "'{encoding}' codec can't encode characters in position {start}-{}: {reason}",
2573                    end - 1,
2574                )
2575            }))
2576        }
2577    }
2578
2579    impl Initializer for PyUnicodeEncodeError {
2580        type Args = FuncArgs;
2581
2582        fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
2583            type Args = (PyStrRef, PyStrRef, isize, isize, PyStrRef);
2584            let (encoding, object, start, end, reason): Args = args.bind(vm)?;
2585            zelf.set_attr("encoding", encoding, vm)?;
2586            zelf.set_attr("object", object, vm)?;
2587            zelf.set_attr("start", vm.ctx.new_int(start), vm)?;
2588            zelf.set_attr("end", vm.ctx.new_int(end), vm)?;
2589            zelf.set_attr("reason", reason, vm)?;
2590            Ok(())
2591        }
2592
2593        fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
2594            unreachable!("slot_init is defined")
2595        }
2596    }
2597
2598    #[pyexception(name, base = PyUnicodeError, ctx = "unicode_translate_error")]
2599    #[derive(Debug)]
2600    #[repr(transparent)]
2601    pub struct PyUnicodeTranslateError(PyUnicodeError);
2602
2603    #[pyexception(with(Initializer))]
2604    impl PyUnicodeTranslateError {
2605        #[pymethod]
2606        fn __str__(zelf: &Py<PyBaseException>, vm: &VirtualMachine) -> PyResult<PyStrRef> {
2607            let Ok(object) = zelf.as_object().get_attr("object", vm) else {
2608                return Ok(vm.ctx.empty_str.to_owned());
2609            };
2610            let object: PyStrRef = object.try_into_value(vm)?;
2611            let start: usize = zelf.as_object().get_attr("start", vm)?.try_into_value(vm)?;
2612            let end: usize = zelf.as_object().get_attr("end", vm)?.try_into_value(vm)?;
2613            let reason: PyStrRef = zelf
2614                .as_object()
2615                .get_attr("reason", vm)?
2616                .try_into_value(vm)?;
2617            Ok(vm.ctx.new_str(
2618                if start < object.char_len() && end <= object.char_len() && end == start + 1 {
2619                    let ch = object.as_wtf8().code_points().nth(start).unwrap();
2620                    format!(
2621                        "can't translate character '{}' in position {start}: {reason}",
2622                        UnicodeEscapeCodepoint(ch)
2623                    )
2624                } else {
2625                    format!(
2626                        "can't translate characters in position {start}-{}: {reason}",
2627                        end - 1,
2628                    )
2629                },
2630            ))
2631        }
2632    }
2633
2634    impl Initializer for PyUnicodeTranslateError {
2635        type Args = FuncArgs;
2636
2637        fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> {
2638            type Args = (PyStrRef, isize, isize, PyStrRef);
2639            let (object, start, end, reason): Args = args.bind(vm)?;
2640            zelf.set_attr("object", object, vm)?;
2641            zelf.set_attr("start", vm.ctx.new_int(start), vm)?;
2642            zelf.set_attr("end", vm.ctx.new_int(end), vm)?;
2643            zelf.set_attr("reason", reason, vm)?;
2644            Ok(())
2645        }
2646
2647        fn init(_zelf: PyRef<Self>, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> {
2648            unreachable!("slot_init is defined")
2649        }
2650    }
2651
2652    /// JIT error.
2653    #[cfg(feature = "jit")]
2654    #[pyexception(name, base = PyException, ctx = "jit_error", impl)]
2655    #[derive(Debug)]
2656    #[repr(transparent)]
2657    pub struct PyJitError(PyException);
2658
2659    // Warnings
2660    #[pyexception(name, base = PyException, ctx = "warning", impl)]
2661    #[derive(Debug)]
2662    #[repr(transparent)]
2663    pub struct PyWarning(PyException);
2664
2665    #[pyexception(name, base = PyWarning, ctx = "deprecation_warning", impl)]
2666    #[derive(Debug)]
2667    #[repr(transparent)]
2668    pub struct PyDeprecationWarning(PyWarning);
2669
2670    #[pyexception(name, base = PyWarning, ctx = "pending_deprecation_warning", impl)]
2671    #[derive(Debug)]
2672    #[repr(transparent)]
2673    pub struct PyPendingDeprecationWarning(PyWarning);
2674
2675    #[pyexception(name, base = PyWarning, ctx = "runtime_warning", impl)]
2676    #[derive(Debug)]
2677    #[repr(transparent)]
2678    pub struct PyRuntimeWarning(PyWarning);
2679
2680    #[pyexception(name, base = PyWarning, ctx = "syntax_warning", impl)]
2681    #[derive(Debug)]
2682    #[repr(transparent)]
2683    pub struct PySyntaxWarning(PyWarning);
2684
2685    #[pyexception(name, base = PyWarning, ctx = "user_warning", impl)]
2686    #[derive(Debug)]
2687    #[repr(transparent)]
2688    pub struct PyUserWarning(PyWarning);
2689
2690    #[pyexception(name, base = PyWarning, ctx = "future_warning", impl)]
2691    #[derive(Debug)]
2692    #[repr(transparent)]
2693    pub struct PyFutureWarning(PyWarning);
2694
2695    #[pyexception(name, base = PyWarning, ctx = "import_warning", impl)]
2696    #[derive(Debug)]
2697    #[repr(transparent)]
2698    pub struct PyImportWarning(PyWarning);
2699
2700    #[pyexception(name, base = PyWarning, ctx = "unicode_warning", impl)]
2701    #[derive(Debug)]
2702    #[repr(transparent)]
2703    pub struct PyUnicodeWarning(PyWarning);
2704
2705    #[pyexception(name, base = PyWarning, ctx = "bytes_warning", impl)]
2706    #[derive(Debug)]
2707    #[repr(transparent)]
2708    pub struct PyBytesWarning(PyWarning);
2709
2710    #[pyexception(name, base = PyWarning, ctx = "resource_warning", impl)]
2711    #[derive(Debug)]
2712    #[repr(transparent)]
2713    pub struct PyResourceWarning(PyWarning);
2714
2715    #[pyexception(name, base = PyWarning, ctx = "encoding_warning", impl)]
2716    #[derive(Debug)]
2717    #[repr(transparent)]
2718    pub struct PyEncodingWarning(PyWarning);
2719}
2720
2721/// Check if match_type is valid for except* (must be exception type, not ExceptionGroup).
2722fn check_except_star_type_valid(match_type: &PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
2723    let base_exc: PyObjectRef = vm.ctx.exceptions.base_exception_type.to_owned().into();
2724    let base_eg: PyObjectRef = vm.ctx.exceptions.base_exception_group.to_owned().into();
2725
2726    // Helper to check a single type
2727    let check_one = |exc_type: &PyObjectRef| -> PyResult<()> {
2728        // Must be a subclass of BaseException
2729        if !exc_type.is_subclass(&base_exc, vm)? {
2730            return Err(vm.new_type_error(
2731                "catching classes that do not inherit from BaseException is not allowed",
2732            ));
2733        }
2734        // Must not be a subclass of BaseExceptionGroup
2735        if exc_type.is_subclass(&base_eg, vm)? {
2736            return Err(vm.new_type_error(
2737                "catching ExceptionGroup with except* is not allowed. Use except instead.",
2738            ));
2739        }
2740        Ok(())
2741    };
2742
2743    // If it's a tuple, check each element
2744    if let Ok(tuple) = match_type.clone().downcast::<PyTuple>() {
2745        for item in tuple.iter() {
2746            check_one(item)?;
2747        }
2748    } else {
2749        check_one(match_type)?;
2750    }
2751    Ok(())
2752}
2753
2754/// Match exception against except* handler type.
2755/// Returns (rest, match) tuple.
2756pub fn exception_group_match(
2757    exc_value: &PyObjectRef,
2758    match_type: &PyObjectRef,
2759    vm: &VirtualMachine,
2760) -> PyResult<(PyObjectRef, PyObjectRef)> {
2761    // Implements _PyEval_ExceptionGroupMatch
2762
2763    // If exc_value is None, return (None, None)
2764    if vm.is_none(exc_value) {
2765        return Ok((vm.ctx.none(), vm.ctx.none()));
2766    }
2767
2768    // Validate match_type and reject ExceptionGroup/BaseExceptionGroup
2769    check_except_star_type_valid(match_type, vm)?;
2770
2771    // Check if exc_value matches match_type
2772    if exc_value.is_instance(match_type, vm)? {
2773        // Full match of exc itself
2774        let is_eg = exc_value.fast_isinstance(vm.ctx.exceptions.base_exception_group);
2775        let matched = if is_eg {
2776            exc_value.clone()
2777        } else {
2778            // Naked exception - wrap it in ExceptionGroup
2779            let excs = vm.ctx.new_tuple(vec![exc_value.clone()]);
2780            let eg_type: PyObjectRef = crate::exception_group::exception_group().to_owned().into();
2781            let wrapped = eg_type.call((vm.ctx.new_str(""), excs), vm)?;
2782            // Copy traceback from original exception
2783            if let Ok(exc) = exc_value.clone().downcast::<types::PyBaseException>()
2784                && let Some(tb) = exc.__traceback__()
2785                && let Ok(wrapped_exc) = wrapped.clone().downcast::<types::PyBaseException>()
2786            {
2787                let _ = wrapped_exc.set___traceback__(tb.into(), vm);
2788            }
2789            wrapped
2790        };
2791        return Ok((vm.ctx.none(), matched));
2792    }
2793
2794    // Check for partial match if it's an exception group
2795    if exc_value.fast_isinstance(vm.ctx.exceptions.base_exception_group) {
2796        let pair = vm.call_method(exc_value, "split", (match_type.clone(),))?;
2797        if !pair.class().is(vm.ctx.types.tuple_type) {
2798            return Err(vm.new_type_error(format!(
2799                "{}.split must return a tuple, not {}",
2800                exc_value.class().name(),
2801                pair.class().name()
2802            )));
2803        }
2804        let pair_tuple: PyTupleRef = pair.try_into_value(vm)?;
2805        if pair_tuple.len() < 2 {
2806            return Err(vm.new_type_error(format!(
2807                "{}.split must return a 2-tuple, got tuple of size {}",
2808                exc_value.class().name(),
2809                pair_tuple.len()
2810            )));
2811        }
2812        let matched = pair_tuple[0].clone();
2813        let rest = pair_tuple[1].clone();
2814        return Ok((rest, matched));
2815    }
2816
2817    // No match
2818    Ok((exc_value.clone(), vm.ctx.none()))
2819}
2820
2821/// Prepare exception for reraise in except* block.
2822/// Implements _PyExc_PrepReraiseStar
2823pub fn prep_reraise_star(orig: PyObjectRef, excs: PyObjectRef, vm: &VirtualMachine) -> PyResult {
2824    use crate::builtins::PyList;
2825
2826    let excs_list = excs
2827        .downcast::<PyList>()
2828        .map_err(|_| vm.new_type_error("expected list for prep_reraise_star"))?;
2829
2830    let excs_vec: Vec<PyObjectRef> = excs_list.borrow_vec().to_vec();
2831
2832    // If no exceptions to process, return None
2833    if excs_vec.is_empty() {
2834        return Ok(vm.ctx.none());
2835    }
2836
2837    // Special case: naked exception (not an ExceptionGroup)
2838    // Only one except* clause could have executed, so there's at most one exception to raise
2839    if !orig.fast_isinstance(vm.ctx.exceptions.base_exception_group) {
2840        // Find first non-None exception
2841        let first = excs_vec.into_iter().find(|e| !vm.is_none(e));
2842        return Ok(first.unwrap_or_else(|| vm.ctx.none()));
2843    }
2844
2845    // Split excs into raised (new) and reraised (from original) by comparing metadata
2846    let mut raised: Vec<PyObjectRef> = Vec::new();
2847    let mut reraised: Vec<PyObjectRef> = Vec::new();
2848
2849    for exc in excs_vec {
2850        if vm.is_none(&exc) {
2851            continue;
2852        }
2853        // Check if this exception came from the original group
2854        if is_exception_from_orig(&exc, &orig, vm) {
2855            reraised.push(exc);
2856        } else {
2857            raised.push(exc);
2858        }
2859    }
2860
2861    // If no exceptions to reraise, return None
2862    if raised.is_empty() && reraised.is_empty() {
2863        return Ok(vm.ctx.none());
2864    }
2865
2866    // Project reraised exceptions onto original structure to preserve nesting
2867    let reraised_eg = exception_group_projection(&orig, &reraised, vm)?;
2868
2869    // If no new raised exceptions, just return the reraised projection
2870    if raised.is_empty() {
2871        return Ok(reraised_eg);
2872    }
2873
2874    // Combine raised with reraised_eg
2875    if !vm.is_none(&reraised_eg) {
2876        raised.push(reraised_eg);
2877    }
2878
2879    // If only one exception, return it directly
2880    if raised.len() == 1 {
2881        return Ok(raised.into_iter().next().unwrap());
2882    }
2883
2884    // Create new ExceptionGroup for multiple exceptions
2885    let excs_tuple = vm.ctx.new_tuple(raised);
2886    let eg_type: PyObjectRef = crate::exception_group::exception_group().to_owned().into();
2887    eg_type.call((vm.ctx.new_str(""), excs_tuple), vm)
2888}
2889
2890/// Check if an exception came from the original group (for reraise detection).
2891/// Instead of comparing metadata (which can be modified when caught), we compare
2892/// leaf exception object IDs. split() preserves leaf exception identity.
2893fn is_exception_from_orig(exc: &PyObjectRef, orig: &PyObjectRef, vm: &VirtualMachine) -> bool {
2894    // Collect leaf exception IDs from exc
2895    let mut exc_leaf_ids = HashSet::new();
2896    collect_exception_group_leaf_ids(exc, &mut exc_leaf_ids, vm);
2897
2898    if exc_leaf_ids.is_empty() {
2899        return false;
2900    }
2901
2902    // Collect leaf exception IDs from orig
2903    let mut orig_leaf_ids = HashSet::new();
2904    collect_exception_group_leaf_ids(orig, &mut orig_leaf_ids, vm);
2905
2906    // If ALL of exc's leaves are in orig's leaves, it's a reraise
2907    exc_leaf_ids.iter().all(|id| orig_leaf_ids.contains(id))
2908}
2909
2910/// Collect all leaf exception IDs from an exception (group).
2911fn collect_exception_group_leaf_ids(
2912    exc: &PyObjectRef,
2913    leaf_ids: &mut HashSet<usize>,
2914    vm: &VirtualMachine,
2915) {
2916    if vm.is_none(exc) {
2917        return;
2918    }
2919
2920    // If not an exception group, it's a leaf - add its ID
2921    if !exc.fast_isinstance(vm.ctx.exceptions.base_exception_group) {
2922        leaf_ids.insert(exc.get_id());
2923        return;
2924    }
2925
2926    // Recurse into exception group's exceptions
2927    if let Ok(excs_attr) = exc.get_attr("exceptions", vm)
2928        && let Ok(tuple) = excs_attr.downcast::<PyTuple>()
2929    {
2930        for e in tuple.iter() {
2931            collect_exception_group_leaf_ids(e, leaf_ids, vm);
2932        }
2933    }
2934}
2935
2936/// Project orig onto keep list, preserving nested structure.
2937/// Returns an exception group containing only the exceptions from orig
2938/// that are also in the keep list.
2939fn exception_group_projection(
2940    orig: &PyObjectRef,
2941    keep: &[PyObjectRef],
2942    vm: &VirtualMachine,
2943) -> PyResult {
2944    if keep.is_empty() {
2945        return Ok(vm.ctx.none());
2946    }
2947
2948    // Collect all leaf IDs from keep list
2949    let mut leaf_ids = HashSet::new();
2950    for e in keep {
2951        collect_exception_group_leaf_ids(e, &mut leaf_ids, vm);
2952    }
2953
2954    // Split orig by matching leaf IDs, preserving structure
2955    split_by_leaf_ids(orig, &leaf_ids, vm)
2956}
2957
2958/// Recursively split an exception (group) by leaf IDs.
2959/// Returns the projection containing only matching leaves with preserved structure.
2960fn split_by_leaf_ids(
2961    exc: &PyObjectRef,
2962    leaf_ids: &HashSet<usize>,
2963    vm: &VirtualMachine,
2964) -> PyResult {
2965    if vm.is_none(exc) {
2966        return Ok(vm.ctx.none());
2967    }
2968
2969    // If not an exception group, check if it's in our set
2970    if !exc.fast_isinstance(vm.ctx.exceptions.base_exception_group) {
2971        if leaf_ids.contains(&exc.get_id()) {
2972            return Ok(exc.clone());
2973        }
2974        return Ok(vm.ctx.none());
2975    }
2976
2977    // Exception group - recurse and reconstruct
2978    let excs_attr = exc.get_attr("exceptions", vm)?;
2979    let tuple: PyTupleRef = excs_attr.try_into_value(vm)?;
2980
2981    let mut matched = Vec::new();
2982    for e in tuple.iter() {
2983        let m = split_by_leaf_ids(e, leaf_ids, vm)?;
2984        if !vm.is_none(&m) {
2985            matched.push(m);
2986        }
2987    }
2988
2989    if matched.is_empty() {
2990        return Ok(vm.ctx.none());
2991    }
2992
2993    // Reconstruct using derive() to preserve the structure (not necessarily the subclass type)
2994    let matched_tuple = vm.ctx.new_tuple(matched);
2995    vm.call_method(exc, "derive", (matched_tuple,))
2996}