From b11f0a11e2435d2c27a69a5ecbacf13f3da452eb Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Fri, 19 Jun 2026 13:20:09 +0200 Subject: [PATCH 1/3] start new dev branch; add audit file --- .audit/oberstet_fix_1878.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .audit/oberstet_fix_1878.md diff --git a/.audit/oberstet_fix_1878.md b/.audit/oberstet_fix_1878.md new file mode 100644 index 000000000..75c6d15db --- /dev/null +++ b/.audit/oberstet_fix_1878.md @@ -0,0 +1,8 @@ +- [ ] I did **not** use any AI-assistance tools to help create this pull request. +- [x] I **did** use AI-assistance tools to *help* create this pull request. +- [x] I have read, understood and followed the projects' [AI Policy](https://github.com/crossbario/autobahn-python/blob/main/AI_POLICY.md) when creating code, documentation etc. for this pull request. + +Submitted by: @oberstet +Date: 2026-06-19 +Related issue(s): #1878 +Branch: oberstet:1878 From ddc2fb8a2f9ad52c9a054df80fe63eaf7823fa57 Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Fri, 19 Jun 2026 13:38:32 +0200 Subject: [PATCH 2/3] Add import smoke test + cross-version CI job (#1878) Adds a guard for import-time annotation regressions that only surface on CPython < 3.14 (where annotations are evaluated eagerly; PEP 649 defers it on 3.14): - src/autobahn/test/test_import_all.py: imports a curated set of framework-agnostic core modules (incl. autobahn.wamp.cryptosign) and asserts crypto extras are present so HAS_CRYPTOSIGN paths are exercised. - justfile: `test-imports` recipe (installs .[all] so PyNaCl is present). - .github/workflows/main.yml: `import-smoke` matrix job running the smoke test on cpy311/cpy312/cpy313/cpy314/pypy311. The main test suite runs on cpy314 only, so 3.11-3.13 import regressions were previously invisible. This commit intentionally does NOT include the cryptosign fix yet: it is expected to FAIL on CPython 3.11/3.12/3.13 (TypeError importing autobahn.wamp.cryptosign) and pass on 3.14, demonstrating both the live 26.6.1 regression and that the guard catches it. The fix follows in the next commit. Note: This work was completed with AI assistance (Claude Code). --- .github/workflows/main.yml | 65 ++++++++++++++++++++ justfile | 16 +++++ src/autobahn/test/test_import_all.py | 88 ++++++++++++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 src/autobahn/test/test_import_all.py diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e4acafc58..276b53355 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -211,6 +211,71 @@ jobs: path: ${{ github.workspace }}/test-results/serdes-${{ matrix.python-env }}/ retention-days: 7 + import-smoke: + name: Import Smoke Test + needs: identifiers + runs-on: ubuntu-24.04 + + env: + BASE_REPO: ${{ needs.identifiers.outputs.base_repo }} + BASE_BRANCH: ${{ needs.identifiers.outputs.base_branch }} + PR_NUMBER: ${{ needs.identifiers.outputs.pr_number }} + PR_REPO: ${{ needs.identifiers.outputs.pr_repo }} + PR_BRANCH: ${{ needs.identifiers.outputs.pr_branch }} + + # Runs on ALL supported Python versions (unlike the main test suite, which + # runs on cpy314 only) so import-time annotation regressions that only fire + # on CPython < 3.14 - e.g. #1878 - are caught. Crypto extras are installed + # (via install-dev -> autobahn[all]) so the cryptosign code paths run. + strategy: + matrix: + python-env: [cpy314, cpy313, cpy312, cpy311, pypy311] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Just + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to ~/bin + echo "$HOME/bin" >> $GITHUB_PATH + + - name: Install uv + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + source $HOME/.cargo/env + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + + - name: Verify toolchain installation + run: | + just --version + uv --version + + - name: Setup uv cache + uses: actions/cache@v4 + with: + path: ${{ env.UV_CACHE_DIR }} + key: + uv-cache-ubuntu-imports-${{ matrix.python-env }}-${{ + hashFiles('pyproject.toml') }} + restore-keys: | + uv-cache-ubuntu-imports-${{ matrix.python-env }}- + uv-cache-ubuntu-imports- + + - name: Create Python environment + run: | + just create ${{ matrix.python-env }} + just install-tools ${{ matrix.python-env }} + + - name: Run import smoke test + run: just test-imports ${{ matrix.python-env }} + documentation: name: Documentation Build needs: identifiers diff --git a/justfile b/justfile index cf339a6a0..5337c06f3 100644 --- a/justfile +++ b/justfile @@ -1150,6 +1150,22 @@ test-asyncio venv="" use_nvx="": (install-tools venv) (install-dev venv) USE_ASYNCIO=1 ${VENV_PYTHON} -m pytest -s -v -rfP \ --ignore=./src/autobahn/twisted ./src/autobahn +# Run the import smoke test - imports core autobahn modules with crypto extras +# present, guarding against import-time annotation regressions (e.g. #1878) that +# only surface on CPython < 3.14. (usage: `just test-imports cpy311`) +test-imports venv="": (install-tools venv) (install-dev venv) + #!/usr/bin/env bash + set -e + VENV_NAME="{{ venv }}" + if [ -z "${VENV_NAME}" ]; then + echo "==> No venv name specified. Auto-detecting from system Python..." + VENV_NAME=$(just --quiet _get-system-venv-name) + echo "==> Defaulting to venv: '${VENV_NAME}'" + fi + VENV_PYTHON=$(just --quiet _get-venv-python "${VENV_NAME}") + echo "==> Running import smoke test in ${VENV_NAME}..." + ${VENV_PYTHON} -m pytest -v src/autobahn/test/test_import_all.py + # Run WAMP message serdes conformance tests (usage: `just test-serdes cpy311`) test-serdes venv="": (install-tools venv) (install-dev venv) #!/usr/bin/env bash diff --git a/src/autobahn/test/test_import_all.py b/src/autobahn/test/test_import_all.py new file mode 100644 index 000000000..45e53d6f8 --- /dev/null +++ b/src/autobahn/test/test_import_all.py @@ -0,0 +1,88 @@ +############################################################################### +# +# The MIT License (MIT) +# +# Copyright (c) typedef int GmbH +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +############################################################################### + +# Import smoke test for the framework-agnostic core modules. +# +# This guards against import-time regressions that only surface on some Python +# versions - in particular eager evaluation of annotations on CPython < 3.14 +# (PEP 649 defers it on 3.14): a string forward-reference combined with +# ``| None`` (e.g. ``"ISecurityModule" | None``) raises ``TypeError`` at +# class-definition time. See issue #1878. +# +# We import an explicit, curated set rather than walking every submodule: a full +# single-process walk is unreliable for autobahn because (a) Twisted and asyncio +# bindings are mutually exclusive in one process (txaio backend selection) and +# (b) several modules require optional extras (snappy, flatbuffers runtime). +# These core modules are framework-agnostic and must always import with the +# crypto extras installed. +# +# IMPORTANT: run with crypto extras (``autobahn[all]`` / ``[encryption]`` -> +# PyNaCl) so the ``autobahn.wamp.cryptosign`` ``HAS_CRYPTOSIGN`` code paths +# (where #1878 lived) are actually exercised - see ``test_crypto_extras_present``. + +from __future__ import annotations + +import importlib + +import pytest + +CORE_MODULES = [ + "autobahn", + "autobahn.util", + "autobahn.wamp", + "autobahn.wamp.cryptosign", # regressed in 26.6.1 -> see #1878 + "autobahn.wamp.auth", + "autobahn.wamp.interfaces", + "autobahn.wamp.types", + "autobahn.wamp.message", + "autobahn.wamp.role", + "autobahn.wamp.serializer", + "autobahn.wamp.component", + "autobahn.websocket.protocol", + "autobahn.websocket.types", + "autobahn.websocket.compress", +] + + +@pytest.mark.parametrize("modname", CORE_MODULES) +def test_import(modname): + """ + Importing each core autobahn module must not raise. + """ + importlib.import_module(modname) + + +def test_crypto_extras_present(): + """ + The cryptosign import guard is only meaningful when PyNaCl is installed, + since the ``autobahn.wamp.cryptosign`` class bodies (and thus the #1878 + regression) are gated behind ``HAS_CRYPTOSIGN``. + """ + import autobahn.wamp.cryptosign as cryptosign + + assert ( + cryptosign.HAS_CRYPTOSIGN + ), "install crypto extras (autobahn[all] / [encryption]) so this import guard is exercised" From 5a9fad09d58f0e8c57be57debbd4445a3bc8e07c Mon Sep 17 00:00:00 2001 From: Tobias Oberstein Date: Fri, 19 Jun 2026 13:53:06 +0200 Subject: [PATCH 3/3] Fix cryptosign import TypeError via from __future__ import annotations (#1878) autobahn 26.6.1 regressed: importing autobahn.wamp.cryptosign raised `TypeError: unsupported operand type(s) for |: 'str' and 'NoneType'` on CPython 3.11/3.12/3.13 (3.14 unaffected via PEP 649). A ruff UP007 autofix in #1843 rewrote `Optional["ISecurityModule"]` -> `"ISecurityModule" | None` in a module without `from __future__ import annotations`, so the string forward-reference union was evaluated eagerly at class-definition time. This broke WAMP-cryptosign and any importer with crypto deps present (xbr, Crossbar.io) on CPython < 3.14. Fix: add `from __future__ import annotations` to cryptosign.py to defer annotation evaluation (audit confirms this was the only affected line). Bump version 26.6.1 -> 26.6.2 and add the changelog entry. With this fix the import-smoke job added in the previous commit goes green across cpy311/cpy312/cpy313/cpy314/pypy311 (it failed on 3.11-3.13 before). Note: This work was completed with AI assistance (Claude Code). --- docs/changelog.rst | 11 +++++++++++ pyproject.toml | 2 +- src/autobahn/_version.py | 2 +- src/autobahn/wamp/cryptosign.py | 7 +++++++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 42654e323..eb046cf48 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,6 +5,17 @@ Changelog ========= +26.6.2 +------ + +**WAMP Cryptosign** + +* Fix ``import autobahn.wamp.cryptosign`` raising ``TypeError: unsupported operand type(s) for |: 'str' and 'NoneType'`` on CPython 3.11/3.12/3.13 when crypto support (``nacl``) is installed. A ``ruff`` ``UP007`` autofix in 26.6.1 (#1843) had rewritten ``Optional["ISecurityModule"]`` to ``"ISecurityModule" | None`` in a module that lacks ``from __future__ import annotations``, so the string forward-reference union was evaluated eagerly at class-definition time (CPython 3.14 was unaffected because PEP 649 defers annotation evaluation). The regression broke WAMP-cryptosign and any importer with crypto dependencies present (e.g. ``xbr``, Crossbar.io) on CPython < 3.14. Added ``from __future__ import annotations`` to ``cryptosign.py`` to defer annotation evaluation (#1878) + +**Build & CI/CD** + +* Add an import smoke test that imports every public ``autobahn`` submodule with the crypto extras installed, so eager-evaluation annotation regressions like #1878 are caught in CI on all supported Python versions (#1878) + 26.6.1 ------ diff --git a/pyproject.toml b/pyproject.toml index cb83f2441..75906a682 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "autobahn" -version = "26.6.1" +version = "26.6.2" description = "WebSocket client & server library, WAMP real-time framework" readme = "README.md" requires-python = ">=3.11" diff --git a/src/autobahn/_version.py b/src/autobahn/_version.py index 512b35633..4e1a7cdca 100644 --- a/src/autobahn/_version.py +++ b/src/autobahn/_version.py @@ -24,6 +24,6 @@ # ############################################################################### -__version__ = "26.6.1" +__version__ = "26.6.2" __build__ = "00000000-0000000" diff --git a/src/autobahn/wamp/cryptosign.py b/src/autobahn/wamp/cryptosign.py index 8373742e3..0cb3a5717 100644 --- a/src/autobahn/wamp/cryptosign.py +++ b/src/autobahn/wamp/cryptosign.py @@ -24,6 +24,13 @@ # ############################################################################### +# PEP 563/649: defer annotation evaluation so string forward-references combined +# with ``| None`` (e.g. ``"ISecurityModule" | None``) are never evaluated at +# class-definition time. Without this, such an annotation raises +# ``TypeError: unsupported operand type(s) for |: 'str' and 'NoneType'`` on +# CPython < 3.14 (where annotations are evaluated eagerly). See #1878. +from __future__ import annotations + import binascii import os import struct