Giter VIP home page Giter VIP logo

ziggy-pydust's Introduction

Ziggy Pydust

A framework for writing and packaging native Python extension modules written in Zig.

Actions Package version Python version License


Documentation: https://pydust.fulcrum.so/latest

API: https://pydust.fulcrum.so/latest/zig

Source Code: https://github.com/fulcrum-so/ziggy-pydust

Template: https://github.com/fulcrum-so/ziggy-pydust-template


Ziggy Pydust is a framework for writing and packaging native Python extension modules written in Zig.

  • Package Python extension modules written in Zig.
  • Pytest plugin to discover and run Zig tests.
  • Comptime argument wrapping / unwrapping for interop with native Zig types.
const py = @import("pydust");

pub fn fibonacci(args: struct { n: u64 }) u64 {
    if (args.n < 2) return args.n;

    var sum: u64 = 0;
    var last: u64 = 0;
    var curr: u64 = 1;
    for (1..args.n) {
        sum = last + curr;
        last = curr;
        curr = sum;
    }
    return sum;
}

comptime {
    py.rootmodule(@This());
}

Compatibility

Pydust supports:

Please reach out if you're interested in helping us to expand compatibility.

Getting Started

Pydust docs can be found here. Zig documentation (beta) can be found here.

There is also a template repository including Poetry build, Pytest and publishing from Github Actions.

Contributing

We welcome contributions! Pydust is in its early stages so there is lots of low hanging fruit when it comes to contributions.

  • Assist other Pydust users with GitHub issues or discussions.
  • Suggest or implement features, fix bugs, fix performance issues.
  • Improve our documentation.
  • Write articles or other content demonstrating how you have used Pydust.

License

Pydust is released under the Apache-2.0 license.

ziggy-pydust's People

Contributors

renovate[bot] avatar gatesn avatar robert3005 avatar delta003 avatar shakfu avatar lwwmanning avatar isfakeaccount avatar

Stargazers

Christopher Bradley avatar Chaos Yu avatar jfs avatar Anderson Bravalheri avatar Patrick Park avatar Alexandru Pisarenco avatar Flynn avatar amiya avatar  avatar jadon avatar  avatar Elijah Roussos avatar Henrique Santos avatar 咚咔 avatar ButenkoMS avatar Sandalots avatar Jacques Tardie avatar Kyle Barron avatar Lucas Arriesse avatar DooMWhite avatar Leonard Aukea avatar  avatar  avatar Vladimir Lukiyanov avatar meng zhao avatar Sid Kshatriya avatar Philip Blankenau avatar  avatar Guido Schmidt avatar Yinameah avatar Denilson avatar Josh Meads avatar Ethan Costa avatar  avatar  avatar Oleg avatar Stefane Fermigier avatar Kaizhao Zhang avatar Aquib Javed avatar lucky.dev avatar Alejandro avatar Gabriel Pereira avatar Bartłomiej Nowak avatar Altagos avatar Sam Rogers avatar AperGra avatar Overguacho avatar Sivaharan Rajkumar avatar Anthony Perrett avatar Loibl-Vincent avatar Friedemann Göcking avatar Bartosz Sokorski avatar  avatar  avatar  avatar Michael Bilow avatar  avatar Hans Musgrave avatar Marcelo Rodrigues avatar Diego Queiroz avatar Leonardo Gregianin avatar Bruno Rocha avatar Florian Obersteiner avatar Anthony Corletti avatar Tom Molvik avatar Jan Krecke avatar MetaSky avatar 29 avatar  avatar Steve Coffman avatar Alex avatar  avatar Kemal Akkoyun avatar Gianluca Romanin avatar  avatar  avatar Alexander Gherm avatar Brian Bonsignore  avatar Lento Manickathan avatar Jake Mehlman avatar Jackie Li avatar Maximilian Gärber avatar Andrew Ross avatar Andrew Brent Gossage avatar Lincoln Colling avatar Pavel Gromov avatar Takumasa N. avatar  avatar Eugene Glybin avatar Irfius avatar Jeroen JM Sangers avatar Jevgeni Tarassov avatar Ali Aliyev avatar Roman Inflianskas avatar Patrick Winter avatar x19 avatar Drew Nutter avatar Francis Stokes avatar  avatar Sasha Lopoukhine avatar

