Skip to content

Commit f21c7d4

Browse files
committed
Add Python 3.14 support, experimental subinterpreter/freethreading support
The bulk of the changes here is a rewrite of `recordobj.c` to use modern CPython API to properly isolate the module (PEP 489, PEP 573, PEP 630). This, along with Cython flags, enables support for safely importing `asyncpg` in subinterpreters. The `Record` freelist is now thread-specific, so asyncpg should be thread-safe *at the C level*. Both subinterpreter and freethreading support is EXPERIMENTAL.
1 parent 6fe1c49 commit f21c7d4

22 files changed

+4106
-1133
lines changed

.clang-format

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# A clang-format style that approximates Python's PEP 7
2+
BasedOnStyle: Google
3+
AlwaysBreakAfterReturnType: All
4+
AllowShortIfStatementsOnASingleLine: false
5+
AlignAfterOpenBracket: Align
6+
BreakBeforeBraces: Stroustrup
7+
ColumnLimit: 95
8+
DerivePointerAlignment: false
9+
IndentWidth: 4
10+
Language: Cpp
11+
PointerAlignment: Right
12+
ReflowComments: true
13+
SpaceBeforeParens: ControlStatements
14+
SpacesInParentheses: false
15+
TabWidth: 4
16+
UseTab: Never
17+
SortIncludes: false

.clangd

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Diagnostics:
2+
Includes:
3+
IgnoreHeader:
4+
- "pythoncapi_compat.*\\.h"

.github/workflows/tests.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@ jobs:
1717
# job.
1818
strategy:
1919
matrix:
20-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
20+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
2121
os: [ubuntu-latest, macos-latest, windows-latest]
2222
loop: [asyncio, uvloop]
2323
exclude:
2424
# uvloop does not support windows
2525
- loop: uvloop
2626
os: windows-latest
27+
# or python 3.14 (yet)
28+
- loop: uvloop
29+
python-version: "3.14"
2730

2831
runs-on: ${{ matrix.os }}
2932

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ docs/_build
3333
/.pytest_cache/
3434
/.eggs
3535
/.vscode
36+
/.zed
3637
/.mypy_cache
3738
/.venv*
3839
/.tox
40+
/compile_commands.json

asyncpg/connection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2751,8 +2751,8 @@ def _check_record_class(record_class):
27512751
and issubclass(record_class, protocol.Record)
27522752
):
27532753
if (
2754-
record_class.__new__ is not object.__new__
2755-
or record_class.__init__ is not object.__init__
2754+
record_class.__new__ is not protocol.Record.__new__
2755+
or record_class.__init__ is not protocol.Record.__init__
27562756
):
27572757
raise exceptions.InterfaceError(
27582758
'record_class must not redefine __new__ or __init__'

asyncpg/protocol/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@
88

99
from __future__ import annotations
1010

11-
from .protocol import Protocol, Record, NO_TIMEOUT, BUILTIN_TYPE_NAME_MAP
11+
from .protocol import Protocol, NO_TIMEOUT, BUILTIN_TYPE_NAME_MAP
12+
from ..record import Record

asyncpg/protocol/codecs/base.pyx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ cdef class Codec:
6767
)
6868

6969
if element_names is not None:
70-
self.record_desc = record.ApgRecordDesc_New(
70+
self.record_desc = RecordDescriptor(
7171
element_names, tuple(element_names))
7272
else:
7373
self.record_desc = None
@@ -271,7 +271,7 @@ cdef class Codec:
271271
schema=self.schema,
272272
data_type=self.name,
273273
)
274-
result = record.ApgRecord_New(asyncpg.Record, self.record_desc, elem_count)
274+
result = self.record_desc.make_record(asyncpg.Record, elem_count)
275275
for i in range(elem_count):
276276
elem_typ = self.element_type_oids[i]
277277
received_elem_typ = <uint32_t>hton.unpack_int32(frb_read(buf, 4))
@@ -301,7 +301,7 @@ cdef class Codec:
301301
settings, frb_slice_from(&elem_buf, buf, elem_len))
302302

303303
cpython.Py_INCREF(elem)
304-
record.ApgRecord_SET_ITEM(result, i, elem)
304+
recordcapi.ApgRecord_SET_ITEM(result, i, elem)
305305

306306
return result
307307

asyncpg/protocol/prepared_stmt.pyx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ cdef class PreparedStatementState:
230230
return
231231

232232
if self.cols_num == 0:
233-
self.cols_desc = record.ApgRecordDesc_New({}, ())
233+
self.cols_desc = RecordDescriptor({}, ())
234234
return
235235

236236
cols_mapping = collections.OrderedDict()
@@ -252,7 +252,7 @@ cdef class PreparedStatementState:
252252

253253
codecs.append(codec)
254254

