Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-112217: Add check to call result for do_raise() where cause is a type. #112216

Merged
merged 5 commits into from
Nov 27, 2023

Conversation

apaz-cli
Copy link
Contributor

@apaz-cli apaz-cli commented Nov 17, 2023

Consider the following:

class ConstructsNone(BaseException):
  @classmethod
  def __new__(*args, **kwargs): return None

raise Exception("Printing this exception raises an exception. Mwa-ha-nyaa~ >:3") from ConstructsNone
TypeError: print_exception(): Exception expected for value, NoneType found

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
Exception: Printing this exception raises an exception. Mwa-ha-nyaa~ >:3

In Python/ceval.c, in do_raise(), when you raise an object, cpython checks if it's an exception type, and if it is, constructs it by calling it with no arguments. Then it checks to make sure that what was constructed is in fact an exception.

Then it does the same thing for the exception's cause. If it's a type, it constructs the cause by calling it with no arguments. But, for the cause, it actually doesn't check to make sure that the result of the call is in fact an exception, it just stores the result without checking. This seems like a bug. Not a catastrophic one by any means, but probably unintentional considering that the very same condition is checked a few lines above.

That doesn't necessarily explain the result above though. We've created an exception object where the cause is None (or any other sort of object that we want). Then, when the interpreter (interactive mode) goes to print the exception, it expects the cause to be an exception. This leads to yet another exception being raised, telling you that the cause is the wrong type.

The solution of course is just to add the check when the cause is called.

Copy link

cpython-cla-bot bot commented Nov 17, 2023

All commit authors signed the Contributor License Agreement.
CLA signed

@bedevere-app
Copy link

bedevere-app bot commented Nov 17, 2023

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@apaz-cli apaz-cli changed the title Add check to call result for do_raise() where cause is a type. gh-112216: Add check to call result for do_raise() where cause is a type. Nov 17, 2023
@JelleZijlstra
Copy link
Member

Thanks for contributing a fix.

You'll need a unit test to demonstrate this problem. Also, please open an issue; all substantive changes to CPython need an accompanying issue.

@apaz-cli apaz-cli changed the title gh-112216: Add check to call result for do_raise() where cause is a type. gh-112217: Add check to call result for do_raise() where cause is a type. Nov 17, 2023
@bedevere-app
Copy link

bedevere-app bot commented Nov 17, 2023

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@sunmy2019
Copy link
Member

sunmy2019 commented Nov 18, 2023

What is the new output then?

class ConstructsNone(BaseException):
  @classmethod
  def __new__(*args, **kwargs): return None

raise Exception("Printing this exception raises an exception. Mwa-ha-nyaa~ >:3") from ConstructsNone

Copy link
Member

@sunmy2019 sunmy2019 left a comment

Choose a reason for hiding this comment

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

looks good

@apaz-cli
Copy link
Contributor Author

@sunmy2019 The new output would be:

Traceback (most recent call last):
  File "/home/apaz/e.py", line 5, in <module>
    raise Exception("Printing this exception raises an exception. Mwa-ha-nyaa~~ >:3") from ConstructsNone
TypeError: calling <class '__main__.ConstructsNone'> should have returned an instance of BaseException, not <class 'NoneType'>

@iritkatriel
Copy link
Member

Which Python version are you looking at? I don't see this on main:

