Worth noting is that there is an existing loop-breaking mechanism, 
but only for the newest exception being raised. In particular, option (4)
is actually the current behavior if the the most recent exception
participates in a cycle:

    Python 3.9.0b1
    >>> A, B, C, D, E = map(Exception, "ABCDE")
    >>> A.__context__ = B
    >>> B.__context__ = C
    >>> C.__context__ = D
    >>> D.__context__ = E
    >>> try:
    ...     raise A
    ... except Exception:
    ...     raise C
    ...
    Exception: B

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File "<stdin>", line 2, in <module>
    Exception: A

    During handling of the above exception, another exception occurred:

    Traceback (most recent call last):
    File "<stdin>", line 4, in <module>
    Exception: C

This cycle-breaking is not due to any magic in the ``PyException_SetContext()``,
which is currently a basic one-liner, but instead comes from
``_PyErr_SetObject`` in errors.c, which has something along the lines of:

    def _PyErr_SetObject(new_exc):
        top = existing_topmost_exc()

        if top is None:
            # no context
            set_top_exception(new_exc)
            return

        # convert new_exc class to instance if applicable.
        ...

        if top is new_exc:
            # already on top
            return

        e = top
        while True:
            context = e.__context__
            if context is None:
                # no loop
                break
            if context is new_exc:
                # unlink the existing exception
                e.__context__ = None
                break
            e = context
    
        new_exc.__context__ = top    
        set_top_exception(new_exc)

The only trouble comes about when there is a "rho-shaped" linked list,
in which we have a cycle not involving the new exception being raised.
For instance,

    Raising A on top of (B -> C -> D -> C -> D -> C -> ...)
    results in an infinite loop.

Two possible fixes would be to either (I) use a magical ``__context__``
setter to ensure that there is never a rho-shaped sequence, or (II)
allow arbitrary ``__context__`` graphs and then correctly handle
rho-shaped sequences in ``_PyErr_SetObject`` (i.e. at raise-time).

Fix type (I) could result in surprising things like:

    >>> A = Exception()
    >>> A.__context__ = A
    >>> A.__context__ is None
    True

so I propose fix type (II). This PR is such a fix:
https://github.com/python/cpython/pull/20539

It basically extends the existing behavior (4) to the rho-shaped case.

It also prevents the cycle-detecting logic from sitting in two places
(both _PyErr_SetObject and PyException_SetContext) and does not make any
visible functionality more magical. The only Python-visible change
should be that the infinite loop is no longer possible.
_______________________________________________
Python-Dev mailing list -- python-dev@python.org
To unsubscribe send an email to python-dev-le...@python.org
https://mail.python.org/mailman3/lists/python-dev.python.org/
Message archived at 
https://mail.python.org/archives/list/python-dev@python.org/message/R5J5JVUJX3V4DBKVLUI2SUBRD3TRF6PV/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to