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

add init, del, repr, doc for pyType #308

Merged
merged 6 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
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]
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__, __del__, and __repr__ if the requirements are met. [Export Python Types](./docs/export_python_type.md)

```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
96 changes: 96 additions & 0 deletions docs/export_python_type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
## 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.

#### 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__`, `__del__`, 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 only one argument
YesDrX marked this conversation as resolved.
Show resolved Hide resolved
## 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 `destroy##TypeName`
## 2) there is only 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 __del__ (tp_finalize in PyTypeObject)
## !! Warning, this is only available since Python3.4, for older versions, the destroySimpleObj
## below will be ignore.
proc destroySimpleObj*(self : SimpleObj) {.exportpy.} =
echo "Calling destroySimpleObj for SimpleObj"

## 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)
obj.__del__()
```
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