Skip to content

Commit f57fb4f

Browse files
eustascopybara-github
authored andcommitted
Add py free-threaded support
Modernize testing (-> re-enable tests on Windows) PiperOrigin-RevId: 830892275
1 parent fa925d0 commit f57fb4f

File tree

4 files changed

+351
-280
lines changed

4 files changed

+351
-280
lines changed

.github/workflows/build_test.yml

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,22 @@ jobs:
133133
c_compiler: clang
134134
cxx_compiler: clang++
135135

136-
- name: python3.10-win
136+
- name: python3.14:clang
137137
build_system: python
138-
python_version: "3.10"
139-
# TODO: investigate why win-builds can't run tests
140-
py_setuptools_cmd: build_ext
141-
os: windows-2022
138+
python_version: "3.14"
139+
c_compiler: clang
140+
cxx_compiler: clang++
141+
142+
- name: python3.14t:clang
143+
build_system: python
144+
python_version: "3.14t"
145+
c_compiler: clang
146+
cxx_compiler: clang++
147+
148+
- name: python3.14-win
149+
build_system: python
150+
python_version: "3.14"
151+
os: windows-latest
142152

143153
- name: maven
144154
build_system: maven
@@ -332,9 +342,9 @@ jobs:
332342
if: ${{ matrix.build_system == 'python' }}
333343
run: |
334344
python -VV
335-
python -c "import sys; sys.exit('Invalid python version') if '.'.join(map(str,sys.version_info[0:2])) != '${{ matrix.python_version }}' else True"
336-
pip install setuptools==51.3.3
337-
python setup.py ${{ matrix.py_setuptools_cmd || 'test'}}
345+
pip install "setuptools>=70.0.0" pytest
346+
python setup.py build_ext --inplace
347+
pytest ./python/tests
338348
339349
build_test_dotnet:
340350
name: Build and test with .NET

.github/workflows/codeql.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ jobs:
7171
- if: matrix.language == 'cpp' || matrix.language == 'python'
7272
name: Build Python
7373
run: |
74+
python -VV
75+
pip install "setuptools>=70.0.0"
7476
python setup.py build_ext
7577
7678
- name: Perform CodeQL Analysis

python/_brotli.c

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,28 @@
2222
#error "Only Python 3.10+ is supported"
2323
#endif
2424

25+
/*
26+
Decoder / encoder nature does not support concurrent access. Attempt to enter
27+
concurrently will result in an exception.
28+
29+
"Critical" parts used in prologues to ensure that only one thread enters.
30+
For consistency, we use them in epilogues as well. "Critical" is essential for
31+
free-threaded. In GIL environment those rendered as a scope (i.e. `{` and `}`).
32+
33+
NB: `Py_BEGIN_ALLOW_THREADS` / `Py_END_ALLOW_THREADS` are still required to
34+
unblock the stop-the-world GC.
35+
*/
36+
#ifdef Py_GIL_DISABLED
37+
#if PY_MAJOR_VERSION < 3 || (PY_MAJOR_VERSION == 3 && PY_MINOR_VERSION < 13)
38+
#error "Critical sections are only available in Python 3.13+"
39+
#endif
40+
#define BROTLI_CRITICAL_START Py_BEGIN_CRITICAL_SECTION(self)
41+
#define BROTLI_CRITICAL_END Py_END_CRITICAL_SECTION()
42+
#else
43+
#define BROTLI_CRITICAL_START {
44+
#define BROTLI_CRITICAL_END }
45+
#endif
46+
2547
static const char kErrorAttr[] = "error";
2648
static const char kModuleAttr[] = "_module";
2749

@@ -522,13 +544,22 @@ static PyObject* brotli_Compressor_process(PyBrotli_Compressor* self,
522544
PyObject* ret = NULL;
523545
PyObject* input_object = NULL;
524546
Py_buffer input;
547+
int ok = 1;
525548

549+
BROTLI_CRITICAL_START;
526550
if (self->healthy == 0) {
527551
set_brotli_exception(self_type, kCompressUnhealthyError);
528-
return NULL;
552+
ok = 0;
529553
}
530554
if (self->processing != 0) {
531555
set_brotli_exception(self_type, kCompressConcurrentError);
556+
ok = 0;
557+
}
558+
if (ok) {
559+
self->processing = 1;
560+
}
561+
BROTLI_CRITICAL_END;
562+
if (!ok) {
532563
return NULL;
533564
}
534565

@@ -539,49 +570,76 @@ static PyObject* brotli_Compressor_process(PyBrotli_Compressor* self,
539570
return NULL;
540571
}
541572

542-
self->processing = 1;
543573
ret = compress_stream(self, BROTLI_OPERATION_PROCESS, (uint8_t*)input.buf,
544574
input.len);
545575
PyBuffer_Release(&input);
576+
577+
BROTLI_CRITICAL_START;
546578
self->processing = 0;
579+
BROTLI_CRITICAL_END;
580+
547581
return ret;
548582
}
549583

