Skip to content

Commit 2535170

Browse files
authored
Merge pull request #38 from pyozig/dev
v0.11.4: Signature stub override, PyMemoryView_Check, branch quota fix
2 parents 33a6b37 + 70b71e2 commit 2535170

31 files changed

Lines changed: 368 additions & 102 deletions

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ All notable changes to PyOZ will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.11.4] - 2026-02-19
9+
10+
### Added
11+
- **`pyoz.Signature(T, "python_type")` -- stub return type override** - New comptime wrapper type that overrides the Python type annotation in generated `.pyi` stubs without affecting runtime behavior. Use this when the Zig return type doesn't map cleanly to the desired Python type, most commonly when `?T` is used for CPython exception signaling (returning `null` + `PyErr_SetString`) rather than representing Python `None`. For example, `fn probe() pyoz.Signature(?Dict, "dict[str, bool]")` generates `def probe() -> dict[str, bool]` instead of the incorrect `def probe() -> dict[str, bool] | None`. Also supports `pyoz.Signature(?void, "Never")` for functions that only raise. Works uniformly on module-level functions, class instance/static/class methods, `__call__`, `__new__`, and `allowThreads`/`allowThreadsTry`.
12+
- **`PyMemoryView_Check`** - Added type check function for `memoryview` objects, following the same `isTypeOrSubtype` pattern as other type checks. Uses `PyMemoryView_Type` which is part of the stable ABI since Python 3.2, so works across 3.8–3.13 in both normal and ABI3 modes.
13+
14+
### Fixed
15+
- **Comptime branch quota exceeded with large modules** - Modules with many functions would fail to compile with `evaluation exceeded 1000 backwards branches` in `anyFuncUsesDateTime`/`anyFuncUsesDecimal`. Fixed by setting `@setEvalBranchQuota(std.math.maxInt(u32))` in both functions.
16+
17+
### Refactored
18+
- **Type check functions** - `PySet_Check`, `PyFrozenSet_Check`, `PyBytes_Check`, `PyByteArray_Check`, and `PyObject_TypeCheck` now use the shared `isTypeOrSubtype` helper for consistency.
19+
20+
### Removed
21+
- **`method__returns__` class method stub override** - The `pub const method_name__returns__: []const u8 = "..."` convention for overriding class method return type stubs has been removed in favor of the unified `pyoz.Signature(T, "python_type")` approach, which works identically for both module-level functions and class methods.
22+
823
## [0.11.3] - 2026-02-10
924

1025
### Fixed

