nimpy

repository·master·Indexed 21 days ago

https://github.com/yglukhov/nimpy

A library providing native language integration between Nim and Python. It allows developers to implement Python modules in Nim using the {.exportpy.} pragma, call Python code and built-in functions from within Nim, and export Nim ref objects as Python classes. The library includes support for NumPy interop, enabling direct access to C-contiguous memory buffers and integration with ArrayMancer Tensors.

Tokens
3.1K
Snippets
8
Records
10
Agent score
80%

What's inside nimpy

  1. Export Nim types as Python classes

    master

    You can export Nim types as Python classes by defining a ref object that inherits from PyNimObjectExperimental (directly or indirectly).

    Requirements and Behavior:

    • Inheritance: The type must be a ref object of PyNimObjectExperimental.
    • Method Exporting: A type is only exported to Python if at least one exported method is defined.
    • Method vs. Global Function: A proc is exported as a Python type method only if its first argument is named self and is of the corresponding type. If the first argument is not named self, the proc is exported as a global module function.
    • Special Methods: Specific naming patterns allow you to implement Python magic methods like __init__ and __repr__.

    Warning: This feature is experimental.

    # mymodule.nim
    import nimpy
    
    type TestType = ref object of PyNimObjectExperimental
      myField: string
    
    proc setMyField(self: TestType, value: string) {.exportpy.} =
      self.myField = value
    
    proc getMyField(self: TestType): string {.exportpy.} =
      self.myField
  2. Integrate NumPy arrays with ArrayMancer Tensors

    master
    You can bridge NumPy arrays and ArrayMancer Tensors without performing a memory copy. Once you have exposed the buffer from a NumPy array (using asNimArray or NimNumpyArray), pass that buffer to ArrayMancer's cpuStorageFromBuffer function to create a Tensor that points to the same underlying memory.
  3. Implement a Python module in Nim

    master

    To create a Python module using Nim, follow these steps:

    1. Create a .nim file where the filename matches the module name you intend to import in Python.
    2. Use the {.exportpy.} pragma on procedures you want to expose to Python.
    3. Compile the file into a shared library (.pyd on Windows, .so on other platforms).

    Windows Compilation: Use --app:lib, --out:mymodule.pyd, --threads:on, --tlsEmulation:off, and --passL:-static to ensure compatibility with the MinGW toolchain and Python's threading model.

    Linux/macOS Compilation: Use --app:lib and --out:mymodule.so with --threads:on.

    Python Usage: Import the compiled module normally using standard Python import statements.

    # mymodule.nim
    import nimpy
    
    proc greet(name: string): string {.exportpy.} =
      return "Hello, " & name & "!"
    # Compile on Windows:
    nim c --app:lib --out:mymodule.pyd --threads:on --tlsEmulation:off --passL:-static mymodule
    
    # Compile on everything else:
    nim c --app:lib --out:mymodule.so --threads:on mymodule
    # test.py
    import mymodule
    assert mymodule.greet("world") == "Hello, world!"
    assert mymodule.greet(name="world") == "Hello, world!"
  4. Compile Nim module as a Python extension (.so)

    master

    To use a Nim module in Python, compile it as a shared library (.so on Linux/macOS or .pyd on Windows). Use the following command:

    nim c --app:lib -o:./simple.so ./simple.nim

    Note: If your Nim filename is not the name you intend to use in Python (e.g., your file is simple.nim but you want import mymodule), you must call pyExportModule("mymodule") inside your Nim code.

  5. Implement `__init__` and `__repr__` for Nim types

    master

    To implement Python magic methods for your exported Nim types, follow these specific naming and signature conventions:

    __init__ (Constructor)

    To export a proc as the Python __init__ method, it must meet all these criteria:

    1. The function name must follow the pattern init##TypeName (e.g., initSimpleObj for type SimpleObj).
    2. There must be at least one argument.
    3. The first argument must be named self.
    4. The first argument type must be the corresponding type (e.g., SimpleObj).
    5. There must be no return type.

    __repr__ (String Representation)

    To export a proc as the Python __repr__ method, it must meet all these criteria:

    1. The function name must be $.
    2. There must be exactly one argument.
    3. The first argument must be named self.
    4. The first argument type must be the corresponding type.
    5. The return type must be string.

    Documentation

    You can set documentation strings for the module and types using:

    • setModuleDocString("string")
    • setDocStringForType(TypeName, "string")
    import nimpy
    import strformat
    
    # Required if filename is not the module name
    pyExportModule("simple") 
    
    type
      SimpleObj* = ref object of PyNimObjectExperimental
        a* : int
    
    # Implements __init__
    proc initSimpleObj*(self : SimpleObj, a : int = 1) {.exportpy} =
      self.a = a
    
    # Implements __repr__
    proc `$`*(self : SimpleObj): string {.exportpy.} =
       &"SimpleObj : a={self.a}"
    
    setModuleDocString("This is a test module")
    setDocStringForType(SimpleObj, "This is a test type")
  6. Export Nim types as Python classes (Experimental)

    master

    You can expose Nim ref object types as Python classes. This feature is currently experimental and requires specific implementation patterns:

    1. Inheritance: The exported type must be a ref object that inherits from PyNimObjectExperimental (directly or indirectly).
    2. Methods: At least one exported method must be defined for the type to be exported.
    3. Method Signature: To export a procedure as a Python class method, the first argument must be of the corresponding type and named self. If the first argument is not named self, it will be exported as a global module function.
    4. Special Methods: Procedures named like initTestType, destroyTestType, or $ can be mapped to Python's __init__ and __repr__ if requirements are met.
    # mymodule.nim
    type TestType = ref object of PyNimObjectExperimental
      myField: string
    
    proc setMyField(self: TestType, value: string) {.exportpy.} =
      self.myField = value
    
    proc getMyField(self: TestType): string {.exportpy.} =
      self.myField
    # test.py
    import mymodule
    tt = mymodule.TestType()
    tt.setMyField("Hello")
    assert(tt.getMyField() == "Hello")
  7. Troubleshoot Python/Nim integration issues

    master

    Common issues when using nimpy:

    • ImportError (dynamic module does not define module export function): Ensure the compiled module filename exactly matches the name of the .nim file used to implement it.
    • libpython not found: Use the find_libpython Python package to locate the library path, then set nimpy.py_lib.pyInitLibPath in your Nim code.
      pip3 install find_libpython
      python3 -c 'import find_libpython; print(find_libpython.find_libpython())'
    • Nim strings as Python bytes: If a Nim string contains invalid UTF-8 sequences, nimpy will fallback to converting it to Python bytes instead of a Python str.
    • Numpy Interop: While nimpy allows manipulating numpy objects, for performance-critical work, consider using scinim or the lower-level Buffer protocol exposed via nimpy/raw_buffers.nim.
  8. Use NimNumpyArray to represent NumPy data

    master

    For a more convenient way to work with NumPy arrays, use the NimNumpyArray[T] object. This object wraps the PyObject and provides metadata such as shape, strides, and contiguity flags, allowing you to treat the Python array as a structured Nim object.

    Accessing elements in a 2D matrix

    You can use accessNumpyMatrix to retrieve elements from a 2D array using row and column indices. This function calculates the offset using the array's strides.

    Note: accessNumpyMatrix requires the matrix to have a shape of 2 and strides of 2.

    type
        NimNumpyArray*[T] = object
            originalPtr*   : PyObject
            buf*           : ptr UncheckedArray[T]
            shape*         : seq[int]
            strides*       : seq[int]
            c_contiguous*  : bool
            f_contiguous*  : bool
    
    proc asNimNumpyArray*[T](arr : PyObject, mode : int = PyBUF_READ) : NimNumpyArray[T] =
        result.originalPtr        = arr
        result.buf                = asNimArray[T](arr, mode)
        result.shape              = getAttr(arr, "shape").to(seq[int])
        result.strides            = getAttr(arr, "strides").to(seq[int])
        result.c_contiguous       = arr.flags["C_CONTIGUOUS"].to(bool)
        result.f_contiguous       = arr.flags["F_CONTIGUOUS"].to(bool)
    
    proc accessNumpyMatrix*[T](matrix : NimNumpyArray[T], row, col : int): T =
        doAssert matrix.shape == 2 and matrix.strides == 2
        return matrix.buf[
            row * matrix.strides[0] + col * matrix.strides[1]
        ]
  9. Call Python from Nim

    master

    You can interact with the Python runtime from within Nim using nimpy. This allows you to import Python modules and call built-in functions.

    • Use pyImport("module_name") to import a Python module.
    • Use pyBuiltinsModule() to access Python's built-in functions (like sum or range).
    • Use the .to(type) method to convert Python objects back into Nim types (e.g., .to(string) or .to(int)).

    Note: nimpy relies on your local Python installation being present on the system.

    import nimpy
    let os = pyImport("os")
    echo "Current dir is: ", os.getcwd().to(string)
    
    # sum(range(1, 5))
    let py = pyBuiltinsModule()
    let s = py.sum(py.range(0, 5)).to(int)
    assert s == 10
  10. Access NumPy arrays from Python in Nim

    master

    You can access the underlying memory buffer of a NumPy array (represented as a PyObject) directly in Nim using asNimArray.

    Important Requirements:

    • NumPy will only export the buffer if the underlying memory block is C-Contiguous.
    • If the data is not C-Contiguous, attempting to access the buffer will throw a PythonException.

    Use the mode parameter (defaulting to PyBUF_READ) to specify access permissions.

    proc asNimArray*[T](arr : PyObject, mode : int = PyBUF_READ) : ptr UncheckedArray[T] =
        var
            buf : RawPyBuffer
        getBuffer(arr, buf, mode.cint)