Skip to content

Commit 779cf84

Browse files
authored
Support the free-threaded build of Python 3.14
1 parent c42980a commit 779cf84

File tree

4 files changed

+309
-53
lines changed

4 files changed

+309
-53
lines changed

.github/workflows/ci.yml

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,16 @@ jobs:
2222
fail-fast: false
2323
matrix:
2424
os: [ubuntu-latest, macos-latest, windows-latest]
25-
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
25+
python-version: [
26+
["3.8", "py38"],
27+
["3.9", "py39"],
28+
["3.10", "py310"],
29+
["3.11", "py311"],
30+
["3.12", "py312"],
31+
["3.13", "py313"],
32+
["3.14", "py314"],
33+
["3.14t", "py314t"],
34+
]
2635
steps:
2736
- name: Set git to use LF on Windows
2837
if: runner.os == 'Windows'
@@ -34,12 +43,12 @@ jobs:
3443
submodules: recursive
3544
- uses: actions/setup-python@v6
3645
with:
37-
python-version: ${{ matrix.python-version }}
46+
python-version: ${{ matrix.python-version[0] }}
3847
allow-prereleases: true
3948
- name: Run tests
4049
run: |
4150
python -m pip install tox
42-
tox --skip-missing-interpreters
51+
tox -e ${{ matrix.python-version[1] }}
4352
4453
package-sdist:
4554
runs-on: ubuntu-latest
@@ -80,21 +89,21 @@ jobs:
8089
# run: choco install vcpython27 -f -y
8190
- name: Install QEMU
8291
if: runner.os == 'Linux'
83-
uses: docker/setup-qemu-action@v1
92+
uses: docker/setup-qemu-action@v3
8493
with:
8594
platforms: all
8695