build.zig.zon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.{
22
.name = .PyOZ,
3-
.version = "0.11.3",
3+
.version = "0.11.4",
44
.fingerprint = 0x4d3668413e69d99e,
55
.dependencies = .{},
66
.paths = .{

docs/guide/classes.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -654,23 +654,38 @@ class Node:
654654
...
655655
```
656656

657-
### Return Type Override (`__returns__`)
657+
### Return Type Override (`Signature`)
658658

659-
When a method returns `?*pyoz.PyObject`, the stub shows `Any | None`. Override it with the concrete Python type:
659+
When a method returns `?T` only to signal errors (not to return `None`), or returns `?*pyoz.PyObject` for a complex type, the inferred stub annotation won't match the actual Python API. Use `pyoz.Signature(T, "stub_string")` as the return type to override it:
660660

661661
```zig
662662
const Node = struct {
663-
pub const children__returns__: []const u8 = "list[Node]";
663+
pub fn children(self: *const Node) pyoz.Signature(?*pyoz.PyObject, "list[Node]") {
664+
const list = buildChildList(self) orelse {
665+
_ = pyoz.raiseRuntimeError("failed to build children");
666+
return .{ .value = null };
667+
};
668+
return .{ .value = list };
669+
}
664670
665-
pub fn children(self: *const Node) ?*pyoz.PyObject { ... }
671+
pub fn find(self: *const Node, name: []const u8) pyoz.Signature(?Node, "Node") {
672+
// null signals an error, not a None return
673+
return .{ .value = self.doFind(name) orelse {
674+
_ = pyoz.raiseKeyError("not found");
675+
return .{ .value = null };
676+
}};
677+
}
666678
};
667679
```
668680

669681
Generated stub:
670682
```python
671683
def children(self) -> list[Node]: ...
684+
def find(self, arg0: str) -> Node: ...
672685
```
673686

687+
`Signature` works the same way for instance methods, static methods, and class methods. See [Return Type Override](stubs.md#return-type-override-signature) for full details.
688+
674689
### Parameter Names (`__params__`)
675690

676691
Zig's `@typeInfo` does not expose function parameter names, so stubs default to `arg0, arg1, ...`. Override with actual names:

docs/guide/functions.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Struct fields with defaults become optional keyword arguments. Fields without de
7575
| `struct { T, U }` | `tuple` |
7676
| `[]const T` | `list` |
7777
| `pyoz.Owned(T)` | Same as `T` (frees backing memory) |
78+
| `pyoz.Signature(T, "S")` | Stub shows `S` instead of inferred type |
7879

7980
## Error Handling
8081

@@ -117,6 +118,19 @@ fn heavy_compute(n: i64) i64 {
117118

118119
See [GIL Management](gil.md) for details.
119120

121+
## Stub Return Type Override
122+
123+
When a function returns `?T` only to signal errors (not to return `None` to Python), the generated stub shows `T | None` — which is misleading. Use `pyoz.Signature(T, "stub_string")` to override the stub annotation:
124+
125+
```zig
126+
fn validate(n: i64) pyoz.Signature(?i64, "int") {
127+
if (n < 0) return pyoz.raiseValueError("must be non-negative");
128+
return .{ .value = n };
129+
}
130+
```
131+
132+
The stub shows `-> int` instead of `-> int | None`. At runtime, `Signature` is transparent — PyOZ unwraps the `.value` field automatically. See [Type Stubs: Return Type Override](stubs.md#return-type-override-signature) for more details.
133+
120134
## Docstrings
121135

122136
The third argument to function registrations becomes the Python docstring:

docs/guide/stubs.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ PyOZ generates stubs for all exported items:
3333
| `[]const u8` | `str` |
3434
| `void` | `None` |
3535
| `?T` | `T \| None` |
36+
| `pyoz.Signature(?T, "U")` | `U` (user-defined override) |
3637
| `!T` | `T` |
3738
| `pyoz.Complex` | `complex` |
3839
| `pyoz.Date` | `datetime.date` |
@@ -110,6 +111,63 @@ mypy my_script.py
110111
pyright my_script.py
111112
```
112113

114+
## Return Type Override (`Signature`)
115+
116+
Sometimes the automatically inferred stub type doesn't match the intended Python-level API. The most common case: a function returns `?T` (optional) only to signal errors via `null`, but the Python caller never sees `None` — they see a raised exception instead. The stub would show `T | None` when it should just show `T`.
117+
118+
Use `pyoz.Signature(T, "stub_string")` as the return type to override the stub annotation while preserving runtime behavior:
119+
120+
```zig
121+
fn validate_positive(n: i64) pyoz.Signature(?i64, "int") {
122+
if (n < 0) {
123+
_ = pyoz.raiseValueError("must be non-negative");
124+
return .{ .value = null };
125+
}
126+
return .{ .value = n };
127+
}
128+
```
129+
130+
Generated stub:
131+
```python
132+
def validate_positive(arg0: int) -> int: ...
133+
```
134+
135+
Without `Signature`, the stub would show `-> int | None`.
136+
137+
### How It Works
138+
139+
`Signature` is a comptime wrapper type. At runtime it's a struct with a single `.value` field — PyOZ unwraps it automatically, so Python never sees the wrapper. The second parameter (the string) is used verbatim as the return type annotation in the generated `.pyi` file.
140+
141+
### Works Everywhere
142+
143+
`Signature` works identically for module-level functions and class methods (instance, static, and class methods):
144+
145+
```zig
146+
const Parser = struct {
147+
pub fn parse(self: *const Parser, input: []const u8) pyoz.Signature(?Node, "Node") {
148+
// null signals an error, not a None return
149+
if (input.len == 0) {
150+
_ = pyoz.raiseValueError("empty input");
151+
return .{ .value = null };
152+
}
153+
return .{ .value = self.doParse(input) };
154+
}
155+
};
156+
```
157+
158+
Generated stub:
159+
```python
160+
def parse(self, arg0: str) -> Node: ...
161+
```
162+
163+
### When to Use
164+
165+
| Scenario | Without Signature | With Signature |
166+
|----------|-------------------|----------------|
167+
| `?T` where `null` = error | `T \| None` | Use `Signature(?T, "T")` |
168+
| Raw `*PyObject` return | `Any` | Use `Signature(*PyObject, "list[Node]")` |
169+
| Complex generic return | `Any` | Use `Signature(*PyObject, "dict[str, list[int]]")` |
170+
113171
## Limitations
114172

115173
- Generic types may show as `Any` for complex cases

docs/reference/api.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,27 @@ return pyoz.owned(allocator, data); // returns Owned([]const u8)
276276

277277
Supports `!Owned(T)` (error union) and `?Owned(T)` (optional) return types.
278278

279+
## Stub Return Type Override
280+
281+
### `pyoz.Signature(T, "stub_string")`
282+
283+
Override the `.pyi` stub return type annotation while preserving runtime behavior. `T` is the actual Zig return type; `"stub_string"` is written verbatim into the generated stub.
284+
285+
```zig
286+
fn validate(n: i64) pyoz.Signature(?i64, "int") {
287+
if (n < 0) return pyoz.raiseValueError("must be non-negative");
288+
return .{ .value = n };
289+
}
290+
```
291+
292+
At runtime, `Signature` is a struct with a `.value` field — PyOZ unwraps it automatically. Works for module-level functions and class methods (instance, static, class).
293+
294+
| Usage | Stub Output |
295+
|-------|-------------|
296+
| `pyoz.Signature(?i64, "int")` | `-> int` |
297+
| `pyoz.Signature(?*PyObject, "list[Node]")` | `-> list[Node]` |
298+
| `pyoz.Signature(?Node, "Node")` | `-> Node` |
299+
279300
## Strong References
280301

281302
### `pyoz.Ref(T)`

examples/example_module.zig

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ fn validate_positive(n: i64) ?i64 {
4444
return n;
4545
}
4646

47+
/// Validate positive with Signature override - stubs should show `int` not `int | None`
48+
fn validate_positive_sig(n: i64) pyoz.Signature(?i64, "int") {
49+
if (n < 0) {
50+
Example.getException(0).raise("Value must be non-negative");
51+
return .{ .value = null };
52+
}
53+
return .{ .value = n };
54+
}
55+
4756
/// Safe divide using custom exception
4857
fn safe_divide(a: f64, b: f64) ?f64 {
4958
if (b == 0.0) {
@@ -2957,6 +2966,7 @@ const Example = pyoz.module(.{
29572966
pyoz.func("multiply", multiply, "Multiply two floats"),
29582967
pyoz.func("divide", divide, "Divide two numbers (raises error if b=0)"),
29592968
pyoz.func("validate_positive", validate_positive, "Validate that a number is non-negative"),
2969+
pyoz.func("validate_positive_sig", validate_positive_sig, "Validate positive (Signature override)"),
29602970
pyoz.func("safe_divide", safe_divide, "Divide with custom exception on zero"),
29612971
pyoz.kwfunc("greet_person", greet_person, "Greet a person with optional greeting and times"),
29622972
pyoz.kwfunc("power", power, "Calculate base^exponent (default exponent=2)"),

pypi/build.zig.zon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.{
22
.name = .pyoz,
3-
.version = "0.11.3",
3+
.version = "0.11.4",
44
.fingerprint = 0x43eec3150282fd1f,
55
.dependencies = .{
66
.PyOZ = .{

pypi/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "pyoz"
7-
version = "0.11.3"
7+
version = "0.11.4"
88
description = "Python extension modules in Zig, made easy"
99
readme = "README.md"
1010
license = "MIT"

pypi/setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = pyoz
3-
version = 0.11.3
3+
version = 0.11.4
44

55
[options]
66
packages = pyoz

0 commit comments

Comments
 (0)