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

Make Exceptions raised in nim catchable as native python Exception subclasses #300

Draft
wants to merge 19 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions nimpy.nim
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,7 @@ proc updateStackBottom() {.inline.} =
setupForeignThreadGC()

proc pythonException(e: ref Exception): PPyObject =
let err = pyLib.PyErr_NewException(cstring("nimpy" & "." & $(e.name)), pyLib.NimPyException, nil)
decRef err
let err = nimValueToPy(e)
let errMsg: string =
when compileOption("stackTrace"):
"Unexpected error encountered: " & e.msg & "\nstack trace: (most recent call last)\n" & e.getStackTrace()
Expand Down
35 changes: 35 additions & 0 deletions nimpy/nim_py_marshalling.nim
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,38 @@ proc nimValueToPy*[T: tuple](o: T): PPyObject =
for f in fields(o):
discard pyLib.PyTuple_SetItem(result, i, nimValueToPy(f))
inc i

proc nimValueToPy*(e: ref Exception): PPyObject =
let pyExcName = cstring("nimpy" & "." & $(e.name))
var pyExc: PPyObject
if e of AssertionDefect:
pyExc = pyLib.PyExc_AssertionError
elif e of EOFError:
pyExc = pyLib.PyExc_EOFError
elif e of LibraryError:
pyExc = pyLib.PyExc_ImportError
elif e of IndexDefect:
pyExc = pyLib.PyExc_IndexError
elif e of IOError:
pyExc = pyLib.PyExc_IOError
elif e of KeyError:
pyExc = pyLib.PyExc_KeyError
elif e of ObjectConversionDefect:
pyExc = pyLib.PyExc_TypeError
elif e of StackOverflowDefect:
pyExc = pyLib.PyExc_RecursionError
elif e of DivByZeroDefect or e of FloatDivByZeroDefect:
pyExc = pyLib.PyExc_ZeroDivisionError
elif e of FloatingPointDefect:
pyExc = pyLib.PyExc_FloatingPointError
elif e of OsError:
pyExc = pyLib.PyExc_OSError
elif e of OutOfMemDefect:
pyExc = pyLib.PyExc_MemoryError
else:
return pyLib.PyErr_NewException(pyExcName, pyLib.NimPyException, nil)
decRef pyExc
let nimpyExceptionSubclass = pyLib.PyTuple_Pack(2, pyLib.NimPyException, pyExc)
result = pyLib.PyErr_NewException(pyExcName, nimpyExceptionSubclass, nil)
decref nimpyExceptionSubclass
decref result
74 changes: 59 additions & 15 deletions nimpy/py_lib.nim
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type

Py_BuildValue*: proc(f: cstring): PPyObject {.pyfunc, varargs.}
PyTuple_New*: proc(sz: Py_ssize_t): PPyObject {.pyfunc.}
PyTuple_Pack*: proc(sz: Py_ssize_t): PPyObject {.pyfunc, varargs.}
PyTuple_Size*: proc(f: PPyObject): Py_ssize_t {.pyfunc.}
PyTuple_GetItem*: proc(f: PPyObject, i: Py_ssize_t): PPyObject {.pyfunc.}
PyTuple_SetItem*: proc(f: PPyObject, i: Py_ssize_t, v: PPyObject): cint {.pyfunc.}
Expand Down Expand Up @@ -90,7 +91,6 @@ type
PyErr_Clear*: proc() {.pyfunc.}
PyErr_SetString*: proc(o: PPyObject, s: cstring) {.pyfunc.}
PyErr_Occurred*: proc(): PPyObject {.pyfunc.}
PyExc_TypeError*: PPyObject

PyCapsule_New*: proc(p: pointer, name: cstring, destr: proc(o: PPyObject) {.pyfunc.}): PPyObject {.pyfunc.}
PyCapsule_GetPointer*: proc(c: PPyObject, name: cstring): pointer {.pyfunc.}
Expand All @@ -114,17 +114,57 @@ type
PyExc_BaseException*: PPyObject # should always match any exception?
PyExc_Exception*: PPyObject
PyExc_ArithmeticError*: PPyObject
PyExc_FloatingPointError*: PPyObject
PyExc_OverflowError*: PPyObject
PyExc_ZeroDivisionError*: PPyObject
PyExc_AssertionError*: PPyObject
PyExc_OSError*: PPyObject
PyExc_IOError*: PPyObject # in Python 3 IOError *is* OSError
PyExc_ValueError*: PPyObject
#PyExc_AttributeError*: PPyObject
# PyExc_BlockingIOError # no
Copy link
Author

