#     Copyright 2023, Kay Hayen, mailto:kay.hayen@gmail.com
#
#     Python tests originally created or extracted from other peoples work. The
#     parts were too small to be protected.
#
#     Licensed under the Apache License, Version 2.0 (the "License");
#     you may not use this file except in compliance with the License.
#     You may obtain a copy of the License at
#
#        http://www.apache.org/licenses/LICENSE-2.0
#
#     Unless required by applicable law or agreed to in writing, software
#     distributed under the License is distributed on an "AS IS" BASIS,
#     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#     See the License for the specific language governing permissions and
#     limitations under the License.
#
from __future__ import print_function

import sys

print("Raising an exception type in a function:")


def raiseExceptionClass():
    raise ValueError


try:
    raiseExceptionClass()
except Exception as e:
    print("Caught exception type", e, repr(e), type(e))
    print("Inside handler, sys.exc_info is this", sys.exc_info())

print("After catching, sys.exc_info is this", sys.exc_info())
print("*" * 20)

print("Raising an exception instance in a function:")


def raiseExceptionInstance():
    raise ValueError("hallo")


try:
    raiseExceptionInstance()
except Exception as f:
    print("Caught exception instance", f, repr(f), type(f))
    print("Inside handler, sys.exc_info is this", sys.exc_info())

print("After catching, sys.exc_info is this", sys.exc_info())
print("*" * 20)

print("Raising an exception, then catch it to re-raise it:")


def raiseExceptionAndReraise(arg):
    try:
        return arg / arg
    except:
        raise


try:
    raiseExceptionAndReraise(0)
except:
    print("Caught reraised", sys.exc_info())

print("After catching, sys.exc_info is this", sys.exc_info())
print("*" * 20)

print("Access an undefined global variable in a function:")


def raiseNonGlobalError():
    return undefined_value


try:
    raiseNonGlobalError()
except:
    print("NameError caught", sys.exc_info())

print("After catching, sys.exc_info is this", sys.exc_info())
print("*" * 20)

print("Raise a new style class as an exception, should be rejected:")


def raiseIllegalError():
    class X(object):
        pass

    raise X()


try:
    raiseIllegalError()
except TypeError as E:
    print("New style class exception correctly rejected:", E)
except:
    print(sys.exc_info())
    assert False, "Error, new style class exception was not rejected"

print("After catching, sys.exc_info is this", sys.exc_info())
print("*" * 20)

print("Raise an old-style class, version dependent outcome:")


class ClassicClassException:
    pass


def raiseCustomError():
    raise ClassicClassException()


try:
    try:
        raiseCustomError()
    except ClassicClassException:
        print("Caught classic class exception")
    except:
        print("Default catch", sys.exc_info())

        assert False, "Error, old style class exception was not caught"
except TypeError as e:
    print("Python3 hates to even try and catch classic classes", e)
else:
    print("Classic exception catching was considered fine.")

print("After catching, sys.exc_info is this", sys.exc_info())
print("*" * 20)

print("Check lazy exception creation:")


def checkExceptionConversion():
    try:
        raise Exception("some string")
    except Exception as err:
        print("Caught raised object", err, type(err))

    try:
        raise Exception, "some string"
    except Exception as err:
        print("Caught raised type, value pair", err, type(err))


checkExceptionConversion()
print("*" * 20)

print("Check exc_info scope:")


def checkExcInfoScope():
    try:
        raise ValueError
    except:
        assert sys.exc_info()[0] is not None
        assert sys.exc_info()[1] is not None
        assert sys.exc_info()[2] is not None

    if sys.version_info[0] < 3:
        print("Exc_info remains visible after exception handler for Python2")

        assert sys.exc_info()[0] is not None
        assert sys.exc_info()[1] is not None
        assert sys.exc_info()[2] is not None
    else:
        print("Exc_info is clear after exception handler for Python3")

        assert sys.exc_info()[0] is None
        assert sys.exc_info()[1] is None
        assert sys.exc_info()[2] is None

    def subFunction():
        print("Entering with exception info", sys.exc_info())

        assert sys.exc_info()[0] is not None
        assert sys.exc_info()[1] is not None
        assert sys.exc_info()[2] is not None

        try:
            print("Trying")
        except:
            pass

        print(
            "After trying something and didn't have an exception, info is",
            sys.exc_info(),
        )

    print("Call a function inside the exception handler and check there too.")

    try:
        raise KeyError
    except:
        assert sys.exc_info()[0] is not None
        assert sys.exc_info()[1] is not None
        assert sys.exc_info()[2] is not None

        subFunction()

    print("Call it twice and see.")

    try:
        raise "me"
    except:
        assert sys.exc_info()[0] is not None
        assert sys.exc_info()[1] is not None
        assert sys.exc_info()[2] is not None

        subFunction()
        subFunction()