Watchers

Brantley Harris avatar Filip Stanis avatar Hessam Mehr avatar  avatar  avatar  avatar  avatar Denilson avatar  avatar

ziggy-pydust's Issues

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

github-actions
.github/workflows/ci.yml
  • actions/checkout v4
  • actions/setup-python v5
  • actions/cache v4
  • snok/install-poetry v1
  • actions/cache v4
.github/workflows/publish.yml
  • actions/checkout v4
  • actions/setup-python v5
  • actions/cache v4
  • snok/install-poetry v1
  • actions/cache v4
  • battila7/get-version-action v2
pep621
pyproject.toml
poetry
pyproject.toml
  • python ^3.11
  • ziglang ^0.11.0
  • pydantic ^2.3.0
  • setuptools ^69.0.0
  • black ^23.9.1
  • pytest ^8.0.0
  • ruff ^0.4.0
  • ziglang ^0.11.0
  • mkdocs-material ^9.2.6
  • mkdocs-include-markdown-plugin ^6.0.1
  • mike ^2.0.0
  • numpy ^1.25.2

  • Check this box to trigger a request for Renovate to run again on this repository

Proposal: make all PyX.create functions behave like equivalent Python constructor

Currently a PyTuple.create only supports converting from a Zig tuple struct.

When wrapping/unwrapping, it's the trampoline that decides which type a value should be converted into. In some cases, e.g. for call arguments, we know we want an args tuple and kwargs dict. It is then ambiguous whether an empty Zig struct should be converted to a PyTuple or PyDict.

Similarly, we would never convert anything into a PySet. And there is an ambiguity in how to handle []u8 as PyString or PyBytes.

The proposal suggests the trampoline has a default target for each type, but all types can be explicitly constructed from any compatible object using their create function (possibly rename to new?)

Zig trace backs in Python exception

I don't know if this is possible, but in Zig debug mode it would be great when raising a Python exception to insert a bunch of stack frames into the Python traceback corresponding to the Zig frames.

Investigate ZLS not working

I believe this is because ZLS only evaluates Options steps when it parses the build.zig. Currently the discovery of our Python include/library paths are wrapped up inside a Pydust step, which means ZLS isn't finding it.

I wonder if eagerly shelling out to Python to get this information is worth doing?

Alternatives to poetry

Hi,

Really cool project, I'm very pleased to discover it given that I am trying to learn zig.

I'd be interested to explore alternative ways to use ziggy-pydust.

I'm thinking that a single python script (akin to Django's manage.py but self-contained) could drive project creation, project mgmt tasks.py and virtualenv creation. This could be an alternative to using poetry.

I would also like to be able to use my builtin installation of zig v12 rather than having to use v11 if possible.

Generate *.pyi stub files and py.typed for extensions

Having text_signature is not sufficient to get ides to offer code completion. Most of them will not load modules to offer suggestions and only parse them. SInce there's nothing to parse for a native module there's no type hints available.

However, since we generate text_signature we can create a simple python script that would load the build library code and use inspect module to produce type stub files. Additionally we need to include py.typed file in the generated package.

If/When https://github.com/python/cpython/pull/101872/files merges we could offer type hints and not only argument hints.

We should skip generation of stub file if there exists a *.pyi file with the name of the module

Custom test runner to avoid linking libpython

Currently we link libpython in order to run Zig tests that spawn python.
We then launch these Zig tests from Pytest in order to run all tests together.

This proposal is to invert the control flow by building a custom Zig test runner that gets built as a shared library. This library is then loaded into Pytest as a native extension and executed that way.

This means we can use Pythons that have been statically linked to libpython, and our startup time is probably faster. We should probably still fork/exec in order to isolate segfaults though.

Can't py.init a nested class

Currently we try to import the class from its containing module, but we fail to navigate a hierarchy of nested classes

Support tracebacks for non PyExc exceptions

Often we call into CPython and use catch return null. At these call sites we could enrich the traceback to include Zig stack frames the same way we do for Python exceptions that we raise ourselves with e.g. py.ValueError.raise

Support Python base classes

Probably something like:

const Subclass = py.class(struct {
    pub const __mro__ = &.{ "collections.abc.Sized" };
});

Support GIL management

Both