@PaarthShah PaarthShah Jan 14, 2024

Choose a reason for hiding this comment

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

I mapped many new exceptions in from https://docs.python.org/3/c-api/exceptions.html#standard-exceptions, but most of these probably will not be required as they don't directly map to a standard library nim exception as per https://nim-lang.org/docs/exceptions.html.

Before I move this out of draft, I'll make a point of removing the commented/unmapped entries.

I didn't delete any, but I made a point of alphabetizing them

# PyExc_BrokenPipeError
# PyExc_BufferError
# PyExc_ChildProcessError
# PyExc_ConnectionAbortedError
# PyExc_ConnectionError
# PyExc_ConnectionRefusedError
# PyExc_ConnectionResetError
PyExc_EOFError*: PPyObject
PyExc_MemoryError*: PPyObject
# PyExc_FileExistsError
# PyExc_FileNotFoundError
PyExc_FloatingPointError*: PPyObject
# PyExc_GeneratorExit
PyExc_ImportError*: PPyObject
# PyExc_IndentationError # no
PyExc_IndexError*: PPyObject
# PyExc_InterruptedError
PyExc_IOError*: PPyObject # in Python 3 IOError *is* OSError
# PyExc_IsADirectoryError
PyExc_KeyError*: PPyObject
# PyExc_KeyboardInterrupt
# PyExc_LookupError
PyExc_MemoryError*: PPyObject
# PyExc_ModuleNotFoundError
# PyExc_NameError
# PyExc_NotADirectoryError
# PyExc_NotImplementedError
PyExc_OSError*: PPyObject
PyExc_OverflowError*: PPyObject
# PyExc_PermissionError
# PyExc_ProcessLookupError
PyExc_RecursionError*: PPyObject
# PyExc_ReferenceError
# PyExc_RuntimeError
# PyExc_StopAsyncIteration
# PyExc_StopIteration
# PyExc_SyntaxError
# PyExc_SystemError
# PyExc_SystemExit
# PyExc_TabError
# PyExc_TimeoutError
PyExc_TypeError*: PPyObject
# PyExc_UnboundLocalError
# PyExc_UnicodeDecodeError
# PyExc_UnicodeEncodeError
# PyExc_UnicodeError
# PyExc_UnicodeTranslateError
PyExc_ValueError*: PPyObject
PyExc_ZeroDivisionError*: PPyObject

NimPyException*: PPyObject

Expand Down Expand Up @@ -212,6 +252,7 @@ proc loadPyLibFromModule(m: LibHandle): PyLib =

load Py_BuildValue, "_Py_BuildValue_SizeT"
load PyTuple_New
load PyTuple_Pack
Copy link
Author

@PaarthShah PaarthShah Jan 14, 2024

Choose a reason for hiding this comment

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

https://docs.python.org/3.13/c-api/tuple.html
It's also part of the stable c API, and has been present since at least 3.8 (probably much longer).
Added it as a convenience function for myself.

load PyTuple_Size
load PyTuple_GetItem
load PyTuple_SetItem
Expand Down Expand Up @@ -327,17 +368,20 @@ proc loadPyLibFromModule(m: LibHandle): PyLib =
load PyErr_NewException

loadVar PyExc_ArithmeticError
loadVar PyExc_FloatingPointError
loadVar PyExc_OverflowError
loadVar PyExc_ZeroDivisionError
loadVar PyExc_AssertionError
loadVar PyExc_OSError
loadVar PyExc_IOError
loadVar PyExc_ValueError
loadVar PyExc_EOFError
loadVar PyExc_MemoryError
loadVar PyExc_FloatingPointError
loadVar PyExc_ImportError
loadVar PyExc_IndexError
loadVar PyExc_IOError
loadVar PyExc_KeyError
loadVar PyExc_MemoryError
loadVar PyExc_OSError
loadVar PyExc_OverflowError
loadVar PyExc_RecursionError
loadVar PyExc_TypeError
loadVar PyExc_ValueError
loadVar PyExc_ZeroDivisionError

