diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f21de8e..fdcfe28 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -98,6 +98,7 @@ jobs: - "3.12" - "3.13" - "3.14" + - "3.15" steps: - uses: actions/checkout@v7 with: @@ -200,10 +201,10 @@ jobs: key: ${{ needs.blazingmq-dependency.outputs.deps_cache_key }} - name: Restore cached BlazingMQ build artifacts run: tar xzf blazingmq_artifacts.tar.gz - - name: Set up uv with Python 3.14 + - name: Set up uv with Python 3.15 uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - python-version: "3.14" + python-version: "3.15" activate-environment: true venv-path: venv - name: Install Python dependencies @@ -251,7 +252,7 @@ jobs: - name: Set up uv with Python 3.14 uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - python-version: "3.14" + python-version: "3.15" activate-environment: true venv-path: venv - name: Install Python dependencies diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml index 30e8382..537c597 100644 --- a/.github/workflows/build_wheels.yml +++ b/.github/workflows/build_wheels.yml @@ -69,7 +69,7 @@ jobs: matrix: os: [ubuntu-latest] cibw_python: - ["cp39", "cp310", "cp311", "cp312", "cp313", "cp314"] + ["cp39", "cp310", "cp311", "cp312", "cp313", "cp314", "cp315"] cibw_arch: ${{ fromJSON(needs.choose_architectures.outputs.cibw_arches) }} steps: @@ -112,7 +112,7 @@ jobs: matrix: os: [macos-14] cibw_python: - ["cp39", "cp310", "cp311", "cp312", "cp313", "cp314"] + ["cp39", "cp310", "cp311", "cp312", "cp313", "cp314", "cp315"] cibw_arch: ["arm64"] steps: diff --git a/README.md b/README.md index adb1934..953cd5a 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ [![MessageQueue](https://img.shields.io/badge/MessageQueue-blue)](#) [![Documentation](https://img.shields.io/badge/Documentation-blue)](https://bloomberg.github.io/blazingmq) [![Code Style](https://img.shields.io/badge/Black-black)](#) -![Supported Python versions](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue?logo=python) +![Supported Python versions](https://img.shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14%20%7C%203.15-blue?logo=python) # BlazingMQ Python SDK diff --git a/docs/BUILD.md b/docs/BUILD.md index 407b256..9c7b604 100644 --- a/docs/BUILD.md +++ b/docs/BUILD.md @@ -8,7 +8,7 @@ CI to test on all other combinations in your pull request. See `.github/workflows/build.yaml` for more details on how we build and test on all supported interpreters and architectures. -The instructions below assume `PYEXEC=python3.14` and will focus on +The instructions below assume `PYEXEC=python3.15` and will focus on Linux only. This should be sufficient in most cases. Before following any of the instructions, make sure to `git clone` the project onto the host machine. diff --git a/news/98.misc.rst b/news/98.misc.rst new file mode 100644 index 0000000..4f5f20d --- /dev/null +++ b/news/98.misc.rst @@ -0,0 +1 @@ +Support Python 3.15 diff --git a/pyproject.toml b/pyproject.toml index e7ddb44..7e10b3f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ build-backend = "setuptools.build_meta" name = "blazingmq" description = "Python BlazingMQ API" readme = "README.md" -requires-python = ">=3.9,<3.15" +requires-python = ">=3.9,<3.16" authors = [ {name = "Bloomberg Finance LP"} ] @@ -29,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Libraries :: Python Modules", "Typing :: Typed", @@ -52,7 +53,7 @@ filename = "CHANGELOG.md" directory = "news" [tool.cibuildwheel] -build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*"] +build = ["cp39-*", "cp310-*", "cp311-*", "cp312-*", "cp313-*", "cp314-*", "cp315-*"] skip = "*-musllinux_*" manylinux-x86_64-image = "manylinux_2_28" diff --git a/src/blazingmq/_session.py b/src/blazingmq/_session.py index a49c50a..8564a89 100644 --- a/src/blazingmq/_session.py +++ b/src/blazingmq/_session.py @@ -103,7 +103,11 @@ def _construct_user_agent_prefix(user_agent_prefix: bytes) -> bytes: """Construct the user agent prefix for use by the Cython layer.""" python_version = platform.python_version().encode("ascii", errors="strict") blazingmq_version = __version__.encode("ascii", errors="strict") - sdk_identifier = b"blazingmq(python" + python_version + b"):" + blazingmq_version + sdk_identifier = b"blazingmq(py" + python_version + b"):" + blazingmq_version + # libbmq requires the user agent to be shorter than 128 bytes. The prefix + # is at most 96 bytes, plus one byte for the separating space, which leaves + # 30 bytes for the SDK identifier. + sdk_identifier = sdk_identifier[:30] if user_agent_prefix: return user_agent_prefix + b" " + sdk_identifier return sdk_identifier diff --git a/tests/unit/test_session.py b/tests/unit/test_session.py index df9d436..21f37a5 100644 --- a/tests/unit/test_session.py +++ b/tests/unit/test_session.py @@ -290,7 +290,7 @@ def dummy2(): # THEN _, kwargs = ext_cls.call_args assert re.fullmatch( - rb"mylib:1\.0 blazingmq\(python[^)]+\):\S+", + rb"mylib:1\.0 blazingmq\(py[^)]+\):\S+", kwargs["user_agent_prefix"], ) @@ -964,7 +964,7 @@ def dummy(): # THEN _, kwargs = ext_cls.call_args assert re.fullmatch( - rb"mylib:1\.0 blazingmq\(python[^)]+\):\S+", + rb"mylib:1\.0 blazingmq\(py[^)]+\):\S+", kwargs["user_agent_prefix"], ) @@ -987,7 +987,7 @@ def dummy(): # THEN _, kwargs = ext_cls.call_args assert re.fullmatch( - rb"blazingmq\(python[^)]+\):\S+", + rb"blazingmq\(py[^)]+\):\S+", kwargs["user_agent_prefix"], ) @@ -1011,7 +1011,7 @@ def dummy(): # THEN _, kwargs = ext_cls.call_args assert re.fullmatch( - rb"blazingmq\(python[^)]+\):\S+", + rb"blazingmq\(py[^)]+\):\S+", kwargs["user_agent_prefix"], ) @@ -1037,6 +1037,50 @@ def dummy(): assert len(kwargs["user_agent_prefix"]) < 128 +@mock.patch("blazingmq._session.platform.python_version", return_value="3.15.0rc2") +@mock.patch("blazingmq._session.__version__", "1.2.3.dev456") +@mock.patch("blazingmq._session.ExtSession") +def test_session_user_agent_prefix_with_long_versions_satisfies_bmq_length( + ext_cls, _python_version +): + # GIVEN + ext_cls.mock_add_spec([]) + user_agent_prefix = b"x" * 96 + + def dummy(): + pass + + # WHEN + Session( + dummy, + on_message=dummy, + broker="some_uri", + user_agent_prefix=user_agent_prefix, + ) + + # THEN + _, kwargs = ext_cls.call_args + assert len(kwargs["user_agent_prefix"]) < 128 + assert kwargs["user_agent_prefix"].startswith(user_agent_prefix + b" ") + + +@mock.patch("blazingmq._session.platform.python_version", return_value="3.15.0rc2") +@mock.patch("blazingmq._session.__version__", "1.2.3.dev456") +def test_user_agent_sdk_identifier_does_not_depend_on_prefix(_python_version): + # GIVEN + from blazingmq._session import _construct_user_agent_prefix + + # WHEN + without_prefix = _construct_user_agent_prefix(b"") + with_short_prefix = _construct_user_agent_prefix(b"mylib:1.0") + with_long_prefix = _construct_user_agent_prefix(b"x" * 96) + + # THEN + assert with_short_prefix == b"mylib:1.0 " + without_prefix + assert with_long_prefix == b"x" * 96 + b" " + without_prefix + assert len(with_long_prefix) < 128 + + @mock.patch("blazingmq._session.ExtSession") def test_session_user_agent_prefix_satisfies_bmq_printable_precondition(ext_cls): # GIVEN @@ -1165,7 +1209,7 @@ def dummy(): # THEN _, kwargs = ext_cls.call_args assert re.fullmatch( - rb"my lib:1\.0 blazingmq\(python[^)]+\):\S+", + rb"my lib:1\.0 blazingmq\(py[^)]+\):\S+", kwargs["user_agent_prefix"], ) diff --git a/tox.ini b/tox.ini index 5e91012..76a6f31 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py314,lint, lint-cython, lint-types, black, docs +envlist = py315,lint, lint-cython, lint-types, black, docs minversion = 3.2.1 isolated_build = true @@ -32,23 +32,23 @@ description = [testenv:.package] description = Isolated build environment -basepython = python3.14 +basepython = python3.15 -[testenv:py314-cov] +[testenv:py315-cov] usedevelop = True commands = {[testenv]commands_cov} deps = -rrequirements-test-coverage.txt [testenv:lint] description = Run lint checker on Python files -basepython = python3.14 +basepython = python3.15 deps = flake8 skip_install = true commands = {basepython} -m flake8 --config={toxinidir}/.flake8 src [testenv:lint-cython] description = Run lint checker on Cython files -basepython = python3.14 +basepython = python3.15 deps = flake8 skip_install = true commands = {basepython} -m flake8 --config={toxinidir}/.flake8.cython src @@ -56,21 +56,21 @@ commands = {basepython} -m flake8 --config={toxinidir}/.flake8.cython src [testenv:lint-types] description = Run mypy checker on examples -basepython = python3.14 +basepython = python3.15 deps = mypy commands = {basepython} -m mypy --strict examples [testenv:black] description = Check that python files are formatted with black -basepython = python3.14 +basepython = python3.15 deps = black skip_install = true commands = {basepython} -m black --check --verbose src tests [testenv:docs] description = Generate library documentation -basepython = python3.14 +basepython = python3.15 deps = -rrequirements-lint-docs.txt commands = make -C docs html {basepython} -c 'print("documentation available under file://{toxworkdir}/docs_out/index.html")'