Skip to content

Commit 9e42642

Browse files
authored
Add Python 3.14 support, experimental subinterpreter/freethreading support (#1279)
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*. Support for subinterpreters and freethreading is EXPERIMENTAL.
1 parent 6fe1c49 commit 9e42642

24 files changed

+3538
-519
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ 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", "3.14t"]
2121
os: [ubuntu-latest, macos-latest, windows-latest]
2222
loop: [asyncio, uvloop]
2323
exclude:

.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.pxd

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,18 @@ ctypedef object (*codec_decode_func)(Codec codec,
2222
FRBuffer *buf)
2323

2424

25+
cdef class CodecMap:
26+
cdef:
27+
void** binary_codec_map
28+
void** text_codec_map
29+
dict extra_codecs
30+
31+
cdef inline void *get_binary_codec_ptr(self, uint32_t idx)
32+
cdef inline void set_binary_codec_ptr(self, uint32_t idx, void *ptr)
33+
cdef inline void *get_text_codec_ptr(self, uint32_t idx)
34+
cdef inline void set_text_codec_ptr(self, uint32_t idx, void *ptr)
35+
36+
2537
cdef enum CodecType:
2638
CODEC_UNDEFINED = 0
2739
CODEC_C = 1

asyncpg/protocol/codecs/base.pyx

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,33 @@ import asyncpg
1111
from asyncpg import exceptions
1212

1313

14-
cdef void* binary_codec_map[(MAXSUPPORTEDOID + 1) * 2]
15-
cdef void* text_codec_map[(MAXSUPPORTEDOID + 1) * 2]
16-
cdef dict EXTRA_CODECS = {}
14+
# The class indirection is needed because Cython
15+
# does not (as of 3.1.0) store global cdef variables
16+
# in module state.
17+
@cython.final
18+
cdef class CodecMap:
19+
20+
def __cinit__(self):
21+
self.extra_codecs = {}
22+
self.binary_codec_map = <void **>cpython.PyMem_Calloc(
23+
(MAXSUPPORTEDOID + 1) * 2, sizeof(void *))
24+
self.text_codec_map = <void **>cpython.PyMem_Calloc(
25+
(MAXSUPPORTEDOID + 1) * 2, sizeof(void *))
26+
27+
cdef inline void *get_binary_codec_ptr(self, uint32_t idx):
28+
return <void*>self.binary_codec_map[idx]
29+
30+
cdef inline void set_binary_codec_ptr(self, uint32_t idx, void *ptr):
31+
self.binary_codec_map[idx] = ptr
32+
33+
cdef inline void *get_text_codec_ptr(self, uint32_t idx):
34+
return <void*>self.text_codec_map[idx]
35+
36+
cdef inline void set_text_codec_ptr(self, uint32_t idx, void *ptr):
37+
self.text_codec_map[idx] = ptr
38+
39+
40+
codec_map = CodecMap()
1741

1842

1943
@cython.final
@@ -67,7 +91,7 @@ cdef class Codec:
6791
)
6892

6993
if element_names is not None:
70-
self.record_desc = record.ApgRecordDesc_New(
94+
self.record_desc = RecordDescriptor(
7195
element_names, tuple(element_names))
7296
else:
7397
self.record_desc = None
@@ -271,7 +295,7 @@ cdef class Codec:
271295
schema=self.schema,
272296
data_type=self.name,
273297
)
274-
result = record.ApgRecord_New(asyncpg.Record, self.record_desc, elem_count)
298+
result = self.record_desc.make_record(asyncpg.Record, elem_count)
275299
for i in range(elem_count):
276300
elem_typ = self.element_type_oids[i]
277301
received_elem_typ = <uint32_t>hton.unpack_int32(frb_read(buf, 4))
@@ -301,7 +325,7 @@ cdef class Codec:
301325
settings, frb_slice_from(&elem_buf, buf, elem_len))
302326

303327
cpython.Py_INCREF(elem)
304-
record.ApgRecord_SET_ITEM(result, i, elem)
328+
recordcapi.ApgRecord_SET_ITEM(result, i, elem)
305329

