From 45f4fc9f03119f3d2bca9539112a0274a04251d3 Mon Sep 17 00:00:00 2001 From: Ethan Lin Date: Thu, 27 Aug 2026 20:53:25 +0800 Subject: [PATCH 1/4] ci(python): fold the build task into test The build task compiled the wheel and threw it away: its dist/ never reached the artifact upload, which only runs under test. Building it in test instead keeps the same coverage on one runner and makes the wheel reach the upload it was always listed in. Closes #3977 --- .../python-maturin/pre-merge/action.yml | 32 +++++++++---------- .github/config/components.yml | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/actions/python-maturin/pre-merge/action.yml b/.github/actions/python-maturin/pre-merge/action.yml index f97462f34d..5b03f52bc3 100644 --- a/.github/actions/python-maturin/pre-merge/action.yml +++ b/.github/actions/python-maturin/pre-merge/action.yml @@ -20,7 +20,7 @@ description: Python pre-merge testing with maturin github iggy actions inputs: task: - description: "Task to run (lint, test, build)" + description: "Task to run (lint, test)" required: true runs: @@ -90,6 +90,21 @@ runs: echo "pyrefly version: $(uv run pyrefly --version)" shell: bash + - name: Build Python wheel + if: inputs.task == 'test' + run: | + cd foreign/python + + # Build the module + echo "Building Python wheel..." + uv run maturin build -o dist + + # List built artifacts + echo "" + echo "Build artifacts:" + ls -la dist/ + shell: bash + - name: Build Python wheel with coverage instrumentation if: inputs.task == 'test' run: | @@ -105,21 +120,6 @@ runs: uv run --no-sync maturin develop shell: bash - - name: Build Python wheel - if: inputs.task == 'build' - run: | - cd foreign/python - - # Build the module - echo "Building Python wheel..." - uv run maturin build -o dist - - # List built artifacts - echo "" - echo "Build artifacts:" - ls -la dist/ - shell: bash - - name: Build server Docker image for TLS tests if: inputs.task == 'test' run: | diff --git a/.github/config/components.yml b/.github/config/components.yml index 5fef0e88a0..082aec83b5 100644 --- a/.github/config/components.yml +++ b/.github/config/components.yml @@ -232,7 +232,7 @@ components: - "ci-infrastructure" # CI changes trigger full regression paths: - "foreign/python/**" - tasks: ["lint", "test", "build"] + tasks: ["lint", "test"] sdk-php: depends_on: From 31f9983778c9ff599c6e035e959cd8ff4bda4e23 Mon Sep 17 00:00:00 2001 From: Ethan Lin Date: Thu, 27 Aug 2026 21:34:10 +0800 Subject: [PATCH 2/4] fix(python): regenerate apache_iggy.pyi from the pyo3 source The tracked stub had drifted from stub_gen output. Two docstrings were hand-edited to drop a rustdoc intra-doc link, which renders as noise in Python, so the link is now plain backticks at the source instead. --- foreign/python/apache_iggy.pyi | 50 +++++++++++++++++----------------- foreign/python/src/topic.rs | 4 +-- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/foreign/python/apache_iggy.pyi b/foreign/python/apache_iggy.pyi index 254b23f41c..6c3715b54b 100644 --- a/foreign/python/apache_iggy.pyi +++ b/foreign/python/apache_iggy.pyi @@ -872,6 +872,27 @@ class IggyClient: Sends a ping request to the server to check connectivity. Raises `RuntimeError` if the connection fails. """ + def describe_options( + self, scope: builtins.str + ) -> collections.abc.Awaitable[list[OptionSpec]]: + r""" + Describe the option catalog for a resource scope. + + This is the discovery surface for the `options` argument on + `create_topic`/`update_topic`: a key outside the catalog is refused at + create, and the binary transports carry only the error code back. + + Args: + scope: One of `"topic"`, `"stream"`, `"user"`. + + Returns: + An awaitable that resolves to `list[OptionSpec]`, empty for a scope + with no keys yet. + + Raises: + ValueError: If the scope name is not one of the three above. + RuntimeError: If the request fails. + """ def login_user( self, username: builtins.str, password: builtins.str ) -> collections.abc.Awaitable[None]: @@ -1034,27 +1055,6 @@ class IggyClient: Returns the stream details, or `None` if the stream does not exist. Raises `RuntimeError` on failure. """ - def describe_options( - self, scope: builtins.str - ) -> collections.abc.Awaitable[builtins.list[OptionSpec]]: - r""" - Describe the option catalog for a resource scope. - - This is the discovery surface for the `options` argument on - `create_topic`/`update_topic`: a key outside the catalog is refused at - create, and the binary transports carry only the error code back. - - Args: - scope: One of `"topic"`, `"stream"`, `"user"`. - - Returns: - An awaitable that resolves to `list[OptionSpec]`, empty for a scope - with no keys yet. - - Raises: - ValueError: If the scope name is not one of the three above. - RuntimeError: If the request fails. - """ def create_topic( self, stream: builtins.str | builtins.int, @@ -2103,8 +2103,8 @@ class Topic: r""" Options admission resolved for the keys the client did not send. - Same shape as `options`. These would have resolved differently under - another server configuration. + Same shape as `options`. These would have resolved differently + under another server configuration. """ @typing.final @@ -2168,8 +2168,8 @@ class TopicDetails: r""" Options admission resolved for the keys the client did not send. - Same shape as `options`. These would have resolved differently under - another server configuration. + Same shape as `options`. These would have resolved differently + under another server configuration. """ @property def partitions(self) -> builtins.list[Partition]: diff --git a/foreign/python/src/topic.rs b/foreign/python/src/topic.rs index e5f86facd8..0d6e35c803 100644 --- a/foreign/python/src/topic.rs +++ b/foreign/python/src/topic.rs @@ -254,7 +254,7 @@ impl Topic { /// Options admission resolved for the keys the client did not send. /// - /// Same shape as [`Self::options`]. These would have resolved differently + /// Same shape as `options`. These would have resolved differently /// under another server configuration. #[getter] pub fn derived_options<'a>(&self, py: Python<'a>) -> PyResult> { @@ -345,7 +345,7 @@ impl TopicDetails { /// Options admission resolved for the keys the client did not send. /// - /// Same shape as [`Self::options`]. These would have resolved differently + /// Same shape as `options`. These would have resolved differently /// under another server configuration. #[getter] pub fn derived_options<'a>(&self, py: Python<'a>) -> PyResult> { From 11e73dafb3e4ea687530078efcdf4d071b6383f6 Mon Sep 17 00:00:00 2001 From: Ethan Lin Date: Thu, 27 Aug 2026 21:34:48 +0800 Subject: [PATCH 3/4] ci(python): check apache_iggy.pyi is in sync Nothing verified stub freshness, so the file could drift from the pyo3 source unnoticed. The check needs a built crate, so it sits in test next to the wheel build rather than in lint. --- .../python-maturin/pre-merge/action.yml | 18 ++++++++++++++++++ foreign/python/README.md | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/actions/python-maturin/pre-merge/action.yml b/.github/actions/python-maturin/pre-merge/action.yml index 5b03f52bc3..6a28d0e5be 100644 --- a/.github/actions/python-maturin/pre-merge/action.yml +++ b/.github/actions/python-maturin/pre-merge/action.yml @@ -105,6 +105,24 @@ runs: ls -la dist/ shell: bash + - name: Check apache_iggy.pyi is up to date + if: inputs.task == 'test' + run: | + # stub_gen must run from foreign/python; a subdirectory corrupts the stub. + cd foreign/python + cargo run --bin stub_gen + + # The tracked stub is post-ruff. Format first: the raw output leaves + # whitespace on blank lines, which `check` reports but only `format` fixes. + uv run --no-sync ruff format apache_iggy.pyi + uv run --no-sync ruff check --fix apache_iggy.pyi + + if ! git diff --exit-code -- apache_iggy.pyi; then + echo "::error::apache_iggy.pyi is out of date. Run 'cargo run --bin stub_gen' from foreign/python, let the ruff hooks format it, and commit the result." + exit 1 + fi + shell: bash + - name: Build Python wheel with coverage instrumentation if: inputs.task == 'test' run: | diff --git a/foreign/python/README.md b/foreign/python/README.md index dbbdbe02ca..0dc06883e9 100644 --- a/foreign/python/README.md +++ b/foreign/python/README.md @@ -92,7 +92,7 @@ Every installation below compiles the Rust extension, so you'll need: pytest tests/ -v # make sure iggy-server is running and the venv is activated ``` -4. To update the stubs, only after changing the pyo3 API surface (nothing in CI checks stub freshness, so unconditional regen just invites `.pyi` churn), use +4. To update the stubs, after changing the pyo3 API surface, use ```bash # run from foreign/python From 39d50b76746bcebb7f07976e1497bef71d577eff Mon Sep 17 00:00:00 2001 From: Ethan Lin Date: Thu, 27 Aug 2026 22:26:57 +0800 Subject: [PATCH 4/4] fix(python): anchor stub_gen on the manifest dir Running it from a subdirectory stripped the license header from the tracked stub: `stub.generate()` writes to the crate root, but the header was prepended through a path resolved against the current directory. --- foreign/python/README.md | 1 - foreign/python/src/bin/stub_gen.rs | 11 +++-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/foreign/python/README.md b/foreign/python/README.md index 0dc06883e9..e2a4fffc2d 100644 --- a/foreign/python/README.md +++ b/foreign/python/README.md @@ -97,7 +97,6 @@ Every installation below compiles the Rust extension, so you'll need: ```bash # run from foreign/python cargo run --bin stub_gen - # TODO: Known bug: running this from a subdirectory of `foreign/python` corrupts the tracked stub, see https://github.com/apache/iggy/pull/3825/changes/BASE..773a27971b4ddb7b44773ded395ed23afb1de4c9#r3727691619 ``` 5. Before committing, test the pre-commit and pre-push hooks. `prek` only inspects staged content, so stage your work first: diff --git a/foreign/python/src/bin/stub_gen.rs b/foreign/python/src/bin/stub_gen.rs index e9db76059d..82693da1a4 100644 --- a/foreign/python/src/bin/stub_gen.rs +++ b/foreign/python/src/bin/stub_gen.rs @@ -47,14 +47,9 @@ fn main() -> Result<()> { // `stub_info` is a function defined by `define_stub_info_gatherer!` macro. let stub = apache_iggy::client::stub_info()?; stub.generate()?; - let path = Path::new(file!()) - .parent() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap() - .join("apache_iggy.pyi"); + // Anchor on the manifest dir: `stub.generate()` writes to the crate root, so + // a cwd-relative path leaves the tracked stub without its license header. + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("apache_iggy.pyi"); let mut f = File::open(&path)?; let mut content = LICENSE.as_bytes().to_owned(); f.read_to_end(&mut content)?;