├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── build.zig ├── docs ├── .gitignore ├── CNAME ├── getting_started.md ├── guide │ ├── _4_testing.md │ ├── _5_memory.md │ ├── _6_buffers.md │ ├── classes.md │ ├── exceptions.md │ ├── functions.md │ ├── gil.md │ ├── index.md │ └── modules.md ├── index.md └── stylesheets │ └── extra.css ├── example ├── args_types.pyi ├── args_types.zig ├── buffers.pyi ├── buffers.zig ├── classes.pyi ├── classes.zig ├── code.pyi ├── code.zig ├── exceptions.pyi ├── exceptions.zig ├── functions.pyi ├── functions.zig ├── gil.pyi ├── gil.zig ├── hello.pyi ├── hello.zig ├── iterators.pyi ├── iterators.zig ├── memory.pyi ├── memory.zig ├── modules.pyi ├── modules.zig ├── operators.pyi ├── operators.zig ├── py.typed ├── pytest.pyi ├── pytest.zig ├── result_types.pyi └── result_types.zig ├── mkdocs.yml ├── poetry.lock ├── pyconf.dummy.zig ├── pydust ├── __init__.py ├── __main__.py ├── build.py ├── buildzig.py ├── config.py ├── generate_stubs.py ├── py.typed ├── pytest_plugin.py └── src │ ├── attributes.zig │ ├── builtins.zig │ ├── conversions.zig │ ├── discovery.zig │ ├── errors.zig │ ├── ffi.zig │ ├── functions.zig │ ├── mem.zig │ ├── modules.zig │ ├── pydust.build.zig │ ├── pydust.zig │ ├── pytypes.zig │ ├── trampoline.zig │ ├── types.zig │ └── types │ ├── bool.zig │ ├── buffer.zig │ ├── bytes.zig │ ├── code.zig │ ├── dict.zig │ ├── error.zig │ ├── float.zig │ ├── frame.zig │ ├── gil.zig │ ├── iter.zig │ ├── list.zig │ ├── long.zig │ ├── memoryview.zig │ ├── module.zig │ ├── obj.zig │ ├── sequence.zig │ ├── slice.zig │ ├── str.zig │ ├── tuple.zig │ └── type.zig ├── pyproject.toml ├── renovate.json └── test ├── __init__.py ├── conftest.py ├── test_argstypes.py ├── test_buffers.py ├── test_classes.py ├── test_code.py ├── test_exceptions.py ├── test_functions.py ├── test_gil.py ├── test_hello.py ├── test_iterator.py ├── test_memory.py ├── test_modules.py ├── test_operators.py ├── test_resulttypes.py └── test_zigonly.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["develop"] 6 | pull_request: 7 | branches: ["develop"] 8 | 9 | permissions: 10 | actions: read 11 | contents: write 12 | 13 | env: 14 | POETRY_VERSION: "2.1.2" 15 | PYTHON_VERSION: "3.13" 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | # Needed for docs to discover gh-pages branch 24 | fetch-depth: 0 25 | - uses: actions/setup-python@v5 26 | id: setup-python 27 | with: 28 | python-version: ${{ env.PYTHON_VERSION }} 29 | 30 | - name: Load cached Poetry installation 31 | id: cached-poetry 32 | uses: actions/cache@v4 33 | with: 34 | path: ~/.local 35 | key: poetry-${{ runner.os }}-${{ env.POETRY_VERSION }}-${{ steps.setup-python.outputs.python-version }} 36 | - name: Install Poetry 37 | id: install-poetry 38 | if: steps.cached-poetry.outputs.cache-hit != 'true' 39 | uses: snok/install-poetry@v1 40 | # If changing any of these, you must comment out the if statement above. 41 | with: 42 | version: ${{ env.POETRY_VERSION }} 43 | virtualenvs-create: true 44 | virtualenvs-in-project: true 45 | installer-parallel: true 46 | - name: Poetry git plugin 47 | run: poetry self add poetry-git-version-plugin 48 | 49 | - name: Load cached venv 50 | id: cached-poetry-dependencies 51 | uses: actions/cache@v4 52 | with: 53 | path: .venv 54 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 55 | 56 | - name: Poetry Install 57 | run: poetry install -n -v 58 | - name: Poetry Build 59 | run: poetry build -v 60 | 61 | - name: Ruff 62 | run: poetry run ruff check . 63 | - name: Black 64 | run: poetry run ruff format --check . 65 | - name: Pytest 66 | run: poetry run pytest 67 | - name: Check generated stubs 68 | run: poetry run python -m ziglang build --build-file pytest.build.zig generate-stubs -Dcheck-stubs=true 69 | 70 | - name: Zig Docs Build 71 | run: poetry run python -m ziglang build docs 72 | - name: Setup doc deploy 73 | run: | 74 | git config --global user.name Docs Deploy 75 | git config --global user.email docs@dummy.bot.com 76 | - name: MKDocs Build 77 | run: poetry run mike deploy develop --push 78 | if: ${{ github.ref == 'refs/heads/develop' }} 79 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: ['*'] 6 | 7 | permissions: 8 | id-token: write 9 | contents: write 10 | 11 | env: 12 | POETRY_VERSION: "1.6.1" 13 | PYTHON_VERSION: "3.11" 14 | 15 | jobs: 16 | publish: 17 | environment: 18 | name: pypi 19 | url: https://pypi.org/p/ziggy-pydust 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | # Needed for docs to discover gh-pages branch 25 | fetch-depth: 0 26 | - uses: actions/setup-python@v5 27 | id: setup-python 28 | with: 29 | python-version: ${{ env.PYTHON_VERSION }} 30 | 31 | - name: Load cached Poetry installation 32 | id: cached-poetry 33 | uses: actions/cache@v4 34 | with: 35 | path: ~/.local 36 | key: poetry-${{ runner.os }}-${{ env.POETRY_VERSION }}-${{ steps.setup-python.outputs.python-version }} 37 | - name: Install Poetry 38 | id: install-poetry 39 | if: steps.cached-poetry.outputs.cache-hit != 'true' 40 | uses: snok/install-poetry@v1 41 | # If changing any of these, you must comment out the if statement above. 42 | with: 43 | version: ${{ env.POETRY_VERSION }} 44 | virtualenvs-create: true 45 | virtualenvs-in-project: true 46 | installer-parallel: true 47 | - name: Poetry git plugin 48 | run: poetry self add poetry-git-version-plugin 49 | 50 | - name: Load cached venv 51 | id: cached-poetry-dependencies 52 | uses: actions/cache@v4 53 | with: 54 | path: .venv 55 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 56 | 57 | - name: Poetry Build 58 | run: poetry build -v 59 | 60 | - name: Poetry Install 61 | run: poetry install -v --only=docs 62 | - name: Zig Docs Build 63 | run: poetry run python -m ziglang build docs 64 | - name: Setup doc deploy 65 | run: | 66 | git config --global user.name Docs Deploy 67 | git config --global user.email docs@dummy.bot.com 68 | - name: Get Version 69 | id: get_version 70 | uses: battila7/get-version-action@v2 71 | - name: MKDocs Build 72 | run: poetry run mike deploy ${{ steps.get_version.outputs.major }}.${{ steps.get_version.outputs.minor }} latest --push --update-aliases 73 | 74 | - name: Publish package distributions to PyPI 75 | uses: pypa/gh-action-pypi-publish@release/v1 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | *.so.o 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | 75 | # PyBuilder 76 | .pybuilder/ 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | # For a library or package, you might want to ignore these files since the code is 88 | # intended to run in multiple environments; otherwise, check them in: 89 | # .python-version 90 | 91 | # pipenv 92 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 93 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 94 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 95 | # install all needed dependencies. 96 | #Pipfile.lock 97 | 98 | # poetry 99 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 100 | # This is especially recommended for binary packages to ensure reproducibility, and is more 101 | # commonly ignored for libraries. 102 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 103 | #poetry.lock 104 | 105 | # pdm 106 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 107 | #pdm.lock 108 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 109 | # in version control. 110 | # https://pdm.fming.dev/#use-with-ide 111 | .pdm.toml 112 | 113 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 114 | __pypackages__/ 115 | 116 | # Celery stuff 117 | celerybeat-schedule 118 | celerybeat.pid 119 | 120 | # SageMath parsed files 121 | *.sage.py 122 | 123 | # Environments 124 | .env 125 | .venv 126 | env/ 127 | venv/ 128 | ENV/ 129 | env.bak/ 130 | venv.bak/ 131 | 132 | # Spyder project settings 133 | .spyderproject 134 | .spyproject 135 | 136 | # Rope project settings 137 | .ropeproject 138 | 139 | # mkdocs documentation 140 | /site 141 | 142 | # mypy 143 | .mypy_cache/ 144 | .dmypy.json 145 | dmypy.json 146 | 147 | # Pyre type checker 148 | .pyre/ 149 | 150 | # pytype static type analyzer 151 | .pytype/ 152 | 153 | # Cython debug symbols 154 | cython_debug/ 155 | 156 | # Zig 157 | .zig-cache 158 | zig-cache/ 159 | zig-out/ 160 | /release/ 161 | /debug/ 162 | /build/ 163 | /build-*/ 164 | /docgen_tmp/ 165 | 166 | .idea 167 | 168 | /pydust.build.zig 169 | pytest.build.zig 170 | test.build.zig 171 | zls.build.json 172 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "vadimcn.vscode-lldb", 4 | "ziglang.vscode-zig", 5 | "psioniq.psi-header" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "lldb", 6 | "request": "launch", 7 | "name": "Debug", 8 | "preLaunchTask": "zig-test-bin", 9 | "program": "${workspaceFolder}/zig-out/bin/test.bin", 10 | "args": [], 11 | "cwd": "${workspaceFolder}/", 12 | }, 13 | { 14 | "type": "lldb", 15 | "request": "launch", 16 | "name": "Debug Example", 17 | "initCommands": [ 18 | "shell poetry run pydust debug ${file}" 19 | ], 20 | "program": "zig-out/bin/debug.bin", 21 | }, 22 | { 23 | "type": "lldb", 24 | "request": "launch", 25 | "name": "LLDB Python", 26 | "program": "${command:python.interpreterPath}", 27 | "args": [ 28 | "-m", 29 | "pytest", 30 | ], 31 | "cwd": "${workspaceFolder}" 32 | }, 33 | ] 34 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter" 4 | }, 5 | "ruff.args": [ 6 | "--config=pyproject.toml" 7 | ], 8 | "commandOnAllFiles.commands": { 9 | "header": { 10 | "command": "psi-header.insertFileHeader", 11 | "includeFolders": [ 12 | "example", 13 | "pydust", 14 | "test" 15 | ], 16 | "includeFileExtensions": [ 17 | ".zig", 18 | ".py" 19 | ], 20 | } 21 | }, 22 | "psi-header.config": { 23 | "forceToTop": true, 24 | "blankLinesAfter": 1, 25 | }, 26 | "psi-header.changes-tracking": { 27 | "isActive": true, 28 | "autoHeader": "manualSave", 29 | "enforceHeader": true, 30 | "include": [ 31 | "zig", 32 | "python", 33 | ], 34 | "exclude": [ 35 | "js", 36 | "md" 37 | ], 38 | }, 39 | "psi-header.templates": [ 40 | { 41 | "language": "*", 42 | "template": [ 43 | "Licensed under the Apache License, Version 2.0 (the \"License\");", 44 | "you may not use this file except in compliance with the License.", 45 | "You may obtain a copy of the License at", 46 | "", 47 | " http://www.apache.org/licenses/LICENSE-2.0", 48 | "", 49 | "Unless required by applicable law or agreed to in writing, software", 50 | "distributed under the License is distributed on an \"AS IS\" BASIS,", 51 | "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.", 52 | "See the License for the specific language governing permissions and", 53 | "limitations under the License.", 54 | ] 55 | } 56 | ], 57 | "psi-header.lang-config": [ 58 | { 59 | "language": "zig", 60 | "forceToTop": true, 61 | "begin": "", 62 | "end": "", 63 | "prefix": "// ", 64 | }, 65 | { 66 | "language": "python", 67 | "begin": "\"\"\"", 68 | "end": "\"\"\"", 69 | } 70 | ], 71 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "zig-test-bin", 6 | "command": "zig", 7 | "args": [ 8 | "build", 9 | "-Dtest-debug-root=${file}" 10 | ], 11 | "options": { 12 | "cwd": "${workspaceFolder}" 13 | }, 14 | "type": "shell" 15 | }, 16 | ] 17 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ziggy Pydust 2 | 3 |

4 | 5 | 6 | 7 |

8 |

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

11 |

12 | 13 | Actions 14 | 15 | 16 | Package version 17 | 18 | 19 | Python version 20 | 21 | 22 | License 23 | 24 |