306330
return result
307331

@@ -811,9 +835,9 @@ cdef inline Codec get_core_codec(
811835
if oid > MAXSUPPORTEDOID:
812836
return None
813837
if format == PG_FORMAT_BINARY:
814-
ptr = binary_codec_map[oid * xformat]
838+
ptr = (<CodecMap>codec_map).get_binary_codec_ptr(oid * xformat)
815839
elif format == PG_FORMAT_TEXT:
816-
ptr = text_codec_map[oid * xformat]
840+
ptr = (<CodecMap>codec_map).get_text_codec_ptr(oid * xformat)
817841

818842
if ptr is NULL:
819843
return None
@@ -839,7 +863,10 @@ cdef inline Codec get_any_core_codec(
839863

840864

841865
cdef inline int has_core_codec(uint32_t oid):
842-
return binary_codec_map[oid] != NULL or text_codec_map[oid] != NULL
866+
return (
867+
(<CodecMap>codec_map).get_binary_codec_ptr(oid) != NULL
868+
or (<CodecMap>codec_map).get_text_codec_ptr(oid) != NULL
869+
)
843870

844871

845872
cdef register_core_codec(uint32_t oid,
@@ -867,9 +894,9 @@ cdef register_core_codec(uint32_t oid,
867894
cpython.Py_INCREF(codec) # immortalize
868895

869896
if format == PG_FORMAT_BINARY:
870-
binary_codec_map[oid * xformat] = <void*>codec
897+
(<CodecMap>codec_map).set_binary_codec_ptr(oid * xformat, <void*>codec)
871898
elif format == PG_FORMAT_TEXT:
872-
text_codec_map[oid * xformat] = <void*>codec
899+
(<CodecMap>codec_map).set_text_codec_ptr(oid * xformat, <void*>codec)
873900
else:
874901
raise exceptions.InternalClientError(
875902
'invalid data format: {}'.format(format))
@@ -888,8 +915,8 @@ cdef register_extra_codec(str name,
888915
codec = Codec(INVALIDOID)
889916
codec.init(name, None, kind, CODEC_C, format, PG_XFORMAT_OBJECT,
890917
encode, decode, None, None, None, None, None, None, None, 0)
891-
EXTRA_CODECS[name, format] = codec
918+
(<CodecMap>codec_map).extra_codecs[name, format] = codec
892919

893920

894921
cdef inline Codec get_extra_codec(str name, ServerDataFormat format):
895-
return EXTRA_CODECS.get((name, format))
922+
return (<CodecMap>codec_map).extra_codecs.get((name, format))

asyncpg/protocol/coreproto.pyx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import hashlib
1111
include "scram.pyx"
1212

1313

14-
cdef dict AUTH_METHOD_NAME = {
14+
AUTH_METHOD_NAME = {
1515
AUTH_REQUIRED_KERBEROS: 'kerberosv5',
1616
AUTH_REQUIRED_PASSWORD: 'password',
1717
AUTH_REQUIRED_PASSWORDMD5: 'md5',
@@ -1229,5 +1229,5 @@ cdef class CoreProtocol:
12291229
pass
12301230

12311231

1232-
cdef bytes SYNC_MESSAGE = bytes(WriteBuffer.new_message(b'S').end_message())
1233-
cdef bytes FLUSH_MESSAGE = bytes(WriteBuffer.new_message(b'H').end_message())
1232+
SYNC_MESSAGE = bytes(WriteBuffer.new_message(b'S').end_message())
1233+
FLUSH_MESSAGE = bytes(WriteBuffer.new_message(b'H').end_message())

asyncpg/protocol/encodings.pyx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
https://www.postgresql.org/docs/current/static/multibyte.html#CHARSET-TABLE
1111
'''
1212

13-
cdef dict ENCODINGS_MAP = {
13+
ENCODINGS_MAP = {
1414
'abc': 'cp1258',
1515
'alt': 'cp866',
1616
'euc_cn': 'euccn',

0 commit comments

Comments
 (0)