const gil = py.gil();
defer gil.release();
const nogil = py.nogil();
defer nogil.acquire();

Support nested classes

pub const Range = py.class("Range", struct {
    
    pub const Iterator = py.class("Iterator", struct {
        ...
    });

    ...
});

Currently fails with pydust/src/pydust.zig:201:5: error: Class has no associated module

Update the iterator example and pydust template after this :)

Action Required: Fix Renovate Configuration

There is an error with this repository's Renovate configuration that needs to be fixed. As a precaution, Renovate will stop PRs until it is resolved.

Error type: Cannot find preset's package (local>fulcrum-so/renovate-config)

zero-dependency pydust

While playing around with pydust in my recent PR, I was thinking about whether it would be possible to reduce pydust's dependencies to zero (outside of python and a zig compiler in PATH):

Pydust's current dependencies are as follows (please correct me if I'm wrong here):

  1. ziglang: pip installable zig compiler (redundant if zig compiler is in your system PATH)
  2. setuptools: presumably required for the pyproject.toml build system integration
  3. black: provides stub generation: eg fib.pyi, ..
  4. pydantic: provides type-checking, serialization / deserialization and more (dataclasses on steroids)

If one has an objective of making a zero-dependency pydust variant, one could consider dropping ziglang and setuptools and make stub_generation optionally available only if black is installed. As for the use of pydantic, it's only used in the two config classes: config.ExtModule and config.ToolPydust and could conceivably be dropped for some stdlib-only-dependent code (dataclasses?) .

While it is debatable why one would want a zero-dependency project, could this be considered as an objective by the project owners? Or are the above dependencies strategically important for the project's roadmap?

Change module __new__ to __init__

Currently we allow the user to initialize module state in a __new__() !Self function.

It would align better with our class instantiation semantics if instead we used a __init__(self: *Self) !void function.

Errors filename is not null terminated

  File "./Include/internal/pycore_ceval.h\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaaI", line 73, in _PyEval_EvalFrame
  File "Python/ceval.c\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaa\udcaaI", line 4774, in _PyEval_EvalFrameDefault
  File "Objects/call.c", line 299, in PyObject_Vectorcall

Support *args and **kwargs

Most urgent use-case is for __new__ method to be defined, but to ignore / allow all subclasses to decide their own args/kwargs.

Convention for returning new/borrowed references

One proposal I quite like is "verbs" for new references, and "nouns" for borrowed references.

e.g.

py.getSelf(...) // Returns new reference to PyType object
py.type_(...) /// Returns borrowed reference.

Add py.alloc

py.init initially existed because it did more complicated object initialisation, such as calling tp_new implementations.

Now however it acts more like allocator.create. So I propose adding a py.alloc(comptime T: type) !*T that passes back the allocated, but un-initialized, struct. This enables use-cases where fields of the struct contain pointers to other fields in the struct.

build error referencing /usr/lib/libpython with conda/mac

I tried to walk through the steps to try it from a conda environment, but ran into an error, on mac:

poetry run pytest
============================================== test session starts ===============================================
platform darwin -- Python 3.11.5, pytest-7.4.2, pluggy-1.3.0
rootdir: /private/var/tmp/poetry-test/poetry-demo
plugins: ziggy-pydust-0.2.12
collected 1 item / 1 error

# ,,,

.../opt/miniconda3/envs/poetry.3.11/lib/python3.11/site-packages/_pytest/runner.py:341: in from_call
    result: Optional[TResult] = func()
.../opt/miniconda3/envs/poetry.3.11/lib/python3.11/site-packages/_pytest/runner.py:372: in <lambda>
    call = CallInfo.from_call(lambda: list(collector.collect()), "collect")
.../opt/miniconda3/envs/poetry.3.11/lib/python3.11/site-packages/pydust/pytest_plugin.py:62: in collect
    h = TestProtocol.Header.unpack(proc.stdout)
.../opt/miniconda3/envs/poetry.3.11/lib/python3.11/site-packages/pydust/pytest_plugin.py:230: in unpack
    (tag, bytes_len) = struct.unpack("<II", buffer.read(8))
