Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Where's the logic that returns an instance of a subclass of OSError exception class?

Tags:

python

I've been hunting for something that could be relatively stupid to some people, but to me very interesting! :-)

Input and output errors have been merged with OSError in Python 3.3, so there's a change in the exception class hierarchy. One interesting feature about the builtin class OSError is that, it returns a subclass of it when passed errno and strerror

>>> OSError(2, os.strerror(2))
FileNotFoundError(2, 'No such file or directory')

>>> OSError(2, os.strerror(2)).errno
2
>>> OSError(2, os.strerror(2)).strerror
'No such file or directory'

As you can see passing errno and strerror to the constructor of OSError returns FileNotFoundError instance which is a subclass of OSError.

Python Doc:

The constructor often actually returns a subclass of OSError, as described in OS exceptions below. The particular subclass depends on the final errno value. This behaviour only occurs when constructing OSError directly or via an alias, and is not inherited when subclassing.

I wanted to code a subclass that would behave in this way. It's mostly curiosity and not real world code. I'm also trying to know, where's the logic that creates the subclass object, is it coded in __new__ for example? If __new__ contains the logic for creating the instances of the subclasses, then inheriting from OSError would typically return this behavior, unless if there's some sort of type checking in __new__:

>>> class A(OSError): pass 
>>> A(2, os.strerror(2))
A(2, 'No such file or directory')

There must be type checking then:

# If passed OSError, returns subclass instance
>>> A.__new__(OSError, 2, os.strerror(2))         
FileNotFoundError(2, 'No such file or directory')

# Not OSError? Return instance of A
>>> A.__new__(A, 2, os.strerror(2)
A(2, 'No such file or directory')

I've been digging through C code to find out where's this code is placed exactly and since I'm not an expert in C, I suspect this is really the logic and (I'm quite skeptical about that to be frank):

exceptions.c

if (myerrno && PyLong_Check(myerrno) &&
    errnomap && (PyObject *) type == PyExc_OSError) {
    PyObject *newtype;
    newtype = PyDict_GetItem(errnomap, myerrno);
    if (newtype) {
        assert(PyType_Check(newtype));
        type = (PyTypeObject *) newtype;
    }
    else if (PyErr_Occurred())
        goto error;
}
}

Now I'm wondering about the possibility of expanding errnomap from Python itself without playing with C code, so that OSErro can make instances of user-defined classes, if you ask me why would you do that? I would say, just for fun.

like image 226
direprobs Avatar asked Sep 14 '16 19:09

direprobs


People also ask

What causes OSError in Python?

OSError is a built-in exception in Python and serves as the error class for the os module, which is raised when an os specific system function returns a system-related error, including I/O failures such as “file not found” or “disk full”.

What is the exception class in Python?

In Python, all exceptions must be instances of a class that derives from BaseException . In a try statement with an except clause that mentions a particular class, that clause also handles any exception classes derived from that class (but not exception classes from which it is derived).

Which of the following built-in exceptions is the root class for all exceptions in Python?

BaseException. The BaseException class is, as the name suggests, the base class for all built-in exceptions in Python. Typically, this exception is never raised on its own, and should instead be inherited by other, lesser exception classes that can be raised.

What is base exception in Python?

The BaseException is the base class of all other exceptions. User defined classes cannot be directly derived from this class, to derive user defied class, we need to use Exception class. The Python Exception Hierarchy is like below. BaseException.


1 Answers

You're correct that errnomap is the variable that holds the mapping from errno values to OSError subclasses, but unfortunately it's not exported outside the exceptions.c source file, so there's no portable way to modify it.


It is possible to access it using highly non-portable hacks, and I present one possible method for doing so (using a debugger) below purely in a spirit of fun. This should work on any x86-64 Linux system.

>>> import os, sys
>>> os.system("""gdb -p %d \
-ex 'b PyDict_GetItem if (PyLong_AsLongLong($rsi) == -1 ? \
(PyErr_Clear(), 0) : PyLong_AsLongLong($rsi)) == 0xbaadf00d' \
-ex c \
-ex 'call PySys_SetObject("errnomap", $rdi)' --batch >/dev/null 2>&1 &""" % os.getpid()) 
0
>>> OSError(0xbaadf00d, '')
OSError(3131961357, '')
>>> sys.errnomap
{32: <class 'BrokenPipeError'>, 1: <class 'PermissionError'> [...]}
>>> class ImATeapotError(OSError):
    pass
>>> sys.errnomap[99] = ImATeapotError
>>> OSError(99, "I'm a teapot")
ImATeapotError(99, "I'm a teapot")

Quick explanation of how this works:

gdb -p %d [...] --batch >/dev/null 2>&1 &

Attach a debugger to the current Python process (os.getpid()), in unattended mode (--batch), discarding output (>/dev/null 2>&1) and in the background (&), allowing Python to continue running.

b PyDict_GetItem if (PyLong_AsLongLong($rsi) == -1 ? (PyErr_Clear(), 0) : PyLong_AsLongLong($rsi)) == 0xbaadf00d

When the Python program accesses any dictionary, break if the key is an int with a magic value (used as OSError(0xbaadf00d, '') later); if it isn't an int, we've just raised TypeError, so suppress it.

call PySys_SetObject("errnomap", $rdi)

When this happens, we know the dictionary being looked up in is the errnomap; store it as an attribute on the sys module.

like image 126
ecatmur Avatar answered Oct 20 '22 18:10

ecatmur