if sys.version_info[0] < 3:
    sys.exc_clear()

checkExcInfoScope()

print("*" * 20)

# Check that the sys.exc_info is cleared again, after being set inside the
# function checkExcInfoScope, it should now be clear again.
assert sys.exc_info()[0] is None, sys.exc_info()[0]
assert sys.exc_info()[1] is None
assert sys.exc_info()[2] is None

print("Check catching subclasses")


def checkDerivedCatch():
    class A(BaseException):
        pass

    class B(A):
        def __init__(self):
            pass

    a = A()
    b = B()

    try:
        raise A, b
    except B, v:
        print("Caught B", v)
    except A, v:
        print("Didn't catch as B, but as A, Python3 does that", v)
    else:
        print("Not caught A class, not allowed to happen.")

    try:
        raise B, a
    except TypeError, e:
        print("TypeError with pair form for class not taking args:", e)


checkDerivedCatch()

print("*" * 20)


def checkNonCatch1():
    print("Testing if the else branch is executed in the optimizable case:")

    try:
        0
    except TypeError:
        print("Should not catch")
    else:
        print("Executed else branch correctly")


checkNonCatch1()
print("*" * 20)


def checkNonCatch2():
    try:
        print("Testing if the else branch is executed in the non-optimizable case:")
    except TypeError:
        print("Should not catch")
    else:
        print("Executed else branch correctly")


checkNonCatch2()
print("*" * 20)

print("Checking raise that with exception arguments that raise error themselves.")


def checkRaisingRaise():
    def geterror():
        return 1 / 0

    try:
        geterror()
    except Exception as e:
        print("Had exception", e)

    try:
        raise TypeError, geterror()

    except Exception as e:
        print("Had exception", e)

    try:
        raise TypeError, 7, geterror()

    except Exception as e:
        print("Had exception", e)


checkRaisingRaise()
print("*" * 20)

print("Checking a re-raise that isn't one:")


def checkMisRaise():
    raise


try:
    checkMisRaise()
except Exception as e:
    print("Without existing exception, re-raise gives:", e)

print("*" * 20)

print("Raising an exception in an exception handler gives:")


def nestedExceptions(a, b):
    try:
        a / b
    except ZeroDivisionError:
        a / b


try:
    nestedExceptions(1, 0)
except Exception as e:
    print("Nested exception gives", e)

print("*" * 20)

print("Checking unpacking from an exception as a sequence:")


def unpackingCatcher():
    try:
        raise ValueError(1, 2)
    except ValueError as (a, b):
        print("Unpacking caught exception and unpacked", a, b)


unpackingCatcher()
print("Afterwards, exception info is", sys.exc_info())

print("*" * 20)

print("Testing exception that escapes __del__ and therefore cannot be raised")


def divide(a, b):
    return a / b


def unraisableExceptionInDel():
    class C:
        def __del__(self):
            c = divide(1, 0)
            print(c)

    def f():
        C()

    f()


unraisableExceptionInDel()
print("*" * 20)

print("Testing exception changes between generator switches:")


def yieldExceptionInteraction():
    def yield_raise():
        print("Yield finds at generator entry", sys.exc_info()[0])
        try:
            raise KeyError("caught")
        except KeyError:
            yield sys.exc_info()[0]
            yield sys.exc_info()[0]
        yield sys.exc_info()[0]

    g = yield_raise()
    print("Initial yield from catch in generator", next(g))
    print("Checking from outside of generator", sys.exc_info()[0])
    print("Second yield from the catch reentered", next(g))
    print("Checking from outside of generator", sys.exc_info()[0])
    print("After leaving the catch generator yielded", next(g))


yieldExceptionInteraction()
print("*" * 20)

print(
    "Testing exception change between generator switches while handling an own exception"
)


