diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 60894797..0bcbaebf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: os: ["ubuntu-latest", "windows-latest", "macos-latest"] - py: ["3.14-dev", "3.13", "3.12", "3.11", "3.10", "3.9"] + py: ["3.14-dev", "3.14t-dev", "3.13", "3.13t", "3.12", "3.11", "3.10", "3.9"] runs-on: ${{ matrix.os }} name: Run test with Python ${{ matrix.py }} on ${{ matrix.os }} @@ -29,9 +29,15 @@ jobs: - name: Prepare shell: bash run: | - pip install -U pip + # TODO: Workaround for Windows tests failing when upgrading `pip` with exit code 1 + # pip install -U pip pip install -r requirements.txt pytest + - name: Install pytest-run-parallel under free-threading + if: contains(matrix.py, 't') + run: | + pip install pytest-run-parallel + - name: Build shell: bash run: | @@ -39,15 +45,29 @@ jobs: pip install . - name: Test (C extension) + if: ! contains(matrix.py, 't') shell: bash run: | pytest -v test - name: Test (pure Python fallback) + if: ! contains(matrix.py, 't') shell: bash run: | MSGPACK_PUREPYTHON=1 pytest -v test + - name: Test (C extension) in parallel under free-threading + if: contains(matrix.py, 't') + shell: bash + run: | + pytest -v --parallel-threads=auto --iterations=20 test + + - name: Test (pure Python fallback) in parallel under free-threading + if: contains(matrix.py, 't') + shell: bash + run: | + MSGPACK_PUREPYTHON=1 pytest -v --parallel-threads=auto --iterations=20 test + - name: build packages shell: bash run: | diff --git a/.github/workflows/wheel.yml b/.github/workflows/wheel.yml index 12600363..2b03660d 100644 --- a/.github/workflows/wheel.yml +++ b/.github/workflows/wheel.yml @@ -33,7 +33,8 @@ jobs: env: CIBW_TEST_REQUIRES: "pytest" CIBW_TEST_COMMAND: "pytest {package}/test" - CIBW_SKIP: "pp* cp38-*" + CIBW_SKIP: "pp* cp38-macosx_*" + CIBW_ENABLE: cpython-freethreading - name: Build sdist if: runner.os == 'Linux' && runner.arch == 'X64' diff --git a/msgpack/_cmsgpack.pyx b/msgpack/_cmsgpack.pyx index 1faaac3a..41340ee0 100644 --- a/msgpack/_cmsgpack.pyx +++ b/msgpack/_cmsgpack.pyx @@ -1,5 +1,5 @@ # coding: utf-8 -#cython: embedsignature=True, c_string_encoding=ascii, language_level=3 +#cython: embedsignature=True, c_string_encoding=ascii, language_level=3, freethreading_compatible=True from cpython.datetime cimport import_datetime, datetime_new import_datetime() diff --git a/msgpack/_packer.pyx b/msgpack/_packer.pyx index 402b6946..94d6600b 100644 --- a/msgpack/_packer.pyx +++ b/msgpack/_packer.pyx @@ -1,5 +1,5 @@ # coding: utf-8 - +# cython: freethreading_compatible = True from cpython cimport * from cpython.bytearray cimport PyByteArray_Check, PyByteArray_CheckExact from cpython.datetime cimport ( diff --git a/msgpack/_unpacker.pyx b/msgpack/_unpacker.pyx index 34ff3304..717666d6 100644 --- a/msgpack/_unpacker.pyx +++ b/msgpack/_unpacker.pyx @@ -1,5 +1,5 @@ # coding: utf-8 - +# cython: freethreading_compatible = True from cpython cimport * cdef extern from "Python.h": ctypedef struct PyObject diff --git a/requirements.txt b/requirements.txt index 5d2e20b1..301c8767 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ Cython~=3.1.2 +#pytest-run-parallel[psutil] diff --git a/test/test_buffer.py b/test/test_buffer.py index 2c5a14c5..ca097222 100644 --- a/test/test_buffer.py +++ b/test/test_buffer.py @@ -17,7 +17,7 @@ def test_unpack_bytearray(): obj = unpackb(buf, use_list=1) assert [b"foo", b"bar"] == obj expected_type = bytes - assert all(type(s) == expected_type for s in obj) + assert all(type(s) is expected_type for s in obj) def test_unpack_memoryview(): @@ -26,7 +26,7 @@ def test_unpack_memoryview(): obj = unpackb(view, use_list=1) assert [b"foo", b"bar"] == obj expected_type = bytes - assert all(type(s) == expected_type for s in obj) + assert all(type(s) is expected_type for s in obj) def test_packer_getbuffer(): diff --git a/test/test_multithreading.py b/test/test_multithreading.py new file mode 100644 index 00000000..6694fdc6 --- /dev/null +++ b/test/test_multithreading.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +import threading +from concurrent.futures import ThreadPoolExecutor + +from msgpack import Packer + + +def run_threaded( + func, + num_threads=8, + pass_count=False, + pass_barrier=False, + outer_iterations=1, + prepare_args=None, +): + """Runs a function many times in parallel""" + for _ in range(outer_iterations): + with ThreadPoolExecutor(max_workers=num_threads) as tpe: + if prepare_args is None: + args = [] + else: + args = prepare_args() + if pass_barrier: + barrier = threading.Barrier(num_threads) + args.append(barrier) + if pass_count: + all_args = [(func, i, *args) for i in range(num_threads)] + else: + all_args = [(func, *args) for i in range(num_threads)] + try: + futures = [] + for arg in all_args: + futures.append(tpe.submit(*arg)) + finally: + if len(futures) < num_threads and pass_barrier: + barrier.abort() + for f in futures: + f.result() + + +def test_multithread_packing(): + output = [] + test_data = "abcd" * 10_000_000 + packer = Packer() + + def closure(b): + data = packer.pack(test_data) + output.append(data) + b.wait() + + run_threaded(closure, num_threads=10, pass_barrier=True, pass_count=False)