255-
self.cols_desc = record.ApgRecordDesc_New(
255+
self.cols_desc = RecordDescriptor(
256256
cols_mapping, tuple(cols_names))
257257

258258
self.rows_codecs = tuple(codecs)
@@ -310,7 +310,7 @@ cdef class PreparedStatementState:
310310
'different from what was described ({})'.format(
311311
fnum, self.cols_num))
312312

313-
dec_row = record.ApgRecord_New(self.record_class, self.cols_desc, fnum)
313+
dec_row = self.cols_desc.make_record(self.record_class, fnum)
314314
for i in range(fnum):
315315
flen = hton.unpack_int32(frb_read(&rbuf, 4))
316316

@@ -333,7 +333,7 @@ cdef class PreparedStatementState:
333333
frb_set_len(&rbuf, bl - flen)
334334

335335
cpython.Py_INCREF(val)
336-
record.ApgRecord_SET_ITEM(dec_row, i, val)
336+
recordcapi.ApgRecord_SET_ITEM(dec_row, i, val)
337337

338338
if frb_get_len(&rbuf) != 0:
339339
raise BufferError('unexpected trailing {} bytes in buffer'.format(

asyncpg/protocol/protocol.pyi

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import asyncio
22
import asyncio.protocols
33
import hmac
44
from codecs import CodecInfo
5-
from collections.abc import Callable, Iterable, Iterator, Sequence
5+
from collections.abc import Callable, Iterable, Sequence
66
from hashlib import md5, sha256
77
from typing import (
88
Any,
@@ -22,8 +22,8 @@ import asyncpg.pgproto.pgproto
2222
from ..connect_utils import _ConnectionParameters
2323
from ..pgproto.pgproto import WriteBuffer
2424
from ..types import Attribute, Type
25+
from ..record import Record
2526

26-
_T = TypeVar('_T')
2727
_Record = TypeVar('_Record', bound=Record)
2828
_OtherRecord = TypeVar('_OtherRecord', bound=Record)
2929
_PreparedStatementState = TypeVar(
@@ -254,24 +254,6 @@ class DataCodecConfig:
254254

255255
class Protocol(BaseProtocol[_Record], asyncio.protocols.Protocol): ...
256256

257-
class Record:
258-
@overload
259-
def get(self, key: str) -> Any | None: ...
260-
@overload
261-
def get(self, key: str, default: _T) -> Any | _T: ...
262-
def items(self) -> Iterator[tuple[str, Any]]: ...
263-
def keys(self) -> Iterator[str]: ...
264-
def values(self) -> Iterator[Any]: ...
265-
@overload
266-
def __getitem__(self, index: str) -> Any: ...
267-
@overload
268-
def __getitem__(self, index: int) -> Any: ...
269-
@overload
270-
def __getitem__(self, index: slice) -> tuple[Any, ...]: ...
271-
def __iter__(self) -> Iterator[Any]: ...
272-
def __contains__(self, x: object) -> bool: ...
273-
def __len__(self) -> int: ...
274-
275257
class Timer:
276258
def __init__(self, budget: float | None) -> None: ...
277259
def __enter__(self) -> None: ...

asyncpg/protocol/protocol.pyx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ from asyncpg.pgproto.pgproto cimport (
3434

3535
from asyncpg.pgproto cimport pgproto
3636
from asyncpg.protocol cimport cpythonx
37-
from asyncpg.protocol cimport record
37+
from asyncpg.protocol cimport recordcapi
3838

3939
from libc.stdint cimport int8_t, uint8_t, int16_t, uint16_t, \
4040
int32_t, uint32_t, int64_t, uint64_t, \
@@ -46,6 +46,7 @@ from asyncpg import types as apg_types
4646
from asyncpg import exceptions as apg_exc
4747

4848
from asyncpg.pgproto cimport hton
49+
from asyncpg.record import Record, RecordDescriptor
4950

5051

5152
include "consts.pxi"
@@ -1049,17 +1050,14 @@ def _create_record(object mapping, tuple elems):
10491050
int32_t i
10501051

10511052
if mapping is None:
1052-
desc = record.ApgRecordDesc_New({}, ())
1053+
desc = RecordDescriptor({}, ())
10531054
else:
1054-
desc = record.ApgRecordDesc_New(
1055+
desc = RecordDescriptor(
10551056
mapping, tuple(mapping) if mapping else ())
10561057

1057-
rec = record.ApgRecord_New(Record, desc, len(elems))
1058+
rec = desc.make_record(Record, len(elems))
10581059
for i in range(len(elems)):
10591060
elem = elems[i]
10601061
cpython.Py_INCREF(elem)
1061-
record.ApgRecord_SET_ITEM(rec, i, elem)
1062+
recordcapi.ApgRecord_SET_ITEM(rec, i, elem)
10621063
return rec
1063-
1064-
1065-
Record = <object>record.ApgRecord_InitTypes()

0 commit comments

Comments
 (0)