550584
static PyObject* brotli_Compressor_flush(PyBrotli_Compressor* self) {
551585
PyObject* self_type = (PyObject*)Py_TYPE((PyObject*)self);
552586
PyObject* ret = NULL;
587+
int ok = 1;
553588

589+
BROTLI_CRITICAL_START;
554590
if (self->healthy == 0) {
555591
set_brotli_exception(self_type, kCompressUnhealthyError);
556-
return NULL;
592+
ok = 0;
557593
}
558594
if (self->processing != 0) {
559595
set_brotli_exception(self_type, kCompressConcurrentError);
596+
ok = 0;
597+
}
598+
if (ok) {
599+
self->processing = 1;
600+
}
601+
BROTLI_CRITICAL_END;
602+
if (!ok) {
560603
return NULL;
561604
}
562605

563-
self->processing = 1;
564606
ret = compress_stream(self, BROTLI_OPERATION_FLUSH, NULL, 0);
607+
608+
BROTLI_CRITICAL_START;
565609
self->processing = 0;
610+
BROTLI_CRITICAL_END;
611+
566612
return ret;
567613
}
568614

569615
static PyObject* brotli_Compressor_finish(PyBrotli_Compressor* self) {
570616
PyObject* self_type = (PyObject*)Py_TYPE((PyObject*)self);
571617
PyObject* ret = NULL;
618+
int ok = 1;
572619

620+
BROTLI_CRITICAL_START;
573621
if (self->healthy == 0) {
574622
set_brotli_exception(self_type, kCompressUnhealthyError);
575-
return NULL;
623+
ok = 0;
576624
}
577625
if (self->processing != 0) {
578626
set_brotli_exception(self_type, kCompressConcurrentError);
627+
ok = 0;
628+
}
629+
if (ok) {
630+
self->processing = 1;
631+
}
632+
BROTLI_CRITICAL_END;
633+
if (!ok) {
579634
return NULL;
580635
}
581636

582-
self->processing = 1;
583637
ret = compress_stream(self, BROTLI_OPERATION_FINISH, NULL, 0);
638+
639+
BROTLI_CRITICAL_START;
584640
self->processing = 0;
641+
BROTLI_CRITICAL_END;
642+
585643
if (ret != NULL) {
586644
assert(BrotliEncoderIsFinished(self->enc));
587645
}
@@ -664,13 +722,22 @@ static PyObject* brotli_Decompressor_process(PyBrotli_Decompressor* self,
664722
uint8_t* new_tail = NULL;
665723
size_t new_tail_length = 0;
666724
int oom = 0;
725+
int ok = 1;
667726

727+
BROTLI_CRITICAL_START;
668728
if (self->healthy == 0) {
669729
set_brotli_exception(self_type, kDecompressUnhealthyError);
670-
return NULL;
730+
ok = 0;
671731
}
672732
if (self->processing != 0) {
673733
set_brotli_exception(self_type, kDecompressConcurrentError);
734+
ok = 0;
735+
}
736+
if (ok) {
737+
self->processing = 1;
738+
}
739+
BROTLI_CRITICAL_END;
740+
if (!ok) {
674741
return NULL;
675742
}
676743

@@ -683,7 +750,6 @@ static PyObject* brotli_Decompressor_process(PyBrotli_Decompressor* self,
683750
}
684751

685752
Buffer_Init(&buffer);
686-
self->processing = 1;
687753

688754
if (self->unconsumed_data_length > 0) {
689755
if (input.len > 0) {
@@ -759,17 +825,22 @@ static PyObject* brotli_Decompressor_process(PyBrotli_Decompressor* self,
759825
}
760826
PyBuffer_Release(&input);
761827
Buffer_Cleanup(&buffer);
828+
829+
ok = (PyErr_Occurred() == NULL) ? 1 : 0;
830+
BROTLI_CRITICAL_START;
762831
if (self->unconsumed_data) {
763832
free(self->unconsumed_data);
764833
self->unconsumed_data = NULL;
765834
}
766835
self->unconsumed_data = new_tail;
767836
self->unconsumed_data_length = new_tail_length;
768-
if (PyErr_Occurred() != NULL) {
837+
if (!ok) {
769838
assert(ret == NULL);
770839
self->healthy = 0;
771840
}
772841
self->processing = 0;
842+
BROTLI_CRITICAL_END;
843+
773844
return ret;
774845
}
775846

@@ -1003,7 +1074,9 @@ static PyMethodDef brotli_methods[] = {
10031074

10041075
static PyModuleDef_Slot brotli_mod_slots[] = {
10051076
{Py_mod_exec, brotli_init_mod},
1006-
#if (PY_MAJOR_VERSION > 3) || (PY_MINOR_VERSION >= 12)
1077+
#ifdef Py_GIL_DISABLED
1078+
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
1079+
#elif (PY_MAJOR_VERSION > 3) || (PY_MINOR_VERSION >= 12)
10071080
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
10081081
#endif
10091082
{0, NULL}};

0 commit comments

Comments
 (0)