Skip to content

Commit

Permalink
Merge pull request #140 from stealthrocket/py309
Browse files Browse the repository at this point in the history
Python 3.9 support
  • Loading branch information
chriso authored Mar 27, 2024
2 parents 0648bb1 + d9cef96 commit bc89165
Show file tree
Hide file tree
Showing 27 changed files with 462 additions and 331 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.10', '3.11', '3.12']
python: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python }}
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "dispatch-py"
description = "Develop reliable distributed systems on the Dispatch platform."
readme = "README.md"
dynamic = ["version"]
requires-python = ">= 3.10"
requires-python = ">= 3.9"
dependencies = [
"grpcio >= 1.60.0",
"protobuf >= 4.24.0",
Expand All @@ -17,7 +17,8 @@ dependencies = [
"tblib >= 3.0.0",
"docopt >= 0.6.2",
"types-docopt >= 0.6.11.4",
"httpx >= 0.27.0"
"httpx >= 0.27.0",
"typing_extensions >= 4.10"
]

[project.optional-dependencies]
Expand Down
6 changes: 3 additions & 3 deletions src/dispatch/coroutine.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,17 @@ def race(*awaitables: Awaitable[Any]) -> list[Any]: # type: ignore[misc]
return (yield RaceDirective(awaitables))


@dataclass(slots=True)
@dataclass
class AllDirective:
awaitables: tuple[Awaitable[Any], ...]


@dataclass(slots=True)
@dataclass
class AnyDirective:
awaitables: tuple[Awaitable[Any], ...]


@dataclass(slots=True)
@dataclass
class RaceDirective:
awaitables: tuple[Awaitable[Any], ...]

Expand Down
18 changes: 11 additions & 7 deletions src/dispatch/experimental/durable/frame.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@
#define PY_SSIZE_T_CLEAN
#include <Python.h>

#if PY_MAJOR_VERSION != 3 || (PY_MINOR_VERSION < 10 || PY_MINOR_VERSION > 13)
# error Python 3.10-3.13 is required
#if PY_MAJOR_VERSION != 3 || (PY_MINOR_VERSION < 9 || PY_MINOR_VERSION > 13)
# error Python 3.9-3.13 is required
#endif

// This is a redefinition of the private PyTryBlock from 3.10.
// This is a redefinition of the private PyTryBlock from <= 3.10.
// https://github.com/python/cpython/blob/3.9/Include/cpython/frameobject.h#L11
// https://github.com/python/cpython/blob/3.10/Include/cpython/frameobject.h#L22
typedef struct {
int b_type;
int b_handler;
int b_level;
} PyTryBlock;

// This is a redefinition of the private PyCoroWrapper from 3.10-3.13.
// This is a redefinition of the private PyCoroWrapper from 3.9-3.13.
// https://github.com/python/cpython/blob/3.9/Objects/genobject.c#L830
// https://github.com/python/cpython/blob/3.10/Objects/genobject.c#L884
// https://github.com/python/cpython/blob/3.11/Objects/genobject.c#L1016
// https://github.com/python/cpython/blob/3.12/Objects/genobject.c#L1003
Expand Down Expand Up @@ -51,7 +53,9 @@ static int get_frame_iblock(Frame *frame);
static void set_frame_iblock(Frame *frame, int iblock);
static PyTryBlock *get_frame_blockstack(Frame *frame);

#if PY_MINOR_VERSION == 10
#if PY_MINOR_VERSION == 9
#include "frame309.h"
#elif PY_MINOR_VERSION == 10
#include "frame310.h"
#elif PY_MINOR_VERSION == 11
#include "frame311.h"
Expand All @@ -78,7 +82,7 @@ static const char *get_type_name(PyObject *obj) {

static PyGenObject *get_generator_like_object(PyObject *obj) {
if (PyGen_Check(obj) || PyCoro_CheckExact(obj) || PyAsyncGen_CheckExact(obj)) {
// Note: In Python 3.10-3.13, the PyGenObject, PyCoroObject and PyAsyncGenObject
// Note: In Python 3.9-3.13, the PyGenObject, PyCoroObject and PyAsyncGenObject
// have the same layout, they just have different field prefixes (gi_, cr_, ag_).
// We cast to PyGenObject here so that the remainder of the code can use the gi_
// prefix for all three cases.
Expand Down Expand Up @@ -386,7 +390,7 @@ static PyObject *ext_set_frame_stack_at(PyObject *self, PyObject *args) {
}
PyObject **localsplus = get_frame_localsplus(frame);
PyObject *prev = localsplus[index];
if (Py_IsTrue(unset)) {
if (PyObject_IsTrue(unset)) {
localsplus[index] = NULL;
} else {
Py_INCREF(stack_obj);
Expand Down
32 changes: 19 additions & 13 deletions src/dispatch/experimental/durable/frame.pyi
Original file line number Diff line number Diff line change
@@ -1,55 +1,61 @@
from types import FrameType
from typing import Any, AsyncGenerator, Coroutine, Generator, Tuple
from typing import Any, AsyncGenerator, Coroutine, Generator, Tuple, Union

def get_frame_ip(frame: FrameType | Coroutine | Generator | AsyncGenerator) -> int:
def get_frame_ip(frame: Union[FrameType, Coroutine, Generator, AsyncGenerator]) -> int:
"""Get instruction pointer of a generator or coroutine."""

def set_frame_ip(frame: FrameType | Coroutine | Generator | AsyncGenerator, ip: int):
def set_frame_ip(
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], ip: int
):
"""Set instruction pointer of a generator or coroutine."""

def get_frame_sp(frame: FrameType | Coroutine | Generator | AsyncGenerator) -> int:
def get_frame_sp(frame: Union[FrameType, Coroutine, Generator, AsyncGenerator]) -> int:
"""Get stack pointer of a generator or coroutine."""

def set_frame_sp(frame: FrameType | Coroutine | Generator | AsyncGenerator, sp: int):
def set_frame_sp(
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], sp: int
):
"""Set stack pointer of a generator or coroutine."""

def get_frame_bp(frame: FrameType | Coroutine | Generator | AsyncGenerator) -> int:
def get_frame_bp(frame: Union[FrameType, Coroutine, Generator, AsyncGenerator]) -> int:
"""Get block pointer of a generator or coroutine."""

def set_frame_bp(frame: FrameType | Coroutine | Generator | AsyncGenerator, bp: int):
def set_frame_bp(
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], bp: int
):
"""Set block pointer of a generator or coroutine."""

def get_frame_stack_at(
frame: FrameType | Coroutine | Generator | AsyncGenerator, index: int
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], index: int
) -> Tuple[bool, Any]:
"""Get an object from a generator or coroutine's stack, as an (is_null, obj) tuple."""

def set_frame_stack_at(
frame: FrameType | Coroutine | Generator | AsyncGenerator,
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator],
index: int,
unset: bool,
value: Any,
):
"""Set or unset an object on the stack of a generator or coroutine."""

def get_frame_block_at(
frame: FrameType | Coroutine | Generator | AsyncGenerator, index: int
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], index: int
) -> Tuple[int, int, int]:
"""Get a block from a generator or coroutine."""

