feat: PEP 489 multi-phase module initialization #43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - "src/lib/**" | |
| - "src/cli/**" | |
| - "src/tests.zig" | |
| - "src/tests_abi3.zig" | |
| - "examples/**" | |
| - "build.zig" | |
| - "build.zig.zon" | |
| - ".github/workflows/ci.yml" | |
| jobs: | |
| format: | |
| name: Check Formatting | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Zig | |
| uses: goto-bus-stop/setup-zig@v2 | |
| with: | |
| version: 0.15.2 | |
| - name: Check format | |
| run: zig fmt --check src/ build.zig examples/ | |
| test: | |
| name: Test (Python ${{ matrix.python-version }}) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Zig | |
| uses: goto-bus-stop/setup-zig@v2 | |
| with: | |
| version: 0.15.2 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install numpy | |
| run: pip install numpy | |
| - name: Cache Zig | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/zig | |
| zig-cache | |
| key: zig-cache-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('build.zig', 'build.zig.zon') }} | |
| restore-keys: | | |
| zig-cache-${{ runner.os }}-py${{ matrix.python-version }}- | |
| - name: Run tests | |
| run: zig build test | |
| test-abi3: | |
| name: Test ABI3 (Python ${{ matrix.python-version }}) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Zig | |
| uses: goto-bus-stop/setup-zig@v2 | |
| with: | |
| version: 0.15.2 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install numpy | |
| run: pip install numpy | |
| - name: Cache Zig | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/zig | |
| zig-cache | |
| key: zig-cache-${{ runner.os }}-abi3-py${{ matrix.python-version }}-${{ hashFiles('build.zig', 'build.zig.zon') }} | |
| restore-keys: | | |
| zig-cache-${{ runner.os }}-abi3-py${{ matrix.python-version }}- | |
| - name: Run ABI3 tests | |
| run: zig build test_abi3 | |
| build: | |
| name: Build Library & CLI | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Zig | |
| uses: goto-bus-stop/setup-zig@v2 | |
| with: | |
| version: 0.15.2 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.11" | |
| - name: Cache Zig | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/zig | |
| zig-cache | |
| key: zig-cache-${{ runner.os }}-build-${{ hashFiles('build.zig', 'build.zig.zon') }} | |
| restore-keys: | | |
| zig-cache-${{ runner.os }}-build- | |
| - name: Build library | |
| run: zig build | |
| - name: Build CLI | |
| run: zig build cli | |
| example-module: | |
| name: Example Module (${{ matrix.os }}, Python ${{ matrix.python-version }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| python-version: ["3.10", "3.11", "3.12", "3.13"] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Zig | |
| uses: goto-bus-stop/setup-zig@v2 | |
| with: | |
| version: 0.15.2 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install numpy | |
| run: pip install numpy | |
| - name: Cache Zig | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/zig | |
| zig-cache | |
| key: zig-cache-${{ runner.os }}-example-py${{ matrix.python-version }}-${{ hashFiles('build.zig', 'build.zig.zon') }} | |
| restore-keys: | | |
| zig-cache-${{ runner.os }}-example-py${{ matrix.python-version }}- | |
| - name: Build example module | |
| run: zig build example | |
| - name: Test example module (Unix) | |
| if: runner.os != 'Windows' | |
| run: | | |
| python -c " | |
| import sys | |
| sys.path.insert(0, 'zig-out/lib') | |
| import example | |
| # Basic functions | |
| assert example.add(2, 3) == 5, 'add failed' | |
| assert example.multiply(4, 5) == 20.0, 'multiply failed' | |
| assert example.is_even(4) == True, 'is_even failed' | |
| assert example.greet('World') == 'Hello from Zig!', 'greet failed' | |
| assert example.answer() == 42, 'answer failed' | |
| # Point class | |
| p1 = example.Point(3.0, 4.0) | |
| assert p1.x == 3.0 and p1.y == 4.0, 'Point init failed' | |
| assert p1.magnitude() == 5.0, 'magnitude failed' | |
| p2 = example.Point(1.0, 2.0) | |
| p3 = p1 + p2 | |
| assert p3.x == 4.0 and p3.y == 6.0, 'Point add failed' | |
| assert (p1 == example.Point(3.0, 4.0)), 'Point eq failed' | |
| # FrozenPoint (immutable) | |
| fp = example.FrozenPoint(1.0, 2.0) | |
| try: | |
| fp.x = 5.0 | |
| assert False, 'FrozenPoint should be immutable' | |
| except AttributeError: | |
| pass | |
| # Stack (__base__ inheritance from list) | |
| s = example.Stack() | |
| s.push(1) | |
| s.push(2) | |
| s.append(3) # inherited from list | |
| assert len(s) == 3, 'Stack len failed' | |
| assert list(s) == [1, 2, 3], 'Stack content failed' | |
| # DefaultDict (__base__ inheritance from dict) | |
| dd = example.DefaultDict() | |
| dd['key'] = 100 | |
| assert dd['key'] == 100, 'DefaultDict get failed' | |
| assert dd['missing'] == 'missing', 'DefaultDict __missing__ failed' | |
| # Circle class attributes | |
| assert example.Circle.PI > 3.14, 'Circle.PI failed' | |
| c = example.Circle(5.0) | |
| assert c.area() > 78, 'Circle.area failed' | |
| # Iterator views | |
| assert example.iter_sum([1, 2, 3, 4, 5]) == 15, 'iter_sum failed' | |
| assert example.iter_sum(x for x in range(1, 6)) == 15, 'iter_sum generator failed' | |
| assert example.iter_max([3, 1, 4, 1, 5, 9]) == 9, 'iter_max failed' | |
| # Dict/Set views | |
| assert example.dict_len({'a': 1, 'b': 2}) == 2, 'dict_len failed' | |
| assert example.sum_dict_values({'a': 1, 'b': 2, 'c': 3}) == 6, 'sum_dict_values failed' | |
| assert example.set_len({1, 2, 3}) == 3, 'set_len failed' | |
| assert example.sum_set({1, 2, 3, 4, 5}) == 15, 'sum_set failed' | |
| # DateTime types (make_date returns hardcoded 2024-07-04) | |
| d = example.make_date() | |
| assert d.year == 2024 and d.month == 7 and d.day == 4, 'make_date failed' | |
| # Error handling | |
| try: | |
| example.divide(1.0, 0.0) | |
| assert False, 'divide by zero should raise' | |
| except RuntimeError: | |
| pass | |
| # Custom exception | |
| try: | |
| example.validate_positive(-5) | |
| assert False, 'validate_positive should raise' | |
| except example.ValidationError: | |
| pass | |
| # NumPy (if available) | |
| import numpy as np | |
| arr = np.array([1.0, 2.0, 3.0, 4.0, 5.0], dtype=np.float64) | |
| assert example.numpy_sum(arr) == 15.0, 'numpy_sum failed' | |
| assert example.numpy_mean(arr) == 3.0, 'numpy_mean failed' | |
| arr2 = np.array([1.0, 2.0, 3.0], dtype=np.float64) | |
| example.numpy_scale(arr2, 2.0) | |
| assert list(arr2) == [2.0, 4.0, 6.0], 'numpy_scale failed' | |
| # Submodule | |
| assert example.math.factorial(5) == 120, 'math.factorial failed' | |
| assert example.math.gcd(12, 8) == 4, 'math.gcd failed' | |
| assert example.math.is_prime(17) == True, 'math.is_prime failed' | |
| print('All tests passed!') | |
| " | |
| - name: Test example module (Windows) | |
| if: runner.os == 'Windows' | |
| run: | | |
| python -c "import sys; sys.path.insert(0, 'zig-out/bin'); import example; assert example.add(2, 3) == 5; print('Basic import works')" | |
| python -c @" | |
| import sys | |
| sys.path.insert(0, 'zig-out/bin') | |
| import example | |
| # Basic functions | |
| assert example.add(2, 3) == 5, 'add failed' | |
| assert example.multiply(4, 5) == 20.0, 'multiply failed' | |
| assert example.is_even(4) == True, 'is_even failed' | |
| assert example.greet('World') == 'Hello from Zig!', 'greet failed' | |
| assert example.answer() == 42, 'answer failed' | |
| # Point class | |
| p1 = example.Point(3.0, 4.0) | |
| assert p1.x == 3.0 and p1.y == 4.0, 'Point init failed' | |
| assert p1.magnitude() == 5.0, 'magnitude failed' | |
| p2 = example.Point(1.0, 2.0) | |
| p3 = p1 + p2 | |
| assert p3.x == 4.0 and p3.y == 6.0, 'Point add failed' | |
| assert (p1 == example.Point(3.0, 4.0)), 'Point eq failed' | |
| # FrozenPoint (immutable) | |
| fp = example.FrozenPoint(1.0, 2.0) | |
| try: | |
| fp.x = 5.0 | |
| assert False, 'FrozenPoint should be immutable' | |
| except AttributeError: | |
| pass | |
| # Stack (__base__ inheritance from list) | |
| s = example.Stack() | |
| s.push(1) | |
| s.push(2) | |
| s.append(3) | |
| assert len(s) == 3, 'Stack len failed' | |
| assert list(s) == [1, 2, 3], 'Stack content failed' | |
| # DefaultDict (__base__ inheritance from dict) | |
| dd = example.DefaultDict() | |
| dd['key'] = 100 | |
| assert dd['key'] == 100, 'DefaultDict get failed' | |
| assert dd['missing'] == 'missing', 'DefaultDict __missing__ failed' | |
| # Circle class attributes | |
| assert example.Circle.PI > 3.14, 'Circle.PI failed' | |
| c = example.Circle(5.0) | |
| assert c.area() > 78, 'Circle.area failed' | |
| # Iterator views | |
| assert example.iter_sum([1, 2, 3, 4, 5]) == 15, 'iter_sum failed' | |
| assert example.iter_sum(x for x in range(1, 6)) == 15, 'iter_sum generator failed' | |
| assert example.iter_max([3, 1, 4, 1, 5, 9]) == 9, 'iter_max failed' | |
| # Dict/Set views | |
| assert example.dict_len({'a': 1, 'b': 2}) == 2, 'dict_len failed' | |
| assert example.sum_dict_values({'a': 1, 'b': 2, 'c': 3}) == 6, 'sum_dict_values failed' | |
| assert example.set_len({1, 2, 3}) == 3, 'set_len failed' | |
| assert example.sum_set({1, 2, 3, 4, 5}) == 15, 'sum_set failed' | |
| # DateTime types (make_date returns hardcoded 2024-07-04) | |
| d = example.make_date() | |
| assert d.year == 2024 and d.month == 7 and d.day == 4, 'make_date failed' | |
| # Error handling | |
| try: | |
| example.divide(1.0, 0.0) | |
| assert False, 'divide by zero should raise' | |
| except RuntimeError: | |
| pass | |
| # Custom exception | |
| try: | |
| example.validate_positive(-5) | |
| assert False, 'validate_positive should raise' | |
| except example.ValidationError: | |
| pass | |
| # NumPy (if available) | |
| import numpy as np | |
| arr = np.array([1.0, 2.0, 3.0, 4.0, 5.0], dtype=np.float64) | |
| assert example.numpy_sum(arr) == 15.0, 'numpy_sum failed' | |
| assert example.numpy_mean(arr) == 3.0, 'numpy_mean failed' | |
| arr2 = np.array([1.0, 2.0, 3.0], dtype=np.float64) | |
| example.numpy_scale(arr2, 2.0) | |
| assert list(arr2) == [2.0, 4.0, 6.0], 'numpy_scale failed' | |
| # Submodule | |
| assert example.math.factorial(5) == 120, 'math.factorial failed' | |
| assert example.math.gcd(12, 8) == 4, 'math.gcd failed' | |
| assert example.math.is_prime(17) == True, 'math.is_prime failed' | |
| print('All tests passed!') | |
| "@ |