Skip to content

Commit

Permalink
add init, del, repr, doc for pyType (#308)
Browse files Browse the repository at this point in the history
* add init, del, repr, doc for pyType

* add a export type test

* minor fix on the test case

* use 2 spaces rather than 4 spaces

* use 2 spaces rather than 4 spaces

* remove __del__

---------

Co-authored-by: Wei Xiang <[email protected]>
  • Loading branch information
YesDrX and Wei Xiang authored Jan 22, 2025
1 parent 0645af5 commit 114e1b9
Show file tree
Hide file tree
Showing 10 changed files with 359 additions and 32 deletions.
17 changes: 13 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,26 @@ jobs:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
nim-channel: [stable, devel]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
exclude:
- os: ubuntu-latest
python-version: "3.7"
- os: macos-latest
python-version: "3.7"

name: ${{ matrix.os }}-${{ matrix.python-version }}-${{ matrix.nim-channel }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- uses: actions/setup-python@v2
- name: Debug Environment
run: |
echo "OS: $(uname -a)"
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Setup nim
uses: jiro4989/setup-nim-action@v1
with:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ tests/tbuiltinpyfromnim
*.pyc
*.pyd
*.so
nimble.develop
nimble.paths
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ Nimpy also exposes lower level [Buffer protocol](https://docs.python.org/3/c-api
see [raw_buffers.nim](https://github.com/yglukhov/nimpy/blob/master/nimpy/raw_buffers.nim).
[tpyfromnim.nim](https://github.com/yglukhov/nimpy/blob/master/tests/numpytest.nim)
contains a very basic test for this.

[Examples to use raw_buffers with numpy](./docs/numpy.md)
</details>

<details>
Expand Down Expand Up @@ -124,11 +126,13 @@ contains a very basic test for this.

</details>

## Exporting Nim types as Python classes
## [Exporting Nim types as Python classes](./docs/export_python_type.md)
Warning! This is experimental.
* An exported type should be a ref object and inherit `PyNimObjectExperimental` directly or indirectly.
* The type will only be exported if at least one exported "method" is defined.
* A proc will be exported as python type method *only* if it's first argument is of the corresponding type and is called `self`. If the first argument is not called `self`, the proc will exported as a global module function.
* If you define functions that looks like initTestType, destroyTestType, `$`, they can be exported as `__init__` and `__repr__` if the requirements are met.

```nim
# mymodule.nim
type TestType = ref object of PyNimObjectExperimental
Expand Down
4 changes: 4 additions & 0 deletions config.nims
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# begin Nimble config (version 2)
when withDir(thisDir(), system.fileExists("nimble.paths")):
include "nimble.paths"
# end Nimble config
84 changes: 84 additions & 0 deletions docs/export_python_type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
## Exporting Nim types as Python classes

Warning! This is experimental.
* An exported type should be a ref object and inherits `PyNimObjectExperimental` directly or indirectly.
* The type will only be exported if at least one exported "method" is defined.
* A proc will be exported as python type method *only* if it's first argument is of the corresponding type and is called `self`. If the first argument is not called `self`, the proc will exported as a global module function.
* If you define functions that looks like initTestType or `$`, they will be exported as `__init__` and `__repr__` if the requirements are met.

#### Simple Example
```nim
# 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
```

``` py
# test.py
import mymodule
tt = mymodule.TestType()
tt.setMyField("Hello")
assert(tt.getMyField() == "Hello")
```

#### `__init__`, and `__repr__`
* [example](../tests/export_pytype.nim)
```nim
# simple.nim
## compile as simple.so
import nimpy
import strformat
pyExportModule("simple") # only needed if your filename is not simple.nim
type
SimpleObj* = ref object of PyNimObjectExperimental
a* : int
## if
## 1) the function name is like `init##TypeName`
## 2) there is at least one argument
## 3) the first argument name is "self"
## 4) the first argument type is `##TypeName`
## 5) there is no return type
## we export this function as a python object method __init__ (tp_init in PyTypeObject)
proc initSimpleObj*(self : SimpleObj, a : int = 1) {.exportpy} =
echo "Calling initSimpleObj for SimpleObj"
self.a = a
## if
## 1) the function name is like `$`
## 2) there is only one argument
## 3) the first argument name is "self"
## 4) the first argument type is `##TypeName`
## 5) the return type is `string`
## we export this function as a python object method __repr__ (tp_repr in PyTypeObject)
proc `$`*(self : SimpleObj): string {.exportpy.} =
&"SimpleObj : a={self.a}"
## Change doc string
setModuleDocString("This is a test module")
setDocStringForType(SimpleObj, "This is a test type")
```

* Compile as `simple.so`
```bash
nim c --app:lib -o:./simple.so ./simple.nim
```

* Use the exported python type in python
```python
import simple
print(simple.__doc__)
print(simple.SimpleObj.__doc__)
obj = simple.SimpleObj(a = 2)
print(obj)
```
44 changes: 44 additions & 0 deletions docs/numpy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
## Use nimpy to access numpy arrays from python

* Get buffer from numpy array (PyObject)
- Note
- numpy will export buffer when the underlying memory block is C-Contiguous
- if the underlying data is not C-Contiguous, there will be a PythonException thrown by numpy
```nim
proc asNimArray*[T](arr : PyObject, mode : int = PyBUF_READ) : ptr UncheckedArray[T] =
var
buf : RawPyBuffer
getBuffer(arr, buf, mode.cint)
```

* Shape and Strides
```nim
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] =
#[
Function to translate numpy array into an object in Nim to represent the data for convinience.
]#
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]
]
```

* ArrayMancer Tensor
- Given the exposed buffer, you can use cpuStorageFromBuffer from ArrayMancer Tensor wihout making a copy
Loading

0 comments on commit 114e1b9

Please sign in to comment.