Skip to content

Commit 011bab2

Browse files
authored
INTPYTHON-392 Add support for free-threaded Python (#311)
1 parent 64e6214 commit 011bab2

File tree

12 files changed

+940
-824
lines changed

12 files changed

+940
-824
lines changed

.github/workflows/dist-python.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,17 @@ jobs:
3939
- [ubuntu-24.04, manylinux_aarch64]
4040
- [macos-14, macosx_*]
4141
- [windows-2019, win_amd64]
42-
python: ["cp39", "cp310", "cp311", "cp312", "cp313"]
42+
python: ["cp39", "cp310", "cp311", "cp312", "cp313", "cp313t"]
4343
exclude:
4444
- buildplat: [macos-14, macosx_*]
4545
python: "cp39"
46+
- buildplat: [windows-2019, win_amd64]
47+
python: "cp313t"
4648
include:
4749
- buildplat: [macos-13, macosx_*]
4850
python: "cp39"
4951

52+
5053
steps:
5154
- name: Checkout pymongoarrow
5255
uses: actions/checkout@v4
@@ -85,13 +88,15 @@ jobs:
8588
env:
8689
MACOS_TEST_SKIP: "*arm64"
8790
CIBW_BUILD: cp39-macosx_*
91+
CIBW_ENABLE: cpython-freethreading
8892
MACOSX_DEPLOYMENT_TARGET: "10.14"
8993
run: python -m cibuildwheel --output-dir wheelhouse
9094

9195
- name: Build wheels
9296
if: ${{ matrix.buildplat[0] != 'macos-11' }}
9397
env:
9498
CIBW_BUILD: ${{ matrix.python }}-${{ matrix.buildplat[1] }}
99+
CIBW_ENABLE: cpython-freethreading
95100
MACOSX_DEPLOYMENT_TARGET: "12.0"
96101
run: python -m cibuildwheel --output-dir wheelhouse
97102

.github/workflows/test-python.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ jobs:
3737
strategy:
3838
matrix:
3939
os: ["ubuntu-latest", "macos-latest", "windows-latest"]
40-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
40+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.13t"]
41+
exclude:
42+
- os: "windows-latest"
43+
python-version: "3.13t"
4144
fail-fast: false
4245
name: CPython ${{ matrix.python-version }}-${{ matrix.os }}
4346
steps:
@@ -86,6 +89,8 @@ jobs:
8689
- name: Ensure imports with no test deps
8790
run: just import-check
8891
- name: Run the tests
92+
env:
93+
UV_PYTHON: ${{matrix.python-version}}
8994
run: just test
9095

9196
docs:

bindings/python/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# Changes in Version 1.8.0 (2025/MM/YY)
77

88
- Add support for PyArrow 20.0.
9+
- Add support for free-threaded python on Linux and MacOS.
910

1011
# Changes in Version 1.7.2 (2025/04/23)
1112

bindings/python/justfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import-check:
1414
uv run python -c "from pymongoarrow.lib import libbson_version"
1515

1616
benchmark *args:
17-
uv sync --dev --extra test
17+
uv sync --dev --extra test --extra test-polars
1818
uv run asv run -e --python=$(uv run python -c "import sys;print(sys.executable)") {{args}}
1919

2020
install:
@@ -23,7 +23,7 @@ install:
2323
uv run pre-commit install
2424

2525
test *args:
26-
uv sync --extra test
26+
uv sync --extra test --extra test-polars || uv sync --extra test
2727
uv run pytest {{args}}
2828

2929
lint:

bindings/python/pymongoarrow/lib.pyx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# Cython compiler directives
1616
# distutils: language=c++
1717
# cython: language_level=3
18+
# cython: freethreading_compatible = True
1819

1920
# Stdlib imports
2021
import sys

bindings/python/pymongoarrow/libarrow.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# Cython compiler directives
1616
# cython: language_level=3
1717
# distutils: language=c++
18+
# cython: freethreading_compatible = True
1819
from libcpp.vector cimport vector
1920
from libc.stdint cimport int32_t, uint8_t
2021
from pyarrow.lib cimport *

bindings/python/pymongoarrow/libbson.pxd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# Cython compiler directives
1616
# cython: language_level=3
1717
# distutils: language=c
18+
# cython: freethreading_compatible = True
1819
from libc.stdint cimport int32_t, int64_t, uint8_t, uint32_t, uint64_t
1920

2021

bindings/python/pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@ Source = "https://github.com/mongodb-labs/mongo-arrow/tree/main/bindings/python"
5151
Tracker = "https://jira.mongodb.org/projects/INTPYTHON/issues"
5252

5353
[project.optional-dependencies]
54-
test = ["pytz", "pytest", "polars"]
54+
test = ["pytz", "pytest"]
55+
test-polars = ["polars"]
5556

5657
[tool.setuptools]
5758
zip-safe = false
@@ -103,6 +104,7 @@ faulthandler_timeout = 1500
103104
xfail_strict = true
104105
filterwarnings = [
105106
"error",
107+
"module:The global interpreter lock:RuntimeWarning", # from pandas
106108
]
107109

108110
[tool.ruff]

bindings/python/test/test_arrow.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import concurrent.futures
1516
import io
1617
import json
1718
import tempfile
19+
import threading
1820
import unittest
1921
import unittest.mock as mock
2022
from datetime import date, datetime
@@ -244,8 +246,8 @@ def test_aggregate_omits_id_if_not_in_schema(self):
244246
def round_trip(self, data, schema, coll=None):
245247
if coll is None:
246248
coll = self.coll
247-
self.coll.drop()
248-
res = write(self.coll, data)
249+
coll.drop()
250+
res = write(coll, data)
249251
self.assertEqual(len(data), res.raw_result["insertedCount"])
250252
self.assertEqual(data, find_arrow_all(coll, {}, schema=schema))
251253
return res
@@ -1052,6 +1054,29 @@ def test_empty_embedded_array(self):
10521054
assert pmapatable2.to_pylist()[0] == doc2
10531055
write_table(pmapatable2, io.BytesIO())
10541056

1057+
def test_threading(self):
1058+
schema, data = self._create_data()
1059+
1060+
def run_test():
1061+
client = client_context.get_client(
1062+
event_listeners=[self.getmore_listener, self.cmd_listener]
1063+
)
1064+
name = f"test-{threading.current_thread().name}"
1065+
coll = client.pymongoarrow_test.get_collection(
1066+
name, write_concern=WriteConcern(w="majority")
1067+
)
1068+
coll.drop()
1069+
self.round_trip(data, Schema(schema), coll=coll)
1070+
client.close()
1071+
1072+
with concurrent.futures.ThreadPoolExecutor() as executor:
1073+
futures = []
1074+
for i in range(5):
1075+
futures.append(executor.submit(run_test))
1076+
concurrent.futures.wait(futures)
1077+
for future in futures:
1078+
future.result()
1079+
10551080

10561081
class TestArrowExplicitApi(ArrowApiTestMixin, unittest.TestCase):
10571082
def run_find(self, *args, **kwargs):

bindings/python/test/test_pandas.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
# from datetime import datetime, timedelta
15+
import concurrent.futures
1516
import datetime
1617
import tempfile
18+
import threading
1719
import unittest
1820
import unittest.mock as mock
1921
import warnings
@@ -266,18 +268,22 @@ def inner(i):
266268
raw_data["nested"] = [inner(i) for i in range(3)]
267269
return pd.DataFrame(data=raw_data).astype(schema)
268270

269-
def test_auto_schema(self):
271+
def _check_auto_schema(self, coll):
272+
coll.drop()
270273
data = self._create_nested_data()
271-
self.coll.drop()
272-
res = write(self.coll, data)
274+
res = write(coll, data)
273275
self.assertEqual(len(data), res.raw_result["insertedCount"])
274276
for func in [find_pandas_all, aggregate_pandas_all]:
275-
out = func(self.coll, {} if func == find_pandas_all else []).drop(columns=["_id"])
277+
out = func(coll, {} if func == find_pandas_all else []).drop(columns=["_id"])
276278
for name in data.columns:
277279
val = out[name]
278280
if str(val.dtype) == "object":
279281
val = val.astype(data[name].dtype)
280282
pd.testing.assert_series_equal(data[name], val)
283+
coll.drop()
284+
285+
def test_auto_schema(self):
286+
self._check_auto_schema(self.coll)
281287

282288
def test_auto_schema_heterogeneous(self):
283289
vals = [1, "2", True, 4]
@@ -345,6 +351,26 @@ def test_exclude_none(self):
345351
col_data = list(self.coll.find({}))
346352
assert "b" not in col_data[3]
347353

354+
def test_threading(self):
355+
def run_test():
356+
client = client_context.get_client(
357+
event_listeners=[self.getmore_listener, self.cmd_listener]
358+
)
359+
name = f"test-{threading.current_thread().name}"
360+
coll = client.pymongoarrow_test.get_collection(
361+
name, write_concern=WriteConcern(w="majority")
362+
)
363+
self._check_auto_schema(coll)
364+
client.close()
365+
366+
with concurrent.futures.ThreadPoolExecutor() as executor:
367+
futures = []
368+
for i in range(5):
369+
futures.append(executor.submit(run_test))
370+
concurrent.futures.wait(futures)
371+
for future in futures:
372+
future.result()
373+
348374

349375
class TestBSONTypes(PandasTestBase):
350376
@classmethod

0 commit comments

Comments
 (0)