Python 3.13.0a1+ (heads/dis_labels-dirty:852c5c4cbf, Nov 16 2023, 21:21:44) [Clang 15.0.0 (clang-1500.0.40.1)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> class ConstructsNone(BaseException):
...   @classmethod
...   def __new__(*args, **kwargs): return None
... 
>>> raise Exception("Printing this exception raises an exception. Mwa-ha-nyaa~ >:3") from ConstructsNone
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    raise Exception("Printing this exception raises an exception. Mwa-ha-nyaa~ >:3") from ConstructsNone
Exception: Printing this exception raises an exception. Mwa-ha-nyaa~ >:3

Then, when the interpreter (interactive mode) goes to print the exception, it expects the cause to be an exception. > This leads to yet another exception being raised, telling you that the cause is the wrong type.

This doesn't sound right. The __cause__ can (and often is) None.

@iritkatriel
Copy link
Member

If the exception constructor returns an int rather than None, we do get this:

>>> class ConstructsNone(BaseException):
...    @classmethod
...    def __new__(*args, **kwargs): return 42
... 
>>> raise Exception("Printing this exception raises an exception. Mwa-ha-nyaa~ >:3") from ConstructsNone
Exception ignored in the internal traceback machinery:
Traceback (most recent call last):
  File "/Users/iritkatriel/src/cpython-654/Lib/traceback.py", line 133, in _print_exception_bltin
    return print_exception(exc, limit=BUILTIN_EXCEPTION_LIMIT, file=file)
  File "/Users/iritkatriel/src/cpython-654/Lib/traceback.py", line 124, in print_exception
    te = TracebackException(type(value), value, tb, limit=limit, compact=True)
  File "/Users/iritkatriel/src/cpython-654/Lib/traceback.py", line 809, in __init__
    e.__cause__.__traceback__,
AttributeError: 'int' object has no attribute '__traceback__'
TypeError: print_exception(): Exception expected for value, int found

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>-3", line 1, in <module>
Exception: Printing this exception raises an exception. Mwa-ha-nyaa~ >:3

I'm not sure how much we should care. I'm of the view that there is no use case for "raise from Type" and it should never have been a feature.

@sunmy2019
Copy link
Member

@sunmy2019 The new output would be:

Traceback (most recent call last):
  File "/home/apaz/e.py", line 5, in <module>
    raise Exception("Printing this exception raises an exception. Mwa-ha-nyaa~~ >:3") from ConstructsNone
TypeError: calling <class '__main__.ConstructsNone'> should have returned an instance of BaseException, not <class 'NoneType'>

But it would shadow the original exception message, right?

@apaz-cli
Copy link
Contributor Author

apaz-cli commented Nov 18, 2023

@iritkatriel Yeah, it's minor. I just thought it was really strange that the result of the call was checked for constructing exceptions, but not for constructing causes. I also thought it was strange that printing the exception threw an exception when the cause was None, because __cause__ is annotated as None | BaseException. I assumed that there was builtin shenanigans going on to get the cause, or that print_exception() was actually checking whether the attribute was undefined, but I never verified any of those assumptions.

I tested on 3.11.2 (Debian), 3.10.13+, and I could have sworn that I tested it on main, but I just recompiled, and you're right, it doesn't do this anymore, at least when the cause is None.

And yeah, I dislike this feature too.

@sunmy2019 Yes, it shadows the original exception message. That is (presumably) intentional. Your exception cannot be raised if you passed invalid arguments to raise. Presumably.

Copy link
Member

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

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

Seems fine to me.

Errors reported from mistakes in error raising always amuse me, like how raise 42 is invalid so it raises TypeError.

@bedevere-app
Copy link

bedevere-app bot commented Nov 26, 2023

Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool.

If this change has little impact on Python users, wait for a maintainer to apply the skip news label instead.

@iritkatriel
Copy link
Member

@apaz-cli This needs a news file.

There is a form for adding it, which you can reach through the "Details" link next to the failing "bedevere/news" check.
The component is "Core and Builtins".
The text can be something like this:
"Add check for the type of the __cause__ returned from calling the typeT in "raise from T".

@iritkatriel iritkatriel added the interpreter-core (Objects, Python, Grammar, and Parser dirs) label Nov 26, 2023
@apaz-cli
Copy link
Contributor Author

News file added @iritkatriel

@iritkatriel iritkatriel enabled auto-merge (squash) November 27, 2023 20:52
@iritkatriel iritkatriel merged commit 8f71b34 into python:main Nov 27, 2023
aisk pushed a commit to aisk/cpython that referenced this pull request Feb 11, 2024
Glyphack pushed a commit to Glyphack/cpython that referenced this pull request Sep 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
interpreter-core (Objects, Python, Grammar, and Parser dirs)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants