Skip to content

Commit de5489e

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*. Support for subinterpreters and freethreading is EXPERIMENTAL.
1 parent 6fe1c49 commit de5489e

26 files changed

+4159
-1148
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: 38 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,31 @@ 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((MAXSUPPORTEDOID + 1) * 2, sizeof(void *));
23+
self.text_codec_map = <void **>cpython.PyMem_Calloc((MAXSUPPORTEDOID + 1) * 2, sizeof(void *));
24+
25+
cdef inline void *get_binary_codec_ptr(self, uint32_t idx):
26+
return <void*>self.binary_codec_map[idx]
27+
28+
cdef inline void set_binary_codec_ptr(self, uint32_t idx, void *ptr):
29+
self.binary_codec_map[idx] = ptr
30+
31+
cdef inline void *get_text_codec_ptr(self, uint32_t idx):
32+
return <void*>self.text_codec_map[idx]
33+
34+
cdef inline void set_text_codec_ptr(self, uint32_t idx, void *ptr):
35+
self.text_codec_map[idx] = ptr
36+
37+
38+
codec_map = CodecMap()
1739

1840

1941
@cython.final
@@ -67,7 +89,7 @@ cdef class Codec:
6789
)
6890

6991
if element_names is not None:
70-
self.record_desc = record.ApgRecordDesc_New(
92+
self.record_desc = RecordDescriptor(
7193
element_names, tuple(element_names))
7294
else:
7395
self.record_desc = None
@@ -271,7 +293,7 @@ cdef class Codec:
271293
schema=self.schema,
272294
data_type=self.name,
273295
)
274-
result = record.ApgRecord_New(asyncpg.Record, self.record_desc, elem_count)
296+
result = self.record_desc.make_record(asyncpg.Record, elem_count)
275297
for i in range(elem_count):
276298
elem_typ = self.element_type_oids[i]
277299
received_elem_typ = <uint32_t>hton.unpack_int32(frb_read(buf, 4))
@@ -301,7 +323,7 @@ cdef class Codec:
301323
settings, frb_slice_from(&elem_buf, buf, elem_len))
302324

303325
cpython.Py_INCREF(elem)
304-
record.ApgRecord_SET_ITEM(result, i, elem)
326+
recordcapi.ApgRecord_SET_ITEM(result, i, elem)
305327

306328
return result
307329

@@ -811,9 +833,9 @@ cdef inline Codec get_core_codec(
811833
if oid > MAXSUPPORTEDOID:
812834
return None
813835
if format == PG_FORMAT_BINARY:
814-
ptr = binary_codec_map[oid * xformat]
836+
ptr = (<CodecMap>codec_map).get_binary_codec_ptr(oid * xformat)
815837
elif format == PG_FORMAT_TEXT:
816-
ptr = text_codec_map[oid * xformat]
838+
ptr = (<CodecMap>codec_map).get_text_codec_ptr(oid * xformat)
817839

818840
if ptr is NULL:
819841
return None
@@ -839,7 +861,10 @@ cdef inline Codec get_any_core_codec(
839861

840862

841863
cdef inline int has_core_codec(uint32_t oid):
842-
return binary_codec_map[oid] != NULL or text_codec_map[oid] != NULL
864+
return (
865+
(<CodecMap>codec_map).get_binary_codec_ptr(oid) != NULL
866+
or (<CodecMap>codec_map).get_text_codec_ptr(oid) != NULL
867+
)
843868

844869

845870
cdef register_core_codec(uint32_t oid,
@@ -867,9 +892,9 @@ cdef register_core_codec(uint32_t oid,
867892
cpython.Py_INCREF(codec) # immortalize
868893

869894
if format == PG_FORMAT_BINARY:
870-
binary_codec_map[oid * xformat] = <void*>codec
895+
(<CodecMap>codec_map).set_binary_codec_ptr(oid * xformat, <void*>codec)
871896
elif format == PG_FORMAT_TEXT:
872-
text_codec_map[oid * xformat] = <void*>codec
897+
(<CodecMap>codec_map).set_text_codec_ptr(oid * xformat, <void*>codec)
873898
else:
874899
raise exceptions.InternalClientError(
875900
'invalid data format: {}'.format(format))
@@ -888,8 +913,8 @@ cdef register_extra_codec(str name,
888913
codec = Codec(INVALIDOID)
889914
codec.init(name, None, kind, CODEC_C, format, PG_XFORMAT_OBJECT,
890915
encode, decode, None, None, None, None, None, None, None, 0)
891-
EXTRA_CODECS[name, format] = codec
916+
(<CodecMap>codec_map).extra_codecs[name, format] = codec
892917

893918

894919
cdef inline Codec get_extra_codec(str name, ServerDataFormat format):
895-
return EXTRA_CODECS.get((name, format))
920+
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)