E   struct.error: unpack requires a buffer of 8 bytes
------------------------------------------------ Captured stderr -------------------------------------------------
dyld[32874]: Library not loaded: @rpath/libpython3.11.dylib
  Referenced from: <98F79F69-BB8F-365C-8D5A-E3C2161D139D> /private/var/tmp/poetry-test/poetry-demo/zig-out/bin/hello.test.bin
  Reason: tried: '/usr/local/lib/libpython3.11.dylib' (no such file), '/usr/lib/libpython3.11.dylib' (no such file, not in dyld cache)

Steps:

conda create -n poetry.3.11 python=3.11
conda activate --stack poetry.3.11
python -m pip install poetry pytest
poetry new poetry-demo
cd !$
poetry add -G dev ziggy-pydust
# add files and stuff
poetry run pytest

Add more ergonomic usage of Buffer protocol with SIMPLE flag.

PyBUF_SIMPLE is a flag used to denote a buffer is a pure 1-dimensional contiguous unsigned u8 array.

This is a pretty common use-case, so we should make it close to trivial to map back and forth to Zig []u8.

In fact, we could support quite a lot of the 1-dimensional buffer protocol using comptime.

  • []u8 -> writable simple
  • []u32 -> writable, check format string, check alignment, etc.
  • []const u32 -> readonly, etc.

Allow customizing the build.zig

We can either:

  • Build a way to take a dependency on Pydust as a Zig module (incl. an addPythonLibrary build step)
  • Take build.zig-like arguments in the pyproject.toml and hook them up. Though there's only so far this would get you. Sufficiently complex projects would still want more control of build.zig

@vitalnodo

Unexpectedly slow class method calls

Calling a class method is unexpectedly much slower (10-20x) than calling a function. This is not observed with comparable Cython code, so not an issue with Python class in C extensions.

pydust: calling function is fast (compiled with "zig build -Doptimize=ReleaseFast")

$ python -m timeit -s 'from fibonacci._lib import get1' 'get1()'
5000000 loops, best of 5: 37.7 nsec per loop

$ python -m timeit -s 'from fibonacci._lib import get1' 'get1()'
5000000 loops, best of 5: 46.1 nsec per loop

Calling class method is slow:

$ python -m timeit -s 'from fibonacci._lib import Class; s = Class(10)' 's.get()'
200000 loops, best of 5: 981 nsec per loop

$ python -m timeit -s 'from fibonacci._lib import Class; s = Class(10)' 's.get()'
200000 loops, best of 5: 922 nsec per loop

Code: diff against fresh checkout of ziggy-pydust-template:

$ git diff
diff --git a/src/fib.zig b/src/fib.zig
index 3fde4f7..c32e91c 100644
--- a/src/fib.zig
+++ b/src/fib.zig
@@ -80,6 +80,28 @@ pub const FibonacciIterator = py.class(struct {
     }
 });
 
+pub const Class = py.class(struct {
+    const Self = @This();
+
+    i: u64,
+
+    pub fn __new__(args: struct { i: u64 }) !Self {
+        return .{ .i = args.i };
+    }
+
+    pub fn get(self: *Self) u64 {
+        return self.i;
+    }
+});
+
+pub fn get1() u64 {
+    return 10;
+}

Same thing done with Cython - same performance for function and method call:

$ python -m timeit -s 'from testclass import get1' 'get1()'
5000000 loops, best of 5: 39.3 nsec per loop

$ python -m timeit -s 'from testclass import get1' 'get1()'
5000000 loops, best of 5: 44.1 nsec per loop

$ python -m timeit -s 'from testclass import Class; s = Class(10)' 's.get()'
5000000 loops, best of 5: 44.5 nsec per loop

$ python -m timeit -s 'from testclass import Class; s = Class(10)' 's.get()'
5000000 loops, best of 5: 47 nsec per loop

$ cat testclass.pyx 
cdef class Class:
    cdef int i

    def __init__(self, i):
        self.i = i

    cpdef get(self):
        return self.i

cpdef get1():
    return 10

$ cat setup.py 
from setuptools import setup
from Cython.Build import cythonize

setup(
    ext_modules = cythonize("testclass.pyx")
)

$ python setup.py build_ext --inplace

How to refer to class types that are defined in pydust

Right now it's possible to instantiate a class via py.init but that returns py.PyObject. How does a user who creates a class instance supposed to remember type of it. The call returns py.PyObject but where should the type be stored?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.