├── .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 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
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 |
--------------------------------------------------------------------------------