25 | 26 | --- 27 | 28 | **Documentation**: https://pydust.fulcrum.so/latest 29 | 30 | **API**: https://pydust.fulcrum.so/latest/zig 31 | 32 | **Source Code**: https://github.com/fulcrum-so/ziggy-pydust 33 | 34 | **Template**: https://github.com/fulcrum-so/ziggy-pydust-template 35 | 36 | --- 37 | 38 | Ziggy Pydust is a framework for writing and packaging native Python extension modules written in Zig. 39 | 40 | - Package Python extension modules written in Zig. 41 | - Pytest plugin to discover and run Zig tests. 42 | - Comptime argument wrapping / unwrapping for interop with native Zig types. 43 | 44 | ```zig 45 | const py = @import("pydust"); 46 | 47 | pub fn fibonacci(args: struct { n: u64 }) u64 { 48 | if (args.n < 2) return args.n; 49 | 50 | var sum: u64 = 0; 51 | var last: u64 = 0; 52 | var curr: u64 = 1; 53 | for (1..args.n) { 54 | sum = last + curr; 55 | last = curr; 56 | curr = sum; 57 | } 58 | return sum; 59 | } 60 | 61 | comptime { 62 | py.rootmodule(@This()); 63 | } 64 | ``` 65 | 66 | ## Compatibility 67 | 68 | Pydust supports: 69 | 70 | - [Zig 0.14.0](https://ziglang.org/download/0.14.0/release-notes.html) 71 | - [CPython >=3.11](https://docs.python.org/3.11/c-api/stable.html) 72 | 73 | Please reach out if you're interested in helping us to expand compatibility. 74 | 75 | ## Getting Started 76 | 77 | Pydust docs can be found [here](https://pydust.fulcrum.so). 78 | Zig documentation (beta) can be found [here](https://pydust.fulcrum.so/latest/zig). 79 | 80 | There is also a [template repository](https://github.com/fulcrum-so/ziggy-pydust-template) including Poetry build, Pytest and publishing from Github Actions. 81 | 82 | ## Contributing 83 | 84 | We welcome contributions! Pydust is in its early stages so there is lots of low hanging 85 | fruit when it comes to contributions. 86 | 87 | - Assist other Pydust users with GitHub issues or discussions. 88 | - Suggest or implement features, fix bugs, fix performance issues. 89 | - Improve our documentation. 90 | - Write articles or other content demonstrating how you have used Pydust. 91 | 92 | ## License 93 | 94 | Pydust is released under the [Apache-2.0 license](https://opensource.org/licenses/APACHE-2.0). 95 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const builtin = @import("builtin"); 14 | const std = @import("std"); 15 | 16 | pub fn build(b: *std.Build) void { 17 | const target = b.standardTargetOptions(.{}); 18 | const optimize = b.standardOptimizeOption(.{}); 19 | 20 | const python_exe = b.option([]const u8, "python-exe", "Python executable to use") orelse "python"; 21 | 22 | const pythonInc = getPythonIncludePath(python_exe, b.allocator) catch @panic("Missing python"); 23 | const pythonLib = getPythonLibraryPath(python_exe, b.allocator) catch @panic("Missing python"); 24 | const pythonVer = getPythonLDVersion(python_exe, b.allocator) catch @panic("Missing python"); 25 | const pythonLibName = std.fmt.allocPrint(b.allocator, "python{s}", .{pythonVer}) catch @panic("Missing python"); 26 | 27 | const test_step = b.step("test", "Run library tests"); 28 | const docs_step = b.step("docs", "Generate docs"); 29 | 30 | // We never build this lib, but we use it to generate docs. 31 | const pydust_lib = b.addSharedLibrary(.{ 32 | .name = "pydust", 33 | .root_source_file = b.path("pydust/src/pydust.zig"), 34 | .target = target, 35 | .optimize = optimize, 36 | }); 37 | const pydust_lib_mod = b.createModule(.{ .root_source_file = b.path("./pyconf.dummy.zig") }); 38 | pydust_lib_mod.addIncludePath(.{ .cwd_relative = pythonInc }); 39 | pydust_lib.root_module.addImport("pyconf", pydust_lib_mod); 40 | 41 | const pydust_docs = b.addInstallDirectory(.{ 42 | .source_dir = pydust_lib.getEmittedDocs(), 43 | // Emit the Zig docs into zig-out/../docs/zig 44 | .install_dir = .{ .custom = "../docs" }, 45 | .install_subdir = "zig", 46 | }); 47 | docs_step.dependOn(&pydust_docs.step); 48 | 49 | const main_tests = b.addTest(.{ 50 | .root_source_file = b.path("pydust/src/pydust.zig"), 51 | .target = target, 52 | .optimize = optimize, 53 | }); 54 | main_tests.linkLibC(); 55 | main_tests.addLibraryPath(.{ .cwd_relative = pythonLib }); 56 | main_tests.linkSystemLibrary(pythonLibName); 57 | main_tests.addRPath(.{ .cwd_relative = pythonLib }); 58 | const main_tests_mod = b.createModule(.{ .root_source_file = b.path("./pyconf.dummy.zig") }); 59 | main_tests_mod.addIncludePath(.{ .cwd_relative = pythonInc }); 60 | main_tests.root_module.addImport("pyconf", main_tests_mod); 61 | 62 | const run_main_tests = b.addRunArtifact(main_tests); 63 | test_step.dependOn(&run_main_tests.step); 64 | 65 | // Setup a library target to trick the Zig Language Server into providing completions for @import("pydust") 66 | const example_lib = b.addSharedLibrary(.{ 67 | .name = "example", 68 | .root_source_file = b.path("example/hello.zig"), 69 | .target = target, 70 | .optimize = optimize, 71 | }); 72 | example_lib.linkLibC(); 73 | example_lib.addLibraryPath(.{ .cwd_relative = pythonLib }); 74 | example_lib.linkSystemLibrary(pythonLibName); 75 | example_lib.addRPath(.{ .cwd_relative = pythonLib }); 76 | const example_lib_mod = b.createModule(.{ .root_source_file = b.path("pydust/src/pydust.zig") }); 77 | example_lib_mod.addIncludePath(.{ .cwd_relative = pythonInc }); 78 | example_lib.root_module.addImport("pydust", example_lib_mod); 79 | example_lib.root_module.addImport( 80 | "pyconf", 81 | b.createModule(.{ .root_source_file = b.path("./pyconf.dummy.zig") }), 82 | ); 83 | 84 | // Option for emitting test binary based on the given root source. 85 | // This is used for debugging as in .vscode/tasks.json 86 | const test_debug_root = b.option([]const u8, "test-debug-root", "The root path of a file emitted as a binary for use with the debugger"); 87 | if (test_debug_root) |root| { 88 | main_tests.root_module.root_source_file = b.path(root); 89 | const test_bin_install = b.addInstallBinFile(main_tests.getEmittedBin(), "test.bin"); 90 | b.getInstallStep().dependOn(&test_bin_install.step); 91 | } 92 | } 93 | 94 | fn getPythonIncludePath( 95 | python_exe: []const u8, 96 | allocator: std.mem.Allocator, 97 | ) ![]const u8 { 98 | const includeResult = try runProcess(.{ 99 | .allocator = allocator, 100 | .argv = &.{ python_exe, "-c", "import sysconfig; print(sysconfig.get_path('include'), end='')" }, 101 | }); 102 | defer allocator.free(includeResult.stderr); 103 | return includeResult.stdout; 104 | } 105 | 106 | fn getPythonLibraryPath(python_exe: []const u8, allocator: std.mem.Allocator) ![]const u8 { 107 | const includeResult = try runProcess(.{ 108 | .allocator = allocator, 109 | .argv = &.{ python_exe, "-c", "import sysconfig; print(sysconfig.get_config_var('LIBDIR'), end='')" }, 110 | }); 111 | defer allocator.free(includeResult.stderr); 112 | return includeResult.stdout; 113 | } 114 | 115 | fn getPythonLDVersion(python_exe: []const u8, allocator: std.mem.Allocator) ![]const u8 { 116 | const includeResult = try runProcess(.{ 117 | .allocator = allocator, 118 | .argv = &.{ python_exe, "-c", "import sysconfig; print(sysconfig.get_config_var('LDVERSION'), end='')" }, 119 | }); 120 | defer allocator.free(includeResult.stderr); 121 | return includeResult.stdout; 122 | } 123 | 124 | const runProcess = if (builtin.zig_version.minor >= 12) std.process.Child.run else std.process.Child.exec; 125 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | zig/ -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | pydust.fulcrum.so -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Pydust is currently designed to be embedded within a Python [Poetry](https://python-poetry.org/) project. [Reach out](https://github.com/fulcrum-so/ziggy-pydust/issues) if you'd like help integrating Pydust with other build setups. 4 | 5 | See also the [generated Zig documentation](https://pydust.fulcrum.so/zig). 6 | 7 | ## GitHub Template 8 | 9 | By far the easiest way to get started is by creating a project from our GitHub template: [github.com/fulcrum-so/ziggy-pydust-template/](https://github.com/fulcrum-so/ziggy-pydust-template/) 10 | 11 | This template includes: 12 | 13 | - A Python Poetry project 14 | - A `src/` directory containing a Pydust Python module 15 | - Pytest setup for running both Python and Zig unit tests. 16 | - GitHub Actions workflows for building and publishing the package. 17 | - VSCode settings for recommended extensions, debugger configurations, etc. 18 | 19 | ## Poetry Setup 20 | 21 | Assuming you have an existing Poetry project, these are the changes you need to make to 22 | your `pyproject.toml` to setup Ziggy Pydust. But first, add Pydust as a dev dependency: 23 | 24 | ```bash 25 | poetry add -G dev ziggy-pydust 26 | ``` 27 | 28 | ```diff title="pyproject.toml" 29 | [tool.poetry] 30 | name = "your-package" 31 | packages = [ { include = "your-module" } ] 32 | + include = [ { path = "src/", format = "sdist" }, { path = "your-module/*.so", format = "wheel" } ] 33 | 34 | + [tool.poetry.build] 35 | + script = "build.py" 36 | 37 | [build-system] 38 | - requires = ["poetry-core"] 39 | + requires = ["poetry-core", "ziggy-pydust==TODO_SET_VERSION"] 40 | build-backend = "poetry.core.masonry.api" 41 | ``` 42 | 43 | As well as creating the `build.py` for Poetry to invoke the Pydust build. 44 | 45 | ```python title="build.py" 46 | from pydust.build import build 47 | 48 | build() 49 | ``` 50 | 51 | ## My First Module 52 | 53 | Once Poetry is configured, add a Pydust module to your `pyproject.toml` and start writing some Zig! 54 | 55 | ```toml title="pyproject.toml" 56 | [[tool.pydust.ext_module]] 57 | name = "example.hello" 58 | root = "src/hello.zig" 59 | ``` 60 | 61 | ```zig title="src/hello.zig" 62 | --8<-- "example/hello.zig:ex" 63 | ``` 64 | 65 | Running `poetry install` will build your modules. After this, you will be 66 | able to import your module from within `poetry shell` or `poetry run pytest`. 67 | 68 | ```python title="test/test_hello.py" 69 | --8<-- "test/test_hello.py:ex" 70 | ``` 71 | 72 | ## Zig Language Server 73 | 74 | !!! warning 75 | 76 | Currently ZLS (at least when running in VSCode) requires a small amount of manual setup. 77 | 78 | In the root of your project, create a `zls.build.json` file containing the path to your python executable. 79 | This can be obtained by running `poetry env info -e`. 80 | 81 | ```json title="zls.build.json" 82 | { 83 | "build_options": [ 84 | { 85 | "name": "python-exe", 86 | "value": "/path/to/your/poetry/venv/bin/python", 87 | } 88 | ] 89 | } 90 | ``` 91 | 92 | ## Self-managed Mode 93 | 94 | Pydust makes it easy to get started building a Zig extension for Python. But when your use-case becomes sufficiently 95 | complex, you may wish to have full control of your `build.zig` file. 96 | 97 | By default, Pydust will generated two files: 98 | 99 | * `pydust.build.zig` - a Zig file used for bootstrapping Pydust and configuring Python modules. 100 | * `build.zig` - a valid Zig build configuration based on the `tool.pydust.ext_module` entries in your `pyproject.toml`. 101 | 102 | In self-managed mode, Pydust will only generate the `pydust.build.zig` file and your are free to manage your own `build.zig`. 103 | To enable this mode, set the flag in your `pyproject.toml` and remove any `ext_module` entries. 104 | 105 | ```diff title="pyproject.toml" 106 | [tool.pydust] 107 | + self_managed = true 108 | 109 | - [[tool.pydust.ext_module]] 110 | - name = "example.hello" 111 | - root = "example/hello.zig" 112 | ``` 113 | 114 | You can then configure Python modules from a custom `build.zig` file: 115 | 116 | ```zig title="build.zig" 117 | const std = @import("std"); 118 | const py = @import("./pydust.build.zig"); 119 | 120 | pub fn build(b: *std.Build) void { 121 | const target = b.standardTargetOptions(.{}); 122 | const optimize = b.standardOptimizeOption(.{}); 123 | 124 | // Each Python module consists of a library_step and a test_step 125 | const module = pydust.addPythonModule(.{ 126 | .name = "example.hello", 127 | .root_source_file = .{ .path = "example/hello.zig" }, 128 | .target = target, 129 | .optimize = optimize, 130 | }); 131 | module.library_step.addModule(..., ...); 132 | module.test_step.addModule(..., ...); 133 | } 134 | ``` -------------------------------------------------------------------------------- /docs/guide/_4_testing.md: -------------------------------------------------------------------------------- 1 | # Pytest Plugin 2 | 3 | Pydust ships with a Pytest plugin that builds, collects and executes your Zig tests. This 4 | means testing should work out-of-the-box with your existing Python project and you never 5 | need to interact directly with the Zig build system if you don't want to. 6 | 7 | The Zig documentation provides an [excellent introduction to writing tests](https://ziglang.org/documentation/master/#Zig-Test). 8 | 9 | !!! Note 10 | 11 | Zig tests are currently spawned as a separate process. This means you must manually call `py.initialize()` and 12 | `defer py.finalize()` in order to setup and teardown a Python interpreter. 13 | 14 | ``` zig title="example/pytest.zig" 15 | --8<-- "example/pytest.zig:example" 16 | ``` 17 | 18 | Add `ziggy-pydust` as a dev dependency: 19 | 20 | ```bash 21 | poetry add -G dev ziggy-pydust 22 | ``` 23 | 24 | ```diff title="pyproject.toml" 25 | [tool.poetry.group.dev.dependencies] 26 | + ziggy-pydust = "TODO_SET_VERSION" 27 | ``` 28 | 29 | 30 | After running `poetry run pytest` you should see your Zig tests included in your Pytest output: 31 | 32 | ``` bash linenums="0" 33 | ================================== test session starts ================================== 34 | platform darwin -- Python 3.11.5, pytest-7.4.0, pluggy-1.3.0 35 | plugins: ziggy-pydust-0.2.1 36 | collected 7 items 37 | 38 | example/pytest.zig .x [100%] 39 | 40 | ============================= 1 passed, 1 xfailed in 0.30s ============================== 41 | ``` -------------------------------------------------------------------------------- /docs/guide/_5_memory.md: -------------------------------------------------------------------------------- 1 | # Memory Management 2 | 3 | Pydust, like Zig, doesn't perform any implicit memory management. Pydust is designed to be a relatively 4 | thin layer around the CPython API, and therefore the same semantics apply. 5 | 6 | All Pydust Python types (such as `py.PyObject(root)` and `py.Py(root)`) have `incref()` and `decref()` member 7 | functions. These correspond to `Py_INCREF` and `Py_DECREF` respectively. 8 | 9 | To create a contrived example, here we take a string `left` as an argument passed in from Python and we 10 | append a Zig slice `right` to it. Since we create a new `py.PyString(root)` containing `right`, it is our 11 | responsibility to ensure `decref()` is called on it. 12 | 13 | The reason we call `incref()` on `left` is more subtle. When being called from Python, we are essentially 14 | _borrowing_ references to each of the arguments. The `PyString(root).append` function actually _steals_ 15 | a reference to itself (for performance improvements and ergonomics when chaining multiple `append` calls). 16 | Since we had only borrowed a reference to `left`, we must call `.incref()` in order to allow `left.append()` 17 | to steal the new reference back again. 18 | 19 | ``` zig 20 | --8<-- "example/memory.zig:append" 21 | ``` 22 | 23 | Of course, this could be implemeneted much more simply using `PyString(root).concatSlice` (which returns a new reference 24 | without stealing one) and also internally creates and decref's `right`. 25 | 26 | ``` zig 27 | --8<-- "example/memory.zig:concat" 28 | ``` 29 | 30 | In general, Pydust functions do not steal references. They should be loudly documented in the rare cases that 31 | they do, and typically will have the naming convention `fromOwned` meaning they take ownership (steal a reference) 32 | to the argument being passed in. 33 | 34 | The `PyString(root).append` function can however be useful. For example, when chaining several appends together. 35 | 36 | ``` zig 37 | var s = py.PyString(root).fromSlice("Hello "); 38 | s = s.appendSlice("1, "); 39 | s = s.appendSlice("2, "); 40 | s = s.appendSlice("3"); 41 | return s; 42 | ``` 43 | 44 | !!! tip "Upcoming Feature!" 45 | 46 | Work is underway to provide a test harness that uses Zig's `GeneralPurposeAllocator` to 47 | catch memory leaks within your Pydust extension code and surface them to pytest. 48 | -------------------------------------------------------------------------------- /docs/guide/_6_buffers.md: -------------------------------------------------------------------------------- 1 | # Python Buffer Protocol 2 | 3 | Python objects implementing the [Buffer Protocol](https://docs.python.org/3/c-api/buffer.html#) can be used with zero copy. 4 | 5 | ```zig 6 | --8<-- "example/buffers.zig:sum" 7 | ``` 8 | 9 | This function accepts Python [arrays](https://docs.python.org/3/library/array.html#module-array), Numpy arrays, or any other buffer protocol implementation. 10 | 11 | ```python 12 | --8<-- "test/test_buffers.py:sum" 13 | ``` 14 | 15 | !!! Note 16 | 17 | Understanding [request types](https://docs.python.org/3/c-api/buffer.html#buffer-request-types) is important when working with buffers. Common request types are implemented as `py.PyBuffer(root).Flags`, e.g. `py.PyBuffer(root).Flags.FULL_RO`. 18 | 19 | 20 | You can implement a buffer protocol in a Pydust module by implementing `__buffer__` and optionally `__release_buffer__` methods. 21 | 22 | ```zig 23 | --8<-- "example/buffers.zig:protocol" 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/guide/exceptions.md: -------------------------------------------------------------------------------- 1 | # Python Exceptions 2 | 3 | Pydust provides utilities for raising builtin exception types as provided by the 4 | [Stable API](https://docs.python.org/3/c-api/stable.html) under `PyExc_`. 5 | 6 | ``` zig 7 | --8<-- "example/exceptions.zig:valueerror" 8 | ``` 9 | 10 | Exceptions can be raise with any of the following: 11 | 12 | * `#!zig .raise(message: [:0]const u8)` 13 | * `#!zig .raiseFmt(comptime fmt: [:0]const u8, args: anytype)` 14 | * `#!zig .raiseComptimeFmt(comptime fmt: [:0]const u8, comptime args: anytype)` -------------------------------------------------------------------------------- /docs/guide/functions.md: -------------------------------------------------------------------------------- 1 | # Python Functions 2 | 3 | Python functions can be declared as usual zig functions at top level of the module 4 | 5 | ```zig 6 | --8<-- "example/functions.zig:function" 7 | ``` 8 | 9 | Functions come with \_\_text_signature\_\_ support out of the box. Function `double` 10 | will have signature `(x, /)`. 11 | 12 | Functions also accept keyword arguments as regular python function. Argument is deemed 13 | to be a kwarg argument if it has a default value associated with it for a function: 14 | 15 | ```zig 16 | --8<-- "example/functions.zig:kwargs" 17 | ``` 18 | 19 | The generated signature will be `(x, /, *, y=42.0)` 20 | 21 | Python's variadic arguments `*args` and `**kwargs` are represented by including `py.Args` and `py.Kwargs` attributes 22 | in your args struct. 23 | 24 | ```zig 25 | --8<-- "example/functions.zig:varargs" 26 | ``` -------------------------------------------------------------------------------- /docs/guide/gil.md: -------------------------------------------------------------------------------- 1 | # Global Interpreter Lock 2 | 3 | Pydust provides two functions for managing GIL state: `py.nogil` and `py.gil`. 4 | 5 | ## No GIL / Allow Threads 6 | 7 | The `py.nogil` function allows Pydust code to release the Python GIL. This allows Python threads to continue to 8 | make progress while Zig code is running. 9 | 10 | Each call to `py.nogil()` must have a corresponding `acquire()` call. 11 | 12 | See the [Python documentation](https://docs.python.org/3.11/c-api/init.html#releasing-the-gil-from-extension-code) for more information. 13 | 14 | === "Zig" 15 | 16 | ```zig 17 | --8<-- "example/gil.zig:gil" 18 | ``` 19 | 20 | === "Python" 21 | 22 | ```python 23 | --8<-- "test/test_gil.py:gil" 24 | ``` 25 | 26 | ## Acquire GIL 27 | 28 | The `py.gil` function allows Pydust code to re-acquire the Python GIL before calling back into Python code. 29 | This can be particularly useful with Zig or C libraries that make use of callbacks. 30 | 31 | Each call to `py.gil()` must have a corresponding `release()` call. 32 | 33 | See the [Python documentation](https://docs.python.org/3.11/c-api/init.html#non-python-created-threads) for more information. 34 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | # User Guide 2 | 3 | The user guide details every single feature Pydust offers. All code snippets are taken 4 | from our `example/` directory and are tested during CI. 5 | 6 | If you do struggle or find any issues with the examples, please do [let us know!](https://github.com/fulcrum-so/ziggy-pydust/issues) 7 | 8 | ## Conventions 9 | 10 | Pydust maintains a consistent set of conventions around structs, function naming, and memory 11 | management to assist with development. 12 | 13 | ### Passing root type 14 | 15 | Pydust requires access to your top-level zig module. As a convention, we recommend assigning 16 | zig's [`@This`](https://ziglang.org/documentation/master/#This) to a global variable named 17 | `root` which you need to pass to many Pydust functions. 18 | 19 | ### Conversion Functions 20 | 21 | When converting from Python to Zig types: 22 | 23 | * `.as(root, T, anytype)` - return a view of the object *as* the given type. This will leave the `refcnt` of the original object untouched. 24 | 25 | When creating Python types from Zig types: 26 | 27 | * `.create(root, anytype)` - create a new Python object from a Zig type. Zig slices are copied. 28 | * `PyFoo.checked(root, py.PyObject(root))` - checks a `PyObject(root)` is indeed a `PyFoo` before wrapping it up as one. 29 | * `PyFoo.unchecked(root, py.PyObject(root))` - wraps a `PyObject(root)` as a `PyFoo` without checking the type. 30 | 31 | ## Type Conversions 32 | 33 | At comptime, Pydust wraps your function definitions such that native Zig types can be accepted 34 | or returned from functions and automatically converted into Python objects. 35 | 36 | ### Zig Primitives 37 | 38 | | Zig Type | Python Type | 39 | |:----------------------| :----------- | 40 | | `void` | `None` | 41 | | `bool` | `bool` | 42 | | `i32`, `i64` | `int` | 43 | | `u32`, `u64` | `int` | 44 | | `f16`, `f32`, `f64` | `float` | 45 | | `struct` | `dict` | 46 | | `tuple struct` | `tuple` | 47 | | `[]const u8` | `str` | 48 | | `*[_]u8` | `str` | 49 | 50 | !!! tip "" 51 | 52 | Slices (e.g. `[]const u8` strings) cannot be returned from Pydust functions since Pydust has 53 | no way to deallocate them after they're copied into Python. 54 | 55 | Slices _can_ be taken as arguments to a function, but the bytes underlying that slice are only 56 | guaranteed to live for the duration of the function call. They should be copied if you wish to extend 57 | the lifetime. 58 | 59 | ### Pydust Objects 60 | 61 | Pointers to any Pydust Zig structs will convert to their corresponding Python instance. 62 | 63 | For example, given the class `Foo` below, 64 | if the class is initialized with `const foo: *Foo = py.init(Foo, .{})`, 65 | then a result of `foo` will be wrapped into the corresponding Python instance of 66 | `Foo`. 67 | 68 | ```zig title="foo.zig" 69 | const Foo = py.class(struct { a: u32 = 0 }); 70 | 71 | pub fn create_foo() *const Foo { 72 | return py.init(Foo, .{}); 73 | } 74 | ``` 75 | 76 | ### Pydust Type Wrappers 77 | 78 | The Pydust Python type wrappers convert as expected. 79 | 80 | | Zig Type | Python Type | 81 | | :------------ | :----------- | 82 | | `py.PyObject` | `object` | 83 | | `py.PyBool` | `bool` | 84 | | `py.PyBytes` | `bytes` | 85 | | `py.PyLong` | `int` | 86 | | `py.PyFloat` | `float` | 87 | | `py.PyTuple` | `tuple` | 88 | | `py.PyDict` | `dict` | 89 | | `py.PyString` | `str` | 90 | -------------------------------------------------------------------------------- /docs/guide/modules.md: -------------------------------------------------------------------------------- 1 | # Python Modules 2 | 3 | Python modules represent the entrypoint into your Zig code. You can create a new 4 | module by adding an entry into your `pyproject.toml`: 5 | 6 | ```toml title="pyproject.toml" 7 | [[tool.pydust.ext_module]] 8 | name = "example.modules" # A fully-qualified python module name 9 | root = "src/modules.zig" # Path to a Zig file that exports this module. 10 | ``` 11 | 12 | !!! note 13 | 14 | Poetry doesn't support building exclusively native modules without a containing 15 | python package. In this example, you would need to create an empty `example/__init__.py`. 16 | 17 | In Pydust, all Python declarations start life as a struct. When a struct is registered with 18 | Pydust as a module, a `#!c PyObject *PyInit_(void)` function is created automatically 19 | and exported from the compiled shared library. This allows the module to be imported by Python. 20 | 21 | ## Example Module 22 | 23 | Please refer to the annotations in this example module for an explanation of the Pydust features. 24 | 25 | ```zig title="src/modules.zig" 26 | --8<-- "example/modules.zig:ex" 27 | ``` 28 | 29 | 1. In Zig, every file is itself a struct. So assigning `Self = @This();` allows you to get a reference to your own type. 30 | 31 | 2. Unlike regular Python modules, native Python modules are able to carry private internal state. 32 | 33 | 3. Any fields that cannot be defaulted at comptime (i.e. if they require calling into Python) 34 | must be initialized in the module's `__init__` function. 35 | 36 | 4. Module functions taking a `*Self` or `*const Self` argument are passed a pointer 37 | to their internal state. 38 | 39 | 5. Arguments in Pydust are accepted as a pointer to a const struct. This allows Pydust to generate 40 | function docstrings using the struct field names. 41 | 42 | 6. Submodules can also be arbitrarily nested. Note however that submodules are not Python packages. 43 | That means `#!python from example.modules import submodule` will work, but `#!python from example.modules.submodule import world` will not. 44 | 45 | 7. All modules must be registered with Pydust such that a `PyInit_` function is 46 | generated and exported from the object file. -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | {% include-markdown "../README.md" %} -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --md-primary-fg-color: #3375a7; 3 | --md-primary-fg-color--light: #3375a7; 4 | --md-primary-fg-color--dark: #3375a7; 5 | 6 | --md-accent-fg-color: #f7a80f; 7 | --md-accent-fg-color--light: #f7a80f; 8 | --md-accent-fg-color--dark: #f7a80f; 9 | } -------------------------------------------------------------------------------- /example/args_types.pyi: -------------------------------------------------------------------------------- 1 | def zigstruct(x, /): ... 2 | -------------------------------------------------------------------------------- /example/args_types.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("pydust"); 15 | 16 | const root = @This(); 17 | 18 | const ArgStruct = struct { 19 | foo: i32, 20 | bar: bool, 21 | }; 22 | 23 | pub fn zigstruct(args: struct { x: ArgStruct }) bool { 24 | return args.x.foo == 1234 and args.x.bar; 25 | } 26 | 27 | comptime { 28 | py.rootmodule(root); 29 | } 30 | -------------------------------------------------------------------------------- /example/buffers.pyi: -------------------------------------------------------------------------------- 1 | def sum(buf, /): ... 2 | 3 | class ConstantBuffer: 4 | """ 5 | A class implementing a buffer protocol 6 | """ 7 | 8 | def __init__(self, elem, length, /) -> None: ... 9 | def __buffer__(self, flags, /) -> memoryview: 10 | """ 11 | Return a buffer object that exposes the underlying memory of the object. 12 | """ 13 | -------------------------------------------------------------------------------- /example/buffers.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("pydust"); 15 | 16 | const root = @This(); 17 | 18 | // --8<-- [start:protocol] 19 | pub const ConstantBuffer = py.class(struct { 20 | pub const __doc__ = "A class implementing a buffer protocol"; 21 | const Self = @This(); 22 | 23 | values: []i64, 24 | shape: []const isize, // isize to be compatible with Python API 25 | format: [:0]const u8 = "l", // i64 26 | 27 | pub fn __init__(self: *Self, args: struct { elem: i64, length: u32 }) !void { 28 | const values = try py.allocator.alloc(i64, args.length); 29 | @memset(values, args.elem); 30 | 31 | const shape = try py.allocator.alloc(isize, 1); 32 | shape[0] = @intCast(args.length); 33 | 34 | self.* = .{ .values = values, .shape = shape }; 35 | } 36 | 37 | pub fn __del__(self: *Self) void { 38 | py.allocator.free(self.values); 39 | py.allocator.free(self.shape); 40 | } 41 | 42 | pub fn __buffer__(self: *const Self, view: *py.PyBuffer(root), flags: c_int) !void { 43 | // For more details on request types, see https://docs.python.org/3/c-api/buffer.html#buffer-request-types 44 | if (flags & py.PyBuffer(root).Flags.WRITABLE != 0) { 45 | return py.BufferError(root).raise("request for writable buffer is rejected"); 46 | } 47 | view.initFromSlice(i64, self.values, self.shape, self); 48 | } 49 | }); 50 | // --8<-- [end:protocol] 51 | 52 | // --8<-- [start:sum] 53 | pub fn sum(args: struct { buf: py.PyObject(root) }) !i64 { 54 | const view = try args.buf.getBuffer(py.PyBuffer(root).Flags.ND); 55 | defer view.release(); 56 | 57 | var bufferSum: i64 = 0; 58 | for (view.asSlice(i64)) |value| bufferSum += value; 59 | return bufferSum; 60 | } 61 | 62 | comptime { 63 | py.rootmodule(root); 64 | } 65 | // --8<-- [end:sum] 66 | -------------------------------------------------------------------------------- /example/classes.pyi: -------------------------------------------------------------------------------- 1 | class Animal: 2 | def species(self, /): ... 3 | 4 | class Callable: 5 | def __init__(self, /) -> None: ... 6 | def __call__(self, /, *args, **kwargs): 7 | """ 8 | Call self as a function. 9 | """ 10 | 11 | class ConstructableClass: 12 | def __init__(self, count, /) -> None: ... 13 | 14 | class Counter: 15 | def __init__(self, /) -> None: ... 16 | def increment(self, /): ... 17 | count: ... 18 | 19 | class GetAttr: 20 | def __init__(self, /) -> None: ... 21 | def __getattribute__(self, name, /): 22 | """ 23 | Return getattr(self, name). 24 | """ 25 | 26 | class Hash: 27 | def __init__(self, x, /) -> None: ... 28 | def __hash__(self, /) -> int: 29 | """ 30 | Return hash(self). 31 | """ 32 | 33 | class Math: 34 | def add(x, y, /): ... 35 | 36 | class SomeClass: 37 | """ 38 | Some class defined in Zig accessible from Python 39 | """ 40 | 41 | class User: 42 | def __init__(self, name, /) -> None: ... 43 | @property 44 | def email(self, /): ... 45 | @property 46 | def greeting(self, /): ... 47 | 48 | class ZigOnlyMethod: 49 | def __init__(self, x, /) -> None: ... 50 | def reexposed(self, /): ... 51 | 52 | class Dog(Animal): 53 | def __init__(self, breed, /) -> None: ... 54 | def breed(self, /): ... 55 | -------------------------------------------------------------------------------- /example/classes.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("pydust"); 15 | 16 | const root = @This(); 17 | // --8<-- [start:gil] 18 | 19 | // --8<-- [start:defining] 20 | pub const SomeClass = py.class(struct { 21 | pub const __doc__ = "Some class defined in Zig accessible from Python"; 22 | 23 | count: u32 = 0, 24 | }); 25 | // --8<-- [end:defining] 26 | 27 | // --8<-- [start:constructor] 28 | pub const ConstructableClass = py.class(struct { 29 | count: u32 = 0, 30 | 31 | pub fn __init__(self: *@This(), args: struct { count: u32 }) void { 32 | self.count = args.count; 33 | } 34 | }); 35 | // --8<-- [end:constructor] 36 | 37 | // --8<-- [start:subclass] 38 | pub const Animal = py.class(struct { 39 | const Self = @This(); 40 | 41 | species_: py.PyString(root), 42 | 43 | pub fn species(self: *Self) py.PyString(root) { 44 | return self.species_; 45 | } 46 | }); 47 | 48 | pub const Dog = py.class(struct { 49 | const Self = @This(); 50 | 51 | animal: Animal.definition, 52 | breed_: py.PyString(root), 53 | 54 | pub fn __init__(self: *Self, args: struct { breed: py.PyString(root) }) !void { 55 | args.breed.incref(); 56 | self.* = .{ 57 | .animal = .{ .species_ = try py.PyString(root).create("dog") }, 58 | .breed_ = args.breed, 59 | }; 60 | } 61 | 62 | pub fn breed(self: *Self) py.PyString(root) { 63 | return self.breed_; 64 | } 65 | }); 66 | 67 | // --8<-- [end:subclass] 68 | 69 | // --8<-- [start:properties] 70 | pub const User = py.class(struct { 71 | const Self = @This(); 72 | 73 | pub fn __init__(self: *Self, args: struct { name: py.PyString(root) }) void { 74 | args.name.incref(); 75 | self.* = .{ .name = args.name, .email = .{} }; 76 | } 77 | 78 | name: py.PyString(root), 79 | email: Email.definition, 80 | greeting: Greeting.definition = .{}, 81 | 82 | pub const Email = py.property(struct { 83 | const Prop = @This(); 84 | 85 | e: ?py.PyString(root) = null, 86 | 87 | pub fn get(prop: *const Prop) ?py.PyString(root) { 88 | if (prop.e) |e| e.incref(); 89 | return prop.e; 90 | } 91 | 92 | pub fn set(prop: *Prop, value: py.PyString(root)) !void { 93 | const self: *Self = @fieldParentPtr("email", prop); 94 | if (std.mem.indexOfScalar(u8, try value.asSlice(), '@') == null) { 95 | return py.ValueError(root).raiseFmt("Invalid email address for {s}", .{try self.name.asSlice()}); 96 | } 97 | value.incref(); 98 | prop.e = value; 99 | } 100 | }); 101 | 102 | pub const Greeting = py.property(struct { 103 | pub fn get(self: *const Self) !py.PyString(root) { 104 | return py.PyString(root).createFmt("Hello, {s}!", .{try self.name.asSlice()}); 105 | } 106 | }); 107 | 108 | pub fn __del__(self: *Self) void { 109 | self.name.decref(); 110 | if (self.email.e) |e| e.decref(); 111 | } 112 | }); 113 | // --8<-- [end:properties] 114 | 115 | // --8<-- [start:attributes] 116 | pub const Counter = py.class(struct { 117 | const Self = @This(); 118 | 119 | count: Count.definition = .{ .value = 0 }, 120 | pub const Count = py.attribute(usize); 121 | 122 | pub fn __init__(self: *Self) void { 123 | _ = self; 124 | } 125 | 126 | pub fn increment(self: *Self) void { 127 | self.count.value += 1; 128 | } 129 | }); 130 | // --8<-- [end:attributes] 131 | 132 | // --8<-- [start:staticmethods] 133 | pub const Math = py.class(struct { 134 | pub fn add(args: struct { x: i32, y: i32 }) i32 { 135 | return args.x + args.y; 136 | } 137 | }); 138 | // --8<-- [end:staticmethods] 139 | 140 | // --8<-- [start:zigonly] 141 | pub const ZigOnlyMethod = py.class(struct { 142 | const Self = @This(); 143 | number: i32, 144 | 145 | pub fn __init__(self: *Self, args: struct { x: i32 }) void { 146 | self.number = args.x; 147 | } 148 | 149 | // pub usingnamespace py.zig(struct { 150 | // pub fn get_number(self: *const Self) i32 { 151 | // return self.number; 152 | // } 153 | // }); 154 | 155 | pub fn reexposed(self: *const Self) i32 { 156 | // return self.get_number(); 157 | return self.number; 158 | } 159 | }); 160 | // --8<-- [end:zigonly] 161 | 162 | pub const Hash = py.class(struct { 163 | const Self = @This(); 164 | number: u32, 165 | 166 | pub fn __init__(self: *Self, args: struct { x: u32 }) void { 167 | self.number = args.x; 168 | } 169 | 170 | pub fn __hash__(self: *const Self) usize { 171 | var hasher = std.hash.Wyhash.init(0); 172 | std.hash.autoHashStrat(&hasher, self, .DeepRecursive); 173 | return hasher.final(); 174 | } 175 | }); 176 | 177 | pub const Callable = py.class(struct { 178 | const Self = @This(); 179 | 180 | pub fn __init__(self: *Self) void { 181 | _ = self; 182 | } 183 | 184 | pub fn __call__(self: *const Self, args: struct { i: u32 }) u32 { 185 | _ = self; 186 | return args.i; 187 | } 188 | }); 189 | 190 | pub const GetAttr = py.class(struct { 191 | const Self = @This(); 192 | 193 | pub fn __init__(self: *Self) void { 194 | _ = self; 195 | } 196 | 197 | pub fn __getattr__(self: *const Self, attr: py.PyString(root)) !py.PyObject(root) { 198 | const name = try attr.asSlice(); 199 | if (std.mem.eql(u8, name, "number")) { 200 | return py.create(root, 42); 201 | } 202 | return py.object(root, self).getAttribute(name); 203 | } 204 | }); 205 | 206 | comptime { 207 | py.rootmodule(root); 208 | } 209 | -------------------------------------------------------------------------------- /example/code.pyi: -------------------------------------------------------------------------------- 1 | def file_name(): ... 2 | def first_line_number(): ... 3 | def function_name(): ... 4 | def line_number(): ... 5 | -------------------------------------------------------------------------------- /example/code.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("pydust"); 15 | 16 | const root = @This(); 17 | 18 | pub fn line_number() u32 { 19 | return py.PyFrame(root).get().?.lineNumber(); 20 | } 21 | 22 | pub fn function_name() !py.PyString(root) { 23 | return py.PyFrame(root).get().?.code().name(); 24 | } 25 | 26 | pub fn file_name() !py.PyString(root) { 27 | return py.PyFrame(root).get().?.code().fileName(); 28 | } 29 | 30 | pub fn first_line_number() !u32 { 31 | return py.PyFrame(root).get().?.code().firstLineNumber(); 32 | } 33 | 34 | comptime { 35 | py.rootmodule(root); 36 | } 37 | -------------------------------------------------------------------------------- /example/exceptions.pyi: -------------------------------------------------------------------------------- 1 | def raise_custom_error(): ... 2 | def raise_value_error(message, /): ... 3 | -------------------------------------------------------------------------------- /example/exceptions.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("pydust"); 15 | 16 | const root = @This(); 17 | 18 | // --8<-- [start:valueerror] 19 | pub fn raise_value_error(args: struct { message: py.PyString(root) }) !void { 20 | return py.ValueError(root).raise(try args.message.asSlice()); 21 | } 22 | // --8<-- [end:valueerror] 23 | 24 | pub const CustomError = error{Oops}; 25 | 26 | pub fn raise_custom_error() !void { 27 | return CustomError.Oops; 28 | } 29 | 30 | comptime { 31 | py.rootmodule(root); 32 | } 33 | -------------------------------------------------------------------------------- /example/functions.pyi: -------------------------------------------------------------------------------- 1 | def double(x, /): ... 2 | def variadic(hello, /, *args, **kwargs): ... 3 | def with_kwargs(x, /, *, y=42.0): ... 4 | -------------------------------------------------------------------------------- /example/functions.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const py = @import("pydust"); 14 | 15 | const root = @This(); 16 | 17 | // --8<-- [start:function] 18 | pub fn double(args: struct { x: i64 }) i64 { 19 | return args.x * 2; 20 | } 21 | // --8<-- [end:function] 22 | 23 | // --8<-- [start:kwargs] 24 | pub fn with_kwargs(args: struct { x: f64, y: f64 = 42.0 }) f64 { 25 | return if (args.x < args.y) args.x * 2 else args.y; 26 | } 27 | // --8<-- [end:kwargs] 28 | 29 | // --8<-- [start:varargs] 30 | pub fn variadic(args: struct { hello: py.PyString(root), args: py.Args(root), kwargs: py.Kwargs(root) }) !py.PyString(root) { 31 | return py.PyString(root).createFmt( 32 | "Hello {s} with {} varargs and {} kwargs", 33 | .{ try args.hello.asSlice(), args.args.len, args.kwargs.count() }, 34 | ); 35 | } 36 | // --8<-- [end:varargs] 37 | 38 | comptime { 39 | py.rootmodule(root); 40 | } 41 | -------------------------------------------------------------------------------- /example/gil.pyi: -------------------------------------------------------------------------------- 1 | def sleep(millis, /): ... 2 | def sleep_release(millis, /): ... 3 | -------------------------------------------------------------------------------- /example/gil.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("pydust"); 15 | 16 | const root = @This(); 17 | 18 | // --8<-- [start:gil] 19 | pub fn sleep(args: struct { millis: u64 }) void { 20 | std.time.sleep(args.millis * 1_000_000); 21 | } 22 | 23 | pub fn sleep_release(args: struct { millis: u64 }) void { 24 | const nogil = py.nogil(); 25 | defer nogil.acquire(); 26 | std.time.sleep(args.millis * 1_000_000); 27 | } 28 | // --8<-- [end:gil] 29 | 30 | comptime { 31 | py.rootmodule(root); 32 | } 33 | -------------------------------------------------------------------------------- /example/hello.pyi: -------------------------------------------------------------------------------- 1 | def hello(): ... 2 | -------------------------------------------------------------------------------- /example/hello.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | // --8<-- [start:ex] 14 | const py = @import("pydust"); 15 | 16 | const root = @This(); 17 | 18 | pub fn hello() !py.PyString(root) { 19 | return try py.PyString(root).create("Hello!"); 20 | } 21 | 22 | comptime { 23 | py.rootmodule(root); 24 | } 25 | // --8<-- [end:ex] 26 | -------------------------------------------------------------------------------- /example/iterators.pyi: -------------------------------------------------------------------------------- 1 | class Range: 2 | """ 3 | An example of iterable class 4 | """ 5 | 6 | def __init__(self, lower, upper, step, /) -> None: ... 7 | def __iter__(self, /): 8 | """ 9 | Implement iter(self). 10 | """ 11 | 12 | class RangeIterator: 13 | """ 14 | Range iterator 15 | """ 16 | 17 | def __next__(self, /): 18 | """ 19 | Implement next(self). 20 | """ 21 | -------------------------------------------------------------------------------- /example/iterators.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("pydust"); 15 | 16 | const root = @This(); 17 | 18 | pub const Range = py.class(struct { 19 | pub const __doc__ = "An example of iterable class"; 20 | 21 | const Self = @This(); 22 | 23 | lower: i64, 24 | upper: i64, 25 | step: i64, 26 | 27 | pub fn __init__(self: *Self, args: struct { lower: i64, upper: i64, step: i64 }) void { 28 | self.* = .{ .lower = args.lower, .upper = args.upper, .step = args.step }; 29 | } 30 | 31 | pub fn __iter__(self: *const Self) !*RangeIterator.definition { 32 | return try py.init(root, RangeIterator.definition, .{ .next = self.lower, .stop = self.upper, .step = self.step }); 33 | } 34 | }); 35 | 36 | pub const RangeIterator = py.class(struct { 37 | pub const __doc__ = "Range iterator"; 38 | 39 | const Self = @This(); 40 | 41 | next: i64, 42 | stop: i64, 43 | step: i64, 44 | 45 | pub fn __next__(self: *Self) ?i64 { 46 | if (self.next >= self.stop) { 47 | return null; 48 | } 49 | defer self.next += self.step; 50 | return self.next; 51 | } 52 | }); 53 | 54 | comptime { 55 | py.rootmodule(root); 56 | } 57 | -------------------------------------------------------------------------------- /example/memory.pyi: -------------------------------------------------------------------------------- 1 | def append(left, /): ... 2 | def concat(left, /): ... 3 | -------------------------------------------------------------------------------- /example/memory.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const py = @import("pydust"); 14 | 15 | const root = @This(); 16 | 17 | // --8<-- [start:append] 18 | pub fn append(args: struct { left: py.PyString(root) }) !py.PyString(root) { 19 | // Since we create right, we must also decref it. 20 | const right = try py.PyString(root).create("right"); 21 | defer right.decref(); 22 | 23 | // Left is given to us as a borrowed reference from the caller. 24 | // Since append steals the left-hand-side, we must incref first. 25 | args.left.incref(); 26 | return args.left.append(right); 27 | } 28 | // --8<-- [end:append] 29 | 30 | // --8<-- [start:concat] 31 | pub fn concat(args: struct { left: py.PyString(root) }) !py.PyString(root) { 32 | return args.left.concatSlice("right"); 33 | } 34 | // --8<-- [end:concat] 35 | 36 | comptime { 37 | py.rootmodule(root); 38 | } 39 | -------------------------------------------------------------------------------- /example/modules.pyi: -------------------------------------------------------------------------------- 1 | """ 2 | Zig multi-line strings make it easy to define a docstring... 3 | 4 | ..with lots of lines! 5 | 6 | P.S. I'm sure one day we'll hook into Zig's AST and read the Zig doc comments ;) 7 | """ 8 | 9 | def count(): ... 10 | def hello(name, /): ... 11 | def increment(): ... 12 | def whoami(): ... 13 | 14 | class submod: 15 | def world(): ... 16 | -------------------------------------------------------------------------------- /example/modules.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | // --8<-- [start:ex] 14 | pub const __doc__ = 15 | \\Zig multi-line strings make it easy to define a docstring... 16 | \\ 17 | \\..with lots of lines! 18 | \\ 19 | \\P.S. I'm sure one day we'll hook into Zig's AST and read the Zig doc comments ;) 20 | ; 21 | 22 | const std = @import("std"); 23 | const py = @import("pydust"); 24 | 25 | const root = @This(); 26 | const Self = root; // (1)! 27 | 28 | count_: u32 = 0, // (2)! 29 | name: py.PyString(root), 30 | 31 | pub fn __init__(self: *Self) !void { // (3)! 32 | self.* = .{ .name = try py.PyString(root).create("Ziggy") }; 33 | } 34 | 35 | pub fn __del__(self: Self) void { 36 | self.name.decref(); 37 | } 38 | 39 | pub fn increment(self: *Self) void { // (4)! 40 | self.count_ += 1; 41 | } 42 | 43 | pub fn count(self: *const Self) u32 { 44 | return self.count_; 45 | } 46 | 47 | pub fn whoami(self: *const Self) py.PyString(root) { 48 | py.incref(root, self.name); 49 | return self.name; 50 | } 51 | 52 | pub fn hello( 53 | self: *const Self, 54 | args: struct { name: py.PyString(root) }, // (5)! 55 | ) !py.PyString(root) { 56 | return py.PyString(root).createFmt( 57 | "Hello, {s}. It's {s}", 58 | .{ try args.name.asSlice(), try self.name.asSlice() }, 59 | ); 60 | } 61 | 62 | pub const submod = py.module(struct { // (6)! 63 | pub fn world() !py.PyString(root) { 64 | return try py.PyString(root).create("Hello, World!"); 65 | } 66 | }); 67 | 68 | comptime { 69 | py.rootmodule(root); 70 | } // (7)! 71 | // --8<-- [end:ex] 72 | -------------------------------------------------------------------------------- /example/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spiraldb/ziggy-pydust/2b0e3cd7724af8b3c5585c2692acf06ec6440be3/example/py.typed -------------------------------------------------------------------------------- /example/pytest.pyi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spiraldb/ziggy-pydust/2b0e3cd7724af8b3c5585c2692acf06ec6440be3/example/pytest.pyi -------------------------------------------------------------------------------- /example/pytest.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("pydust"); 15 | 16 | const root = @This(); 17 | 18 | // --8<-- [start:example] 19 | test "pydust pytest" { 20 | py.initialize(); 21 | defer py.finalize(); 22 | 23 | const str = try py.PyString(root).create("hello"); 24 | defer str.decref(); 25 | 26 | try std.testing.expectEqualStrings("hello", try str.asSlice()); 27 | } 28 | // --8<-- [end:example] 29 | 30 | test "pydust-expected-failure" { 31 | py.initialize(); 32 | defer py.finalize(); 33 | 34 | const str = try py.PyString(root).create("hello"); 35 | defer str.decref(); 36 | 37 | try std.testing.expectEqualStrings("world", try str.asSlice()); 38 | } 39 | 40 | comptime { 41 | py.rootmodule(root); 42 | } 43 | -------------------------------------------------------------------------------- /example/result_types.pyi: -------------------------------------------------------------------------------- 1 | def pyobject(): ... 2 | def pystring(): ... 3 | def zigbool(): ... 4 | def zigf16(): ... 5 | def zigf32(): ... 6 | def zigf64(): ... 7 | def zigi32(): ... 8 | def zigi64(): ... 9 | def zigstruct(): ... 10 | def zigtuple(): ... 11 | def zigu32(): ... 12 | def zigu64(): ... 13 | def zigvoid(): ... 14 | -------------------------------------------------------------------------------- /example/result_types.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("pydust"); 15 | 16 | const root = @This(); 17 | 18 | pub fn pyobject() !py.PyObject(root) { 19 | return (try py.PyString(root).create("hello")).obj; 20 | } 21 | 22 | pub fn pystring() !py.PyString(root) { 23 | return py.PyString(root).create("hello world"); 24 | } 25 | 26 | pub fn zigvoid() void {} 27 | 28 | pub fn zigbool() bool { 29 | return true; 30 | } 31 | 32 | pub fn zigu32() u32 { 33 | return 32; 34 | } 35 | 36 | pub fn zigu64() u64 { 37 | return 8589934592; 38 | } 39 | 40 | // TODO: support numbers bigger than long 41 | // pub fn zigu128() u128 { 42 | // return 9223372036854775809; 43 | // } 44 | 45 | pub fn zigi32() i32 { 46 | return -32; 47 | } 48 | 49 | pub fn zigi64() i64 { 50 | return -8589934592; 51 | } 52 | 53 | // TODO: support numbers bigger than long 54 | // pub fn zigi128() i128 { 55 | // return -9223372036854775809; 56 | // } 57 | 58 | pub fn zigf16() f16 { 59 | return 32720.0; 60 | } 61 | 62 | pub fn zigf32() f32 { 63 | return 2.71 * std.math.pow(f32, 10, 38); 64 | } 65 | 66 | pub fn zigf64() f64 { 67 | return 2.71 * std.math.pow(f64, 10, 39); 68 | } 69 | 70 | const TupleResult = struct { py.PyObject(root), u64 }; 71 | 72 | pub fn zigtuple() !TupleResult { 73 | return .{ py.object(root, try py.PyString(root).create("hello")), 128 }; 74 | } 75 | 76 | const StructResult = struct { foo: u64, bar: bool }; 77 | 78 | pub fn zigstruct() StructResult { 79 | return .{ .foo = 1234, .bar = true }; 80 | } 81 | 82 | comptime { 83 | py.rootmodule(root); 84 | } 85 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Ziggy Pydust 2 | 3 | repo_name: fulcrum-so/ziggy-pydust 4 | repo_url: https://github.com/fulcrum-so/ziggy-pydust 5 | 6 | theme: 7 | name: material 8 | palette: 9 | primary: custom 10 | accent: custom 11 | features: 12 | - navigation.indexes 13 | #- navigation.tabs 14 | - navigation.expand 15 | - content.code.annotate 16 | 17 | nav: 18 | - "index.md" 19 | - 'Getting Started': "getting_started.md" 20 | - 'User Guide': 21 | - "guide/index.md" 22 | - 'Modules': "guide/modules.md" 23 | - 'Functions': "guide/functions.md" 24 | - 'Classes': "guide/classes.md" 25 | - 'Exceptions': "guide/exceptions.md" 26 | - 'GIL': "guide/gil.md" 27 | - 'Testing': "guide/_4_testing.md" 28 | - 'Memory Management': "guide/_5_memory.md" 29 | - 'Buffer Protocol': "guide/_6_buffers.md" 30 | 31 | extra: 32 | social: 33 | - icon: fontawesome/brands/github 34 | link: https://github.com/fulcrum-so/ziggy-pydust 35 | - icon: fontawesome/brands/twitter 36 | link: https://twitter.com/fulcrum_so 37 | - icon: fontawesome/brands/python 38 | link: https://pypi.org/project/ziggy-pydust/ 39 | version: 40 | provider: mike 41 | 42 | extra_css: 43 | - stylesheets/extra.css 44 | 45 | plugins: 46 | - include-markdown 47 | - mike 48 | 49 | markdown_extensions: 50 | - admonition 51 | - pymdownx.details 52 | - pymdownx.highlight: 53 | linenums: true 54 | anchor_linenums: true 55 | line_spans: __span 56 | use_pygments: true 57 | - pymdownx.inlinehilite 58 | - pymdownx.snippets: 59 | dedent_subsections: true 60 | - pymdownx.superfences 61 | - pymdownx.tabbed: 62 | alternate_style: true 63 | - tables 64 | -------------------------------------------------------------------------------- /pyconf.dummy.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | /// This pyconf module is used during our own tests to represent the typically auto-generated pyconf module. 14 | pub const limited_api = true; 15 | pub const hexversion = "0x030D0000"; // 3.13 16 | -------------------------------------------------------------------------------- /pydust/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | -------------------------------------------------------------------------------- /pydust/__main__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import argparse 16 | import sys 17 | from pathlib import Path 18 | 19 | from pydust import buildzig, config 20 | 21 | parser = argparse.ArgumentParser() 22 | sub = parser.add_subparsers(dest="command", required=True) 23 | 24 | debug_sp = sub.add_parser( 25 | "debug", 26 | help="Compile a Zig file with debug symbols. Useful for running from an IDE.", 27 | ) 28 | debug_sp.add_argument("entrypoint") 29 | 30 | build_sp = sub.add_parser( 31 | "build", 32 | help="Build a zig-based python extension.", 33 | formatter_class=argparse.ArgumentDefaultsHelpFormatter, 34 | ) 35 | build_sp.add_argument("-z", "--zig-exe", help="zig executable path") 36 | build_sp.add_argument("-b", "--build-zig", default="build.zig", help="build.zig file") 37 | build_sp.add_argument("-m", "--self-managed", default=False, action="store_true", help="self-managed mode") 38 | build_sp.add_argument( 39 | "-a", 40 | "--limited-api", 41 | default=True, 42 | action="store_true", 43 | help="use limited python c-api", 44 | ) 45 | build_sp.add_argument("-p", "--prefix", default="", help="prefix of built extension") 46 | build_sp.add_argument( 47 | "extensions", 48 | nargs="+", 49 | help="space separated list of extension '' or '=' entries", 50 | ) 51 | 52 | 53 | def main(): 54 | args = parser.parse_args() 55 | 56 | if args.command == "debug": 57 | debug(args) 58 | 59 | elif args.command == "build": 60 | build(args) 61 | 62 | 63 | def _parse_exts(exts: list[str], limited_api: bool = True, prefix: str = "") -> list[config.ExtModule]: 64 | """parses extensions entries, accepts '=' or """ 65 | _exts = [] 66 | 67 | def _add_ext(name, path: Path): 68 | _exts.append(config.ExtModule(name=name, root=str(path), limited_api=limited_api, prefix=prefix)) 69 | 70 | def _check_path(path: Path): 71 | assert path.exists(), f"path does not exist: {path}" 72 | assert path.suffix == ".zig" and path.is_file(), f"path must be a zig file: {path}" 73 | 74 | for elem in exts: 75 | if "=" in elem: 76 | name, path = elem.split("=") 77 | path = Path(path) 78 | _check_path(path) 79 | _add_ext(name, path) 80 | else: # assume elem is a 81 | path = Path(elem) 82 | _check_path(path) 83 | if len(path.parts) > 1: # >1 part 84 | parts = (path.parent / (prefix + path.stem)).parts 85 | name = ".".join(parts) 86 | _add_ext(name, path) 87 | else: # 1 part 88 | name = prefix + path.stem 89 | _add_ext(name, path) 90 | return _exts 91 | 92 | 93 | def build(args): 94 | """Given a list of '=' or '' entries, builds zig-based python extensions""" 95 | _extensions = _parse_exts(exts=args.extensions, limited_api=args.limited_api, prefix=args.prefix) 96 | buildzig.zig_build( 97 | argv=["install", f"-Dpython-exe={sys.executable}", "-Doptimize=ReleaseSafe"], 98 | conf=config.ToolPydust( 99 | zig_exe=args.zig_exe, 100 | build_zig=args.build_zig, 101 | self_managed=args.self_managed, 102 | ext_module=_extensions, 103 | ), 104 | ) 105 | 106 | 107 | def debug(args): 108 | """Given an entrypoint file, compile it for test debugging. Placing it in a well-known location.""" 109 | entrypoint = args.entrypoint 110 | buildzig.zig_build(["install", f"-Ddebug-root={entrypoint}"]) 111 | 112 | 113 | if __name__ == "__main__": 114 | main() 115 | -------------------------------------------------------------------------------- /pydust/build.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import sys 16 | 17 | from pydust import buildzig 18 | 19 | 20 | def build(): 21 | """The main entry point from Poetry's build script.""" 22 | buildzig.zig_build(["install", f"-Dpython-exe={sys.executable}", "-Doptimize=ReleaseSafe"]) 23 | -------------------------------------------------------------------------------- /pydust/buildzig.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import contextlib 16 | import os 17 | import shutil 18 | import subprocess 19 | import sys 20 | import sysconfig 21 | import textwrap 22 | from pathlib import Path 23 | from typing import TextIO 24 | 25 | import pydust 26 | from pydust import config 27 | 28 | PYVER_MINOR = ".".join(str(v) for v in sys.version_info[:2]) 29 | PYVER_HEX = f"{sys.hexversion:#010x}" 30 | PYLDLIB = sysconfig.get_config_var("LDLIBRARY") 31 | 32 | # Strip libpython3.11.a.so => python3.11.a 33 | PYLDLIB = PYLDLIB[3:] if PYLDLIB.startswith("lib") else PYLDLIB 34 | PYLDLIB = os.path.splitext(PYLDLIB)[0] 35 | 36 | 37 | def zig_build(argv: list[str], conf: config.ToolPydust | None = None): 38 | conf = conf or config.load() 39 | 40 | # Always generate the supporting pydist.build.zig 41 | shutil.copy( 42 | Path(pydust.__file__).parent.joinpath("src/pydust.build.zig"), 43 | conf.pydust_build_zig, 44 | ) 45 | 46 | if not conf.self_managed: 47 | # Generate the build.zig if we're managing the ext_modules ourselves 48 | with conf.build_zig.open(mode="w") as f: 49 | generate_build_zig(f, conf) 50 | 51 | zig_exe = [os.path.expanduser(conf.zig_exe)] if conf.zig_exe else [sys.executable, "-m", "ziglang"] 52 | 53 | cmds = zig_exe + ["build", "--build-file", conf.build_zig] + argv 54 | 55 | subprocess.run(cmds, check=True) 56 | 57 | 58 | def generate_build_zig(fileobj: TextIO, conf=None): 59 | """Generate the build.zig file for the current pyproject.toml. 60 | 61 | Initially we were calling `zig build-lib` directly, and this worked fine except it meant we 62 | would need to roll our own caching and figure out how to convince ZLS to pick up our dependencies. 63 | 64 | It's therefore easier, at least for now, to generate a build.zig in the project root and add it 65 | to the .gitignore. This means ZLS works as expected, we can leverage zig build caching, and the user 66 | can inspect the generated file to assist with debugging. 67 | """ 68 | conf = conf or config.load() 69 | 70 | b = Writer(fileobj) 71 | 72 | b.writeln('const std = @import("std");') 73 | b.writeln('const py = @import("./pydust.build.zig");') 74 | b.writeln() 75 | 76 | with b.block("pub fn build(b: *std.Build) void"): 77 | b.write( 78 | """ 79 | const target = b.standardTargetOptionsQueryOnly(.{}); 80 | const optimize = b.standardOptimizeOption(.{}); 81 | 82 | const test_step = b.step("test", "Run library tests"); 83 | 84 | const pydust = py.addPydust(b, .{ 85 | .test_step = test_step, 86 | }); 87 | """ 88 | ) 89 | 90 | for ext_module in conf.ext_modules: 91 | # TODO(ngates): fix the out filename for non-limited modules 92 | assert ext_module.limited_api, "Only limited_api is supported for now" 93 | 94 | b.write( 95 | f""" 96 | _ = pydust.addPythonModule(.{{ 97 | .name = "{ext_module.name}", 98 | .root_source_file = b.path("{ext_module.root}"), 99 | .limited_api = {str(ext_module.limited_api).lower()}, 100 | .target = target, 101 | .optimize = optimize, 102 | }}); 103 | """ 104 | ) 105 | 106 | 107 | class Writer: 108 | def __init__(self, fileobj: TextIO) -> None: 109 | self.f = fileobj 110 | self._indent = 0 111 | 112 | @contextlib.contextmanager 113 | def indent(self): 114 | self._indent += 4 115 | yield 116 | self._indent -= 4 117 | 118 | @contextlib.contextmanager 119 | def block(self, text: str = ""): 120 | self.write(text) 121 | self.writeln(" {") 122 | with self.indent(): 123 | yield 124 | self.writeln() 125 | self.writeln("}") 126 | self.writeln() 127 | 128 | def write(self, text: str): 129 | if "\n" in text: 130 | text = textwrap.dedent(text).strip() + "\n\n" 131 | self.f.write(textwrap.indent(text, self._indent * " ")) 132 | 133 | def writeln(self, text: str = ""): 134 | self.write(text) 135 | self.f.write("\n") 136 | -------------------------------------------------------------------------------- /pydust/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import functools 16 | import importlib.metadata 17 | from pathlib import Path 18 | 19 | import tomllib 20 | from pydantic import BaseModel, Field, model_validator 21 | 22 | 23 | class ExtModule(BaseModel): 24 | """Config for a single Zig extension module.""" 25 | 26 | name: str 27 | root: Path 28 | limited_api: bool = True 29 | 30 | @property 31 | def libname(self) -> str: 32 | return self.name.rsplit(".", maxsplit=1)[-1] 33 | 34 | @property 35 | def install_path(self) -> Path: 36 | # FIXME(ngates): for non-limited API 37 | assert self.limited_api, "Only limited API modules are supported right now" 38 | return Path(*self.name.split(".")).with_suffix(".abi3.so") 39 | 40 | @property 41 | def test_bin(self) -> Path: 42 | return (Path("zig-out") / "bin" / self.libname).with_suffix(".test.bin") 43 | 44 | 45 | class ToolPydust(BaseModel): 46 | """Model for tool.pydust section of a pyproject.toml.""" 47 | 48 | zig_exe: Path | None = None 49 | build_zig: Path = Path("build.zig") 50 | 51 | # Whether to include Zig tests as part of the pytest collection. 52 | zig_tests: bool = True 53 | 54 | # When true, python module definitions are configured by the user in their own build.zig file. 55 | # When false, ext_modules is used to auto-generated a build.zig file. 56 | self_managed: bool = False 57 | 58 | # We rename pluralized config sections so the pyproject.toml reads better. 59 | ext_modules: list[ExtModule] = Field(alias="ext_module", default_factory=list) 60 | 61 | @property 62 | def pydust_build_zig(self) -> Path: 63 | return self.build_zig.parent / "pydust.build.zig" 64 | 65 | @model_validator(mode="after") 66 | def validate_atts(self): 67 | if self.self_managed and self.ext_modules: 68 | raise ValueError("ext_modules cannot be defined when using Pydust in self-managed mode.") 69 | return self 70 | 71 | 72 | @functools.cache 73 | def load() -> ToolPydust: 74 | with open("pyproject.toml", "rb") as f: 75 | pyproject = tomllib.load(f) 76 | 77 | # Since Poetry doesn't support locking the build-system.requires dependencies, 78 | # we perform a check here to prevent the versions from diverging. 79 | pydust_version = importlib.metadata.version("ziggy-pydust") 80 | 81 | # Skip 0.1.0 as it's the development version when installed locally. 82 | if pydust_version != "0.1.0": 83 | for req in pyproject["build-system"]["requires"]: 84 | if not req.startswith("ziggy-pydust"): 85 | continue 86 | expected = f"ziggy-pydust=={pydust_version}" 87 | if req != expected: 88 | raise ValueError( 89 | "Detected misconfigured ziggy-pydust. " 90 | f'You must include "{expected}" in build-system.requires in pyproject.toml' 91 | ) 92 | 93 | return ToolPydust(**pyproject["tool"].get("pydust", {})) 94 | -------------------------------------------------------------------------------- /pydust/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spiraldb/ziggy-pydust/2b0e3cd7724af8b3c5585c2692acf06ec6440be3/pydust/py.typed -------------------------------------------------------------------------------- /pydust/src/attributes.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | const py = @import("pydust.zig"); 13 | const Type = @import("pytypes.zig").Type; 14 | const State = @import("discovery.zig").State; 15 | 16 | pub fn Attribute(comptime root: type) type { 17 | return struct { 18 | name: [:0]const u8, 19 | ctor: fn (module: py.PyModule(root)) py.PyError!py.PyObject(root), 20 | }; 21 | } 22 | 23 | /// Finds the attributes on a module or class definition. 24 | pub fn Attributes(comptime root: type, comptime definition: type) type { 25 | return struct { 26 | const attr_count = blk: { 27 | var cnt = 0; 28 | for (@typeInfo(definition).@"struct".decls) |decl| { 29 | const value = @field(definition, decl.name); 30 | 31 | if (State.findDefinition(root, value)) |def| { 32 | if (def.type == .class) { 33 | cnt += 1; 34 | } 35 | } 36 | } 37 | break :blk cnt; 38 | }; 39 | 40 | pub const attributes: [attr_count]Attribute(root) = blk: { 41 | var attrs: [attr_count]Attribute(root) = undefined; 42 | var idx = 0; 43 | for (@typeInfo(definition).@"struct".decls) |decl| { 44 | const value = @field(definition, decl.name); 45 | 46 | if (State.findDefinition(root, value)) |def| { 47 | if (def.type == .class) { 48 | const Closure = struct { 49 | pub fn init(module: py.PyModule(root)) !py.PyObject(root) { 50 | const typedef = Type(root, decl.name ++ "", def.definition); 51 | return try typedef.init(module); 52 | } 53 | }; 54 | attrs[idx] = .{ .name = decl.name ++ "", .ctor = Closure.init }; 55 | idx += 1; 56 | } 57 | } 58 | } 59 | break :blk attrs; 60 | }; 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /pydust/src/conversions.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const py = @import("./pydust.zig"); 14 | const tramp = @import("./trampoline.zig"); 15 | const pytypes = @import("./pytypes.zig"); 16 | const State = @import("./discovery.zig").State; 17 | 18 | /// Zig PyObject-like -> ffi.PyObject. Convert a Zig PyObject-like value into a py.PyObject. 19 | /// e.g. py.PyObject, py.PyTuple, ffi.PyObject, etc. 20 | pub inline fn object(comptime root: type, value: anytype) py.PyObject(root) { 21 | return tramp.Trampoline(root, @TypeOf(value)).asObject(value); 22 | } 23 | 24 | /// Zig -> Python. Return a Python representation of a Zig object. 25 | /// For Zig primitives, this constructs a new Python object. 26 | /// For PyObject-like values, this returns the value without creating a new reference. 27 | pub inline fn createOwned(comptime root: type, value: anytype) py.PyError!py.PyObject(root) { 28 | const trampoline = tramp.Trampoline(root, @TypeOf(value)); 29 | defer trampoline.decref_objectlike(value); 30 | return trampoline.wrap(value); 31 | } 32 | 33 | /// Zig -> Python. Convert a Zig object into a Python object. Returns a new object. 34 | pub inline fn create(comptime root: type, value: anytype) py.PyError!py.PyObject(root) { 35 | return tramp.Trampoline(root, @TypeOf(value)).wrap(value); 36 | } 37 | 38 | /// Python -> Zig. Return a Zig object representing the Python object. 39 | pub inline fn as(comptime root: type, comptime T: type, obj: anytype) py.PyError!T { 40 | return tramp.Trampoline(root, T).unwrap(object(root, obj)); 41 | } 42 | 43 | /// Python -> Pydust. Perform a checked cast from a PyObject to a given PyDust class type. 44 | pub inline fn checked(comptime root: type, comptime T: type, obj: py.PyObject) py.PyError!T { 45 | const definition = State.getDefinition(root, @typeInfo(T).pointer.child); 46 | if (definition.type != .class) { 47 | @compileError("Can only perform checked cast into a PyDust class type"); 48 | } 49 | 50 | // TODO(ngates): to perform fast type checking, we need to store our PyType on the parent module. 51 | // See how the Python JSON module did this: https://github.com/python/cpython/commit/33f15a16d40cb8010a8c758952cbf88d7912ee2d#diff-efe183ae0b85e5b8d9bbbc588452dd4de80b39fd5c5174ee499ba554217a39edR1814 52 | // For now, we perform a slow import/isinstance check by using the `as` conversion. 53 | return as(T, obj); 54 | } 55 | 56 | /// Python -> Pydust. Perform an unchecked cast from a PyObject to a given PyDust class type. 57 | pub inline fn unchecked(comptime root: type, comptime T: type, obj: py.PyObject(root)) T { 58 | const Definition = @typeInfo(T).pointer.child; 59 | const definition = State.getDefinition(root, Definition); 60 | if (definition.type != .class) { 61 | @compileError("Can only perform unchecked cast into a PyDust class type. Found " ++ @typeName(Definition)); 62 | } 63 | const instance: *pytypes.PyTypeStruct(Definition) = @ptrCast(@alignCast(obj.py)); 64 | return &instance.state; 65 | } 66 | 67 | const testing = @import("std").testing; 68 | const expect = testing.expect; 69 | 70 | test "as py -> zig" { 71 | py.initialize(); 72 | defer py.finalize(); 73 | 74 | const root = @This(); 75 | 76 | // Start with a Python object 77 | const str = try py.PyString(root).create("hello"); 78 | try expect(py.refcnt(root, str) == 1); 79 | 80 | // Return a slice representation of it, and ensure the refcnt is untouched 81 | _ = try py.as(root, []const u8, str); 82 | try expect(py.refcnt(root, str) == 1); 83 | 84 | // Return a PyObject representation of it, and ensure the refcnt is untouched. 85 | _ = try py.as(root, py.PyObject(root), str); 86 | try expect(py.refcnt(root, str) == 1); 87 | } 88 | 89 | test "create" { 90 | py.initialize(); 91 | defer py.finalize(); 92 | 93 | const root = @This(); 94 | 95 | const str = try py.PyString(root).create("Hello"); 96 | try testing.expectEqual(@as(isize, 1), py.refcnt(root, str)); 97 | 98 | const some_tuple = try py.create(root, .{str}); 99 | defer some_tuple.decref(); 100 | try testing.expectEqual(@as(isize, 2), py.refcnt(root, str)); 101 | 102 | str.decref(); 103 | try testing.expectEqual(@as(isize, 1), py.refcnt(root, str)); 104 | } 105 | 106 | test "createOwned" { 107 | py.initialize(); 108 | defer py.finalize(); 109 | 110 | const root = @This(); 111 | 112 | const str = try py.PyString(root).create("Hello"); 113 | try testing.expectEqual(@as(isize, 1), py.refcnt(root, str)); 114 | 115 | const some_tuple = try py.createOwned(root, .{str}); 116 | defer some_tuple.decref(); 117 | try testing.expectEqual(@as(isize, 1), py.refcnt(root, str)); 118 | } 119 | -------------------------------------------------------------------------------- /pydust/src/discovery.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | const std = @import("std"); 13 | 14 | /// Captures the type of the Pydust object. 15 | pub const Definition = struct { 16 | definition: type, 17 | type: DefinitionType, 18 | }; 19 | 20 | const DefinitionType = enum { module, class, attribute, property }; 21 | 22 | /// Captures the name of and relationships between Pydust objects. 23 | const Identifier = struct { 24 | qualifiedName: []const [:0]const u8, 25 | definition: Definition, 26 | parent: type, 27 | 28 | pub fn name(self: Identifier) [:0]const u8 { 29 | const qualifiedName = self.qualifiedName; 30 | return qualifiedName[qualifiedName.len - 1]; 31 | } 32 | }; 33 | 34 | fn countDefinitions(comptime definition: type) usize { 35 | @setEvalBranchQuota(10000); 36 | comptime var count = 0; 37 | switch (@typeInfo(definition)) { 38 | .@"struct" => |info| { 39 | inline for (info.fields) |f| { 40 | count += countDefinitions(f.type); 41 | } 42 | inline for (info.decls) |d| { 43 | const field = @field(definition, d.name); 44 | count += switch (@TypeOf(field)) { 45 | Definition => 1 + countDefinitions(field.definition), 46 | type => countDefinitions(field), 47 | else => 0, 48 | }; 49 | } 50 | }, 51 | else => {}, 52 | } 53 | return count; 54 | } 55 | 56 | fn getIdentifiers( 57 | comptime definition: type, 58 | comptime qualifiedName: []const [:0]const u8, 59 | comptime parent: type, 60 | ) [countDefinitions(definition)]Identifier { 61 | comptime var identifiers: [countDefinitions(definition)]Identifier = undefined; 62 | comptime var count = 0; 63 | switch (@typeInfo(definition)) { 64 | .@"struct" => |info| { 65 | // Iterate over the fields of the struct 66 | for (info.fields) |f| { 67 | for (getIdentifiers(f.type, qualifiedName ++ .{f.name}, definition)) |identifier| { 68 | // Append the sub-definition to the list. 69 | identifiers[count] = identifier; 70 | count += 1; 71 | } 72 | } 73 | for (info.decls) |d| { 74 | const field = @field(definition, d.name); 75 | const name = qualifiedName ++ .{d.name}; 76 | // Handle the field based on its type 77 | for (switch (@TypeOf(field)) { 78 | Definition => [_]Identifier{.{ 79 | .qualifiedName = name, 80 | .definition = field, 81 | .parent = parent, 82 | }} ++ getIdentifiers(field.definition, name, definition), 83 | type => getIdentifiers(field, name, definition), 84 | else => .{}, 85 | }) |identifier| { 86 | // Append the sub-definition to the list. 87 | identifiers[count] = identifier; 88 | count += 1; 89 | } 90 | } 91 | }, 92 | else => {}, 93 | } 94 | return identifiers; 95 | } 96 | 97 | fn getAllIdentifiers(comptime definition: type) [countDefinitions(definition) + 1]Identifier { 98 | const qualifiedName = &.{@import("pyconf").module_name}; 99 | return [_]Identifier{.{ 100 | .qualifiedName = qualifiedName, 101 | .definition = .{ .definition = definition, .type = .module }, 102 | .parent = definition, 103 | }} ++ getIdentifiers(definition, qualifiedName, definition); 104 | } 105 | 106 | pub const State = struct { 107 | pub fn countFieldsWithType( 108 | comptime root: type, 109 | comptime definition: type, 110 | deftype: DefinitionType, 111 | ) usize { 112 | var cnt = 0; 113 | for (std.meta.fields(definition)) |field| { 114 | if (hasType(root, field.type, deftype)) { 115 | cnt += 1; 116 | } 117 | } 118 | return cnt; 119 | } 120 | 121 | pub fn hasType( 122 | comptime root: type, 123 | comptime definition: type, 124 | deftype: DefinitionType, 125 | ) bool { 126 | if (findDefinition(root, definition)) |def| { 127 | return def.type == deftype; 128 | } 129 | return false; 130 | } 131 | 132 | pub fn getDefinition( 133 | comptime root: type, 134 | comptime definition: type, 135 | ) Definition { 136 | return findDefinition(root, definition) orelse @compileError("Unable to find definition " ++ @typeName(definition)); 137 | } 138 | 139 | pub inline fn findDefinition( 140 | comptime root: type, 141 | comptime definition: anytype, 142 | ) ?Definition { 143 | return switch (@TypeOf(definition)) { 144 | Definition => definition, 145 | type => switch (@typeInfo(definition)) { 146 | .@"struct" => blk: { 147 | for (getAllIdentifiers(root)) |id| { 148 | if (id.definition.definition == definition) 149 | break :blk id.definition; 150 | } 151 | break :blk null; 152 | }, 153 | else => null, 154 | }, 155 | else => null, 156 | }; 157 | } 158 | 159 | pub fn getIdentifier( 160 | comptime root: type, 161 | comptime definition: type, 162 | ) Identifier { 163 | return findIdentifier(root, definition) orelse @compileError("Definition not yet identified " ++ @typeName(definition)); 164 | } 165 | 166 | inline fn findIdentifier( 167 | comptime root: type, 168 | comptime definition: type, 169 | ) ?Identifier { 170 | if (@typeInfo(definition) != .@"struct") { 171 | return null; 172 | } 173 | for (getAllIdentifiers(root)) |id| { 174 | if (id.definition.definition == definition) { 175 | return id; 176 | } 177 | } 178 | return null; 179 | } 180 | 181 | pub fn getContaining( 182 | comptime root: type, 183 | comptime definition: type, 184 | comptime deftype: DefinitionType, 185 | ) type { 186 | return findContaining(root, definition, deftype) orelse @compileError("Cannot find containing object"); 187 | } 188 | 189 | /// Find the nearest containing definition with the given deftype. 190 | fn findContaining( 191 | comptime root: type, 192 | comptime definition: type, 193 | comptime deftype: DefinitionType, 194 | ) ?type { 195 | const identifiers = getAllIdentifiers(root); 196 | var idx = identifiers.len; 197 | var foundOriginal = false; 198 | while (idx > 0) : (idx -= 1) { 199 | const def = identifiers[idx - 1].definition; 200 | 201 | if (def.definition == definition) { 202 | // Only once we found the original definition, should we check for deftype. 203 | foundOriginal = true; 204 | continue; 205 | } 206 | 207 | if (foundOriginal and def.type == deftype) { 208 | return def.definition; 209 | } 210 | } 211 | return null; 212 | } 213 | }; 214 | -------------------------------------------------------------------------------- /pydust/src/errors.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const Allocator = @import("std").mem.Allocator; 14 | 15 | pub const PyError = error{ 16 | // PyError.PyRaised should be returned when an exception has been set but not caught in 17 | // the Python interpreter. This tells Pydust to return PyNULL and allow Python to raise 18 | // the exception to the end user. 19 | PyRaised, 20 | } || Allocator.Error; 21 | -------------------------------------------------------------------------------- /pydust/src/ffi.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | // Export the Limited Python C API for use within PyDust. 14 | const pyconf = @import("pyconf"); 15 | 16 | pub usingnamespace @cImport({ 17 | if (pyconf.limited_api) { 18 | @cDefine("Py_LIMITED_API", pyconf.hexversion); 19 | } 20 | @cDefine("PY_SSIZE_T_CLEAN", {}); 21 | @cInclude("Python.h"); 22 | 23 | // From 3.12 onwards, structmember.h is fixed to be including in Python.h 24 | // See https://github.com/python/cpython/pull/99014 25 | @cInclude("structmember.h"); 26 | }); 27 | -------------------------------------------------------------------------------- /pydust/src/mem.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const mem = std.mem; 15 | const Allocator = std.mem.Allocator; 16 | const ffi = @import("ffi.zig"); 17 | const py = @import("./pydust.zig"); 18 | 19 | pub const PyMemAllocator = struct { 20 | const Self = @This(); 21 | 22 | pub fn allocator(self: *const Self) Allocator { 23 | return .{ 24 | .ptr = @constCast(self), 25 | .vtable = &.{ 26 | .alloc = alloc, 27 | .remap = remap, 28 | .resize = resize, 29 | .free = free, 30 | }, 31 | }; 32 | } 33 | 34 | fn alloc(ctx: *anyopaque, len: usize, ptr_align: mem.Alignment, ret_addr: usize) ?[*]u8 { 35 | // As per this issue, we will hack an aligned allocator. 36 | // https://bugs.python.org/msg232221 37 | _ = ret_addr; 38 | _ = ctx; 39 | 40 | // FIXME(ngates): we should have a separate allocator for re-entrant cases like this 41 | // that require the GIL, without always paying the cost of acquiring it. 42 | const gil = py.gil(); 43 | defer gil.release(); 44 | 45 | // Zig gives us ptr_align as power of 2 46 | // This may not always fit into a byte, we should figure out a better way to store the shift value. 47 | const alignment: u8 = @intCast(ptr_align.toByteUnits()); 48 | 49 | // By default, ptr_align == 1 which gives us our 1 byte header to store the alignment shift 50 | const raw_ptr: usize = @intFromPtr(ffi.PyMem_Malloc(len + alignment) orelse return null); 51 | 52 | const shift: u8 = @intCast(alignment - (raw_ptr % alignment)); 53 | std.debug.assert(0 < shift and shift <= alignment); 54 | 55 | const aligned_ptr: usize = raw_ptr + shift; 56 | 57 | // Store the shift in the first byte before the aligned ptr 58 | // We know from above that we are guaranteed to own that byte. 59 | @as(*u8, @ptrFromInt(aligned_ptr - 1)).* = shift; 60 | 61 | return @ptrFromInt(aligned_ptr); 62 | } 63 | 64 | fn remap(ctx: *anyopaque, memory: []u8, ptr_align: mem.Alignment, new_len: usize, ret_addr: usize) ?[*]u8 { 65 | // As per this issue, we will hack an aligned allocator. 66 | // https://bugs.python.org/msg232221 67 | _ = ret_addr; 68 | _ = ctx; 69 | 70 | // FIXME(ngates): we should have a separate allocator for re-entrant cases like this 71 | // that require the GIL, without always paying the cost of acquiring it. 72 | const gil = py.gil(); 73 | defer gil.release(); 74 | 75 | // get shift 76 | const alignment: u8 = @intCast(ptr_align.toByteUnits()); 77 | const old_shift = @as(*u8, @ptrFromInt(@intFromPtr(memory.ptr) - 1)).*; 78 | const origin_mem_ptr: *anyopaque = @ptrFromInt(@intFromPtr(memory.ptr) - old_shift); 79 | 80 | // By default, ptr_align == 1 which gives us our 1 byte header to store the alignment shift 81 | const raw_ptr: usize = @intFromPtr(ffi.PyMem_Realloc(origin_mem_ptr, new_len + alignment) orelse return null); 82 | 83 | const shift: u8 = @intCast(alignment - (raw_ptr % alignment)); 84 | std.debug.assert(0 < shift and shift <= alignment); 85 | 86 | const aligned_ptr: usize = raw_ptr + shift; 87 | 88 | // Store the shift in the first byte before the aligned ptr 89 | // We know from above that we are guaranteed to own that byte. 90 | @as(*u8, @ptrFromInt(aligned_ptr - 1)).* = shift; 91 | 92 | return @ptrFromInt(aligned_ptr); 93 | } 94 | 95 | fn resize(ctx: *anyopaque, buf: []u8, buf_align: mem.Alignment, new_len: usize, ret_addr: usize) bool { 96 | _ = ret_addr; 97 | _ = new_len; 98 | _ = buf_align; 99 | _ = buf; 100 | _ = ctx; 101 | // We have a couple of options: return true, or return false... 102 | 103 | // Firstly, we can never call PyMem_Realloc since that can internally copy data and return a new ptr. 104 | // We have no way of passing that pointer back to the caller and buf will have been freed. 105 | 106 | // 1) We could say we successfully resized if new_len < buf.len, and not actually do anything. 107 | // This would work since we never use the slice length in the free function and PyMem will internally 108 | // keep track of the initial alloc size. 109 | 110 | // 2) We could say we _always_ fail to resize and force the caller to decide whether to blindly slice 111 | // or to copy data into a new place. 112 | 113 | // 3) We could succeed if new_len > 75% of buf.len. This minimises the amount of "dead" memory we pass 114 | // around, but it seems like a somewhat arbitrary threshold to hard-code in the allocator. 115 | 116 | // For now, we go with 2) 117 | return false; 118 | } 119 | 120 | fn free(ctx: *anyopaque, buf: []u8, buf_align: mem.Alignment, ret_addr: usize) void { 121 | _ = buf_align; 122 | _ = ctx; 123 | _ = ret_addr; 124 | 125 | const gil = py.gil(); 126 | defer gil.release(); 127 | 128 | // Fetch the alignment shift. We could check it matches the buf_align, but it's a bit annoying. 129 | const aligned_ptr: usize = @intFromPtr(buf.ptr); 130 | const shift = @as(*const u8, @ptrFromInt(aligned_ptr - 1)).*; 131 | 132 | const raw_ptr: *anyopaque = @ptrFromInt(aligned_ptr - shift); 133 | ffi.PyMem_Free(raw_ptr); 134 | } 135 | }{}; 136 | -------------------------------------------------------------------------------- /pydust/src/modules.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const State = @import("discovery.zig").State; 15 | const ffi = @import("ffi.zig"); 16 | const py = @import("pydust.zig"); 17 | const PyError = py.PyError; 18 | const Attributes = @import("attributes.zig").Attributes; 19 | const pytypes = @import("pytypes.zig"); 20 | const funcs = @import("functions.zig"); 21 | const tramp = @import("trampoline.zig"); 22 | const PyMemAllocator = @import("mem.zig").PyMemAllocator; 23 | const CPyObject = @import("types/obj.zig").CPyObject; 24 | 25 | pub const ModuleDef = struct { 26 | name: [:0]const u8, 27 | fullname: [:0]const u8, 28 | definition: type, 29 | }; 30 | 31 | /// Discover a Pydust module. 32 | pub fn Module(comptime root: type, comptime name: [:0]const u8, comptime definition: type) type { 33 | return struct { 34 | const slots = Slots(root, definition); 35 | const methods = funcs.Methods(root, definition); 36 | 37 | const doc: ?[:0]const u8 = blk: { 38 | if (@hasDecl(definition, "__doc__")) { 39 | break :blk definition.__doc__; 40 | } 41 | break :blk null; 42 | }; 43 | 44 | const Fns = struct { 45 | pub fn free(module: ?*anyopaque) callconv(.C) void { 46 | const mod: py.PyModule(root) = .{ .obj = .{ .py = @alignCast(@ptrCast(module)) } }; 47 | const state = mod.getState(definition) catch return; 48 | state.__del__(); 49 | } 50 | }; 51 | 52 | /// A function to initialize the Python module from its definition. 53 | pub fn init() !py.PyObject(root) { 54 | const pyModuleDef = try py.allocator.create(ffi.PyModuleDef); 55 | pyModuleDef.* = ffi.PyModuleDef{ 56 | .m_base = std.mem.zeroes(ffi.PyModuleDef_Base), 57 | .m_name = name.ptr, 58 | .m_doc = if (doc) |d| d.ptr else null, 59 | .m_size = @sizeOf(definition), 60 | .m_methods = @constCast(&methods.pydefs), 61 | .m_slots = @constCast(slots.slots.ptr), 62 | .m_traverse = null, 63 | .m_clear = null, 64 | .m_free = if (@hasDecl(definition, "__del__")) &Fns.free else null, 65 | }; 66 | 67 | // Set reference count to 1 so that it is not freed. 68 | const local_obj: *CPyObject = @ptrCast(&pyModuleDef.m_base.ob_base); 69 | local_obj.ob_refcnt = 1; 70 | 71 | return .{ .py = ffi.PyModuleDef_Init(pyModuleDef) orelse return PyError.PyRaised }; 72 | } 73 | }; 74 | } 75 | 76 | fn Slots(comptime root: type, comptime definition: type) type { 77 | return struct { 78 | const Self = @This(); 79 | 80 | const empty = ffi.PyModuleDef_Slot{ .slot = 0, .value = null }; 81 | const attrs = Attributes(root, definition); 82 | const submodules = Submodules(root, definition); 83 | 84 | pub const slots: []const ffi.PyModuleDef_Slot = blk: { 85 | var slots_: []const ffi.PyModuleDef_Slot = &.{}; 86 | 87 | slots_ = slots_ ++ .{ffi.PyModuleDef_Slot{ 88 | .slot = ffi.Py_mod_exec, 89 | .value = @ptrCast(@constCast(&Self.mod_exec)), 90 | }}; 91 | 92 | // Allow the user to add extra module initialization logic 93 | if (@hasDecl(definition, "__exec__")) { 94 | slots_ = slots_ ++ .{ffi.PyModuleDef_Slot{ 95 | .slot = ffi.Py_mod_exec, 96 | .value = @ptrCast(@constCast(&custom_mod_exec)), 97 | }}; 98 | } 99 | 100 | slots_ = slots_ ++ .{empty}; 101 | 102 | break :blk slots_; 103 | }; 104 | 105 | fn custom_mod_exec(pymodule: *ffi.PyObject) callconv(.C) c_int { 106 | const mod: py.PyModule = .{ .obj = .{ .py = pymodule } }; 107 | tramp.coerceError(root, definition.__exec__(mod)) catch return -1; 108 | return 0; 109 | } 110 | 111 | fn mod_exec(pymodule: *ffi.PyObject) callconv(.C) c_int { 112 | tramp.coerceError(root, mod_exec_internal(.{ .obj = .{ .py = pymodule } })) catch return -1; 113 | return 0; 114 | } 115 | 116 | inline fn mod_exec_internal(module: py.PyModule(root)) !void { 117 | // First, initialize the module state using an __init__ function 118 | if (@typeInfo(definition).@"struct".fields.len > 0) { 119 | if (!@hasDecl(definition, "__init__")) { 120 | @compileError("Non-empty module must define `fn __init__(*Self) !void` method to initialize its state: " ++ @typeName(definition)); 121 | } 122 | const state = try module.getState(definition); 123 | if (@typeInfo(@typeInfo(@TypeOf(definition.__init__)).@"fn".return_type.?) == .error_union) { 124 | try state.__init__(); 125 | } else { 126 | state.__init__(); 127 | } 128 | } 129 | 130 | // Add attributes (including class definitions) to the module 131 | inline for (attrs.attributes) |attr| { 132 | const obj = try attr.ctor(module); 133 | try module.addObjectRef(attr.name, obj); 134 | } 135 | 136 | // Add submodules to the module 137 | inline for (submodules.submodules) |submodule| { 138 | // We use PEP489 multi-phase initialization. For this, we create a ModuleSpec 139 | // which is a dumb object containing only a name. 140 | // See https://github.com/python/cpython/blob/042f31da552c19054acd3ef7bb6cfd857bce172b/Python/import.c#L2527-L2539 141 | 142 | const name = comptime State.getIdentifier(root, submodule).name(); 143 | const submodDef = Module(root, name, submodule); 144 | const pySubmodDef: *ffi.PyModuleDef = @ptrCast((try submodDef.init()).py); 145 | 146 | // Create a dumb ModuleSpec with a name attribute using types.SimpleNamespace 147 | const types = try py.import(root, "types"); 148 | defer types.decref(); 149 | const pyname = try py.PyString(root).create(name); 150 | defer pyname.decref(); 151 | const spec = try types.call(py.PyObject(root), "SimpleNamespace", .{}, .{ .name = pyname }); 152 | defer spec.decref(); 153 | 154 | const submod: py.PyObject(root) = .{ .py = ffi.PyModule_FromDefAndSpec(pySubmodDef, spec.py) orelse return PyError.PyRaised }; 155 | 156 | if (ffi.PyModule_ExecDef(submod.py, pySubmodDef) < 0) { 157 | return PyError.PyRaised; 158 | } 159 | 160 | try module.addObjectRef(name, submod); 161 | } 162 | } 163 | }; 164 | } 165 | 166 | fn Submodules(comptime root: type, comptime definition: type) type { 167 | const typeInfo = @typeInfo(definition).@"struct"; 168 | return struct { 169 | const submodules: []const type = blk: { 170 | var mods: []const type = &.{}; 171 | for (typeInfo.decls) |decl| { 172 | if (State.findDefinition(root, @field(definition, decl.name))) |def| { 173 | if (def.type == .module) { 174 | mods = mods ++ .{def.definition}; 175 | } 176 | } 177 | } 178 | break :blk mods; 179 | }; 180 | }; 181 | } 182 | -------------------------------------------------------------------------------- /pydust/src/pydust.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const mem = @import("mem.zig"); 15 | const discovery = @import("discovery.zig"); 16 | const Definition = discovery.Definition; 17 | const Module = @import("modules.zig").Module; 18 | const types = @import("types.zig"); 19 | const pytypes = @import("pytypes.zig"); 20 | const funcs = @import("functions.zig"); 21 | const tramp = @import("trampoline.zig"); 22 | 23 | // Export some useful things for users 24 | pub usingnamespace @import("builtins.zig"); 25 | pub usingnamespace @import("conversions.zig"); 26 | pub usingnamespace types; 27 | pub const ffi = @import("ffi.zig"); 28 | pub const PyError = @import("errors.zig").PyError; 29 | pub const allocator: std.mem.Allocator = mem.PyMemAllocator.allocator(); 30 | 31 | const Self = @This(); 32 | 33 | /// Initialize Python interpreter state 34 | pub fn initialize() void { 35 | ffi.Py_Initialize(); 36 | } 37 | 38 | /// Tear down Python interpreter state 39 | pub fn finalize() void { 40 | ffi.Py_Finalize(); 41 | } 42 | 43 | /// Register the root Pydust module 44 | pub fn rootmodule(comptime definition: type) void { 45 | const name = @import("pyconf").module_name; 46 | 47 | const moddef = Module(definition, name, definition); 48 | 49 | // For root modules, we export a PyInit__name function per CPython API. 50 | const Closure = struct { 51 | pub fn init() callconv(.C) ?*ffi.PyObject { 52 | const obj = @call(.always_inline, moddef.init, .{}) catch return null; 53 | return obj.py; 54 | } 55 | }; 56 | 57 | const short_name = if (std.mem.lastIndexOfScalar(u8, name, '.')) |idx| name[idx + 1 ..] else name; 58 | @export(&Closure.init, .{ .name = "PyInit_" ++ short_name, .linkage = .strong }); 59 | } 60 | 61 | /// Register a Pydust module as a submodule to an existing module. 62 | pub fn module(comptime definition: type) Definition { 63 | return .{ .definition = definition, .type = .module }; 64 | } 65 | 66 | /// Register a struct as a Python class definition. 67 | pub fn class(comptime definition: type) Definition { 68 | return .{ .definition = definition, .type = .class }; 69 | } 70 | 71 | // pub fn zig(comptime definition: type) @TypeOf(definition) { 72 | // for (@typeInfo(definition).@"struct".decls) |decl| { 73 | // State.privateMethod(&@field(definition, decl.name)); 74 | // } 75 | // return definition; 76 | // } 77 | 78 | /// Register a struct field as a Python read-only attribute. 79 | pub fn attribute(comptime T: type) Definition { 80 | return .{ .definition = Attribute(T), .type = .attribute }; 81 | } 82 | 83 | fn Attribute(comptime T: type) type { 84 | return struct { value: T }; 85 | } 86 | 87 | /// Register a property as a field on a Pydust class. 88 | pub fn property(comptime definition: type) Definition { 89 | return .{ .definition = definition, .type = .property }; 90 | } 91 | 92 | /// Zig type representing variadic arguments to a Python function. 93 | pub fn Args(comptime root: type) type { 94 | return []types.PyObject(root); 95 | } 96 | 97 | /// Zig type representing variadic keyword arguments to a Python function. 98 | pub fn Kwargs(comptime root: type) type { 99 | return std.StringHashMap(types.PyObject(root)); 100 | } 101 | 102 | /// Zig type representing `(*args, **kwargs)` 103 | pub fn CallArgs(comptime root: type) type { 104 | return struct { args: Args(root), kwargs: Kwargs(root) }; 105 | } 106 | -------------------------------------------------------------------------------- /pydust/src/types.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | pub usingnamespace @import("types/bool.zig"); 14 | pub usingnamespace @import("types/buffer.zig"); 15 | pub usingnamespace @import("types/bytes.zig"); 16 | pub usingnamespace @import("types/code.zig"); 17 | pub usingnamespace @import("types/dict.zig"); 18 | pub usingnamespace @import("types/error.zig"); 19 | pub usingnamespace @import("types/float.zig"); 20 | pub usingnamespace @import("types/frame.zig"); 21 | pub usingnamespace @import("types/gil.zig"); 22 | pub usingnamespace @import("types/iter.zig"); 23 | pub usingnamespace @import("types/list.zig"); 24 | pub usingnamespace @import("types/long.zig"); 25 | pub usingnamespace @import("types/memoryview.zig"); 26 | pub usingnamespace @import("types/module.zig"); 27 | pub usingnamespace @import("types/obj.zig"); 28 | pub usingnamespace @import("types/slice.zig"); 29 | pub usingnamespace @import("types/str.zig"); 30 | pub usingnamespace @import("types/tuple.zig"); 31 | pub usingnamespace @import("types/type.zig"); 32 | -------------------------------------------------------------------------------- /pydust/src/types/bool.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 16 | const ffi = py.ffi; 17 | const PyError = @import("../errors.zig").PyError; 18 | const State = @import("../discovery.zig").State; 19 | 20 | /// Wrapper for Python PyBool. 21 | /// 22 | /// See: https://docs.python.org/3/c-api/bool.html 23 | /// 24 | /// Note: refcounting semantics apply, even for bools! 25 | pub fn PyBool(comptime root: type) type { 26 | return extern struct { 27 | obj: py.PyObject(root), 28 | 29 | const Self = @This(); 30 | pub usingnamespace PyObjectMixin(root, "bool", "PyBool", Self); 31 | 32 | pub fn create(value: bool) !Self { 33 | return if (value) true_() else false_(); 34 | } 35 | 36 | pub fn asbool(self: Self) bool { 37 | return ffi.Py_IsTrue(self.obj.py) == 1; 38 | } 39 | 40 | pub fn intobool(self: Self) bool { 41 | self.decref(); 42 | return self.asbool(); 43 | } 44 | 45 | pub fn true_() Self { 46 | return .{ .obj = .{ .py = ffi.PyBool_FromLong(1) } }; 47 | } 48 | 49 | pub fn false_() Self { 50 | return .{ .obj = .{ .py = ffi.PyBool_FromLong(0) } }; 51 | } 52 | }; 53 | } 54 | 55 | test "PyBool" { 56 | py.initialize(); 57 | defer py.finalize(); 58 | 59 | const root = @This(); 60 | 61 | const pytrue = PyBool(root).true_(); 62 | defer pytrue.decref(); 63 | 64 | const pyfalse = PyBool(root).false_(); 65 | defer pyfalse.decref(); 66 | 67 | try std.testing.expect(pytrue.asbool()); 68 | try std.testing.expect(!pyfalse.asbool()); 69 | } 70 | -------------------------------------------------------------------------------- /pydust/src/types/buffer.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const ffi = py.ffi; 16 | const PyError = @import("../errors.zig").PyError; 17 | const State = @import("../discovery.zig").State; 18 | 19 | /// Wrapper for Python Py_buffer. 20 | /// See: https://docs.python.org/3/c-api/buffer.html 21 | pub fn PyBuffer(comptime root: type) type { 22 | return extern struct { 23 | const Self = @This(); 24 | 25 | pub const Flags = struct { 26 | pub const SIMPLE: c_int = 0; 27 | pub const WRITABLE: c_int = 0x0001; 28 | pub const FORMAT: c_int = 0x0004; 29 | pub const ND: c_int = 0x0008; 30 | pub const STRIDES: c_int = 0x0010 | ND; 31 | pub const C_CONTIGUOUS: c_int = 0x0020 | STRIDES; 32 | pub const F_CONTIGUOUS: c_int = 0x0040 | STRIDES; 33 | pub const ANY_CONTIGUOUS: c_int = 0x0080 | STRIDES; 34 | pub const INDIRECT: c_int = 0x0100 | STRIDES; 35 | pub const CONTIG: c_int = STRIDES | WRITABLE; 36 | pub const CONTIG_RO: c_int = ND; 37 | pub const STRIDED: c_int = STRIDES | WRITABLE; 38 | pub const STRIDED_RO: c_int = STRIDES; 39 | pub const RECORDS: c_int = STRIDES | FORMAT | WRITABLE; 40 | pub const RECORDS_RO: c_int = STRIDES | FORMAT; 41 | pub const FULL: c_int = STRIDES | FORMAT | WRITABLE | ND; 42 | pub const FULL_RO: c_int = STRIDES | FORMAT | ND; 43 | }; 44 | 45 | buf: [*]u8, 46 | 47 | // Use pyObj to get the PyObject. 48 | // This must be an optional pointer so we can set null value. 49 | obj: ?*ffi.PyObject, 50 | 51 | // product(shape) * itemsize. 52 | // For contiguous arrays, this is the length of the underlying memory block. 53 | // For non-contiguous arrays, it is the length that the logical structure would 54 | // have if it were copied to a contiguous representation. 55 | len: isize, 56 | itemsize: isize, 57 | readonly: bool, 58 | 59 | // If ndim == 0, the memory location pointed to by buf is interpreted as a scalar of size itemsize. 60 | // In that case, both shape and strides are NULL. 61 | ndim: c_int, 62 | // A NULL terminated string in struct module style syntax describing the contents of a single item. 63 | // If this is NULL, "B" (unsigned bytes) is assumed. 64 | format: ?[*:0]const u8, 65 | 66 | shape: ?[*]const isize = null, 67 | // If strides is NULL, the array is interpreted as a standard n-dimensional C-array. 68 | // Otherwise, the consumer must access an n-dimensional array as follows: 69 | // ptr = (char *)buf + indices[0] * strides[0] + ... + indices[n-1] * strides[n-1]; 70 | strides: ?[*]isize = null, 71 | // If all suboffsets are negative (i.e. no de-referencing is needed), 72 | // then this field must be NULL (the default value). 73 | suboffsets: ?[*]isize = null, 74 | internal: ?*anyopaque = null, 75 | 76 | pub fn release(self: *const Self) void { 77 | ffi.PyBuffer_Release(@constCast(@ptrCast(self))); 78 | } 79 | 80 | /// Returns whether the buffer is contiguous in either C or Fortran order. 81 | pub fn isContiguous(self: *const Self) bool { 82 | return ffi.PyBuffer_IsContiguous(&self, 'A') == 1; 83 | } 84 | 85 | pub fn initFromSlice(self: *Self, comptime T: type, values: []T, shape: []const isize, owner: anytype) void { 86 | // We need to incref the owner object because it's being used by the view. 87 | const ownerObj = py.object(root, owner); 88 | ownerObj.incref(); 89 | 90 | self.* = .{ 91 | .buf = std.mem.sliceAsBytes(values).ptr, 92 | .obj = ownerObj.py, 93 | .len = @intCast(values.len * @sizeOf(T)), 94 | .itemsize = @sizeOf(T), 95 | .readonly = true, 96 | .ndim = @intCast(shape.len), 97 | .format = getFormat(T).ptr, 98 | .shape = shape.ptr, 99 | }; 100 | } 101 | 102 | // asSlice returns buf property as Zig slice. The view must have been created with ND flag. 103 | pub fn asSlice(self: Self, comptime value_type: type) []value_type { 104 | return @alignCast(std.mem.bytesAsSlice(value_type, self.buf[0..@intCast(self.len)])); 105 | } 106 | 107 | pub fn getFormat(comptime value_type: type) [:0]const u8 { 108 | // TODO(ngates): support more complex composite types. 109 | switch (@typeInfo(value_type)) { 110 | .int => |i| { 111 | switch (i.signedness) { 112 | .unsigned => switch (i.bits) { 113 | 8 => return "B", 114 | 16 => return "H", 115 | 32 => return "I", 116 | 64 => return "L", 117 | else => {}, 118 | }, 119 | .signed => switch (i.bits) { 120 | 8 => return "b", 121 | 16 => return "h", 122 | 32 => return "i", 123 | 64 => return "l", 124 | else => {}, 125 | }, 126 | } 127 | }, 128 | .float => |f| { 129 | switch (f.bits) { 130 | 16 => return "e", 131 | 32 => return "f", 132 | 64 => return "d", 133 | else => {}, 134 | } 135 | }, 136 | else => {}, 137 | } 138 | 139 | @compileError("Unsupported buffer value type " ++ @typeName(value_type)); 140 | } 141 | }; 142 | } 143 | -------------------------------------------------------------------------------- /pydust/src/types/bytes.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const ffi = py.ffi; 16 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 17 | const PyError = @import("../errors.zig").PyError; 18 | const State = @import("../discovery.zig").State; 19 | 20 | pub fn PyBytes(comptime root: type) type { 21 | return extern struct { 22 | obj: py.PyObject(root), 23 | 24 | const Self = @This(); 25 | pub usingnamespace PyObjectMixin(root, "bytes", "PyBytes", Self); 26 | 27 | pub fn create(value: []const u8) !Self { 28 | const bytes = ffi.PyBytes_FromStringAndSize(value.ptr, @intCast(value.len)) orelse return PyError.PyRaised; 29 | return .{ .obj = .{ .py = bytes } }; 30 | } 31 | 32 | /// Return the bytes representation of object obj that implements the buffer protocol. 33 | pub fn fromObject(obj: anytype) !Self { 34 | const pyobj = py.object(obj); 35 | const bytes = ffi.PyBytes_FromObject(pyobj.py) orelse return PyError.PyRaised; 36 | return .{ .obj = .{ .py = bytes } }; 37 | } 38 | 39 | /// Return the length of the bytes object. 40 | pub fn length(self: Self) !usize { 41 | return @intCast(ffi.PyBytes_Size(self.obj.py)); 42 | } 43 | 44 | /// Returns a view over the PyBytes bytes. 45 | pub fn asSlice(self: Self) ![:0]const u8 { 46 | var buffer: [*]u8 = undefined; 47 | var size: i64 = 0; 48 | if (ffi.PyBytes_AsStringAndSize(self.obj.py, @ptrCast(&buffer), &size) < 0) { 49 | return PyError.PyRaised; 50 | } 51 | return buffer[0..@as(usize, @intCast(size)) :0]; 52 | } 53 | }; 54 | } 55 | 56 | const testing = std.testing; 57 | 58 | test "PyBytes" { 59 | py.initialize(); 60 | defer py.finalize(); 61 | 62 | const root = @This(); 63 | const a = "Hello"; 64 | 65 | var ps = try PyBytes(root).create(a); 66 | defer ps.decref(); 67 | 68 | const ps_slice = try ps.asSlice(); 69 | try testing.expectEqual(a.len, ps_slice.len); 70 | try testing.expectEqual(a.len, try ps.length()); 71 | try testing.expectEqual(@as(u8, 0), ps_slice[ps_slice.len]); 72 | 73 | try testing.expectEqualStrings("Hello", ps_slice); 74 | } 75 | -------------------------------------------------------------------------------- /pydust/src/types/code.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const State = @import("../discovery.zig").State; 16 | 17 | const ffi = py.ffi; 18 | 19 | /// Wrapper for Python PyCode. 20 | /// See: https://docs.python.org/3/c-api/code.html 21 | pub fn PyCode(comptime root: type) type { 22 | return extern struct { 23 | obj: py.PyObject(root), 24 | 25 | const Self = @This(); 26 | 27 | pub inline fn firstLineNumber(self: *const Self) !u32 { 28 | const lineNo = try self.obj.getAs(py.PyLong(root), "co_firstlineno"); 29 | defer lineNo.decref(); 30 | return lineNo.as(u32); 31 | } 32 | 33 | pub inline fn fileName(self: *const Self) !py.PyString(root) { 34 | return self.obj.getAs(py.PyString(root), "co_filename"); 35 | } 36 | 37 | pub inline fn name(self: *const Self) !py.PyString(root) { 38 | return self.obj.getAs(py.PyString(root), "co_name"); 39 | } 40 | }; 41 | } 42 | 43 | test "PyCode" { 44 | py.initialize(); 45 | defer py.finalize(); 46 | 47 | const root = @This(); 48 | 49 | const pf = py.PyFrame(root).get(); 50 | try std.testing.expectEqual(@as(?py.PyFrame(root), null), pf); 51 | } 52 | -------------------------------------------------------------------------------- /pydust/src/types/float.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 16 | 17 | const ffi = py.ffi; 18 | const PyError = @import("../errors.zig").PyError; 19 | const State = @import("../discovery.zig").State; 20 | 21 | /// Wrapper for Python PyFloat. 22 | /// See: https://docs.python.org/3/c-api/float.html 23 | pub fn PyFloat(comptime root: type) type { 24 | return extern struct { 25 | obj: py.PyObject(root), 26 | 27 | const Self = @This(); 28 | pub usingnamespace PyObjectMixin(root, "float", "PyFloat", Self); 29 | 30 | pub fn create(value: anytype) !Self { 31 | const pyfloat = ffi.PyFloat_FromDouble(@floatCast(value)) orelse return PyError.PyRaised; 32 | return .{ .obj = .{ .py = pyfloat } }; 33 | } 34 | 35 | pub fn as(self: Self, comptime T: type) !T { 36 | return switch (T) { 37 | f16, f32, f64 => { 38 | const double = ffi.PyFloat_AsDouble(self.obj.py); 39 | if (ffi.PyErr_Occurred() != null) return PyError.PyRaised; 40 | return @floatCast(double); 41 | }, 42 | else => @compileError("Unsupported float type " ++ @typeName(T)), 43 | }; 44 | } 45 | }; 46 | } 47 | 48 | test "PyFloat" { 49 | py.initialize(); 50 | defer py.finalize(); 51 | 52 | const root = @This(); 53 | 54 | const pf = try PyFloat(root).create(1.0); 55 | defer pf.decref(); 56 | 57 | try std.testing.expectEqual(@as(f32, 1.0), try pf.as(f32)); 58 | } 59 | -------------------------------------------------------------------------------- /pydust/src/types/frame.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const State = @import("../discovery.zig").State; 16 | 17 | const ffi = py.ffi; 18 | 19 | /// Wrapper for Python PyFrame. 20 | /// See: https://docs.python.org/3/c-api/frame.html 21 | pub fn PyFrame(comptime root: type) type { 22 | return extern struct { 23 | obj: py.PyObject(root), 24 | const Self = @This(); 25 | 26 | pub fn get() ?Self { 27 | const frame = ffi.PyEval_GetFrame(); 28 | return if (frame) |f| .{ .obj = .{ .py = objPtr(f) } } else null; 29 | } 30 | 31 | pub fn code(self: Self) py.PyCode(root) { 32 | const codeObj = ffi.PyFrame_GetCode(framePtr(self.obj.py)); 33 | return .{ .obj = .{ .py = @alignCast(@ptrCast(codeObj)) } }; 34 | } 35 | 36 | pub inline fn lineNumber(self: Self) u32 { 37 | return @intCast(ffi.PyFrame_GetLineNumber(framePtr(self.obj.py))); 38 | } 39 | 40 | inline fn framePtr(obj: *ffi.PyObject) *ffi.PyFrameObject { 41 | return @alignCast(@ptrCast(obj)); 42 | } 43 | 44 | inline fn objPtr(obj: *ffi.PyFrameObject) *ffi.PyObject { 45 | return @alignCast(@ptrCast(obj)); 46 | } 47 | }; 48 | } 49 | 50 | test "PyFrame" { 51 | py.initialize(); 52 | defer py.finalize(); 53 | 54 | const root = @This(); 55 | 56 | const pf = PyFrame(root).get(); 57 | try std.testing.expectEqual(@as(?PyFrame(root), null), pf); 58 | } 59 | -------------------------------------------------------------------------------- /pydust/src/types/gil.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 16 | 17 | const ffi = py.ffi; 18 | const PyError = @import("../errors.zig").PyError; 19 | 20 | pub const PyGIL = extern struct { 21 | state: ffi.PyGILState_STATE, 22 | 23 | /// Acqiure the GIL. Ensure to call `release` when done, e.g. using `defer gil.release()`. 24 | pub fn ensure() PyGIL { 25 | return .{ .state = ffi.PyGILState_Ensure() }; 26 | } 27 | 28 | pub fn release(self: PyGIL) void { 29 | ffi.PyGILState_Release(self.state); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /pydust/src/types/iter.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 16 | const ffi = py.ffi; 17 | const PyError = @import("../errors.zig").PyError; 18 | const State = @import("../discovery.zig").State; 19 | 20 | /// Wrapper for Python PyIter. 21 | /// Constructed using py.iter(...) 22 | pub fn PyIter(comptime root: type) type { 23 | return extern struct { 24 | obj: py.PyObject(root), 25 | 26 | const Self = @This(); 27 | pub usingnamespace PyObjectMixin(root, "iterator", "PyIter", Self); 28 | 29 | pub fn next(self: Self, comptime T: type) !?T { 30 | if (ffi.PyIter_Next(self.obj.py)) |result| { 31 | return try py.as(root, T, result); 32 | } 33 | 34 | // If no exception, then the item is missing. 35 | if (ffi.PyErr_Occurred() == null) { 36 | return null; 37 | } 38 | 39 | return PyError.PyRaised; 40 | } 41 | 42 | // TODO(ngates): implement PyIter_Send when required 43 | }; 44 | } 45 | 46 | test "PyIter" { 47 | py.initialize(); 48 | defer py.finalize(); 49 | 50 | const root = @This(); 51 | 52 | const tuple = try py.PyTuple(root).create(.{ 1, 2, 3 }); 53 | defer tuple.decref(); 54 | 55 | const iterator = try py.iter(root, tuple); 56 | var previous: u64 = 0; 57 | while (try iterator.next(u64)) |v| { 58 | try std.testing.expect(v > previous); 59 | previous = v; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pydust/src/types/list.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 16 | 17 | const ffi = py.ffi; 18 | const PyObject = py.PyObject; 19 | const PyLong = py.PyLong; 20 | const PyError = @import("../errors.zig").PyError; 21 | const State = @import("../discovery.zig").State; 22 | 23 | /// Wrapper for Python PyList. 24 | /// See: https://docs.python.org/3/c-api/list.html 25 | pub fn PyList(comptime root: type) type { 26 | return extern struct { 27 | obj: py.PyObject(root), 28 | 29 | const Self = @This(); 30 | pub usingnamespace PyObjectMixin(root, "list", "PyList", Self); 31 | 32 | pub fn new(size: usize) !Self { 33 | const list = ffi.PyList_New(@intCast(size)) orelse return PyError.PyRaised; 34 | return .{ .obj = .{ .py = list } }; 35 | } 36 | 37 | pub fn length(self: Self) usize { 38 | return @intCast(ffi.PyList_Size(self.obj.py)); 39 | } 40 | 41 | // Returns borrowed reference. 42 | pub fn getItem(self: Self, comptime T: type, idx: isize) !T { 43 | if (ffi.PyList_GetItem(self.obj.py, idx)) |item| { 44 | return py.as(root, T, py.PyObject(root){ .py = item }); 45 | } else { 46 | return PyError.PyRaised; 47 | } 48 | } 49 | 50 | // Returns a slice of the list. 51 | pub fn getSlice(self: Self, low: isize, high: isize) !Self { 52 | if (ffi.PyList_GetSlice(self.obj.py, low, high)) |item| { 53 | return .{ .obj = .{ .py = item } }; 54 | } else { 55 | return PyError.PyRaised; 56 | } 57 | } 58 | 59 | /// This function “steals” a reference to item and discards a reference to an item already in the list at the affected position. 60 | pub fn setOwnedItem(self: Self, pos: usize, value: anytype) !void { 61 | // Since this function steals the reference, it can only accept object-like values. 62 | if (ffi.PyList_SetItem(self.obj.py, @intCast(pos), py.object(root, value).py) < 0) { 63 | return PyError.PyRaised; 64 | } 65 | } 66 | 67 | /// Set the item at the given position. 68 | pub fn setItem(self: Self, pos: usize, value: anytype) !void { 69 | const valueObj = try py.create(root, value); 70 | return self.setOwnedItem(pos, valueObj); 71 | } 72 | 73 | // Insert the item item into list list in front of index idx. 74 | pub fn insert(self: Self, idx: isize, value: anytype) !void { 75 | const valueObj = try py.create(root, value); 76 | defer valueObj.decref(); 77 | if (ffi.PyList_Insert(self.obj.py, idx, valueObj.py) < 0) { 78 | return PyError.PyRaised; 79 | } 80 | } 81 | 82 | // Append the object item at the end of list list. 83 | pub fn append(self: Self, value: anytype) !void { 84 | const valueObj = try py.create(root, value); 85 | defer valueObj.decref(); 86 | 87 | if (ffi.PyList_Append(self.obj.py, valueObj.py) < 0) { 88 | return PyError.PyRaised; 89 | } 90 | } 91 | 92 | // Sort the items of list in place. 93 | pub fn sort(self: Self) !void { 94 | if (ffi.PyList_Sort(self.obj.py) < 0) { 95 | return PyError.PyRaised; 96 | } 97 | } 98 | 99 | // Reverse the items of list in place. 100 | pub fn reverse(self: Self) !void { 101 | if (ffi.PyList_Reverse(self.obj.py) < 0) { 102 | return PyError.PyRaised; 103 | } 104 | } 105 | 106 | pub fn toTuple(self: Self) !py.PyTuple(root) { 107 | const pytuple = ffi.PyList_AsTuple(self.obj.py) orelse return PyError.PyRaised; 108 | return py.PyTuple(root).unchecked(.{ .py = pytuple }); 109 | } 110 | }; 111 | } 112 | 113 | const testing = std.testing; 114 | 115 | test "PyList" { 116 | py.initialize(); 117 | defer py.finalize(); 118 | 119 | const root = @This(); 120 | 121 | var list = try PyList(root).new(2); 122 | defer list.decref(); 123 | try list.setItem(0, 1); 124 | try list.setItem(1, 2.0); 125 | 126 | try testing.expectEqual(@as(usize, 2), list.length()); 127 | 128 | try testing.expectEqual(@as(i64, 1), try list.getItem(i64, 0)); 129 | try testing.expectEqual(@as(f64, 2.0), try list.getItem(f64, 1)); 130 | 131 | try list.append(3); 132 | try testing.expectEqual(@as(usize, 3), list.length()); 133 | try testing.expectEqual(@as(i32, 3), try list.getItem(i32, 2)); 134 | 135 | try list.insert(0, 1.23); 136 | try list.reverse(); 137 | try testing.expectEqual(@as(f32, 1.23), try list.getItem(f32, 3)); 138 | 139 | try list.sort(); 140 | try testing.expectEqual(@as(i64, 1), try list.getItem(i64, 0)); 141 | 142 | const tuple = try list.toTuple(); 143 | defer tuple.decref(); 144 | 145 | try std.testing.expectEqual(@as(usize, 4), tuple.length()); 146 | } 147 | 148 | test "PyList setOwnedItem" { 149 | py.initialize(); 150 | defer py.finalize(); 151 | 152 | const root = @This(); 153 | 154 | var list = try PyList(root).new(2); 155 | defer list.decref(); 156 | const py1 = try py.create(root, 1); 157 | defer py1.decref(); 158 | try list.setOwnedItem(0, py1); 159 | const py2 = try py.create(root, 2); 160 | defer py2.decref(); 161 | try list.setOwnedItem(1, py2); 162 | 163 | try std.testing.expectEqual(@as(u8, 1), try list.getItem(u8, 0)); 164 | try std.testing.expectEqual(@as(u8, 2), try list.getItem(u8, 1)); 165 | } 166 | -------------------------------------------------------------------------------- /pydust/src/types/long.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 16 | const ffi = py.ffi; 17 | const PyError = @import("../errors.zig").PyError; 18 | const State = @import("../discovery.zig").State; 19 | 20 | /// Wrapper for Python PyLong. 21 | /// See: https://docs.python.org/3/c-api/long.html#c.PyLongObject 22 | pub fn PyLong(comptime root: type) type { 23 | return extern struct { 24 | obj: py.PyObject(root), 25 | 26 | const Self = @This(); 27 | pub usingnamespace PyObjectMixin(root, "int", "PyLong", Self); 28 | 29 | pub fn create(value: anytype) !Self { 30 | if (@TypeOf(value) == comptime_int) { 31 | return create(@as(i64, @intCast(value))); 32 | } 33 | 34 | const typeInfo = @typeInfo(@TypeOf(value)).int; 35 | 36 | const pylong = switch (typeInfo.signedness) { 37 | .signed => ffi.PyLong_FromLongLong(@intCast(value)), 38 | .unsigned => ffi.PyLong_FromUnsignedLongLong(@intCast(value)), 39 | } orelse return PyError.PyRaised; 40 | 41 | return .{ .obj = .{ .py = pylong } }; 42 | } 43 | 44 | pub fn as(self: Self, comptime T: type) !T { 45 | // TODO(ngates): support non-int conversions 46 | const typeInfo = @typeInfo(T).int; 47 | return switch (typeInfo.signedness) { 48 | .signed => { 49 | const ll = ffi.PyLong_AsLongLong(self.obj.py); 50 | if (ffi.PyErr_Occurred() != null) return PyError.PyRaised; 51 | return @intCast(ll); 52 | }, 53 | .unsigned => { 54 | const ull = ffi.PyLong_AsUnsignedLongLong(self.obj.py); 55 | if (ffi.PyErr_Occurred() != null) return PyError.PyRaised; 56 | return @intCast(ull); 57 | }, 58 | }; 59 | } 60 | }; 61 | } 62 | 63 | test "PyLong" { 64 | py.initialize(); 65 | defer py.finalize(); 66 | 67 | const root = @This(); 68 | 69 | const pl = try PyLong(root).create(100); 70 | defer pl.decref(); 71 | 72 | try std.testing.expectEqual(@as(c_long, 100), try pl.as(c_long)); 73 | try std.testing.expectEqual(@as(c_ulong, 100), try pl.as(c_ulong)); 74 | 75 | const neg_pl = try PyLong(root).create(@as(c_long, -100)); 76 | defer neg_pl.decref(); 77 | 78 | try std.testing.expectError( 79 | PyError.PyRaised, 80 | neg_pl.as(c_ulong), 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /pydust/src/types/memoryview.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const ffi = py.ffi; 16 | const PyError = @import("../errors.zig").PyError; 17 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 18 | const State = @import("../discovery.zig").State; 19 | 20 | pub fn PyMemoryView(comptime root: type) type { 21 | return extern struct { 22 | obj: py.PyObject(root), 23 | 24 | pub const Flags = struct { 25 | const PyBUF_READ: c_int = 0x100; 26 | const PyBUF_WRITE: c_int = 0x200; 27 | }; 28 | 29 | const Self = @This(); 30 | pub usingnamespace PyObjectMixin(root, "memoryview", "PyMemoryView", Self); 31 | 32 | pub fn fromSlice(slice: anytype) !Self { 33 | const sliceType = Slice(@TypeOf(slice)); 34 | const sliceTpInfo = @typeInfo(sliceType); 35 | 36 | const flag = if (sliceTpInfo == .pointer and sliceTpInfo.pointer.is_const) Flags.PyBUF_READ else Flags.PyBUF_WRITE; 37 | return .{ .obj = .{ 38 | .py = py.ffi.PyMemoryView_FromMemory(@constCast(slice.ptr), @intCast(slice.len), flag) orelse return py.PyError.PyRaised, 39 | } }; 40 | } 41 | 42 | pub fn fromObject(obj: py.PyObject) !Self { 43 | return .{ .obj = .{ 44 | .py = py.ffi.PyMemoryView_FromObject(obj.py) orelse return py.PyError.PyRaised, 45 | } }; 46 | } 47 | 48 | fn Slice(comptime T: type) type { 49 | switch (@typeInfo(T)) { 50 | .pointer => |ptr_info| { 51 | var new_ptr_info = ptr_info; 52 | switch (ptr_info.size) { 53 | .slice => {}, 54 | .One => switch (@typeInfo(ptr_info.child)) { 55 | .array => |info| new_ptr_info.child = info.child, 56 | else => @compileError("invalid type given to PyMemoryview"), 57 | }, 58 | else => @compileError("invalid type given to PyMemoryview"), 59 | } 60 | new_ptr_info.size = .slice; 61 | return @Type(.{ .pointer = new_ptr_info }); 62 | }, 63 | else => @compileError("invalid type given to PyMemoryview"), 64 | } 65 | } 66 | }; 67 | } 68 | 69 | test "from array" { 70 | py.initialize(); 71 | defer py.finalize(); 72 | 73 | const root = @This(); 74 | 75 | const array = "static string"; 76 | const mv = try PyMemoryView(root).fromSlice(array); 77 | defer mv.decref(); 78 | 79 | var buf = try mv.obj.getBuffer(py.PyBuffer(root).Flags.ANY_CONTIGUOUS); 80 | try std.testing.expectEqualSlices(u8, array, buf.asSlice(u8)); 81 | try std.testing.expect(buf.readonly); 82 | } 83 | 84 | test "from slice" { 85 | py.initialize(); 86 | defer py.finalize(); 87 | 88 | const root = @This(); 89 | 90 | const array = "This is a static string"; 91 | const slice: []const u8 = try std.testing.allocator.dupe(u8, array); 92 | defer std.testing.allocator.free(slice); 93 | const mv = try PyMemoryView(root).fromSlice(slice); 94 | defer mv.decref(); 95 | 96 | var buf = try mv.obj.getBuffer(py.PyBuffer(root).Flags.ANY_CONTIGUOUS); 97 | try std.testing.expectEqualSlices(u8, array, buf.asSlice(u8)); 98 | try std.testing.expect(buf.readonly); 99 | } 100 | 101 | test "from mutable slice" { 102 | py.initialize(); 103 | defer py.finalize(); 104 | 105 | const root = @This(); 106 | 107 | const array = "This is a static string"; 108 | const slice = try std.testing.allocator.alloc(u8, array.len); 109 | defer std.testing.allocator.free(slice); 110 | const mv = try PyMemoryView(root).fromSlice(slice); 111 | defer mv.decref(); 112 | @memcpy(slice, array); 113 | 114 | var buf = try mv.obj.getBuffer(py.PyBuffer(root).Flags.ANY_CONTIGUOUS); 115 | try std.testing.expectEqualSlices(u8, array, buf.asSlice(u8)); 116 | try std.testing.expect(!buf.readonly); 117 | } 118 | -------------------------------------------------------------------------------- /pydust/src/types/module.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const Allocator = @import("std").mem.Allocator; 15 | const mem = @import("../mem.zig"); 16 | const ffi = @import("../ffi.zig"); 17 | const py = @import("../pydust.zig"); 18 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 19 | const pytypes = @import("../pytypes.zig"); 20 | const tramp = @import("../trampoline.zig"); 21 | const State = @import("../discovery.zig").State; 22 | 23 | const PyError = @import("../errors.zig").PyError; 24 | 25 | pub fn PyModule(comptime root: type) type { 26 | return extern struct { 27 | obj: py.PyObject(root), 28 | 29 | const Self = @This(); 30 | pub usingnamespace PyObjectMixin(root, "module", "PyModule", Self); 31 | 32 | pub fn import(name: [:0]const u8) !Self { 33 | return .{ .obj = .{ .py = ffi.PyImport_ImportModule(name) orelse return PyError.PyRaised } }; 34 | } 35 | 36 | pub fn getState(self: Self, comptime ModState: type) !*ModState { 37 | const statePtr = ffi.PyModule_GetState(self.obj.py) orelse return PyError.PyRaised; 38 | return @ptrCast(@alignCast(statePtr)); 39 | } 40 | 41 | pub fn addObjectRef(self: Self, name: [:0]const u8, obj: anytype) !void { 42 | const pyobject = py.object(root, obj); 43 | if (ffi.PyModule_AddObjectRef(self.obj.py, name.ptr, pyobject.py) < 0) { 44 | return PyError.PyRaised; 45 | } 46 | } 47 | 48 | /// Initialize a class that is defined within this module. 49 | /// Most useful during module.__exec__ initialization. 50 | pub fn init(self: Self, class_name: [:0]const u8, class_state: anytype) !*const @TypeOf(class_state) { 51 | const pytype = try self.obj.get(class_name); 52 | defer pytype.decref(); 53 | 54 | const Cls = @TypeOf(class_state); 55 | 56 | if (State.getDefinition(root, Cls).type != .class) { 57 | @compileError("Can only init class objects"); 58 | } 59 | 60 | if (@hasDecl(Cls, "__init__")) { 61 | @compileError("PyTypes with a __init__ method should be instantiated via Python with ptype.call(...)"); 62 | } 63 | 64 | // Alloc the class 65 | const pyobj: *pytypes.PyTypeStruct(Cls) = @alignCast(@ptrCast(ffi.PyType_GenericAlloc(@ptrCast(pytype.py), 0) orelse return PyError.PyRaised)); 66 | pyobj.root = class_state; 67 | return &pyobj.root; 68 | } 69 | 70 | /// Create and insantiate a PyModule object from a Python code string. 71 | pub fn fromCode(code: []const u8, filename: []const u8, module_name: []const u8) !Self { 72 | // Ensure null-termination of all strings 73 | const codeZ = try py.allocator.dupeZ(u8, code); 74 | defer py.allocator.free(codeZ); 75 | const filenameZ = try py.allocator.dupeZ(u8, filename); 76 | defer py.allocator.free(filenameZ); 77 | const module_nameZ = try py.allocator.dupeZ(u8, module_name); 78 | defer py.allocator.free(module_nameZ); 79 | 80 | const pycode = ffi.Py_CompileString(codeZ.ptr, filenameZ.ptr, ffi.Py_file_input) orelse return PyError.PyRaised; 81 | defer ffi.Py_DECREF(pycode); 82 | 83 | const pymod = ffi.PyImport_ExecCodeModuleEx(module_nameZ.ptr, pycode, filenameZ.ptr) orelse return PyError.PyRaised; 84 | return .{ .obj = .{ .py = pymod } }; 85 | } 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /pydust/src/types/obj.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const ffi = @import("../ffi.zig"); 15 | const py = @import("../pydust.zig"); 16 | const PyError = @import("../errors.zig").PyError; 17 | const State = @import("../discovery.zig").State; 18 | 19 | // NOTE: Use only when accessing ob_refcnt. 20 | // From 3.12, ob_refcnt is anonymous union in CPython and is not accessible from Zig. 21 | pub const CPyObject = extern struct { ob_refcnt: ffi.Py_ssize_t, ob_type: ?*ffi.PyTypeObject }; 22 | 23 | pub fn PyObject(comptime root: type) type { 24 | return extern struct { 25 | py: *ffi.PyObject, 26 | 27 | const Self = @This(); 28 | 29 | pub fn incref(self: Self) void { 30 | ffi.Py_INCREF(self.py); 31 | } 32 | 33 | pub fn decref(self: Self) void { 34 | ffi.Py_DECREF(self.py); 35 | } 36 | 37 | pub fn refcnt(self: Self) isize { 38 | const local_py: *CPyObject = @ptrCast(self.py); 39 | return local_py.ob_refcnt; 40 | } 41 | 42 | pub fn getTypeName(self: Self) ![:0]const u8 { 43 | const pytype: *ffi.PyObject = ffi.PyObject_Type(self.py) orelse return PyError.PyRaised; 44 | const name = py.PyString(root).unchecked(.{ .py = ffi.PyType_GetName(@ptrCast(pytype)) orelse return PyError.PyRaised }); 45 | return name.asSlice(); 46 | } 47 | 48 | /// Call a method on this object with no arguments. 49 | pub fn call0(self: Self, comptime T: type, method: []const u8) !T { 50 | const meth = try self.get(method); 51 | defer meth.decref(); 52 | return py.call0(T, meth); 53 | } 54 | 55 | /// Call a method on this object with the given args and kwargs. 56 | pub fn call(self: Self, comptime T: type, method: []const u8, args: anytype, kwargs: anytype) !T { 57 | const meth = try self.get(method); 58 | defer meth.decref(); 59 | return py.call(root, T, meth, args, kwargs); 60 | } 61 | 62 | /// Returns a new reference to the attribute of the object. 63 | pub fn get(self: Self, attrName: []const u8) !Self { 64 | const attrStr = try py.PyString(root).create(attrName); 65 | defer attrStr.decref(); 66 | 67 | return .{ .py = ffi.PyObject_GetAttr(self.py, attrStr.obj.py) orelse return PyError.PyRaised }; 68 | } 69 | 70 | /// Returns a new reference to the attribute of the object using default lookup semantics. 71 | pub fn getAttribute(self: Self, attrName: []const u8) !Self { 72 | const attrStr = try py.PyString(root).create(attrName); 73 | defer attrStr.decref(); 74 | 75 | return .{ .py = ffi.PyObject_GenericGetAttr(self.py, attrStr.obj.py) orelse return PyError.PyRaised }; 76 | } 77 | 78 | /// Returns a new reference to the attribute of the object. 79 | pub fn getAs(self: Self, comptime T: type, attrName: []const u8) !T { 80 | return try py.as(root, T, try self.get(attrName)); 81 | } 82 | 83 | /// Checks whether object has given attribute 84 | pub fn has(self: Self, attrName: []const u8) !bool { 85 | const attrStr = try py.PyString(root).create(attrName); 86 | defer attrStr.decref(); 87 | return ffi.PyObject_HasAttr(self.py, attrStr.obj.py) == 1; 88 | } 89 | 90 | // See: https://docs.python.org/3/c-api/buffer.html#buffer-request-types 91 | pub fn getBuffer(self: py.PyObject(root), flags: c_int) !py.PyBuffer(root) { 92 | if (ffi.PyObject_CheckBuffer(self.py) != 1) { 93 | return py.BufferError(root).raise("object does not support buffer interface"); 94 | } 95 | var buffer: py.PyBuffer(root) = undefined; 96 | if (ffi.PyObject_GetBuffer(self.py, @ptrCast(&buffer), flags) != 0) { 97 | // Error is already raised. 98 | return PyError.PyRaised; 99 | } 100 | return buffer; 101 | } 102 | 103 | pub fn set(self: Self, attr: []const u8, value: Self) !Self { 104 | const attrStr = try py.PyString(root).create(attr); 105 | defer attrStr.decref(); 106 | 107 | if (ffi.PyObject_SetAttr(self.py, attrStr.obj.py, value.py) < 0) { 108 | return PyError.PyRaised; 109 | } 110 | return self; 111 | } 112 | 113 | pub fn del(self: Self, attr: []const u8) !Self { 114 | const attrStr = try py.PyString(root).create(attr); 115 | defer attrStr.decref(); 116 | 117 | if (ffi.PyObject_DelAttr(self.py, attrStr.obj.py) < 0) { 118 | return PyError.PyRaised; 119 | } 120 | return self; 121 | } 122 | 123 | pub fn repr(self: Self) !Self { 124 | return .{ .py = ffi.PyObject_Repr(@ptrCast(self)) orelse return PyError.PyRaised }; 125 | } 126 | }; 127 | } 128 | 129 | pub fn PyObjectMixin(comptime root: type, comptime name: []const u8, comptime prefix: []const u8, comptime Self: type) type { 130 | const PyCheck = @field(ffi, prefix ++ "_Check"); 131 | 132 | return struct { 133 | /// Check whether the given object is of this type. 134 | pub fn check(obj: py.PyObject(root)) !bool { 135 | return PyCheck(obj.py) == 1; 136 | } 137 | 138 | /// Checked conversion from a PyObject. 139 | pub fn checked(obj: py.PyObject(root)) !Self { 140 | if (PyCheck(obj.py) == 0) { 141 | const typeName = try py.str(root, py.type_(root, obj)); 142 | defer typeName.decref(); 143 | return py.TypeError(root).raiseFmt("expected {s}, found {s}", .{ name, try typeName.asSlice() }); 144 | } 145 | return .{ .obj = obj }; 146 | } 147 | 148 | /// Optionally downcast the object if it is of this type. 149 | pub fn checkedCast(obj: py.PyObject(root)) ?Self { 150 | if (PyCheck(obj.py) == 1) { 151 | return .{ .obj = obj }; 152 | } 153 | return null; 154 | } 155 | 156 | /// Unchecked conversion from a PyObject. 157 | pub fn unchecked(obj: py.PyObject(root)) Self { 158 | return .{ .obj = obj }; 159 | } 160 | 161 | /// Increment the object's refcnt. 162 | pub fn incref(self: Self) void { 163 | self.obj.incref(); 164 | } 165 | 166 | /// Decrement the object's refcnt. 167 | pub fn decref(self: Self) void { 168 | self.obj.decref(); 169 | } 170 | }; 171 | } 172 | 173 | test "call" { 174 | py.initialize(); 175 | defer py.finalize(); 176 | 177 | const root = @This(); 178 | 179 | const math = try py.import(root, "math"); 180 | defer math.decref(); 181 | 182 | const result = try math.call(f32, "pow", .{ 2, 3 }, .{}); 183 | try std.testing.expectEqual(@as(f32, 8.0), result); 184 | } 185 | 186 | test "has" { 187 | py.initialize(); 188 | defer py.finalize(); 189 | 190 | const root = @This(); 191 | 192 | const math = try py.import(root, "math"); 193 | defer math.decref(); 194 | 195 | try std.testing.expect(try math.has("pow")); 196 | } 197 | -------------------------------------------------------------------------------- /pydust/src/types/sequence.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const py = @import("../pydust.zig"); 14 | const ffi = @import("../ffi.zig"); 15 | const PyError = @import("../errors.zig").PyError; 16 | const State = @import("../discovery.zig").State; 17 | 18 | /// Mixin of PySequence functions. 19 | pub fn SequenceMixin(comptime root: type, comptime Self: type) type { 20 | return struct { 21 | pub fn contains(self: Self, value: anytype) !bool { 22 | const result = ffi.PySequence_Contains(self.obj.py, py.object(root, value).py); 23 | if (result < 0) return PyError.PyRaised; 24 | return result == 1; 25 | } 26 | 27 | pub fn index(self: Self, value: anytype) !usize { 28 | const idx = ffi.PySequence_Index(self.obj.py, py.object(root, value).py); 29 | if (idx < 0) return PyError.PyRaised; 30 | return @intCast(idx); 31 | } 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /pydust/src/types/slice.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 16 | const ffi = py.ffi; 17 | const PyError = @import("../errors.zig").PyError; 18 | const State = @import("../discovery.zig").State; 19 | 20 | /// Wrapper for Python PySlice. 21 | pub fn PySlice(comptime root: type) type { 22 | return extern struct { 23 | obj: py.PyObject(root), 24 | 25 | const Self = @This(); 26 | pub usingnamespace PyObjectMixin(root, "slice", "PySlice", Self); 27 | 28 | pub fn create(start: anytype, stop: anytype, step: anytype) !Self { 29 | // TODO(ngates): think about how to improve comptime optional handling? 30 | const pystart = if (@typeInfo(@TypeOf(start)) == .Null) null else (try py.create(root, start)).py; 31 | defer if (@typeInfo(@TypeOf(start)) != .Null) py.decref(root, pystart); 32 | const pystop = if (@typeInfo(@TypeOf(stop)) == .Null) null else (try py.create(root, stop)).py; 33 | defer if (@typeInfo(@TypeOf(stop)) != .Null) py.decref(root, pystop); 34 | const pystep = if (@typeInfo(@TypeOf(step)) == .Null) null else (try py.create(root, step)).py; 35 | defer if (@typeInfo(@TypeOf(step)) != .Null) py.decref(root, pystep); 36 | 37 | const pyslice = ffi.PySlice_New(pystart, pystop, pystep) orelse return PyError.PyRaised; 38 | return .{ .obj = .{ .py = pyslice } }; 39 | } 40 | 41 | pub fn getStart(self: Self, comptime T: type) !T { 42 | return try self.obj.getAs(T, "start"); 43 | } 44 | 45 | pub fn getStop(self: Self, comptime T: type) !T { 46 | return try self.obj.getAs(T, "stop"); 47 | } 48 | 49 | pub fn getStep(self: Self, comptime T: type) !T { 50 | return try self.obj.getAs(T, "step"); 51 | } 52 | }; 53 | } 54 | 55 | test "PySlice" { 56 | py.initialize(); 57 | defer py.finalize(); 58 | 59 | const root = @This(); 60 | 61 | const range = try PySlice(root).create(0, 100, null); 62 | defer range.decref(); 63 | 64 | try std.testing.expectEqual(@as(u64, 0), try range.getStart(u64)); 65 | try std.testing.expectEqual(@as(u64, 100), try range.getStop(u64)); 66 | try std.testing.expectEqual(@as(?u64, null), try range.getStep(?u64)); 67 | } 68 | -------------------------------------------------------------------------------- /pydust/src/types/str.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 16 | 17 | const ffi = py.ffi; 18 | const PyObject = @import("obj.zig").PyObject; 19 | const PyError = @import("../errors.zig").PyError; 20 | const State = @import("../discovery.zig").State; 21 | 22 | pub fn PyString(comptime root: type) type { 23 | return extern struct { 24 | obj: PyObject(root), 25 | 26 | const Self = @This(); 27 | pub usingnamespace PyObjectMixin(root, "str", "PyUnicode", Self); 28 | 29 | pub fn create(value: []const u8) !Self { 30 | const unicode = ffi.PyUnicode_FromStringAndSize(value.ptr, @intCast(value.len)) orelse return PyError.PyRaised; 31 | return .{ .obj = .{ .py = unicode } }; 32 | } 33 | 34 | pub fn createFmt(comptime format: []const u8, args: anytype) !Self { 35 | const str = try std.fmt.allocPrint(py.allocator, format, args); 36 | defer py.allocator.free(str); 37 | return create(str); 38 | } 39 | 40 | /// Append other to self. 41 | /// 42 | /// Warning: a reference to self is stolen. Use concat, or self.incref(), if you don't own a reference to self. 43 | pub fn append(self: Self, other: Self) !Self { 44 | return self.appendObj(other.obj); 45 | } 46 | 47 | /// Append the slice to self. 48 | /// 49 | /// Warning: a reference to self is stolen. Use concat, or self.incref(), if you don't own a reference to self. 50 | pub fn appendSlice(self: Self, str: []const u8) !Self { 51 | const other = try create(str); 52 | defer other.decref(); 53 | return self.appendObj(other.obj); 54 | } 55 | 56 | fn appendObj(self: Self, other: PyObject(root)) !Self { 57 | // This function effectively decref's the left-hand side. 58 | // The semantics therefore sort of imply mutation, and so we expose the same in our API. 59 | // FIXME(ngates): this comment 60 | var self_ptr: ?*ffi.PyObject = self.obj.py; 61 | ffi.PyUnicode_Append(&self_ptr, other.py); 62 | if (self_ptr) |ptr| { 63 | return Self.unchecked(.{ .py = ptr }); 64 | } else { 65 | // If set to null, then it failed. 66 | return PyError.PyRaised; 67 | } 68 | } 69 | 70 | /// Concat other to self. Returns a new reference. 71 | pub fn concat(self: Self, other: Self) !Self { 72 | const result = ffi.PyUnicode_Concat(self.obj.py, other.obj.py) orelse return PyError.PyRaised; 73 | return Self.unchecked(.{ .py = result }); 74 | } 75 | 76 | /// Concat other to self. Returns a new reference. 77 | pub fn concatSlice(self: Self, other: []const u8) !Self { 78 | const otherString = try create(other); 79 | defer otherString.decref(); 80 | 81 | return concat(self, otherString); 82 | } 83 | 84 | /// Return the length of the Unicode object, in code points. 85 | pub fn length(self: Self) !usize { 86 | return @intCast(ffi.PyUnicode_GetLength(self.obj.py)); 87 | } 88 | 89 | /// Returns a view over the PyString bytes. 90 | pub fn asSlice(self: Self) ![:0]const u8 { 91 | var size: i64 = 0; 92 | const buffer: [*:0]const u8 = ffi.PyUnicode_AsUTF8AndSize(self.obj.py, &size) orelse return PyError.PyRaised; 93 | return buffer[0..@as(usize, @intCast(size)) :0]; 94 | } 95 | }; 96 | } 97 | 98 | const testing = std.testing; 99 | 100 | test "PyString" { 101 | py.initialize(); 102 | defer py.finalize(); 103 | 104 | const root = @This(); 105 | const a = "Hello"; 106 | const b = ", world!"; 107 | 108 | var ps = try PyString(root).create(a); 109 | // defer ps.decref(); <-- We don't need to decref here since append steals the reference to self. 110 | ps = try ps.appendSlice(b); 111 | defer ps.decref(); 112 | 113 | const ps_slice = try ps.asSlice(); 114 | 115 | // Null-terminated strings have len == non-null bytes, but are guaranteed to have a null byte 116 | // when indexed by their length. 117 | try testing.expectEqual(a.len + b.len, ps_slice.len); 118 | try testing.expectEqual(@as(u8, 0), ps_slice[ps_slice.len]); 119 | 120 | try testing.expectEqualStrings("Hello, world!", ps_slice); 121 | } 122 | 123 | test "PyString createFmt" { 124 | py.initialize(); 125 | defer py.finalize(); 126 | 127 | const root = @This(); 128 | const a = try PyString(root).createFmt("Hello, {s}!", .{"foo"}); 129 | defer a.decref(); 130 | 131 | try testing.expectEqualStrings("Hello, foo!", try a.asSlice()); 132 | } 133 | -------------------------------------------------------------------------------- /pydust/src/types/tuple.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 16 | const ffi = py.ffi; 17 | const PyLong = @import("long.zig").PyLong; 18 | const PyFloat = @import("float.zig").PyFloat; 19 | const PyObject = @import("obj.zig").PyObject; 20 | const PyError = @import("../errors.zig").PyError; 21 | const seq = @import("./sequence.zig"); 22 | const State = @import("../discovery.zig").State; 23 | 24 | pub fn PyTuple(comptime root: type) type { 25 | return extern struct { 26 | obj: PyObject(root), 27 | 28 | const Self = @This(); 29 | pub usingnamespace PyObjectMixin(root, "tuple", "PyTuple", Self); 30 | pub usingnamespace seq.SequenceMixin(root, Self); 31 | 32 | /// Construct a PyTuple from the given Zig tuple. 33 | pub fn create(values: anytype) !Self { 34 | const s = @typeInfo(@TypeOf(values)).@"struct"; 35 | if (!s.is_tuple and s.fields.len > 0) { 36 | @compileError("Expected a struct tuple " ++ @typeName(@TypeOf(values))); 37 | } 38 | 39 | const tuple = try new(s.fields.len); 40 | inline for (s.fields, 0..) |field, i| { 41 | // Recursively unwrap the field value 42 | try tuple.setOwnedItem(@intCast(i), try py.create(root, @field(values, field.name))); 43 | } 44 | return tuple; 45 | } 46 | 47 | /// Convert this tuple into the given Zig tuple struct. 48 | pub fn as(self: Self, comptime T: type) !T { 49 | const s = @typeInfo(T).@"struct"; 50 | const result: T = undefined; 51 | for (s.fields, 0..) |field, i| { 52 | const value = try self.getItem(field.type, i); 53 | if (value) |val| { 54 | @field(result, field.name) = val; 55 | } else if (field.defaultValue()) |default| { 56 | @field(result, field.name) = default; 57 | } else { 58 | return py.TypeError(root).raise("tuple missing field " ++ field.name ++ ": " ++ @typeName(field.type)); 59 | } 60 | } 61 | return result; 62 | } 63 | 64 | pub fn new(size: usize) !Self { 65 | const tuple = ffi.PyTuple_New(@intCast(size)) orelse return PyError.PyRaised; 66 | return .{ .obj = .{ .py = tuple } }; 67 | } 68 | 69 | pub fn length(self: *const Self) usize { 70 | return @intCast(ffi.PyTuple_Size(self.obj.py)); 71 | } 72 | 73 | pub fn getItem(self: *const Self, comptime T: type, idx: usize) !T { 74 | return self.getItemZ(T, @intCast(idx)); 75 | } 76 | 77 | pub fn getItemZ(self: *const Self, comptime T: type, idx: isize) !T { 78 | if (ffi.PyTuple_GetItem(self.obj.py, idx)) |item| { 79 | return py.as(root, T, PyObject(root){ .py = item }); 80 | } else { 81 | return PyError.PyRaised; 82 | } 83 | } 84 | 85 | /// Insert a reference to object o at position pos of the tuple. 86 | /// 87 | /// Warning: steals a reference to value. 88 | pub fn setOwnedItem(self: *const Self, pos: usize, value: anytype) !void { 89 | if (ffi.PyTuple_SetItem(self.obj.py, @intCast(pos), py.object(root, value).py) < 0) { 90 | return PyError.PyRaised; 91 | } 92 | } 93 | 94 | /// Insert a reference to object o at position pos of the tuple. Does not steal a reference to value. 95 | pub fn setItem(self: *const Self, pos: usize, value: anytype) !void { 96 | if (ffi.PyTuple_SetItem(self.obj.py, @intCast(pos), value.py) < 0) { 97 | return PyError.PyRaised; 98 | } 99 | // PyTuple_SetItem steals a reference to value. We want the default behaviour not to do that. 100 | // See setOwnedItem for an implementation that does steal. 101 | value.incref(); 102 | } 103 | }; 104 | } 105 | 106 | test "PyTuple" { 107 | py.initialize(); 108 | defer py.finalize(); 109 | 110 | const root = @This(); 111 | const first = try PyLong(root).create(1); 112 | defer first.decref(); 113 | const second = try PyFloat(root).create(1.0); 114 | defer second.decref(); 115 | 116 | var tuple = try PyTuple(root).create(.{ first.obj, second.obj }); 117 | defer tuple.decref(); 118 | 119 | try std.testing.expectEqual(@as(usize, 2), tuple.length()); 120 | 121 | try std.testing.expectEqual(@as(usize, 0), try tuple.index(second)); 122 | 123 | try std.testing.expectEqual(@as(c_long, 1), try tuple.getItem(c_long, 0)); 124 | try tuple.setItem(0, second.obj); 125 | try std.testing.expectEqual(@as(f64, 1.0), try tuple.getItem(f64, 0)); 126 | } 127 | 128 | test "PyTuple setOwnedItem" { 129 | py.initialize(); 130 | defer py.finalize(); 131 | 132 | const root = @This(); 133 | var tuple = try PyTuple(root).new(2); 134 | defer tuple.decref(); 135 | const py1 = try py.create(root, 1); 136 | defer py1.decref(); 137 | try tuple.setOwnedItem(0, py1); 138 | const py2 = try py.create(root, 2); 139 | defer py2.decref(); 140 | try tuple.setOwnedItem(1, py2); 141 | 142 | try std.testing.expectEqual(@as(u8, 1), try tuple.getItem(u8, 0)); 143 | try std.testing.expectEqual(@as(u8, 2), try tuple.getItem(u8, 1)); 144 | } 145 | -------------------------------------------------------------------------------- /pydust/src/types/type.zig: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | const std = @import("std"); 14 | const py = @import("../pydust.zig"); 15 | const PyObjectMixin = @import("./obj.zig").PyObjectMixin; 16 | const ffi = py.ffi; 17 | const PyError = @import("../errors.zig").PyError; 18 | const State = @import("../discovery.zig").State; 19 | 20 | /// Wrapper for Python PyType. 21 | /// Since PyTypeObject is opaque in the Python API, we cannot use the PyObject mixin. 22 | /// Instead, we re-implement the mixin functions and insert @ptrCast where necessary. 23 | pub fn PyType(comptime root: type) type { 24 | return extern struct { 25 | obj: py.PyObject(root), 26 | 27 | const Self = @This(); 28 | pub usingnamespace PyObjectMixin(root, "type", "PyType", Self); 29 | 30 | pub fn name(self: Self) !py.PyString(root) { 31 | return py.PyString(root).unchecked(.{ 32 | .py = ffi.PyType_GetName(typePtr(self)) orelse return PyError.PyRaised, 33 | }); 34 | } 35 | 36 | pub fn qualifiedName(self: Self) !py.PyString(root) { 37 | return py.PyString(root).unchecked(.{ 38 | .py = ffi.PyType_GetQualName(typePtr(self)) orelse return PyError.PyRaised, 39 | }); 40 | } 41 | 42 | pub fn getSlot(self: Self, slot: c_int) ?*anyopaque { 43 | return ffi.PyType_GetSlot(typePtr(self), slot); 44 | } 45 | 46 | pub fn hasFeature(self: Self, feature: u64) bool { 47 | return ffi.PyType_GetFlags(typePtr(self)) & feature != 0; 48 | } 49 | 50 | inline fn typePtr(self: Self) *ffi.PyTypeObject { 51 | return @alignCast(@ptrCast(self.obj.py)); 52 | } 53 | 54 | inline fn objPtr(obj: *ffi.PyTypeObject) *ffi.PyObject { 55 | return @alignCast(@ptrCast(obj)); 56 | } 57 | }; 58 | } 59 | 60 | test "PyType" { 61 | py.initialize(); 62 | defer py.finalize(); 63 | 64 | const root = @This(); 65 | const io = try py.import(root, "io"); 66 | defer io.decref(); 67 | 68 | const StringIO = try io.getAs(py.PyType(root), "StringIO"); 69 | try std.testing.expectEqualSlices(u8, "StringIO", try (try StringIO.name()).asSlice()); 70 | 71 | const sio = try py.call0(root, py.PyObject(root), StringIO); 72 | defer sio.decref(); 73 | const sioType = py.type_(root, sio); 74 | try std.testing.expectEqualSlices(u8, "StringIO", try (try sioType.name()).asSlice()); 75 | } 76 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "ziggy-pydust" 3 | version = "0.1.0" 4 | description = "" 5 | authors = ["Nicholas Gates "] 6 | license = "Apache 2.0" 7 | readme = "README.md" 8 | packages = [{ include = "pydust" }] 9 | include = ["src"] 10 | exclude = ["example"] 11 | 12 | [tool.poetry.plugins."pytest11"] 13 | pydust = "pydust.pytest_plugin" 14 | 15 | [tool.poetry.dependencies] 16 | python = "^3.11" 17 | ziglang = "^0.14.0" 18 | pydantic = "^2.3.0" 19 | setuptools = "^80.0.0" 20 | black = "^25.0.0" 21 | 22 | [tool.poetry.group.dev.dependencies] 23 | pytest = "^8.0.0" 24 | ruff = "^0.11.0" 25 | 26 | [tool.poetry.group.docs.dependencies] 27 | # For generating docs 28 | ziglang = "^0.14.0" 29 | mkdocs-material = "^9.2.6" 30 | mkdocs-include-markdown-plugin = "^7.1.5" 31 | mike = "^2.0.0" 32 | 33 | [tool.poetry.group.test.dependencies] 34 | numpy = "^2.0.0" 35 | 36 | [tool.poetry.scripts] 37 | pydust = "pydust.__main__:main" 38 | 39 | [tool.ruff] 40 | line-length = 120 41 | 42 | [tool.ruff.lint] 43 | select = ["F", "E", "W", "UP", "I"] 44 | # Do not auto-fix unused variables. This is really annoying when IntelliJ runs autofix while editing. 45 | unfixable = ["F841"] 46 | 47 | [build-system] 48 | requires = ["poetry-core"] 49 | build-backend = "poetry.core.masonry.api" 50 | 51 | [tool.pytest.ini_options] 52 | testpaths = ["test", "example"] 53 | 54 | # Out test modules 55 | 56 | [tool.pydust] 57 | root = "example/" 58 | build_zig = "pytest.build.zig" 59 | 60 | [[tool.pydust.ext_module]] 61 | name = "example.args_types" 62 | root = "example/args_types.zig" 63 | 64 | [[tool.pydust.ext_module]] 65 | name = "example.exceptions" 66 | root = "example/exceptions.zig" 67 | 68 | [[tool.pydust.ext_module]] 69 | name = "example.hello" 70 | root = "example/hello.zig" 71 | 72 | [[tool.pydust.ext_module]] 73 | name = "example.gil" 74 | root = "example/gil.zig" 75 | 76 | [[tool.pydust.ext_module]] 77 | name = "example.memory" 78 | root = "example/memory.zig" 79 | 80 | [[tool.pydust.ext_module]] 81 | name = "example.modules" 82 | root = "example/modules.zig" 83 | 84 | [[tool.pydust.ext_module]] 85 | name = "example.pytest" 86 | root = "example/pytest.zig" 87 | 88 | [[tool.pydust.ext_module]] 89 | name = "example.result_types" 90 | root = "example/result_types.zig" 91 | 92 | [[tool.pydust.ext_module]] 93 | name = "example.functions" 94 | root = "example/functions.zig" 95 | 96 | [[tool.pydust.ext_module]] 97 | name = "example.classes" 98 | root = "example/classes.zig" 99 | 100 | [[tool.pydust.ext_module]] 101 | name = "example.buffers" 102 | root = "example/buffers.zig" 103 | 104 | [[tool.pydust.ext_module]] 105 | name = "example.iterators" 106 | root = "example/iterators.zig" 107 | 108 | [[tool.pydust.ext_module]] 109 | name = "example.operators" 110 | root = "example/operators.zig" 111 | 112 | [[tool.pydust.ext_module]] 113 | name = "example.code" 114 | root = "example/code.zig" 115 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "local>fulcrum-so/renovate-config" 5 | ] 6 | } -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | -------------------------------------------------------------------------------- /test/conftest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import pytest 16 | 17 | from pydust import buildzig 18 | 19 | 20 | def pytest_collection(session): 21 | """We use the same pydust build system for our example modules, but we trigger it from a pytest hook.""" 22 | # We can't use a temp-file since zig build's caching depends on the file path. 23 | buildzig.zig_build(["install"]) 24 | 25 | 26 | def pytest_collection_modifyitems(session, config, items): 27 | """The Pydust Pytest plugin runs Zig tests from within the examples project. 28 | 29 | To ensure our plugin captures the failures, we have made one of those tests fail. 30 | Therefore we mark it here is "xfail" to test that it actually does so. 31 | """ 32 | for item in items: 33 | if item.nodeid == "example/pytest.zig::pytest.test.pydust-expected-failure": 34 | item.add_marker(pytest.mark.xfail(strict=True)) 35 | -------------------------------------------------------------------------------- /test/test_argstypes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | from example import args_types 16 | 17 | 18 | def test_zigstruct(): 19 | arg = {"foo": 1234, "bar": True} 20 | assert args_types.zigstruct(arg) 21 | -------------------------------------------------------------------------------- /test/test_buffers.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | from example import buffers 16 | 17 | 18 | def test_view(): 19 | buffer = buffers.ConstantBuffer(1, 10) 20 | view = memoryview(buffer) 21 | for i in range(10): 22 | assert view[i] == 1 23 | view.release() 24 | 25 | 26 | # --8<-- [start:sum] 27 | def test_sum(): 28 | import numpy as np 29 | 30 | arr = np.array([1, 2, 3, 4, 5], dtype=np.int64) 31 | assert buffers.sum(arr) == 15 32 | 33 | 34 | # --8<-- [end:sum] 35 | -------------------------------------------------------------------------------- /test/test_classes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import sys 16 | 17 | import pytest 18 | 19 | from example import classes 20 | 21 | 22 | # --8<-- [start:constructor] 23 | def test_constructor(): 24 | from example import classes 25 | 26 | assert isinstance(classes.ConstructableClass(20), classes.ConstructableClass) 27 | 28 | 29 | # --8<-- [end:constructor] 30 | 31 | 32 | def test_non_constructable(): 33 | with pytest.raises(TypeError): 34 | classes.SomeClass() 35 | 36 | 37 | # --8<-- [start:subclass] 38 | def test_subclasses(): 39 | d = classes.Dog("labrador") 40 | assert d.breed() == "labrador" 41 | assert d.species() == "dog" 42 | assert isinstance(d, classes.Animal) 43 | 44 | 45 | # --8<-- [end:subclass] 46 | 47 | 48 | # --8<-- [start:staticmethods] 49 | def test_static_methods(): 50 | assert classes.Math.add(10, 30) == 40 51 | 52 | 53 | # --8<-- [end:staticmethods] 54 | 55 | 56 | # --8<-- [start:properties] 57 | def test_properties(): 58 | u = classes.User("Dave") 59 | assert u.email is None 60 | 61 | u.email = "dave@dave.com" 62 | assert u.email == "dave@dave.com" 63 | 64 | with pytest.raises(ValueError) as exc_info: 65 | u.email = "dave" 66 | assert str(exc_info.value) == "Invalid email address for Dave" 67 | 68 | assert u.greeting == "Hello, Dave!" 69 | 70 | 71 | # --8<-- [end:properties] 72 | 73 | 74 | # --8<-- [start:attributes] 75 | def test_attributes(): 76 | c = classes.Counter() 77 | assert c.count == 0 78 | c.increment() 79 | c.increment() 80 | assert c.count == 2 81 | 82 | 83 | # --8<-- [end:attributes] 84 | 85 | 86 | def test_hash(): 87 | h = classes.Hash(42) 88 | assert hash(h) == -7849439630130923510 89 | 90 | 91 | def test_callable(): 92 | c = classes.Callable() 93 | assert c(30) == 30 94 | 95 | 96 | def test_refcnt(): 97 | # Verify that initializing a class does not leak a reference to the module. 98 | rc = sys.getrefcount(classes) 99 | classes.Hash(42) 100 | assert sys.getrefcount(classes) == rc 101 | 102 | 103 | def test_getattr(): 104 | c = classes.GetAttr() 105 | assert c.number == 42 106 | with pytest.raises(AttributeError) as exc_info: 107 | c.attr 108 | assert str(exc_info.value) == "'example.classes.GetAttr' object has no attribute 'attr'" 109 | -------------------------------------------------------------------------------- /test/test_code.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | from pathlib import Path 16 | 17 | from example import code 18 | 19 | 20 | def test_line_no(): 21 | assert code.line_number() == 21 22 | assert code.first_line_number() == 20 23 | 24 | 25 | def test_function_name(): 26 | assert code.function_name() == "test_function_name" 27 | 28 | 29 | def test_file_name(): 30 | assert Path(code.file_name()).name == "test_code.py" 31 | -------------------------------------------------------------------------------- /test/test_exceptions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import pytest 16 | 17 | from example import exceptions 18 | 19 | 20 | def test_exceptions(): 21 | with pytest.raises(ValueError) as exc: 22 | exceptions.raise_value_error("hello!") 23 | assert str(exc.value) == "hello!" 24 | 25 | 26 | def test_custom_error(): 27 | with pytest.raises(RuntimeError) as exc: 28 | exceptions.raise_custom_error() 29 | assert str(exc.value) == "Oops" 30 | -------------------------------------------------------------------------------- /test/test_functions.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import inspect 16 | 17 | import pytest 18 | 19 | from example import functions 20 | 21 | 22 | def test_double(): 23 | assert functions.double(10) == 20 24 | with pytest.raises(TypeError, match="expected int"): 25 | functions.double(0.1) 26 | 27 | 28 | def test_args_signature(): 29 | assert inspect.signature(functions.double) == inspect.Signature( 30 | [inspect.Parameter("x", kind=inspect._ParameterKind.POSITIONAL_ONLY)] 31 | ) 32 | 33 | 34 | def test_kwargs(): 35 | assert functions.with_kwargs(10.0) == 20 36 | assert functions.with_kwargs(100.0) == 42 37 | assert functions.with_kwargs(100.0, y=99.0) == 99 38 | with pytest.raises(TypeError, match="Unexpected kwarg 'k'"): 39 | functions.with_kwargs(1.0, y=9.0, k=-2) 40 | with pytest.raises(TypeError) as exc_info: 41 | functions.with_kwargs(y=9.0) 42 | assert str(exc_info.value) == "Expected 1 arg" 43 | 44 | 45 | def test_kw_signature(): 46 | assert inspect.signature(functions.with_kwargs) == inspect.Signature( 47 | [ 48 | inspect.Parameter("x", kind=inspect._ParameterKind.POSITIONAL_ONLY), 49 | inspect.Parameter("y", kind=inspect._ParameterKind.KEYWORD_ONLY, default=42.0), 50 | ] 51 | ) 52 | 53 | 54 | def test_variadic(): 55 | assert functions.variadic("world") == "Hello world with 0 varargs and 0 kwargs" 56 | assert functions.variadic("world", 1, 2, 3, a="a", b="b") == "Hello world with 3 varargs and 2 kwargs" 57 | 58 | with pytest.raises(TypeError) as exc_info: 59 | functions.variadic() 60 | assert str(exc_info.value) == "Expected 1 arg" 61 | 62 | assert functions.variadic.__text_signature__ == "(hello, /, *args, **kwargs)" 63 | assert inspect.signature(functions.variadic) == inspect.Signature( 64 | [ 65 | inspect.Parameter("hello", kind=inspect._ParameterKind.POSITIONAL_ONLY), 66 | inspect.Parameter("args", kind=inspect._ParameterKind.VAR_POSITIONAL), 67 | inspect.Parameter("kwargs", kind=inspect._ParameterKind.VAR_KEYWORD), 68 | ] 69 | ) 70 | -------------------------------------------------------------------------------- /test/test_gil.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import time 16 | from concurrent.futures import ThreadPoolExecutor 17 | 18 | from example import gil 19 | 20 | 21 | # --8<-- [start:gil] 22 | def test_gil(): 23 | now = time.time() 24 | with ThreadPoolExecutor(10) as pool: 25 | for _ in range(10): 26 | # Sleep for 100ms 27 | pool.submit(gil.sleep, 100) 28 | 29 | # This should take ~10 * 100ms. Add some leniency and check for >900ms. 30 | duration = time.time() - now 31 | assert duration > 0.9 32 | 33 | 34 | def test_gil_release(): 35 | now = time.time() 36 | with ThreadPoolExecutor(10) as pool: 37 | for _ in range(10): 38 | pool.submit(gil.sleep_release, 100) 39 | 40 | # This should take ~1 * 100ms. Add some leniency and check for <500ms. 41 | duration = time.time() - now 42 | assert duration < 0.5 43 | 44 | 45 | # --8<-- [end:gil] 46 | -------------------------------------------------------------------------------- /test/test_hello.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | # --8<-- [start:ex] 16 | from example import hello 17 | 18 | 19 | def test_hello(): 20 | assert hello.hello() == "Hello!" 21 | 22 | 23 | # --8<-- [end:ex] 24 | -------------------------------------------------------------------------------- /test/test_iterator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import pytest 16 | 17 | from example import iterators 18 | 19 | 20 | def test_range_iterator(): 21 | range_iterator = iter(iterators.Range(0, 10, 1)) 22 | for i in range(10): 23 | assert next(range_iterator) == i 24 | with pytest.raises(StopIteration): 25 | next(range_iterator) 26 | -------------------------------------------------------------------------------- /test/test_memory.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | from example import memory 16 | 17 | 18 | def test_memory_append(): 19 | assert memory.append("hello ") == "hello right" 20 | 21 | 22 | def test_memory_concat(): 23 | assert memory.concat("hello ") == "hello right" 24 | -------------------------------------------------------------------------------- /test/test_modules.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | from example import modules 16 | from example.modules import submod 17 | 18 | 19 | def test_module_docstring(): 20 | assert modules.__doc__.startswith("Zig multi-line strings make it easy to define a docstring...") 21 | 22 | 23 | def test_modules_function(): 24 | assert modules.hello("Nick") == "Hello, Nick. It's Ziggy" 25 | 26 | 27 | def test_modules_state(): 28 | assert modules.whoami() == "Ziggy" 29 | 30 | 31 | def test_modules_mutable_state(): 32 | assert modules.count() == 0 33 | modules.increment() 34 | modules.increment() 35 | assert modules.count() == 2 36 | 37 | 38 | def test_submodules(): 39 | assert submod.world() == "Hello, World!" 40 | -------------------------------------------------------------------------------- /test/test_operators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import operator 16 | 17 | import pytest 18 | 19 | from example import operators 20 | 21 | 22 | @pytest.mark.parametrize( 23 | "op,expected", 24 | [ 25 | (operator.add, 5), 26 | (operator.sub, 1), 27 | (operator.mul, 6), 28 | (operator.mod, 1), 29 | (operator.pow, 9), 30 | (operator.lshift, 12), 31 | (operator.rshift, 0), 32 | (operator.and_, 2), 33 | (operator.xor, 1), 34 | (operator.or_, 3), 35 | (operator.truediv, 1), 36 | (operator.floordiv, 1), 37 | (operator.matmul, 6), 38 | ], 39 | ) 40 | def test_ops(op, expected): 41 | ops = operators.Ops(3) 42 | other = operators.Ops(2) 43 | 44 | assert op(ops, other).num() == expected 45 | assert ops.num() == 3 46 | 47 | 48 | @pytest.mark.parametrize( 49 | "iop,expected", 50 | [ 51 | (operator.iadd, 5), 52 | (operator.isub, 1), 53 | (operator.imul, 6), 54 | (operator.imod, 1), 55 | (operator.ipow, 9), 56 | (operator.ilshift, 12), 57 | (operator.irshift, 0), 58 | (operator.iand, 2), 59 | (operator.ixor, 1), 60 | (operator.ior, 3), 61 | (operator.itruediv, 1), 62 | (operator.ifloordiv, 1), 63 | (operator.imatmul, 6), 64 | ], 65 | ) 66 | def test_iops(iop, expected): 67 | ops = operators.Ops(3) 68 | other = operators.Ops(2) 69 | 70 | iop(ops, other) 71 | assert ops.num() == expected 72 | 73 | 74 | @pytest.mark.parametrize( 75 | "op,expected", 76 | [ 77 | (operator.pos, -3), 78 | (operator.neg, 3), 79 | (operator.invert, 2), 80 | (operator.index, -3), 81 | (operator.abs, 3), 82 | (bool, False), 83 | (int, -3), 84 | (float, -3.0), 85 | ], 86 | ) 87 | def test_unaryops(op, expected): 88 | ops = operators.UnaryOps(-3) 89 | res = op(ops) 90 | 91 | if isinstance(res, operators.UnaryOps): 92 | assert res.num() == expected 93 | else: 94 | assert res == expected 95 | 96 | 97 | def test_divmod(): 98 | ops = operators.Ops(3) 99 | other = operators.Ops(2) 100 | 101 | assert divmod(ops, other) == (1, 1) 102 | 103 | 104 | # --8<-- [start:test_ops] 105 | def test_custom_ops(): 106 | op = operators.Operator(3) 107 | 108 | assert op / 2 == 1 109 | assert op / 2.0 == 1.5 110 | assert (op / operators.Operator(3)).num() == 1 111 | 112 | 113 | # --8<-- [end:test_ops] 114 | 115 | 116 | def test_lessThan(): 117 | cmp1 = operators.Comparator(0) 118 | cmp2 = operators.Comparator(1) 119 | 120 | assert cmp1 < cmp2 121 | assert cmp1 <= cmp2 122 | assert cmp1 != cmp2 123 | 124 | 125 | def test_equals(): 126 | cmp1 = operators.Comparator(1) 127 | cmp2 = operators.Comparator(1) 128 | 129 | assert cmp1 == cmp2 130 | 131 | 132 | def test_greaterThan(): 133 | cmp1 = operators.Comparator(2) 134 | cmp2 = operators.Comparator(1) 135 | 136 | assert cmp1 > cmp2 137 | assert cmp1 >= cmp2 138 | assert cmp1 != cmp2 139 | 140 | 141 | def test_justequals(): 142 | cmp1 = operators.Equals(1) 143 | cmp2 = operators.Equals(1) 144 | 145 | assert cmp1 == cmp2 146 | assert not cmp1 != cmp2 147 | assert cmp1 != operators.Equals(2) 148 | 149 | with pytest.raises(TypeError): 150 | assert cmp1 > cmp2 151 | with pytest.raises(TypeError): 152 | assert cmp1 >= cmp2 153 | with pytest.raises(TypeError): 154 | assert cmp1 < cmp2 155 | with pytest.raises(TypeError): 156 | assert cmp1 <= cmp2 157 | 158 | 159 | # Test short circuit logic in pydust that handles 160 | # cases where __eq__ expects same types but values clearly are not 161 | def test_justequals_different_type(): 162 | cmp1 = operators.Equals(1) 163 | cmp2 = 2 164 | 165 | assert not cmp1 == cmp2 166 | assert cmp1 != cmp2 167 | 168 | 169 | def test_justLessThan(): 170 | cmp1 = operators.LessThan("abc") 171 | cmp2 = operators.LessThan("abd") 172 | 173 | assert cmp1 < cmp2 174 | assert cmp1 <= cmp2 175 | assert not (cmp1 > cmp2) 176 | assert not (cmp1 >= cmp2) 177 | assert not (cmp1 == cmp2) 178 | assert cmp1 != cmp2 179 | -------------------------------------------------------------------------------- /test/test_resulttypes.py: -------------------------------------------------------------------------------- 1 | """ 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software 9 | distributed under the License is distributed on an "AS IS" BASIS, 10 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | See the License for the specific language governing permissions and 12 | limitations under the License. 13 | """ 14 | 15 | import sys 16 | 17 | import pytest 18 | 19 | from example import result_types 20 | 21 | 22 | def test_pyobject(): 23 | result = result_types.pyobject() 24 | assert result == "hello" 25 | assert sys.getrefcount(result) == 2 26 | 27 | 28 | def test_pystring(): 29 | assert result_types.pystring() == "hello world" 30 | 31 | 32 | def test_zigvoid(): 33 | result_types.zigvoid() 34 | 35 | 36 | def test_zigbool(): 37 | assert result_types.zigbool() 38 | 39 | 40 | def test_zigu32(): 41 | assert result_types.zigu32() == 32 42 | 43 | 44 | def test_zigu64(): 45 | assert result_types.zigu64() == 8589934592 46 | 47 | 48 | @pytest.mark.xfail(strict=True) 49 | def test_zigu128(): 50 | assert result_types.zigu128() == 9223372036854775809 51 | 52 | 53 | def test_zigi32(): 54 | assert result_types.zigi32() == -32 55 | 56 | 57 | def test_zigi64(): 58 | assert result_types.zigi64() == -8589934592 59 | 60 | 61 | @pytest.mark.xfail(strict=True) 62 | def test_zigi128(): 63 | assert result_types.zigi128() == -9223372036854775809 64 | 65 | 66 | def test_zigf16(): 67 | assert result_types.zigf16() == 32720.0 68 | 69 | 70 | def test_zigf32(): 71 | assert result_types.zigf32() == 2.71000028756788e38 72 | 73 | 74 | def test_zigf64(): 75 | assert result_types.zigf64() == 2.7100000000000003e39 76 | 77 | 78 | def test_zigtuple(): 79 | s, i = result_types.zigtuple() 80 | assert s == "hello" 81 | assert i == 128 82 | assert sys.getrefcount(s) == 2 83 | 84 | 85 | def test_zigstruct(): 86 | result = result_types.zigstruct() 87 | assert result == {"foo": 1234, "bar": True} 88 | assert sys.getrefcount(result) == 2 89 | -------------------------------------------------------------------------------- /test/test_zigonly.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from example import classes 4 | 5 | 6 | def test_zigonly(): 7 | zigonly = classes.ZigOnlyMethod(3) 8 | with pytest.raises(AttributeError) as exc_info: 9 | zigonly.get_number() 10 | assert str(exc_info.value) == "'example.classes.ZigOnlyMethod' object has no attribute 'get_number'" 11 | 12 | assert zigonly.reexposed() == 3 13 | --------------------------------------------------------------------------------