def set_frame_block_at(
frame: FrameType | Coroutine | Generator | AsyncGenerator,
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator],
index: int,
value: Tuple[int, int, int],
):
"""Restore a block of a generator or coroutine."""

def get_frame_state(
frame: FrameType | Coroutine | Generator | AsyncGenerator,
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator],
) -> int:
"""Get frame state of a generator or coroutine."""

def set_frame_state(
frame: FrameType | Coroutine | Generator | AsyncGenerator, state: int
frame: Union[FrameType, Coroutine, Generator, AsyncGenerator], state: int
):
"""Set frame state of a generator or coroutine."""
144 changes: 144 additions & 0 deletions src/dispatch/experimental/durable/frame309.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
// This is a redefinition of the private/opaque frame object.
// https://github.com/python/cpython/blob/3.9/Include/cpython/frameobject.h#L17
//
// In Python <= 3.10, `struct _frame` is both the PyFrameObject and
// PyInterpreterFrame. From Python 3.11 onwards, the two were split with the
// PyFrameObject (struct _frame) pointing to struct _PyInterpreterFrame.
struct Frame {
PyObject_VAR_HEAD
struct Frame *f_back; // struct _frame
PyCodeObject *f_code;
PyObject *f_builtins;
PyObject *f_globals;
PyObject *f_locals;
PyObject **f_valuestack;
PyObject **f_stacktop;
PyObject *f_trace;
char f_trace_lines;
char f_trace_opcodes;
PyObject *f_gen;
int f_lasti;
int f_lineno;
int f_iblock;
char f_executing;
PyTryBlock f_blockstack[CO_MAXBLOCKS];
PyObject *f_localsplus[1];
};