if pl.pythonVersion.major == 3:
pl.PyDealloc = deallocPythonObj[PyTypeObject3]
Expand Down
16 changes: 8 additions & 8 deletions nimpy/py_types.nim
Original file line number Diff line number Diff line change
Expand Up @@ -307,19 +307,19 @@ type
# the string value corresponds to the Python Exception
# while the enum identifier corresponds to the Nim exception (excl. "pe")
PythonErrorKind* = enum
peException = "Exception" # general exception, if no equivalent Nim Exception
peArithmeticError = "ArithmeticError"
peFloatingPointError = "FloatingPointError"
peOverflowError = "OverflowError"
peDivByZeroError = "ZeroDivisionError"
peAssertionError = "AssertionError"
peCatchableError = "Exception" # general exception, if no equivalent Nim Exception
peArithmeticDefect = "ArithmeticError"
peFloatingPointDefect = "FloatingPointError"
peOverflowDefect = "OverflowError"
peDivByZeroDefect = "ZeroDivisionError"
peAssertionDefect = "AssertionError"
peOSError = "OSError"
peIOError = "IOError"
peValueError = "ValueError"
peEOFError = "EOFError"
peOutOfMemError = "MemoryError"
peOutOfMemDefect = "MemoryError"
peKeyError = "KeyError"
peIndexError = "IndexError"
peIndexDefect = "IndexError"

const
# PyBufferProcs contains bf_getcharbuffer
Expand Down
62 changes: 60 additions & 2 deletions tests/nimfrompy.nim
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import nimpy
import algorithm, complex, tables, json
import algorithm, complex, dynlib, tables, json
from tpyfromnim import nil

import modules/other_module


type
JackError* = object of Exception
JackError* = object of CatchableError


proc greet(name: string, greeting: string="Hello", suffix: string="!"): string {.exportpy.} =
Expand Down Expand Up @@ -165,3 +165,61 @@ proc setMyFieldFromTt(self: AnotherTestType, value: TestType) {.exportpy.} =

proc getMyField(self: AnotherTestType): int {.exportpy.} =
self.myIntField

# Raising Defects

proc assertFalse(): void {.exportpy} =
# AssertionDefect
doAssert false

proc invalidIndex(): int {.exportpy} =
# IndexDefect
let mySequence = @[1, 2, 3]
mySequence[4]

proc endOfFile(): char {.exportpy} =
# EOFError
let file = open("/dev/null", fmRead)
readChar(file)

proc readImpossibleFile(): void {.exportpy} =
# IOError
discard open("/dev/null/impossible", fmRead)

proc invalidKey(): string {.exportpy} =
# KeyError
let myTable = {1: "one", 2: "two"}.toTable
myTable[3]

proc invalidObjectConversion(): void {.exportpy} =
# ObjectConversionDefect
raise newException(ObjectConversionDefect, "Generic ObjectConversionDefect")

proc intDivideByZero(): int {.exportpy} =
# DivByZeroDefect
1 mod 0

proc floatDivideByZero(): float {.exportpy} =
# FloatDivByZeroDefect
raise newException(FloatDivByZeroDefect, "Generic FloatDivByZeroDefect")

proc genericFloatingPointDefect(): float {.exportpy} =
# FloatOverflowDefect of FloatingPointDefect
1 / 0

proc readFakeLibrary(): void {.exportpy} =
# LibraryError
discard checkedSymAddr(nil, "fake_library")

proc stackOverflow(): void {.exportpy} =
# StackOverflowDefect
raise newException(StackOverflowDefect, "Generic StackOverflowDefect")

proc osError(): void {.exportpy} =
# OSError
raise newException(OSError, "Generic OSError")

proc outOfMemory(): void {.exportpy} =
# OutOfMemDefect
raise newException(OutOfMemDefect, "Generic OutOfMemDefect")

