github pyozig/PyOZ v0.11.0
PyOZ v0.11.0

latest releases: v0.12.2, v0.12.1, v0.12.0...
6 months ago

What's New in v0.11.0

Added

  • pyoz init --package -- Python package directory layout - New --package flag for pyoz init that scaffolds a project with a proper Python package directory. Instead of installing a flat .so directly into site-packages, the extension is placed inside a package directory with an __init__.py that re-exports all native symbols. The native module is automatically prefixed with an underscore (e.g., _myproject.so) to avoid name collisions with the package directory. pyoz build and pyoz develop automatically detect package mode when module-name starts with _ and a py-packages entry matches the project name, placing the .so and .pyi inside the package directory in wheels and development installs. This enables combining native extensions with pure Python code in the same importable package.
  • pyoz.Owned(T) -- allocator-backed return types - New generic wrapper for returning heap-allocated data from Zig functions and methods. Owned(T) pairs a value with its allocator; PyOZ converts the inner value to a Python object then automatically frees the backing memory. This eliminates the need for fixed-size stack buffers when building dynamic strings or data. The pyoz.owned(allocator, value) constructor auto-coerces mutable slices ([]u8) to const ([]const u8), so std.fmt.allocPrint results can be returned directly without @as casts. Supports all return type wrappers: !Owned(T) (error union), ?Owned(T) (optional). Works with any slice type that toPy handles.
  • pyoz.fmt() -- inline string formatter - New utility function for formatting strings using Zig's std.fmt syntax. Returns a [*:0]const u8 suitable for passing to PyErr_SetString, raise functions, or any API that copies the string immediately. The 4096-byte buffer lives in the caller's stack frame (the function is inline), so it is safe to use in one-liners like return pyoz.raiseValueError(pyoz.fmt("value {d} exceeds limit {d}", .{ val, limit })). Eliminates the need for manual bufPrintZ boilerplate when building dynamic error messages.
  • pyoz.base(Parent) -- single inheritance between PyOZ classes - New function for declaring that one PyOZ-defined Zig struct inherits from another. The child struct declares pub const __base__ = pyoz.base(Animal); and embeds the parent as _parent: Animal (must be the first field). PyOZ sets tp_base to the parent's type object so isinstance(), Python's MRO, and method/property inheritance all work automatically. The child's __init__ accepts a flattened argument list (parent fields first, then child fields). Parent methods and properties are inherited via MRO — no duplication needed. Works in both non-ABI3 (static type object) and ABI3 (PyType_FromSpecWithBases) modes. Comptime validation ensures correct struct layout and parent registration order. Stub generation emits class Dog(Animal): with the correct flattened __init__ signature.
  • pyoz test -- inline embedded tests - New CLI command that builds the module, extracts embedded Python test code from the compiled .so, and runs it with unittest (stdlib, zero dependencies). Tests are defined inline in the Zig module definition using pyoz.@"test"("name", \\body) for assertion tests and pyoz.testRaises("name", "ExceptionType", \\body) for exception tests. The generated Python file uses unittest.TestCase with proper assertRaises context managers. Supports --verbose/-v for detailed output and --release/-r to build in release mode before testing.
  • pyoz bench -- inline embedded benchmarks - New CLI command that builds the module in release mode, extracts embedded Python benchmark code, and runs it with timeit (stdlib). Benchmarks are defined inline using pyoz.bench("name", \\body). The generated script times each benchmark over 100,000 iterations and prints a formatted results table with ops/s. Both commands are available in the Zig CLI (src/cli) and Python wrapper (pyoz test / pyoz bench).
  • pyoz.TestDef and pyoz.BenchDef types - New struct types for defining inline tests and benchmarks. pyoz.@"test"() creates assertion-based tests, pyoz.testRaises() creates exception-checking tests, and pyoz.bench() creates benchmarks. These are passed to pyoz.module() via the new .tests and .benchmarks optional config fields.
  • Binary section embedding for tests and benchmarks - Test and benchmark Python code is generated at comptime and embedded into the compiled .so as named sections (.pyoztest / .pyozbenc on ELF/PE, __DATA,__pyoztest / __DATA,__pyozbenc on Mach-O), using the same magic-header pattern as stubs (PYOZTEST / PYOZBENC + 8-byte LE length + content).
  • Generic section extraction in symreader.zig - New extractNamedSection() infrastructure that parameterizes section name and magic string across ELF/PE/Mach-O formats. extractTests() and extractBenchmarks() are thin wrappers. Existing extractStubs() is unchanged.
  • Syntax checking before test/bench execution - pyoz test and pyoz bench now run python3 -m py_compile on the generated Python file before executing it. If the user's inline test/benchmark code has syntax errors, a clear error message with line numbers is shown instead of a confusing runtime traceback.

Fixed

  • __hash__ correctness for classes defining __eq__ - When a class defines __eq__ (or any comparison dunder) without explicitly defining __hash__, PyOZ now sets tp_hash = PyObject_HashNotImplemented, making instances correctly unhashable (raises TypeError on hash(), cannot be added to sets or used as dict keys). Previously, these classes silently retained the default id-based hash, violating Python semantics. This fix works for both ABI3 and non-ABI3 modes. Classes that define both __eq__ and __hash__ continue to work as before.
  • Computed property setters returning ?void or !void caused compile error - When a set_X computed property setter returned an optional (?void) or error union (!void) instead of plain void, the generated wrapper in properties.zig discarded the return value, which Zig rejects for non-void types. This prevented using the return pyoz.raiseValueError("msg") one-liner pattern in property setters. All three setter code paths (generateSetter for field-based custom setters, generateComputedSetter for computed properties, and generatePyozPropertySetter for pyoz.property() API setters) now handle ?void, !void, and plain void return types using the same three-branch dispatch pattern used throughout the rest of the codebase (attributes.zig, descriptor.zig, sequence.zig, etc.). Also fixed generateSetter's existing error union branch to preserve already-set Python exceptions instead of overwriting them.
  • Zig errors now map to correct Python exception types - Previously, all Zig errors (including error.TypeError, error.IndexOutOfBounds, error.DivisionByZero, etc.) were incorrectly raised as RuntimeError in Python. Now setError() in wrappers.zig and setErrorFromMapping() in errors.zig use a new mapWellKnownError() function that first tries an exact match against all ExcBase enum variants (covering all 50+ standard Python exceptions like TypeError, ValueError, IndexError, KeyError, ZeroDivisionError, AttributeError, FileNotFoundError, PermissionError, MemoryError, NotImplementedError, StopIteration, etc.), then checks common Zig-idiomatic aliases (DivisionByZero -> ZeroDivisionError, OutOfMemory -> MemoryError, IndexOutOfBounds -> IndexError, KeyNotFound -> KeyError, FileNotFound -> FileNotFoundError, PermissionDenied -> PermissionError, etc.), and falls back to RuntimeError only for truly unrecognized errors.

Installation

Download the binary for your platform and add it to your PATH:

Platform Binary
Linux x86_64 pyoz-x86_64-linux
Linux ARM64 pyoz-aarch64-linux
macOS x86_64 pyoz-x86_64-macos
macOS ARM64 (Apple Silicon) pyoz-aarch64-macos
Windows x86_64 pyoz-x86_64-windows.exe
Windows ARM64 pyoz-aarch64-windows.exe

Source

Download PyOZ-0.11.0.tar.gz for the source code.

Quick Start

pyoz init mymodule
cd mymodule
pyoz build
pip install dist/*.whl

Don't miss a new PyOZ release

NewReleases is sending notifications on new releases.