Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python change Exception printable output, eg overload __builtins__

I am searching for a way to change the printable output of an Exception to a silly message in order to learn more about python internals (and mess with a friend ;), so far without success.

Consider the following code

try:
   x # is not defined
except NameError as exc:
   print(exc)

The code shall output name 'x' is not defined

I would like the change that output to the name 'x' you suggested is not yet defined, my lord. Improve your coding skills.

So far, I understood that you can't change __builtins__ because they're "baked in" as C code, unless:

  1. You use forbiddenfruit.curse method which adds / changes properties of any object
  2. You manually override the dictionnaries of an object

I've tried both solutions, but without success:

forbiddenfruit solution:

from forbiddenfruit import curse

curse(BaseException, 'repr', lambda self: print("Test message for repr"))
curse(BaseException, 'str', lambda self: print("Test message for str"))

try:
    x
except NameError as exc:
    print(exc.str()) # Works, shows test message
    print(exc.repr()) # Works, shows test message
    print(repr(exc)) # Does not work, shows real message
    print(str(exc)) # Does not work, shows real message
    print(exc) # Does not work, shows real message

Dictionnary overriding solution:

import gc

underlying_dict = gc.get_referents(BaseException.__dict__)[0]
underlying_dict["__repr__"] = lambda self: print("test message for repr")
underlying_dict["__str__"] = lambda self: print("test message for str")
underlying_dict["args"] = 'I am an argument list'

try:
    x
except NameError as exc:
    print(exc.__str__()) # Works, shows test message
    print(exc.__repr__()) # Works, shows test message
    print(repr(exc)) # Does not work, shows real message
    print(str(exc)) # Does not work, shows real message
    print(exc) # Does not work, shows real message

AFAIK, using print(exc) should rely on either __repr__ or __str__, but it seems like the print function uses something else, which I cannot find even when reading all properties of BaseException via print(dir(BaseException)). Could anyone give me an insight of what print uses in this case please ?

[EDIT]

To add a bit more context:

The problem I'm trying to solve began as a joke to mess with a programmer friend, but now became a challenge for me to understand more of python's internals.

There's no real business problem I'm trying to solve, I just want to get deeper understanding of things in Python. I'm quite puzzled that print(exc) won't make use of BaseException.__repr__ or __str__ actually.

[/EDIT]

like image 580
Orsiris de Jong Avatar asked Oct 30 '20 15:10

Orsiris de Jong


3 Answers

Intro

I'd go with a more critical approach on why you'd even want to do what you want to do.

Python provides you with an ability to handle specific exceptions. That means if you had a business problem, you'd use a particular exception class and provide a custom message for that specific case. Now, remember this paragraph and let's move on, I'll refer to this later.


TL;DR

Now, let's go top-down:

Catching all kinds of errors with except Exception is generally not a good idea if want you catch let's say a variable name error. You'd use except NameError instead. There's really not much you'd add to it that's why it had a default message that perfectly described the issue. So it's assumed you'd use it as it's given. These are called concrete exceptions.

Now, with your specific case notice the alias as exc. By using the alias you can access arguments passed to the exception object, including the default message.

try:
   x # is not defined
except NameError as exc:
   print(exc.args)

Run that code (I put it in app.py) and you'll see:

$ python app.py
("name 'x' is not defined",)

These args are passed to the exception as a series (list, or in this case immutable list that is a tuple).

This leads to the idea of the possibility of easily passing arguments to exceptions' constructors (__init__). In your case "name 'x' is not defined" was passed as an argument.

You can use this to your advantage to solve your problem without much effort by just providing a custom message, like:

try:
   x # is not defined
except NameError as exc:
   your_custom_message = "the name 'x' you suggested is not yet defined, my lord. Improve your coding skills"
   # Now, you can handle it based on your requirement:
   #  print(your_custom_message)
   #  print(NameError(your_custom_message))
   #  raise NameError(your_custom_message)
   #  raise NameError(your_custom_message) from exc

The output is now what you wanted to achieve.

$ python app.py
the name 'x' you suggested is not yet defined, my lord. Improve your coding skills

Remember the first paragraph when I said I'd refer to it later? I mentioned providing a custom message for a specific case. If you build your own library when you want to handle name errors to specific variables relevant to your product, you assume your users will use your code that might raise that NameError exception. They will most likely catch it with except Exception as exc or except NameError as exc. And when they do print(exc), they will see your message now.


Summary

I hope that makes sense to you, just provide a custom message and pass it as an argument to NameError or simply just print it. IMO, it's better to learn it right together with why you'd use what you use.

like image 54
Eli Halych Avatar answered Nov 15 '22 02:11

Eli Halych


Errors like this are hard-coded into the interpreter (in the case of CPython, anyway, which is most likely what you are using). You will not be able to change the message printed from within Python itself.

The C source code that is executed when the CPython interpreter tries to look up a name can be found here: https://github.com/python/cpython/blob/master/Python/ceval.c#L2602. If you would want to change the error message printed when a name lookup fails, you would need to change this line in the same file:

#define NAME_ERROR_MSG \
    "name '%.200s' is not defined"

Compiling the modified source code would yield a Python interpreter that prints your custom error message when encountering a name that is not defined.

like image 30
CyanoKobalamyne Avatar answered Nov 15 '22 03:11

CyanoKobalamyne


I'll just explain the behaviour you described:

  • exc.__repr__()

This will just call your lambda function and return the expected string. Btw you should return the string, not print it in your lambda functions.

  • print(repr(exc))

Now, this is going a different route in CPython and you can see this in a GDB session, it's something like this:

Python/bltinmodule.c:builtin_repr will call Objects/object.c:PyObject_Repr - this function gets the PyObject *v as the only parameter that it will use to get and call a function that implements the built-in function repr(), BaseException_repr in this case. This function will format the error message based on a value from args structure field:

(gdb) p ((PyBaseExceptionObject *) self)->args 
$188 = ("name 'x' is not defined",)

The args value is set in Python/ceval.c:format_exc_check_arg based on a NAME_ERROR_MSG macro set in the same file.

Update: Sun 8 Nov 20:19:26 UTC 2020

test.py:

import sys
import dis


def main():
    try:
        x
    except NameError as exc:
        tb = sys.exc_info()[2]
        frame, i = tb.tb_frame, tb.tb_lasti
        code = frame.f_code
        arg = code.co_code[i + 1]
        name = code.co_names[arg]
        print(name)


if __name__ == '__main__':
    main()

Test:

# python test.py
x

Note:

I would also recommend to watch this video from PyCon 2016.

like image 37
HTF Avatar answered Nov 15 '22 02:11

HTF