69 changes: 69 additions & 0 deletions tests/tnimfrompy.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,75 @@
expected = "TypeError(\"Can't convert python obj of type 'int' to string\",)"
assert(expected[:-2] in repr(e))

# Generic exception raising
try:
s.assertFalse()
except AssertionError as e:
assert(isinstance(e, s.NimPyException))
assert("`false`" in repr(e))
else:
assert(False)
try:
s.endOfFile(); assert(False)
except EOFError as e:
assert(isinstance(e, s.NimPyException))
assert("EOF reached" in repr(e))
try:
s.invalidIndex(); assert(False)
except IndexError as e:
assert(isinstance(e, s.NimPyException))
assert("index 4 not in 0" in repr(e))
try:
s.readImpossibleFile(); assert(False)
except IOError as e:
assert(isinstance(e, s.NimPyException))
assert("/dev/null/impossible" in repr(e))
try:
s.invalidKey(); assert(False)
except KeyError as e:
assert(isinstance(e, s.NimPyException))
assert("key not found" in repr(e))
try:
x = s.invalidObjectConversion(); assert(False)
except TypeError as e:
assert(isinstance(e, s.NimPyException))
assert("Generic ObjectConversionDefect" in repr(e))
try:
s.intDivideByZero(); assert(False)
except ZeroDivisionError as e:
assert(isinstance(e, s.NimPyException))
assert("division by zero" in repr(e))
try:
x = s.floatDivideByZero(); assert(False)
except ZeroDivisionError as e:
assert(isinstance(e, s.NimPyException))
assert("Generic FloatDivByZeroDefect" in repr(e))
try:
s.genericFloatingPointDefect(); assert(False)
except FloatingPointError as e:
assert(isinstance(e, s.NimPyException))
assert("FPU operation caused an overflow" in repr(e))
try:
s.readFakeLibrary(); assert(False)
except ImportError as e:
assert(isinstance(e, s.NimPyException))
assert("could not find symbol: fake_library" in repr(e))
try:
s.stackOverflow(); assert(False)
except RecursionError as e:
assert(isinstance(e, s.NimPyException))
assert("Generic StackOverflowDefect" in repr(e))
try:
s.osError(); assert(False)
except OSError as e:
assert(isinstance(e, s.NimPyException))
assert("Generic OSError" in repr(e))
try:
s.outOfMemory(); assert(False)
except MemoryError as e:
assert(isinstance(e, s.NimPyException))
assert("Generic OutOfMemDefect" in repr(e))

assert(s.greetEveryoneExceptJack("world") == "Hello, world!")
try:
s.greetEveryoneExceptJack("Jack")
Expand Down
16 changes: 8 additions & 8 deletions tests/tpyfromnim.nim
Original file line number Diff line number Diff line change
Expand Up @@ -177,16 +177,16 @@ proc test*() {.gcsafe.} =
check(pfn.testValueError, ValueError)
check(pfn.testKeyError, KeyError)
check(pfn.testEOFError, EOFError)
check(pfn.testArithmeticError,ArithmeticError)
check(pfn.testZeroDivisionError, DivByZeroError)
check(pfn.testOverflowError, OverflowError)
check(pfn.testAssertionError, AssertionError)
check(pfn.testMemoryError, OutOfMemError)
check(pfn.testIndexError, IndexError)
check(pfn.testFloatingPointError, FloatingPointError)
check(pfn.testArithmeticError,ArithmeticDefect)
check(pfn.testZeroDivisionError, DivByZeroDefect)
check(pfn.testOverflowError, OverflowDefect)
check(pfn.testAssertionError, AssertionDefect)
check(pfn.testMemoryError, OutOfMemDefect)
check(pfn.testIndexError, IndexDefect)
check(pfn.testFloatingPointError, FloatingPointDefect)
check(pfn.testException, Exception)
check(pfn.testUnsupportedException, Exception)
check(pfn.testCustomException, IndexError)
check(pfn.testCustomException, IndexDefect)

block: # Function objects
let pfn = pyImport("pyfromnim")
Expand Down