def yieldExceptionInteraction2():
    def yield_raise():
        print("Yield finds at generator entry", sys.exc_info()[0])
        try:
            raise ValueError("caught")
        except ValueError:
            yield sys.exc_info()[0]
            yield sys.exc_info()[0]
        yield sys.exc_info()[0]

    try:
        undefined_global
    except Exception:
        print("Checking from outside of generator with", sys.exc_info()[0])
        g = yield_raise()
        v = next(g)
        print("Initial yield from catch in generator:", v)
        print("Checking from outside the generator:", sys.exc_info()[0])
        print("Second yield from the catch reentered:", next(g))
        print("Checking from outside the generation again:", sys.exc_info()[0])
        print("After leaving the catch generator yielded:", next(g))

    print("After exiting the trying branch:", sys.exc_info()[0])


yieldExceptionInteraction2()
print("After function exit, no exception", sys.exc_info())
print("*" * 20)

print("Check what happens if a function attempts to clear the exception in a handler")


def clearingException():
    def clearit():
        try:
            if sys.version_info[0] < 3:
                sys.exc_clear()
        except KeyError:
            pass

    try:
        raise KeyError
    except:
        print("Before clearing, it's", sys.exc_info())
        clearit()

        print("After clearing, it's", sys.exc_info())


clearingException()
print("*" * 20)

print("Check that multiple exceptions can be caught in a handler through a variable:")


def multiCatchViaTupleVariable():
    some_exceptions = (KeyError, ValueError)

    try:
        raise KeyError
    except some_exceptions:
        print("Yes, indeed.")


multiCatchViaTupleVariable()


def raiseValueWithValue():
    try:
        raise ValueError(1, 2, 3), (ValueError(1, 2, 3))
    except Exception as e:
        print("Gives", e)


print("Check exception given when value is raised with value", raiseValueWithValue())

# Make sure the "repr" of exceptions is fine

a = IOError
print("IOError is represented correctly:", repr(a))


def raising():
    raise ValueError


def not_raising():
    pass


def raiseWithFinallyNotCorruptingLineNumber():
    try:
        try:
            raising()
        finally:
            not_raising()
    except ValueError:
        print("Traceback is in tried block line", sys.exc_info()[2].tb_lineno)


raiseWithFinallyNotCorruptingLineNumber()


def wideCatchMustPublishException():
    print("At entry, no exception", sys.exc_info())

    try:
        undefined_global
    except:
        print("Inside handler:", sys.exc_info())

        pass

    print("Outside handler:", sys.exc_info())


print("Check that a unqualified catch properly preserves exception")
wideCatchMustPublishException()

print("Check if a nested exception handler does overwrite re-raised")


def checkReraiseAfterNestedTryExcept():
    def reraise():
        try:
            raise TypeError("outer")
        except Exception:
            try:
                raise KeyError("nested")
            except KeyError:
                print("Current exception inside nested handler", sys.exc_info())

                pass

            print("Current exception after nested handler exited", sys.exc_info())

            # Which one does this pick
            raise

    try:
        reraise()
    except Exception as e:
        print("Caught", repr(e))


checkReraiseAfterNestedTryExcept()


def checkReraiseByFunction():
    def reraise():
        raise

    try:
        try:
            raise TypeError("outer")
        except Exception:
            reraise()
    except Exception as e:
        import traceback

        print("Exception traceback of re-raise:")
        print("-" * 40)
        traceback.print_exc()
        print("-" * 40)
        print("OK.")


# TODO: Enable this, once the actual traceback of a function
# re-raise isn't wrong (contains itself) anymore.
if False:
    checkReraiseByFunction()


def checkNoRaiseExceptionDictBuilding(arg):
    a = {(): arg}

    b = {None: arg}

    c = {Ellipsis: arg}

    d = {1.0j: arg}

    e = {1.0: arg}

    f = {long(0): arg}

    g = {0: arg}

    h = {type: arg}

    return a, b, c, d, e, f, g, h


checkNoRaiseExceptionDictBuilding(1)


def checkRaiseExceptionDictBuildingRange(arg):
    try:
        i = {range(10): arg}
    except Exception as e:
        print("Raised", repr(e))
    else:
        print("No exception, OK for Python2")

        return i


print("Check if range as dict key raises:")
checkRaiseExceptionDictBuildingRange(2)


def checkRaiseExceptionDictBuildingTuple(arg):
    try:
        i = {(2, []): arg}
    except Exception as e:
        print("Raised", repr(e))
    else:
        return i


print("Check if mutable tuple as dict key raises:")
checkRaiseExceptionDictBuildingTuple(3)


def checkRaiseExceptionDictBuildingList(arg):
    try:
        i = {[2, ()]: arg}
    except Exception as e:
        print("Raised", repr(e))
    else:
        return i


print("Check if list as dict key raises:")
checkRaiseExceptionDictBuildingList(4)
