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 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/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/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/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/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" 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