// Python 3.9 and prior didn't have an explicit enum of frame states,
// but we can derive them based on the presence of a frame, and other
// information found on the frame, for compatibility with later versions.
typedef enum _framestate {
FRAME_CREATED = -2,
FRAME_EXECUTING = 0,
FRAME_CLEARED = 4
} FrameState;

/*
// This is the definition of PyGenObject for reference to developers
// working on the extension.
//
// Note that PyCoroObject and PyAsyncGenObject have the same layout as
// PyGenObject, however the struct fields have a cr_ and ag_ prefix
// (respectively) rather than a gi_ prefix. In Python <= 3.10, PyCoroObject
// and PyAsyncGenObject have extra fields compared to PyGenObject. In Python
// 3.11 onwards, the three objects are identical (except for field name
// prefixes). The extra fields in Python <= 3.10 are not applicable to the
// extension at this time.
//
// https://github.com/python/cpython/blob/3.9/Include/genobject.h#L15
typedef struct {
PyObject_HEAD
PyFrameObject *gi_frame;
char gi_running;
PyObject *gi_code;
PyObject *gi_weakreflist;
PyObject *gi_name;
PyObject *gi_qualname;
_PyErr_StackItem gi_exc_state;
} PyGenObject;
*/

static Frame *get_frame(PyGenObject *gen_like) {
Frame *frame = (Frame *)(gen_like->gi_frame);
assert(frame);
return frame;
}

static PyCodeObject *get_frame_code(Frame *frame) {
PyCodeObject *code = frame->f_code;
assert(code);
return code;
}

static int get_frame_lasti(Frame *frame) {
return frame->f_lasti;
}

static void set_frame_lasti(Frame *frame, int lasti) {
frame->f_lasti = lasti;
}

static int get_frame_state(PyGenObject *gen_like) {
// Python 3.9 doesn't have frame states, but we can derive
// some for compatibility with later versions and to simplify
// the extension.
Frame *frame = (Frame *)(gen_like->gi_frame);
if (!frame) {
return FRAME_CLEARED;
}
return frame->f_executing ? FRAME_EXECUTING : FRAME_CREATED;
}

static void set_frame_state(PyGenObject *gen_like, int fs) {
Frame *frame = get_frame(gen_like);
frame->f_executing = (fs == FRAME_EXECUTING);
}

static int valid_frame_state(int fs) {
return fs == FRAME_CREATED || fs == FRAME_EXECUTING || fs == FRAME_CLEARED;
}

static int get_frame_stacktop_limit(Frame *frame) {
PyCodeObject *code = get_frame_code(frame);
return code->co_stacksize + code->co_nlocals;
}

static int get_frame_stacktop(Frame *frame) {
assert(frame->f_localsplus);
int stacktop = (int)(frame->f_stacktop - frame->f_localsplus);
assert(stacktop >= 0 && stacktop < get_frame_stacktop_limit(frame));
return stacktop;
}

static void set_frame_stacktop(Frame *frame, int stacktop) {
assert(stacktop >= 0 && stacktop < get_frame_stacktop_limit(frame));
assert(frame->f_localsplus);
frame->f_stacktop = frame->f_localsplus + stacktop;
}

static PyObject **get_frame_localsplus(Frame *frame) {
PyObject **localsplus = frame->f_localsplus;
assert(localsplus);
return localsplus;
}

static int get_frame_iblock_limit(Frame *frame) {
return CO_MAXBLOCKS;
}

static int get_frame_iblock(Frame *frame) {
return frame->f_iblock;
}

static void set_frame_iblock(Frame *frame, int iblock) {
assert(iblock >= 0 && iblock < get_frame_iblock_limit(frame));
frame->f_iblock = iblock;
}

static PyTryBlock *get_frame_blockstack(Frame *frame) {
PyTryBlock *blockstack = frame->f_blockstack;
assert(blockstack);
return blockstack;
}

Loading

0 comments on commit bc89165

Please sign in to comment.