diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e95e301..955ca769 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,6 +59,7 @@ jobs: - "3.12" - "3.13" - "3.14" + - "3.14t" - "pypy-3.10" - "pypy-3.11" os: ["ubuntu-latest"] diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9f430fa1..0cd0f65c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -21,6 +21,8 @@ version 1.8.0-dev + Include test packages in the source distribution, so source distribution installations can be verified. + Fix an issue where some tests failed because they ignored PYTHONPATH. ++ Enable support for free-threading and build free-threaded wheels for + CPython 3.14. Thanks to @lysnikolaou and @ngoldbaum. version 1.7.2 ----------------- diff --git a/src/isal/_isalmodule.c b/src/isal/_isalmodule.c index a429d3a7..55f43917 100644 --- a/src/isal/_isalmodule.c +++ b/src/isal/_isalmodule.c @@ -31,6 +31,11 @@ PyInit__isal(void) if (m == NULL) { return NULL; } + +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + PyModule_AddIntMacro(m, ISAL_MAJOR_VERSION); PyModule_AddIntMacro(m, ISAL_MINOR_VERSION); PyModule_AddIntMacro(m, ISAL_PATCH_VERSION); diff --git a/src/isal/igzip_libmodule.c b/src/isal/igzip_libmodule.c index de94ab01..d498145f 100644 --- a/src/isal/igzip_libmodule.c +++ b/src/isal/igzip_libmodule.c @@ -617,6 +617,11 @@ PyInit_igzip_lib(void) if (m == NULL) return NULL; +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + + IsalError = PyErr_NewException("igzip_lib.IsalError", NULL, NULL); if (IsalError == NULL) { return NULL; diff --git a/src/isal/isal_zlibmodule.c b/src/isal/isal_zlibmodule.c index 67547be9..3a280ec8 100644 --- a/src/isal/isal_zlibmodule.c +++ b/src/isal/isal_zlibmodule.c @@ -2183,6 +2183,10 @@ PyInit_isal_zlib(void) return NULL; } +#ifdef Py_GIL_DISABLED + PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED); +#endif + PyObject *igzip_lib_module = PyImport_ImportModule("isal.igzip_lib"); if (igzip_lib_module == NULL) { return NULL; diff --git a/tests/test_freethreading.py b/tests/test_freethreading.py new file mode 100644 index 00000000..10d1d53b --- /dev/null +++ b/tests/test_freethreading.py @@ -0,0 +1,110 @@ +import concurrent.futures +import random +import string +import threading + +from isal import igzip_lib, isal_zlib + +import pytest + + +HAMLET_SCENE = b""" +LAERTES + + O, fear me not. + I stay too long: but here my father comes. + + Enter POLONIUS + + A double blessing is a double grace, + Occasion smiles upon a second leave. + +LORD POLONIUS + + Yet here, Laertes! aboard, aboard, for shame! + The wind sits in the shoulder of your sail, + And you are stay'd for. There; my blessing with thee! + And these few precepts in thy memory + See thou character. Give thy thoughts no tongue, + Nor any unproportioned thought his act. + Be thou familiar, but by no means vulgar. + Those friends thou hast, and their adoption tried, + Grapple them to thy soul with hoops of steel; + But do not dull thy palm with entertainment + Of each new-hatch'd, unfledged comrade. Beware + Of entrance to a quarrel, but being in, + Bear't that the opposed may beware of thee. + Give every man thy ear, but few thy voice; + Take each man's censure, but reserve thy judgment. + Costly thy habit as thy purse can buy, + But not express'd in fancy; rich, not gaudy; + For the apparel oft proclaims the man, + And they in France of the best rank and station + Are of a most select and generous chief in that. + Neither a borrower nor a lender be; + For loan oft loses both itself and friend, + And borrowing dulls the edge of husbandry. + This above all: to thine ownself be true, + And it must follow, as the night the day, + Thou canst not then be false to any man. + Farewell: my blessing season this in thee! + +LAERTES + + Most humbly do I take my leave, my lord. + +LORD POLONIUS + + The time invites you; go; your servants tend. + +LAERTES + + Farewell, Ophelia; and remember well + What I have said to you. + +OPHELIA + + 'Tis in my memory lock'd, + And you yourself shall keep the key of it. + +LAERTES + + Farewell. +""" + +NUM_THREADS = 10 +NUM_ITERATIONS = 20 +NUM_JOBS = 50 # To simulate 50 jobs running in 10 threads +barrier = threading.Barrier(parties=NUM_THREADS) + + +def isal_compress_decompress(compress, decompress): + for _ in range(NUM_ITERATIONS): + barrier.wait() + x = compress(HAMLET_SCENE) + assert decompress(x) == HAMLET_SCENE + + length = len(HAMLET_SCENE) + hamlet_random = HAMLET_SCENE + b"".join( + [s.encode() for s in random.choices(string.printable, k=length)] + ) + barrier.wait() + x = compress(hamlet_random) + assert decompress(x) == hamlet_random + + +@pytest.mark.parametrize( + "compress,decompress", + [ + pytest.param(isal_zlib.compress, isal_zlib.decompress, id="zlib"), + pytest.param(igzip_lib.compress, igzip_lib.decompress, id="igzip"), + ] +) +def test_isal_compress_decompress_threaded(compress, decompress): + with concurrent.futures.ThreadPoolExecutor(NUM_THREADS) as executor: + futures = [ + executor.submit(isal_compress_decompress, compress, decompress) + for _ in range(NUM_JOBS) + ] + for future in concurrent.futures.as_completed(futures): + future.result() # To fire assertion error if there is one