8796
- name: Build wheels
8897
run: python -m cibuildwheel --output-dir wheelhouse
8998
env:
90-
CIBW_BUILD: cp38-* pp*-*
99+
CIBW_BUILD: "cp38-* pp*-* cp314t-*"
91100
CIBW_SKIP: "*musllinux*"
92101
CIBW_ENABLE: pypy
93102
CIBW_ARCHS_LINUX: auto aarch64
94103
CIBW_BEFORE_BUILD_LINUX: yum install -y libffi-devel
95104
- uses: actions/upload-artifact@v4
96105
with:
97-
name: wheels-${{ matrix.os }}
106+
name: wheels-${{ matrix.os }}-${{ matrix.wheel-tag }}
98107
path: ./wheelhouse/*.whl
99108

100109
publish:
@@ -108,15 +117,15 @@ jobs:
108117
path: dist/
109118
- uses: actions/download-artifact@v6
110119
with:
111-
name: wheels-windows-latest
120+
pattern: wheels-windows-latest-*
112121
path: dist/
113122
- uses: actions/download-artifact@v6
114123
with:
115-
name: wheels-macos-latest
124+
pattern: wheels-macos-latest-*
116125
path: dist/
117126
- uses: actions/download-artifact@v6
118127
with:
119-
name: wheels-ubuntu-latest
128+
pattern: wheels-ubuntu-latest-*
120129
path: dist/
121130
- name: Publish to PyPI
122131
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/')

setup.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import re
44
import platform
55
import sys
6+
import sysconfig
67
from setuptools import find_packages, setup
78
from setuptools.command.build_ext import build_ext
89

@@ -74,11 +75,13 @@ def run(self):
7475
except ImportError:
7576
pass
7677
else:
77-
class BDistWheel(wheel.bdist_wheel.bdist_wheel):
78-
def finalize_options(self):
79-
self.py_limited_api = "cp3{}".format(sys.version_info[1])
80-
wheel.bdist_wheel.bdist_wheel.finalize_options(self)
81-
cmdclass['bdist_wheel'] = BDistWheel
78+
# the limited API is only supported on GIL builds as of Python 3.14
79+
if not bool(sysconfig.get_config_var("Py_GIL_DISABLED")):
80+
class BDistWheel(wheel.bdist_wheel.bdist_wheel):
81+
def finalize_options(self):
82+
self.py_limited_api = "cp3{}".format(sys.version_info[1])
83+
wheel.bdist_wheel.bdist_wheel.finalize_options(self)
84+
cmdclass['bdist_wheel'] = BDistWheel
8285

8386
setup(
8487
name="brotlicffi",
@@ -122,5 +125,6 @@ def finalize_options(self):
122125
"Programming Language :: Python :: 3.12",
123126
"Programming Language :: Python :: 3.13",
124127
"Programming Language :: Python :: 3.14",
128+
"Programming Language :: Python :: Free Threading :: 2 - Beta",
125129
]
126130
)

src/brotlicffi/_api.py

Lines changed: 85 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
import math
33
import enum
4+
import threading
45

56
from ._brotlicffi import ffi, lib
67

@@ -249,6 +250,7 @@ def __init__(self,
249250
quality=lib.BROTLI_DEFAULT_QUALITY,
250251
lgwin=lib.BROTLI_DEFAULT_WINDOW,
251252
lgblock=0):
253+
self.lock = threading.RLock()
252254
enc = lib.BrotliEncoderCreateInstance(
253255
ffi.NULL, ffi.NULL, ffi.NULL
254256
)
@@ -271,28 +273,34 @@ def _compress(self, data, operation):
271273
because almost all of the code uses the exact same setup. It wouldn't
272274
have to, but it doesn't hurt at all.
273275
"""
274-
# The 'algorithm' for working out how big to make this buffer is from
275-
# the Brotli source code, brotlimodule.cc.
276-
original_output_size = int(
277-
math.ceil(len(data) + (len(data) >> 2) + 10240)
278-
)
279-
available_out = ffi.new("size_t *")
280-
available_out[0] = original_output_size
281-
output_buffer = ffi.new("uint8_t []", available_out[0])
282-
ptr_to_output_buffer = ffi.new("uint8_t **", output_buffer)
283-
input_size = ffi.new("size_t *", len(data))
284-
input_buffer = ffi.new("uint8_t []", data)
285-
ptr_to_input_buffer = ffi.new("uint8_t **", input_buffer)
286-
287-
rc = lib.BrotliEncoderCompressStream(
288-
self._encoder,
289-
operation,
290-
input_size,
291-
ptr_to_input_buffer,
292-
available_out,
293-
ptr_to_output_buffer,
294-
ffi.NULL
295-
)
276+
if not self.lock.acquire(blocking=False):
277+
raise error(
278+
"Concurrently sharing Compressor objects is not allowed")
279+
try:
280+
# The 'algorithm' for working out how big to make this buffer is
281+
# from the Brotli source code, brotlimodule.cc.
282+
original_output_size = int(
283+
math.ceil(len(data) + (len(data) >> 2) + 10240)
284+
)
285+
available_out = ffi.new("size_t *")
286+
available_out[0] = original_output_size
287+
output_buffer = ffi.new("uint8_t []", available_out[0])
288+
ptr_to_output_buffer = ffi.new("uint8_t **", output_buffer)
289+
input_size = ffi.new("size_t *", len(data))
290+
input_buffer = ffi.new("uint8_t []", data)
291+
ptr_to_input_buffer = ffi.new("uint8_t **", input_buffer)
292+
293+
rc = lib.BrotliEncoderCompressStream(
294+
self._encoder,
295+
operation,
296+
input_size,
297+
ptr_to_input_buffer,
298+
available_out,
299+
ptr_to_output_buffer,
300+
ffi.NULL
301+
)
302+
finally:
303+
self.lock.release()
296304
if rc != lib.BROTLI_TRUE: # pragma: no cover
297305
raise error("Error encountered compressing data.")
298306

@@ -320,11 +328,17 @@ def flush(self):
320328
will not destroy the compressor. It can be used, for example, to ensure
321329
that given chunks of content will decompress immediately.
322330
"""
323-
chunks = [self._compress(b'', lib.BROTLI_OPERATION_FLUSH)]
324-
325-
while lib.BrotliEncoderHasMoreOutput(self._encoder) == lib.BROTLI_TRUE:
326-
chunks.append(self._compress(b'', lib.BROTLI_OPERATION_FLUSH))
327-
331+
if not self.lock.acquire(blocking=False):
332+
raise error(
333+
"Concurrently sharing Compressor objects is not allowed")
334+
try:
335+
chunks = [self._compress(b'', lib.BROTLI_OPERATION_FLUSH)]
336+
337+
while ((lib.BrotliEncoderHasMoreOutput(self._encoder) ==
338+
lib.BROTLI_TRUE)):
339+
chunks.append(self._compress(b'', lib.BROTLI_OPERATION_FLUSH))
340+
finally:
341+
self.lock.release()
328342
return b''.join(chunks)
329343

330344
def finish(self):
@@ -333,10 +347,16 @@ def finish(self):
333347
transition the compressor to a completed state. The compressor cannot
334348
be used again after this point, and must be replaced.
335349
"""
336-
chunks = []
337-
while lib.BrotliEncoderIsFinished(self._encoder) == lib.BROTLI_FALSE:
338-
chunks.append(self._compress(b'', lib.BROTLI_OPERATION_FINISH))
339-
350+
if not self.lock.acquire(blocking=False):
351+
raise error(
352+
"Concurrently sharing Compressor objects is not allowed")
353+
try:
354+
chunks = []
355+
while ((lib.BrotliEncoderIsFinished(self._encoder) ==
356+
lib.BROTLI_FALSE)):
357+
chunks.append(self._compress(b'', lib.BROTLI_OPERATION_FINISH))
358+
finally:
359+
self.lock.release()
340360
return b''.join(chunks)
341361

342362

@@ -362,6 +382,7 @@ class Decompressor(object):
362382
_unconsumed_data = None
363383

364384
def __init__(self, dictionary=b''):
385+
self.lock = threading.Lock()
365386
dec = lib.BrotliDecoderCreateInstance(ffi.NULL, ffi.NULL, ffi.NULL)
366387
self._decoder = ffi.gc(dec, lib.BrotliDecoderDestroyInstance)
367388
self._unconsumed_data = b''
@@ -409,6 +430,16 @@ def decompress(self, data, output_buffer_limit=None):
409430
:type output_buffer_limit: ``int`` or ``None``
410431
:returns: A bytestring containing the decompressed data.
411432
"""
433+
if not self.lock.acquire(blocking=False):
434+
raise error(
435+
"Concurrently sharing Decompressor instances is not allowed")
436+
try:
437+
chunks = self._decompress(data, output_buffer_limit)
438+
finally:
439+
self.lock.release()
440+
return b''.join(chunks)
441+
442+
def _decompress(self, data, output_buffer_limit):
412443
if self._unconsumed_data and data:
413444
raise error(
414445
"brotli: decoder process called with data when "
@@ -486,8 +517,7 @@ def decompress(self, data, output_buffer_limit=None):
486517
else:
487518
# It's cool if we need more output, we just loop again.
488519
assert rc == lib.BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT
489-
490-
return b''.join(chunks)
520+
return chunks
491521

492522
process = decompress
493523

@@ -527,7 +557,15 @@ def is_finished(self):
527557
Returns ``True`` if the decompression stream
528558
is complete, ``False`` otherwise
529559
"""
530-
return lib.BrotliDecoderIsFinished(self._decoder) == lib.BROTLI_TRUE
560+
if not self.lock.acquire(blocking=False):
561+
raise error(
562+
"Concurrently sharing Decompressor instances is not allowed")
563+
try:
564+
ret = (
565+
lib.BrotliDecoderIsFinished(self._decoder) == lib.BROTLI_TRUE)
566+
finally:
567+
self.lock.release()
568+
return ret
531569

532570
def can_accept_more_data(self):
533571
"""
@@ -550,8 +588,16 @@ def can_accept_more_data(self):
550588
more compressed data.
551589
:rtype: ``bool``
552590
"""
553-
if len(self._unconsumed_data) > 0:
554-
return False
555-
if lib.BrotliDecoderHasMoreOutput(self._decoder) == lib.BROTLI_TRUE:
556-
return False
557-
return True
591+
if not self.lock.acquire(blocking=False):
592+
raise error(
593+
"Concurrently sharing Decompressor instances is not allowed")
594+
try:
595+
ret = True
596+
if len(self._unconsumed_data) > 0:
597+
ret = False
598+
if ((lib.BrotliDecoderHasMoreOutput(self._decoder) ==
599+
lib.BROTLI_TRUE)):
600+
ret = False
601+
finally:
602+
self.lock.release()
603+
return ret

0 commit comments

Comments
 (0)