Use Python "Limited API" in PL/Python

Started by Peter Eisentrautabout 1 year ago13 messages
#1Peter Eisentraut
peter@eisentraut.org
2 attachment(s)

This patch changes PL/Python to use the Python "limited API". This API
has stronger ABI stability guarantees.[0]https://docs.python.org/3/c-api/stable.html This means, you can build
PL/Python against any Python 3.x version and use any other Python 3.x
version at run time.

This is especially useful for binary packages where the operating system
does not come with a fixed suitable version of Python. For example,
Postgres.app (for macOS) would prefer to link against the Python version
supplied by python.org (Python.app). But that has a 3.x version that
changes over time. So instead they bundle a Python version inside
Postgres.app. The Windows installer used to also bundle Python but as of
PG17 you have to get it yourself, but you have to get a very specific
version [1]https://github.com/EnterpriseDB/edb-installers/blob/REL-17/server/resources/installation-notes.html#L34-L36, which is unsatisfactory. This patch fixes that: You can use
any Python version independent of what PL/Python was built against.
(There is a mechanism to say "at least 3.N", but for this patch, we
don't need that, we can stick with the current minimum of 3.2.)

(I have only tested the macOS side of this, not the Windows side. In
fact, the patch currently doesn't build on Windows on CI. I haven't
figured out why.)

For Linux-style packaging, I don't think this would have any benefit for
users right now, since the OS comes with a Python installation and all
the packages are built against that. But it could potentially be helpful
for packagers. For example, on Debian, this could detach the postgresql
packages from python version transitions. But AFAICT, the Python
packaging layout is not prepared for that. (There are only
libpython3.x.so libraries, no libpython3.so that one would have to link
against.)

Finally, I think this patch is part of a path toward making PL/Python
thread-safe. I don't think the patch by itself changes anything, but if
you read through [2]https://docs.python.org/3/howto/isolating-extensions.html, using heap types is part of the things mentioned
there.

[0]: https://docs.python.org/3/c-api/stable.html
[1]: https://github.com/EnterpriseDB/edb-installers/blob/REL-17/server/resources/installation-notes.html#L34-L36
https://github.com/EnterpriseDB/edb-installers/blob/REL-17/server/resources/installation-notes.html#L34-L36
[2]: https://docs.python.org/3/howto/isolating-extensions.html

Attachments:

0001-Remove-obsolete-Python-version-check.patchtext/plain; charset=UTF-8; name=0001-Remove-obsolete-Python-version-check.patchDownload
From 76d4e4b628f9e7913b5c361dbbd4c0ea9d570146 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Mon, 2 Dec 2024 08:53:42 +0100
Subject: [PATCH 1/2] Remove obsolete Python version check

The checked version is already the current minimum supported version
(3.2).
---
 src/pl/plpython/plpy_exec.c | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c
index 0e84bb90829..00747bb811b 100644
--- a/src/pl/plpython/plpy_exec.c
+++ b/src/pl/plpython/plpy_exec.c
@@ -1066,13 +1066,7 @@ PLy_procedure_call(PLyProcedure *proc, const char *kargs, PyObject *vargs)
 
 	PG_TRY();
 	{
-#if PY_VERSION_HEX >= 0x03020000
-		rv = PyEval_EvalCode(proc->code,
-							 proc->globals, proc->globals);
-#else
-		rv = PyEval_EvalCode((PyCodeObject *) proc->code,
-							 proc->globals, proc->globals);
-#endif
+		rv = PyEval_EvalCode(proc->code, proc->globals, proc->globals);
 
 		/*
 		 * Since plpy will only let you close subtransactions that you

base-commit: 2f696453d2b39fea800d5f7d8e5d3e1a2266de24
-- 
2.47.1

0002-Use-Python-Limited-API-in-PL-Python.patchtext/plain; charset=UTF-8; name=0002-Use-Python-Limited-API-in-PL-Python.patchDownload
From a031a128140f3f4c61e6a5468c1a603f547a986b Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Mon, 2 Dec 2024 08:53:42 +0100
Subject: [PATCH 2/2] Use Python "Limited API" in PL/Python

This allows building PL/Python against any Python 3.x version and
using another Python 3.x version at run time.

Implementation details:

- Convert static types to heap types
  (https://docs.python.org/3/howto/isolating-extensions.html#heap-types).

- Replace PyRun_String() with component functions.

- Replace PyList_SET_ITEM() with PyList_SetItem().
---
 src/pl/plpython/plpy_cursorobject.c  | 71 +++++++++++++-------
 src/pl/plpython/plpy_planobject.c    | 61 +++++++++++------
 src/pl/plpython/plpy_procedure.c     |  5 +-
 src/pl/plpython/plpy_resultobject.c  | 98 +++++++++++++++++-----------
 src/pl/plpython/plpy_subxactobject.c | 41 +++++++-----
 src/pl/plpython/plpy_typeio.c        |  6 +-
 src/pl/plpython/plpython.h           |  2 +
 7 files changed, 179 insertions(+), 105 deletions(-)

diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c
index 6108384c9a5..39bcae3f1d9 100644
--- a/src/pl/plpython/plpy_cursorobject.c
+++ b/src/pl/plpython/plpy_cursorobject.c
@@ -20,7 +20,7 @@
 #include "utils/memutils.h"
 
 static PyObject *PLy_cursor_query(const char *query);
-static void PLy_cursor_dealloc(PyObject *arg);
+static void PLy_cursor_dealloc(PLyCursorObject *self);
 static PyObject *PLy_cursor_iternext(PyObject *self);
 static PyObject *PLy_cursor_fetch(PyObject *self, PyObject *args);
 static PyObject *PLy_cursor_close(PyObject *self, PyObject *unused);
@@ -33,22 +33,43 @@ static PyMethodDef PLy_cursor_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_CursorType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyCursor",
-	.tp_basicsize = sizeof(PLyCursorObject),
-	.tp_dealloc = PLy_cursor_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_cursor_doc,
-	.tp_iter = PyObject_SelfIter,
-	.tp_iternext = PLy_cursor_iternext,
-	.tp_methods = PLy_cursor_methods,
+static PyType_Slot PLyCursor_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_cursor_dealloc
+	},
+	{
+		Py_tp_doc, (char *) PLy_cursor_doc
+	},
+	{
+		Py_tp_iter, PyObject_SelfIter
+	},
+	{
+		Py_tp_iternext, PLy_cursor_iternext
+	},
+	{
+		Py_tp_methods, PLy_cursor_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyCursor_spec =
+{
+	.name = "PLyCursor",
+		.basicsize = sizeof(PLyCursorObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyCursor_slots,
+};
+
+static PyTypeObject *PLy_CursorType;
+
 void
 PLy_cursor_init_type(void)
 {
-	if (PyType_Ready(&PLy_CursorType) < 0)
+	PLy_CursorType = (PyTypeObject *) PyType_FromSpec(&PLyCursor_spec);
+	if (!PLy_CursorType)
 		elog(ERROR, "could not initialize PLy_CursorType");
 }
 
@@ -80,7 +101,7 @@ PLy_cursor_query(const char *query)
 	volatile MemoryContext oldcontext;
 	volatile ResourceOwner oldowner;
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
 	cursor->portalname = NULL;
 	cursor->closed = false;
@@ -178,7 +199,7 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 		return NULL;
 	}
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
 	cursor->portalname = NULL;
 	cursor->closed = false;
@@ -276,30 +297,30 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 }
 
 static void
-PLy_cursor_dealloc(PyObject *arg)
+PLy_cursor_dealloc(PLyCursorObject *self)
 {
-	PLyCursorObject *cursor;
+	PyTypeObject *tp = Py_TYPE(self);
 	Portal		portal;
 
-	cursor = (PLyCursorObject *) arg;
-
-	if (!cursor->closed)
+	if (!self->closed)
 	{
-		portal = GetPortalByName(cursor->portalname);
+		portal = GetPortalByName(self->portalname);
 
 		if (PortalIsValid(portal))
 		{
 			UnpinPortal(portal);
 			SPI_cursor_close(portal);
 		}
-		cursor->closed = true;
+		self->closed = true;
 	}
-	if (cursor->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(cursor->mcxt);
-		cursor->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }
 
 static PyObject *
diff --git a/src/pl/plpython/plpy_planobject.c b/src/pl/plpython/plpy_planobject.c
index bbef889329e..986b7179f14 100644
--- a/src/pl/plpython/plpy_planobject.c
+++ b/src/pl/plpython/plpy_planobject.c
@@ -12,7 +12,7 @@
 #include "plpython.h"
 #include "utils/memutils.h"
 
-static void PLy_plan_dealloc(PyObject *arg);
+static void PLy_plan_dealloc(PLyPlanObject *self);
 static PyObject *PLy_plan_cursor(PyObject *self, PyObject *args);
 static PyObject *PLy_plan_execute(PyObject *self, PyObject *args);
 static PyObject *PLy_plan_status(PyObject *self, PyObject *args);
@@ -26,20 +26,37 @@ static PyMethodDef PLy_plan_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_PlanType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyPlan",
-	.tp_basicsize = sizeof(PLyPlanObject),
-	.tp_dealloc = PLy_plan_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_plan_doc,
-	.tp_methods = PLy_plan_methods,
+static PyType_Slot PLyPlan_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_plan_dealloc
+	},
+	{
+		Py_tp_doc, (char *) PLy_plan_doc
+	},
+	{
+		Py_tp_methods, PLy_plan_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyPlan_spec =
+{
+	.name = "PLyPlan",
+		.basicsize = sizeof(PLyPlanObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyPlan_slots,
+};
+
+static PyTypeObject *PLy_PlanType;
+
 void
 PLy_plan_init_type(void)
 {
-	if (PyType_Ready(&PLy_PlanType) < 0)
+	PLy_PlanType = (PyTypeObject *) PyType_FromSpec(&PLyPlan_spec);
+	if (!PLy_PlanType)
 		elog(ERROR, "could not initialize PLy_PlanType");
 }
 
@@ -48,7 +65,7 @@ PLy_plan_new(void)
 {
 	PLyPlanObject *ob;
 
-	if ((ob = PyObject_New(PLyPlanObject, &PLy_PlanType)) == NULL)
+	if ((ob = PyObject_New(PLyPlanObject, PLy_PlanType)) == NULL)
 		return NULL;
 
 	ob->plan = NULL;
@@ -64,25 +81,27 @@ PLy_plan_new(void)
 bool
 is_PLyPlanObject(PyObject *ob)
 {
-	return ob->ob_type == &PLy_PlanType;
+	return ob->ob_type == PLy_PlanType;
 }
 
 static void
-PLy_plan_dealloc(PyObject *arg)
+PLy_plan_dealloc(PLyPlanObject *self)
 {
-	PLyPlanObject *ob = (PLyPlanObject *) arg;
+	PyTypeObject *tp = Py_TYPE(self);
 
-	if (ob->plan)
+	if (self->plan)
 	{
-		SPI_freeplan(ob->plan);
-		ob->plan = NULL;
+		SPI_freeplan(self->plan);
+		self->plan = NULL;
 	}
-	if (ob->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(ob->mcxt);
-		ob->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }
 
 
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index c35a3b801ab..b494eeb474f 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -350,6 +350,7 @@ PLy_procedure_compile(PLyProcedure *proc, const char *src)
 {
 	PyObject   *crv = NULL;
 	char	   *msrc;
+	PyObject   *code0;
 
 	proc->globals = PyDict_Copy(PLy_interp_globals);
 
@@ -368,7 +369,9 @@ PLy_procedure_compile(PLyProcedure *proc, const char *src)
 	msrc = PLy_procedure_munge_source(proc->pyname, src);
 	/* Save the mangled source for later inclusion in tracebacks */
 	proc->src = MemoryContextStrdup(proc->mcxt, msrc);
-	crv = PyRun_String(msrc, Py_file_input, proc->globals, NULL);
+	code0 = Py_CompileString(msrc, "<string>", Py_file_input);
+	if (code0)
+		crv = PyEval_EvalCode(code0, proc->globals, NULL);
 	pfree(msrc);
 
 	if (crv != NULL)
diff --git a/src/pl/plpython/plpy_resultobject.c b/src/pl/plpython/plpy_resultobject.c
index 95acce65493..f2628205669 100644
--- a/src/pl/plpython/plpy_resultobject.c
+++ b/src/pl/plpython/plpy_resultobject.c
@@ -10,7 +10,7 @@
 #include "plpy_resultobject.h"
 #include "plpython.h"
 
-static void PLy_result_dealloc(PyObject *arg);
+static void PLy_result_dealloc(PLyResultObject *self);
 static PyObject *PLy_result_colnames(PyObject *self, PyObject *unused);
 static PyObject *PLy_result_coltypes(PyObject *self, PyObject *unused);
 static PyObject *PLy_result_coltypmods(PyObject *self, PyObject *unused);
@@ -24,17 +24,6 @@ static int	PLy_result_ass_subscript(PyObject *arg, PyObject *item, PyObject *val
 
 static char PLy_result_doc[] = "Results of a PostgreSQL query";
 
-static PySequenceMethods PLy_result_as_sequence = {
-	.sq_length = PLy_result_length,
-	.sq_item = PLy_result_item,
-};
-
-static PyMappingMethods PLy_result_as_mapping = {
-	.mp_length = PLy_result_length,
-	.mp_subscript = PLy_result_subscript,
-	.mp_ass_subscript = PLy_result_ass_subscript,
-};
-
 static PyMethodDef PLy_result_methods[] = {
 	{"colnames", PLy_result_colnames, METH_NOARGS, NULL},
 	{"coltypes", PLy_result_coltypes, METH_NOARGS, NULL},
@@ -44,23 +33,55 @@ static PyMethodDef PLy_result_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_ResultType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyResult",
-	.tp_basicsize = sizeof(PLyResultObject),
-	.tp_dealloc = PLy_result_dealloc,
-	.tp_as_sequence = &PLy_result_as_sequence,
-	.tp_as_mapping = &PLy_result_as_mapping,
-	.tp_str = &PLy_result_str,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_result_doc,
-	.tp_methods = PLy_result_methods,
+static PyType_Slot PLyResult_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_result_dealloc
+	},
+	{
+		Py_sq_length, PLy_result_length
+	},
+	{
+		Py_sq_item, PLy_result_item
+	},
+	{
+		Py_mp_length, PLy_result_length
+	},
+	{
+		Py_mp_subscript, PLy_result_subscript
+	},
+	{
+		Py_mp_ass_subscript, PLy_result_ass_subscript
+	},
+	{
+		Py_tp_str, PLy_result_str
+	},
+	{
+		Py_tp_doc, (char *) PLy_result_doc
+	},
+	{
+		Py_tp_methods, PLy_result_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyResult_spec =
+{
+	.name = "PLyResult",
+		.basicsize = sizeof(PLyResultObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyResult_slots,
+};
+
+static PyTypeObject *PLy_ResultType;
+
 void
 PLy_result_init_type(void)
 {
-	if (PyType_Ready(&PLy_ResultType) < 0)
+	PLy_ResultType = (PyTypeObject *) PyType_FromSpec(&PLyResult_spec);
+	if (!PLy_ResultType)
 		elog(ERROR, "could not initialize PLy_ResultType");
 }
 
@@ -69,7 +90,7 @@ PLy_result_new(void)
 {
 	PLyResultObject *ob;
 
-	if ((ob = PyObject_New(PLyResultObject, &PLy_ResultType)) == NULL)
+	if ((ob = PyObject_New(PLyResultObject, PLy_ResultType)) == NULL)
 		return NULL;
 
 	/* ob->tuples = NULL; */
@@ -89,20 +110,21 @@ PLy_result_new(void)
 }
 
 static void
-PLy_result_dealloc(PyObject *arg)
+PLy_result_dealloc(PLyResultObject *self)
 {
-	PLyResultObject *ob = (PLyResultObject *) arg;
+	PyTypeObject *tp = Py_TYPE(self);
 
-	Py_XDECREF(ob->nrows);
-	Py_XDECREF(ob->rows);
-	Py_XDECREF(ob->status);
-	if (ob->tupdesc)
+	Py_XDECREF(self->nrows);
+	Py_XDECREF(self->rows);
+	Py_XDECREF(self->status);
+	if (self->tupdesc)
 	{
-		FreeTupleDesc(ob->tupdesc);
-		ob->tupdesc = NULL;
+		FreeTupleDesc(self->tupdesc);
+		self->tupdesc = NULL;
 	}
 
-	arg->ob_type->tp_free(arg);
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }
 
 static PyObject *
@@ -125,7 +147,7 @@ PLy_result_colnames(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PLyUnicode_FromString(NameStr(attr->attname)));
+		PyList_SetItem(list, i, PLyUnicode_FromString(NameStr(attr->attname)));
 	}
 
 	return list;
@@ -151,7 +173,7 @@ PLy_result_coltypes(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypid));
+		PyList_SetItem(list, i, PyLong_FromLong(attr->atttypid));
 	}
 
 	return list;
@@ -177,7 +199,7 @@ PLy_result_coltypmods(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypmod));
+		PyList_SetItem(list, i, PyLong_FromLong(attr->atttypmod));
 	}
 
 	return list;
@@ -227,7 +249,7 @@ PLy_result_str(PyObject *arg)
 	PLyResultObject *ob = (PLyResultObject *) arg;
 
 	return PyUnicode_FromFormat("<%s status=%S nrows=%S rows=%S>",
-								Py_TYPE(ob)->tp_name,
+								"PLyResult",
 								ob->status,
 								ob->nrows,
 								ob->rows);
diff --git a/src/pl/plpython/plpy_subxactobject.c b/src/pl/plpython/plpy_subxactobject.c
index 5c92a0e089a..cc7ff3f9df7 100644
--- a/src/pl/plpython/plpy_subxactobject.c
+++ b/src/pl/plpython/plpy_subxactobject.c
@@ -15,7 +15,6 @@
 List	   *explicit_subtransactions = NIL;
 
 
-static void PLy_subtransaction_dealloc(PyObject *subxact);
 static PyObject *PLy_subtransaction_enter(PyObject *self, PyObject *unused);
 static PyObject *PLy_subtransaction_exit(PyObject *self, PyObject *args);
 
@@ -31,21 +30,35 @@ static PyMethodDef PLy_subtransaction_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_SubtransactionType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLySubtransaction",
-	.tp_basicsize = sizeof(PLySubtransactionObject),
-	.tp_dealloc = PLy_subtransaction_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_subtransaction_doc,
-	.tp_methods = PLy_subtransaction_methods,
+static PyType_Slot PLySubtransaction_slots[] =
+{
+	{
+		Py_tp_doc, (char *) PLy_subtransaction_doc
+	},
+	{
+		Py_tp_methods, PLy_subtransaction_methods
+	},
+	{
+		0, NULL
+	}
+};
+
+static PyType_Spec PLySubtransaction_spec =
+{
+	.name = "PLySubtransaction",
+		.basicsize = sizeof(PLySubtransactionObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLySubtransaction_slots,
 };
 
+static PyTypeObject *PLy_SubtransactionType;
+
 
 void
 PLy_subtransaction_init_type(void)
 {
-	if (PyType_Ready(&PLy_SubtransactionType) < 0)
+	PLy_SubtransactionType = (PyTypeObject *) PyType_FromSpec(&PLySubtransaction_spec);
+	if (!PLy_SubtransactionType)
 		elog(ERROR, "could not initialize PLy_SubtransactionType");
 }
 
@@ -55,7 +68,7 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 {
 	PLySubtransactionObject *ob;
 
-	ob = PyObject_New(PLySubtransactionObject, &PLy_SubtransactionType);
+	ob = PyObject_New(PLySubtransactionObject, PLy_SubtransactionType);
 
 	if (ob == NULL)
 		return NULL;
@@ -66,12 +79,6 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 	return (PyObject *) ob;
 }
 
-/* Python requires a dealloc function to be defined */
-static void
-PLy_subtransaction_dealloc(PyObject *subxact)
-{
-}
-
 /*
  * subxact.__enter__() or subxact.enter()
  *
diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c
index db14c5f8dae..27d9b2f1af6 100644
--- a/src/pl/plpython/plpy_typeio.c
+++ b/src/pl/plpython/plpy_typeio.c
@@ -723,7 +723,7 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 
 			sublist = PLyList_FromArray_recurse(elm, dims, ndim, dim + 1,
 												dataptr_p, bitmap_p, bitmask_p);
-			PyList_SET_ITEM(list, i, sublist);
+			PyList_SetItem(list, i, sublist);
 		}
 	}
 	else
@@ -742,14 +742,14 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 			if (bitmap && (*bitmap & bitmask) == 0)
 			{
 				Py_INCREF(Py_None);
-				PyList_SET_ITEM(list, i, Py_None);
+				PyList_SetItem(list, i, Py_None);
 			}
 			else
 			{
 				Datum		itemvalue;
 
 				itemvalue = fetch_att(dataptr, elm->typbyval, elm->typlen);
-				PyList_SET_ITEM(list, i, elm->func(elm, itemvalue));
+				PyList_SetItem(list, i, elm->func(elm, itemvalue));
 				dataptr = att_addlength_pointer(dataptr, elm->typlen, dataptr);
 				dataptr = (char *) att_align_nominal(dataptr, elm->typalign);
 			}
diff --git a/src/pl/plpython/plpython.h b/src/pl/plpython/plpython.h
index d929afefc33..fb95b23730f 100644
--- a/src/pl/plpython/plpython.h
+++ b/src/pl/plpython/plpython.h
@@ -19,6 +19,8 @@
 #error Python.h must be included via plpython.h
 #endif
 
+#define Py_LIMITED_API 0x03020000
+
 /*
  * Pull in Python headers via a wrapper header, to control the scope of
  * the system_header pragma therein.
-- 
2.47.1

#2Peter Eisentraut
peter@eisentraut.org
In reply to: Peter Eisentraut (#1)
Re: Use Python "Limited API" in PL/Python

On 02.12.24 09:51, Peter Eisentraut wrote:

This patch changes PL/Python to use the Python "limited API". This API
has stronger ABI stability guarantees.[0] This means, you can build PL/
Python against any Python 3.x version and use any other Python 3.x
version at run time.

This is especially useful for binary packages where the operating system
does not come with a fixed suitable version of Python. For example,
Postgres.app (for macOS) would prefer to link against the Python version
supplied by python.org (Python.app). But that has a 3.x version that
changes over time. So instead they bundle a Python version inside
Postgres.app. The Windows installer used to also bundle Python but as of
PG17 you have to get it yourself, but you have to get a very specific
version [1], which is unsatisfactory. This patch fixes that: You can use
any Python version independent of what PL/Python was built against.
(There is a mechanism to say "at least 3.N", but for this patch, we
don't need that, we can stick with the current minimum of 3.2.)

(I have only tested the macOS side of this, not the Windows side. In
fact, the patch currently doesn't build on Windows on CI. I haven't
figured out why.)

A bit more exploration of that Windows build failure:

This patch changes it so that, on Windows, plpython is linked against a
library called "python3.lib" instead of previously "python311.lib" or
similar. The build failure is [0]https://cirrus-ci.com/task/5738515637469184

LINK : fatal error LNK1104: cannot open file 'python3.lib'

[0]: https://cirrus-ci.com/task/5738515637469184

which suggests that the file isn't there. But we have tested this
internally and the build succeeded locally. I also don't see anything
in the Python installer code or documentation that suggests that there
is an option not to install that file or something like that. It should
be installed right next to the python3XX.lib file.

How could one debug this? Is there a way to log into the CI image or
get a directory dump or something like that?

#3Jakob Egger
jakob@eggerapps.at
In reply to: Peter Eisentraut (#2)
3 attachment(s)
Re: Use Python "Limited API" in PL/Python

On 07.01.2025, at 08:34, Peter Eisentraut <peter@eisentraut.org> wrote:

On 02.12.24 09:51, Peter Eisentraut wrote:

This patch changes PL/Python to use the Python "limited API". This API has stronger ABI stability guarantees.[0] This means, you can build PL/ Python against any Python 3.x version and use any other Python 3.x version at run time.
This is especially useful for binary packages where the operating system does not come with a fixed suitable version of Python. For example, Postgres.app (for macOS) would prefer to link against the Python version supplied by python.org (Python.app). But that has a 3.x version that changes over time. So instead they bundle a Python version inside Postgres.app. The Windows installer used to also bundle Python but as of PG17 you have to get it yourself, but you have to get a very specific version [1], which is unsatisfactory. This patch fixes that: You can use any Python version independent of what PL/Python was built against. (There is a mechanism to say "at least 3.N", but for this patch, we don't need that, we can stick with the current minimum of 3.2.)
(I have only tested the macOS side of this, not the Windows side. In fact, the patch currently doesn't build on Windows on CI. I haven't figured out why.)

A bit more exploration of that Windows build failure:

This patch changes it so that, on Windows, plpython is linked against a library called "python3.lib" instead of previously "python311.lib" or similar. The build failure is [0]

LINK : fatal error LNK1104: cannot open file 'python3.lib'

[0]: https://cirrus-ci.com/task/5738515637469184

which suggests that the file isn't there. But we have tested this internally and the build succeeded locally. I also don't see anything in the Python installer code or documentation that suggests that there is an option not to install that file or something like that. It should be installed right next to the python3XX.lib file.

I think the problem is that meson doesn't know we want to use the limited API.
I found this related issue on Github: https://github.com/mesonbuild/meson/issues/13824

Someone suggests to change the Python dependency in the meson.build file like this:

py_dep = declare_dependency(
dependencies: py_dep,
compile_args: '-DPy_LIMITED_API='0x030100',
)

I've tried to create a patch with this change. I'm attaching it to this message so that cfbot picks it up. (I was unable to reproduce the issue locally)

Best regards,
Jakob



Attachments:

0001-Remove-obsolete-Python-version-check.patchapplication/octet-stream; name=0001-Remove-obsolete-Python-version-check.patch; x-unix-mode=0644Download
From 0e046101ea8ed321a09b0ed2bb0a0fc0ce0d2399 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Mon, 2 Dec 2024 08:53:42 +0100
Subject: [PATCH 1/3] Remove obsolete Python version check

The checked version is already the current minimum supported version
(3.2).
---
 src/pl/plpython/plpy_exec.c | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c
index 0e84bb90829..00747bb811b 100644
--- a/src/pl/plpython/plpy_exec.c
+++ b/src/pl/plpython/plpy_exec.c
@@ -1066,13 +1066,7 @@ PLy_procedure_call(PLyProcedure *proc, const char *kargs, PyObject *vargs)
 
 	PG_TRY();
 	{
-#if PY_VERSION_HEX >= 0x03020000
-		rv = PyEval_EvalCode(proc->code,
-							 proc->globals, proc->globals);
-#else
-		rv = PyEval_EvalCode((PyCodeObject *) proc->code,
-							 proc->globals, proc->globals);
-#endif
+		rv = PyEval_EvalCode(proc->code, proc->globals, proc->globals);
 
 		/*
 		 * Since plpy will only let you close subtransactions that you
-- 
2.39.5 (Apple Git-154)

0002-Use-Python-Limited-API-in-PL-Python.patchapplication/octet-stream; name=0002-Use-Python-Limited-API-in-PL-Python.patch; x-unix-mode=0644Download
From f2e0b87a5bae68756327b37959812674dcd02668 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Mon, 2 Dec 2024 08:53:42 +0100
Subject: [PATCH 2/3] Use Python "Limited API" in PL/Python

This allows building PL/Python against any Python 3.x version and
using another Python 3.x version at run time.

Implementation details:

- Convert static types to heap types
  (https://docs.python.org/3/howto/isolating-extensions.html#heap-types).

- Replace PyRun_String() with component functions.

- Replace PyList_SET_ITEM() with PyList_SetItem().
---
 src/pl/plpython/plpy_cursorobject.c  | 71 +++++++++++++-------
 src/pl/plpython/plpy_planobject.c    | 61 +++++++++++------
 src/pl/plpython/plpy_procedure.c     |  5 +-
 src/pl/plpython/plpy_resultobject.c  | 98 +++++++++++++++++-----------
 src/pl/plpython/plpy_subxactobject.c | 41 +++++++-----
 src/pl/plpython/plpy_typeio.c        |  6 +-
 src/pl/plpython/plpython.h           |  2 +
 7 files changed, 179 insertions(+), 105 deletions(-)

diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c
index bb3fa8a3909..2a370157f90 100644
--- a/src/pl/plpython/plpy_cursorobject.c
+++ b/src/pl/plpython/plpy_cursorobject.c
@@ -20,7 +20,7 @@
 #include "utils/memutils.h"
 
 static PyObject *PLy_cursor_query(const char *query);
-static void PLy_cursor_dealloc(PyObject *arg);
+static void PLy_cursor_dealloc(PLyCursorObject *self);
 static PyObject *PLy_cursor_iternext(PyObject *self);
 static PyObject *PLy_cursor_fetch(PyObject *self, PyObject *args);
 static PyObject *PLy_cursor_close(PyObject *self, PyObject *unused);
@@ -33,22 +33,43 @@ static PyMethodDef PLy_cursor_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_CursorType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyCursor",
-	.tp_basicsize = sizeof(PLyCursorObject),
-	.tp_dealloc = PLy_cursor_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_cursor_doc,
-	.tp_iter = PyObject_SelfIter,
-	.tp_iternext = PLy_cursor_iternext,
-	.tp_methods = PLy_cursor_methods,
+static PyType_Slot PLyCursor_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_cursor_dealloc
+	},
+	{
+		Py_tp_doc, (char *) PLy_cursor_doc
+	},
+	{
+		Py_tp_iter, PyObject_SelfIter
+	},
+	{
+		Py_tp_iternext, PLy_cursor_iternext
+	},
+	{
+		Py_tp_methods, PLy_cursor_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyCursor_spec =
+{
+	.name = "PLyCursor",
+		.basicsize = sizeof(PLyCursorObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyCursor_slots,
+};
+
+static PyTypeObject *PLy_CursorType;
+
 void
 PLy_cursor_init_type(void)
 {
-	if (PyType_Ready(&PLy_CursorType) < 0)
+	PLy_CursorType = (PyTypeObject *) PyType_FromSpec(&PLyCursor_spec);
+	if (!PLy_CursorType)
 		elog(ERROR, "could not initialize PLy_CursorType");
 }
 
@@ -80,7 +101,7 @@ PLy_cursor_query(const char *query)
 	volatile MemoryContext oldcontext;
 	volatile ResourceOwner oldowner;
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
 	cursor->portalname = NULL;
 	cursor->closed = false;
@@ -177,7 +198,7 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 		return NULL;
 	}
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
 	cursor->portalname = NULL;
 	cursor->closed = false;
@@ -272,30 +293,30 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 }
 
 static void
-PLy_cursor_dealloc(PyObject *arg)
+PLy_cursor_dealloc(PLyCursorObject *self)
 {
-	PLyCursorObject *cursor;
+	PyTypeObject *tp = Py_TYPE(self);
 	Portal		portal;
 
-	cursor = (PLyCursorObject *) arg;
-
-	if (!cursor->closed)
+	if (!self->closed)
 	{
-		portal = GetPortalByName(cursor->portalname);
+		portal = GetPortalByName(self->portalname);
 
 		if (PortalIsValid(portal))
 		{
 			UnpinPortal(portal);
 			SPI_cursor_close(portal);
 		}
-		cursor->closed = true;
+		self->closed = true;
 	}
-	if (cursor->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(cursor->mcxt);
-		cursor->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }
 
 static PyObject *
diff --git a/src/pl/plpython/plpy_planobject.c b/src/pl/plpython/plpy_planobject.c
index 9427674d2f4..1b97b5cbd2a 100644
--- a/src/pl/plpython/plpy_planobject.c
+++ b/src/pl/plpython/plpy_planobject.c
@@ -12,7 +12,7 @@
 #include "plpython.h"
 #include "utils/memutils.h"
 
-static void PLy_plan_dealloc(PyObject *arg);
+static void PLy_plan_dealloc(PLyPlanObject *self);
 static PyObject *PLy_plan_cursor(PyObject *self, PyObject *args);
 static PyObject *PLy_plan_execute(PyObject *self, PyObject *args);
 static PyObject *PLy_plan_status(PyObject *self, PyObject *args);
@@ -26,20 +26,37 @@ static PyMethodDef PLy_plan_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_PlanType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyPlan",
-	.tp_basicsize = sizeof(PLyPlanObject),
-	.tp_dealloc = PLy_plan_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_plan_doc,
-	.tp_methods = PLy_plan_methods,
+static PyType_Slot PLyPlan_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_plan_dealloc
+	},
+	{
+		Py_tp_doc, (char *) PLy_plan_doc
+	},
+	{
+		Py_tp_methods, PLy_plan_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyPlan_spec =
+{
+	.name = "PLyPlan",
+		.basicsize = sizeof(PLyPlanObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyPlan_slots,
+};
+
+static PyTypeObject *PLy_PlanType;
+
 void
 PLy_plan_init_type(void)
 {
-	if (PyType_Ready(&PLy_PlanType) < 0)
+	PLy_PlanType = (PyTypeObject *) PyType_FromSpec(&PLyPlan_spec);
+	if (!PLy_PlanType)
 		elog(ERROR, "could not initialize PLy_PlanType");
 }
 
@@ -48,7 +65,7 @@ PLy_plan_new(void)
 {
 	PLyPlanObject *ob;
 
-	if ((ob = PyObject_New(PLyPlanObject, &PLy_PlanType)) == NULL)
+	if ((ob = PyObject_New(PLyPlanObject, PLy_PlanType)) == NULL)
 		return NULL;
 
 	ob->plan = NULL;
@@ -63,25 +80,27 @@ PLy_plan_new(void)
 bool
 is_PLyPlanObject(PyObject *ob)
 {
-	return ob->ob_type == &PLy_PlanType;
+	return ob->ob_type == PLy_PlanType;
 }
 
 static void
-PLy_plan_dealloc(PyObject *arg)
+PLy_plan_dealloc(PLyPlanObject *self)
 {
-	PLyPlanObject *ob = (PLyPlanObject *) arg;
+	PyTypeObject *tp = Py_TYPE(self);
 
-	if (ob->plan)
+	if (self->plan)
 	{
-		SPI_freeplan(ob->plan);
-		ob->plan = NULL;
+		SPI_freeplan(self->plan);
+		self->plan = NULL;
 	}
-	if (ob->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(ob->mcxt);
-		ob->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }
 
 
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index c35a3b801ab..b494eeb474f 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -350,6 +350,7 @@ PLy_procedure_compile(PLyProcedure *proc, const char *src)
 {
 	PyObject   *crv = NULL;
 	char	   *msrc;
+	PyObject   *code0;
 
 	proc->globals = PyDict_Copy(PLy_interp_globals);
 
@@ -368,7 +369,9 @@ PLy_procedure_compile(PLyProcedure *proc, const char *src)
 	msrc = PLy_procedure_munge_source(proc->pyname, src);
 	/* Save the mangled source for later inclusion in tracebacks */
 	proc->src = MemoryContextStrdup(proc->mcxt, msrc);
-	crv = PyRun_String(msrc, Py_file_input, proc->globals, NULL);
+	code0 = Py_CompileString(msrc, "<string>", Py_file_input);
+	if (code0)
+		crv = PyEval_EvalCode(code0, proc->globals, NULL);
 	pfree(msrc);
 
 	if (crv != NULL)
diff --git a/src/pl/plpython/plpy_resultobject.c b/src/pl/plpython/plpy_resultobject.c
index 95acce65493..f2628205669 100644
--- a/src/pl/plpython/plpy_resultobject.c
+++ b/src/pl/plpython/plpy_resultobject.c
@@ -10,7 +10,7 @@
 #include "plpy_resultobject.h"
 #include "plpython.h"
 
-static void PLy_result_dealloc(PyObject *arg);
+static void PLy_result_dealloc(PLyResultObject *self);
 static PyObject *PLy_result_colnames(PyObject *self, PyObject *unused);
 static PyObject *PLy_result_coltypes(PyObject *self, PyObject *unused);
 static PyObject *PLy_result_coltypmods(PyObject *self, PyObject *unused);
@@ -24,17 +24,6 @@ static int	PLy_result_ass_subscript(PyObject *arg, PyObject *item, PyObject *val
 
 static char PLy_result_doc[] = "Results of a PostgreSQL query";
 
-static PySequenceMethods PLy_result_as_sequence = {
-	.sq_length = PLy_result_length,
-	.sq_item = PLy_result_item,
-};
-
-static PyMappingMethods PLy_result_as_mapping = {
-	.mp_length = PLy_result_length,
-	.mp_subscript = PLy_result_subscript,
-	.mp_ass_subscript = PLy_result_ass_subscript,
-};
-
 static PyMethodDef PLy_result_methods[] = {
 	{"colnames", PLy_result_colnames, METH_NOARGS, NULL},
 	{"coltypes", PLy_result_coltypes, METH_NOARGS, NULL},
@@ -44,23 +33,55 @@ static PyMethodDef PLy_result_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_ResultType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyResult",
-	.tp_basicsize = sizeof(PLyResultObject),
-	.tp_dealloc = PLy_result_dealloc,
-	.tp_as_sequence = &PLy_result_as_sequence,
-	.tp_as_mapping = &PLy_result_as_mapping,
-	.tp_str = &PLy_result_str,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_result_doc,
-	.tp_methods = PLy_result_methods,
+static PyType_Slot PLyResult_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_result_dealloc
+	},
+	{
+		Py_sq_length, PLy_result_length
+	},
+	{
+		Py_sq_item, PLy_result_item
+	},
+	{
+		Py_mp_length, PLy_result_length
+	},
+	{
+		Py_mp_subscript, PLy_result_subscript
+	},
+	{
+		Py_mp_ass_subscript, PLy_result_ass_subscript
+	},
+	{
+		Py_tp_str, PLy_result_str
+	},
+	{
+		Py_tp_doc, (char *) PLy_result_doc
+	},
+	{
+		Py_tp_methods, PLy_result_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyResult_spec =
+{
+	.name = "PLyResult",
+		.basicsize = sizeof(PLyResultObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyResult_slots,
+};
+
+static PyTypeObject *PLy_ResultType;
+
 void
 PLy_result_init_type(void)
 {
-	if (PyType_Ready(&PLy_ResultType) < 0)
+	PLy_ResultType = (PyTypeObject *) PyType_FromSpec(&PLyResult_spec);
+	if (!PLy_ResultType)
 		elog(ERROR, "could not initialize PLy_ResultType");
 }
 
@@ -69,7 +90,7 @@ PLy_result_new(void)
 {
 	PLyResultObject *ob;
 
-	if ((ob = PyObject_New(PLyResultObject, &PLy_ResultType)) == NULL)
+	if ((ob = PyObject_New(PLyResultObject, PLy_ResultType)) == NULL)
 		return NULL;
 
 	/* ob->tuples = NULL; */
@@ -89,20 +110,21 @@ PLy_result_new(void)
 }
 
 static void
-PLy_result_dealloc(PyObject *arg)
+PLy_result_dealloc(PLyResultObject *self)
 {
-	PLyResultObject *ob = (PLyResultObject *) arg;
+	PyTypeObject *tp = Py_TYPE(self);
 
-	Py_XDECREF(ob->nrows);
-	Py_XDECREF(ob->rows);
-	Py_XDECREF(ob->status);
-	if (ob->tupdesc)
+	Py_XDECREF(self->nrows);
+	Py_XDECREF(self->rows);
+	Py_XDECREF(self->status);
+	if (self->tupdesc)
 	{
-		FreeTupleDesc(ob->tupdesc);
-		ob->tupdesc = NULL;
+		FreeTupleDesc(self->tupdesc);
+		self->tupdesc = NULL;
 	}
 
-	arg->ob_type->tp_free(arg);
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }
 
 static PyObject *
@@ -125,7 +147,7 @@ PLy_result_colnames(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PLyUnicode_FromString(NameStr(attr->attname)));
+		PyList_SetItem(list, i, PLyUnicode_FromString(NameStr(attr->attname)));
 	}
 
 	return list;
@@ -151,7 +173,7 @@ PLy_result_coltypes(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypid));
+		PyList_SetItem(list, i, PyLong_FromLong(attr->atttypid));
 	}
 
 	return list;
@@ -177,7 +199,7 @@ PLy_result_coltypmods(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypmod));
+		PyList_SetItem(list, i, PyLong_FromLong(attr->atttypmod));
 	}
 
 	return list;
@@ -227,7 +249,7 @@ PLy_result_str(PyObject *arg)
 	PLyResultObject *ob = (PLyResultObject *) arg;
 
 	return PyUnicode_FromFormat("<%s status=%S nrows=%S rows=%S>",
-								Py_TYPE(ob)->tp_name,
+								"PLyResult",
 								ob->status,
 								ob->nrows,
 								ob->rows);
diff --git a/src/pl/plpython/plpy_subxactobject.c b/src/pl/plpython/plpy_subxactobject.c
index 5c92a0e089a..cc7ff3f9df7 100644
--- a/src/pl/plpython/plpy_subxactobject.c
+++ b/src/pl/plpython/plpy_subxactobject.c
@@ -15,7 +15,6 @@
 List	   *explicit_subtransactions = NIL;
 
 
-static void PLy_subtransaction_dealloc(PyObject *subxact);
 static PyObject *PLy_subtransaction_enter(PyObject *self, PyObject *unused);
 static PyObject *PLy_subtransaction_exit(PyObject *self, PyObject *args);
 
@@ -31,21 +30,35 @@ static PyMethodDef PLy_subtransaction_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_SubtransactionType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLySubtransaction",
-	.tp_basicsize = sizeof(PLySubtransactionObject),
-	.tp_dealloc = PLy_subtransaction_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_subtransaction_doc,
-	.tp_methods = PLy_subtransaction_methods,
+static PyType_Slot PLySubtransaction_slots[] =
+{
+	{
+		Py_tp_doc, (char *) PLy_subtransaction_doc
+	},
+	{
+		Py_tp_methods, PLy_subtransaction_methods
+	},
+	{
+		0, NULL
+	}
+};
+
+static PyType_Spec PLySubtransaction_spec =
+{
+	.name = "PLySubtransaction",
+		.basicsize = sizeof(PLySubtransactionObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLySubtransaction_slots,
 };
 
+static PyTypeObject *PLy_SubtransactionType;
+
 
 void
 PLy_subtransaction_init_type(void)
 {
-	if (PyType_Ready(&PLy_SubtransactionType) < 0)
+	PLy_SubtransactionType = (PyTypeObject *) PyType_FromSpec(&PLySubtransaction_spec);
+	if (!PLy_SubtransactionType)
 		elog(ERROR, "could not initialize PLy_SubtransactionType");
 }
 
@@ -55,7 +68,7 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 {
 	PLySubtransactionObject *ob;
 
-	ob = PyObject_New(PLySubtransactionObject, &PLy_SubtransactionType);
+	ob = PyObject_New(PLySubtransactionObject, PLy_SubtransactionType);
 
 	if (ob == NULL)
 		return NULL;
@@ -66,12 +79,6 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 	return (PyObject *) ob;
 }
 
-/* Python requires a dealloc function to be defined */
-static void
-PLy_subtransaction_dealloc(PyObject *subxact)
-{
-}
-
 /*
  * subxact.__enter__() or subxact.enter()
  *
diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c
index db14c5f8dae..27d9b2f1af6 100644
--- a/src/pl/plpython/plpy_typeio.c
+++ b/src/pl/plpython/plpy_typeio.c
@@ -723,7 +723,7 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 
 			sublist = PLyList_FromArray_recurse(elm, dims, ndim, dim + 1,
 												dataptr_p, bitmap_p, bitmask_p);
-			PyList_SET_ITEM(list, i, sublist);
+			PyList_SetItem(list, i, sublist);
 		}
 	}
 	else
@@ -742,14 +742,14 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 			if (bitmap && (*bitmap & bitmask) == 0)
 			{
 				Py_INCREF(Py_None);
-				PyList_SET_ITEM(list, i, Py_None);
+				PyList_SetItem(list, i, Py_None);
 			}
 			else
 			{
 				Datum		itemvalue;
 
 				itemvalue = fetch_att(dataptr, elm->typbyval, elm->typlen);
-				PyList_SET_ITEM(list, i, elm->func(elm, itemvalue));
+				PyList_SetItem(list, i, elm->func(elm, itemvalue));
 				dataptr = att_addlength_pointer(dataptr, elm->typlen, dataptr);
 				dataptr = (char *) att_align_nominal(dataptr, elm->typalign);
 			}
diff --git a/src/pl/plpython/plpython.h b/src/pl/plpython/plpython.h
index ec822577260..c933419a347 100644
--- a/src/pl/plpython/plpython.h
+++ b/src/pl/plpython/plpython.h
@@ -19,6 +19,8 @@
 #error Python.h must be included via plpython.h
 #endif
 
+#define Py_LIMITED_API 0x03020000
+
 /*
  * Pull in Python headers via a wrapper header, to control the scope of
  * the system_header pragma therein.
-- 
2.39.5 (Apple Git-154)

0003-Attempt-to-fix-meson-build-for-PL-Python-with-limite.patchapplication/octet-stream; name=0003-Attempt-to-fix-meson-build-for-PL-Python-with-limite.patch; x-unix-mode=0644Download
From e4d30a4b7e0ede113cf83e98e3210c3a947451eb Mon Sep 17 00:00:00 2001
From: Jakob Egger <jakob@eggerapps.at>
Date: Tue, 14 Jan 2025 16:31:44 +0100
Subject: [PATCH 3/3] Attempt to fix meson build for PL/Python with limited API

---
 meson.build | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/meson.build b/meson.build
index cfd654d2916..ff31221e4ee 100644
--- a/meson.build
+++ b/meson.build
@@ -1146,6 +1146,10 @@ if not pyopt.disabled()
   python3_inst = pm.find_installation(python.path(), required: pyopt)
   if python3_inst.found()
     python3_dep = python3_inst.dependency(embed: true, required: pyopt)
+    python3_dep = declare_dependency(
+        dependencies: python3_dep,
+        compile_args: '-DPy_LIMITED_API='0x03020000',
+    )
     # Remove this check after we depend on Meson >= 1.1.0
     if not cc.check_header('Python.h', dependencies: python3_dep, required: pyopt, include_directories: postgres_inc)
       python3_dep = not_found_dep
-- 
2.39.5 (Apple Git-154)

#4Jakob Egger
jakob@eggerapps.at
In reply to: Jakob Egger (#3)
Re: Use Python "Limited API" in PL/Python

On 14.01.2025, at 16:51, Jakob Egger <jakob@eggerapps.at> wrote:

I've tried to create a patch with this change. I'm attaching it to this message so that cfbot picks it up. (I was unable to reproduce the issue locally)

Apologies, please disregard my last patch. It does not work.

It looks like meson just can't build libraries that link with Python that use the limited API.

My understanding of Meson is limited, so I am unsure how to best work around this limitation.

I've spent two days trying to figure it out; I'm at the end of my wits.

Best regards,
Jakob

#5Peter Eisentraut
peter@eisentraut.org
In reply to: Jakob Egger (#4)
3 attachment(s)
Re: Use Python "Limited API" in PL/Python

On 15.01.25 12:28, Jakob Egger wrote:

On 14.01.2025, at 16:51, Jakob Egger <jakob@eggerapps.at> wrote:

I've tried to create a patch with this change. I'm attaching it to
this message so that cfbot picks it up. (I was unable to reproduce the
issue locally)

Apologies, please disregard my last patch. It does not work.

It looks like meson just can't build libraries that link with Python
that use the limited API.

My understanding of Meson is limited, so I am unsure how to best work
around this limitation.

I've spent two days trying to figure it out; I'm at the end of my wits.

Thanks for checking. I've also been poking at this, without success.

What we could do is just not activate the limited API on Windows
(actually MSVC, since it works on MinGW). If someone later finds a way
to make it work on Windows/MSVC, then it's easy to change.

So here is an updated patch set with that change. It should pass cfbot
like this.

Attachments:

v2-0001-Remove-obsolete-Python-version-check.patchtext/plain; charset=UTF-8; name=v2-0001-Remove-obsolete-Python-version-check.patchDownload
From 1a981b3979391c75c1b5a9cd686e7e0c14cd58b6 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Mon, 2 Dec 2024 08:53:42 +0100
Subject: [PATCH v2 1/3] Remove obsolete Python version check

The checked version is already the current minimum supported version
(3.2).

Discussion: https://www.postgresql.org/message-id/flat/ee410de1-1e0b-4770-b125-eeefd4726a24@eisentraut.org
---
 src/pl/plpython/plpy_exec.c | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/src/pl/plpython/plpy_exec.c b/src/pl/plpython/plpy_exec.c
index 0e84bb90829..00747bb811b 100644
--- a/src/pl/plpython/plpy_exec.c
+++ b/src/pl/plpython/plpy_exec.c
@@ -1066,13 +1066,7 @@ PLy_procedure_call(PLyProcedure *proc, const char *kargs, PyObject *vargs)
 
 	PG_TRY();
 	{
-#if PY_VERSION_HEX >= 0x03020000
-		rv = PyEval_EvalCode(proc->code,
-							 proc->globals, proc->globals);
-#else
-		rv = PyEval_EvalCode((PyCodeObject *) proc->code,
-							 proc->globals, proc->globals);
-#endif
+		rv = PyEval_EvalCode(proc->code, proc->globals, proc->globals);
 
 		/*
 		 * Since plpy will only let you close subtransactions that you

base-commit: d5221c49a302ee7f3964449cbc730b01ce4e7d40
-- 
2.47.1

v2-0002-Prepare-for-Python-Limited-API-in-PL-Python.patchtext/plain; charset=UTF-8; name=v2-0002-Prepare-for-Python-Limited-API-in-PL-Python.patchDownload
From c921ee3dcae83c489d2963537fcf9e1965004312 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 15 Jan 2025 22:09:29 +0100
Subject: [PATCH v2 2/3] Prepare for Python "Limited API" in PL/Python

Using the Python Limited API would allow building PL/Python against
any Python 3.x version and using another Python 3.x version at run
time.  This commit does not activate that, but it prepares the code to
only use APIs supported by the Limited API.

Implementation details:

- Convert static types to heap types
  (https://docs.python.org/3/howto/isolating-extensions.html#heap-types).

- Replace PyRun_String() with component functions.

- Replace PyList_SET_ITEM() with PyList_SetItem().

Discussion: https://www.postgresql.org/message-id/flat/ee410de1-1e0b-4770-b125-eeefd4726a24@eisentraut.org
---
 src/pl/plpython/plpy_cursorobject.c  | 71 +++++++++++++-------
 src/pl/plpython/plpy_planobject.c    | 61 +++++++++++------
 src/pl/plpython/plpy_procedure.c     |  5 +-
 src/pl/plpython/plpy_resultobject.c  | 98 +++++++++++++++++-----------
 src/pl/plpython/plpy_subxactobject.c | 41 +++++++-----
 src/pl/plpython/plpy_typeio.c        |  6 +-
 6 files changed, 177 insertions(+), 105 deletions(-)

diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c
index bb3fa8a3909..2a370157f90 100644
--- a/src/pl/plpython/plpy_cursorobject.c
+++ b/src/pl/plpython/plpy_cursorobject.c
@@ -20,7 +20,7 @@
 #include "utils/memutils.h"
 
 static PyObject *PLy_cursor_query(const char *query);
-static void PLy_cursor_dealloc(PyObject *arg);
+static void PLy_cursor_dealloc(PLyCursorObject *self);
 static PyObject *PLy_cursor_iternext(PyObject *self);
 static PyObject *PLy_cursor_fetch(PyObject *self, PyObject *args);
 static PyObject *PLy_cursor_close(PyObject *self, PyObject *unused);
@@ -33,22 +33,43 @@ static PyMethodDef PLy_cursor_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_CursorType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyCursor",
-	.tp_basicsize = sizeof(PLyCursorObject),
-	.tp_dealloc = PLy_cursor_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_cursor_doc,
-	.tp_iter = PyObject_SelfIter,
-	.tp_iternext = PLy_cursor_iternext,
-	.tp_methods = PLy_cursor_methods,
+static PyType_Slot PLyCursor_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_cursor_dealloc
+	},
+	{
+		Py_tp_doc, (char *) PLy_cursor_doc
+	},
+	{
+		Py_tp_iter, PyObject_SelfIter
+	},
+	{
+		Py_tp_iternext, PLy_cursor_iternext
+	},
+	{
+		Py_tp_methods, PLy_cursor_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyCursor_spec =
+{
+	.name = "PLyCursor",
+		.basicsize = sizeof(PLyCursorObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyCursor_slots,
+};
+
+static PyTypeObject *PLy_CursorType;
+
 void
 PLy_cursor_init_type(void)
 {
-	if (PyType_Ready(&PLy_CursorType) < 0)
+	PLy_CursorType = (PyTypeObject *) PyType_FromSpec(&PLyCursor_spec);
+	if (!PLy_CursorType)
 		elog(ERROR, "could not initialize PLy_CursorType");
 }
 
@@ -80,7 +101,7 @@ PLy_cursor_query(const char *query)
 	volatile MemoryContext oldcontext;
 	volatile ResourceOwner oldowner;
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
 	cursor->portalname = NULL;
 	cursor->closed = false;
@@ -177,7 +198,7 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 		return NULL;
 	}
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
 	cursor->portalname = NULL;
 	cursor->closed = false;
@@ -272,30 +293,30 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 }
 
 static void
-PLy_cursor_dealloc(PyObject *arg)
+PLy_cursor_dealloc(PLyCursorObject *self)
 {
-	PLyCursorObject *cursor;
+	PyTypeObject *tp = Py_TYPE(self);
 	Portal		portal;
 
-	cursor = (PLyCursorObject *) arg;
-
-	if (!cursor->closed)
+	if (!self->closed)
 	{
-		portal = GetPortalByName(cursor->portalname);
+		portal = GetPortalByName(self->portalname);
 
 		if (PortalIsValid(portal))
 		{
 			UnpinPortal(portal);
 			SPI_cursor_close(portal);
 		}
-		cursor->closed = true;
+		self->closed = true;
 	}
-	if (cursor->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(cursor->mcxt);
-		cursor->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }
 
 static PyObject *
diff --git a/src/pl/plpython/plpy_planobject.c b/src/pl/plpython/plpy_planobject.c
index 9427674d2f4..1b97b5cbd2a 100644
--- a/src/pl/plpython/plpy_planobject.c
+++ b/src/pl/plpython/plpy_planobject.c
@@ -12,7 +12,7 @@
 #include "plpython.h"
 #include "utils/memutils.h"
 
-static void PLy_plan_dealloc(PyObject *arg);
+static void PLy_plan_dealloc(PLyPlanObject *self);
 static PyObject *PLy_plan_cursor(PyObject *self, PyObject *args);
 static PyObject *PLy_plan_execute(PyObject *self, PyObject *args);
 static PyObject *PLy_plan_status(PyObject *self, PyObject *args);
@@ -26,20 +26,37 @@ static PyMethodDef PLy_plan_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_PlanType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyPlan",
-	.tp_basicsize = sizeof(PLyPlanObject),
-	.tp_dealloc = PLy_plan_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_plan_doc,
-	.tp_methods = PLy_plan_methods,
+static PyType_Slot PLyPlan_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_plan_dealloc
+	},
+	{
+		Py_tp_doc, (char *) PLy_plan_doc
+	},
+	{
+		Py_tp_methods, PLy_plan_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyPlan_spec =
+{
+	.name = "PLyPlan",
+		.basicsize = sizeof(PLyPlanObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyPlan_slots,
+};
+
+static PyTypeObject *PLy_PlanType;
+
 void
 PLy_plan_init_type(void)
 {
-	if (PyType_Ready(&PLy_PlanType) < 0)
+	PLy_PlanType = (PyTypeObject *) PyType_FromSpec(&PLyPlan_spec);
+	if (!PLy_PlanType)
 		elog(ERROR, "could not initialize PLy_PlanType");
 }
 
@@ -48,7 +65,7 @@ PLy_plan_new(void)
 {
 	PLyPlanObject *ob;
 
-	if ((ob = PyObject_New(PLyPlanObject, &PLy_PlanType)) == NULL)
+	if ((ob = PyObject_New(PLyPlanObject, PLy_PlanType)) == NULL)
 		return NULL;
 
 	ob->plan = NULL;
@@ -63,25 +80,27 @@ PLy_plan_new(void)
 bool
 is_PLyPlanObject(PyObject *ob)
 {
-	return ob->ob_type == &PLy_PlanType;
+	return ob->ob_type == PLy_PlanType;
 }
 
 static void
-PLy_plan_dealloc(PyObject *arg)
+PLy_plan_dealloc(PLyPlanObject *self)
 {
-	PLyPlanObject *ob = (PLyPlanObject *) arg;
+	PyTypeObject *tp = Py_TYPE(self);
 
-	if (ob->plan)
+	if (self->plan)
 	{
-		SPI_freeplan(ob->plan);
-		ob->plan = NULL;
+		SPI_freeplan(self->plan);
+		self->plan = NULL;
 	}
-	if (ob->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(ob->mcxt);
-		ob->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }
 
 
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index c35a3b801ab..b494eeb474f 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -350,6 +350,7 @@ PLy_procedure_compile(PLyProcedure *proc, const char *src)
 {
 	PyObject   *crv = NULL;
 	char	   *msrc;
+	PyObject   *code0;
 
 	proc->globals = PyDict_Copy(PLy_interp_globals);
 
@@ -368,7 +369,9 @@ PLy_procedure_compile(PLyProcedure *proc, const char *src)
 	msrc = PLy_procedure_munge_source(proc->pyname, src);
 	/* Save the mangled source for later inclusion in tracebacks */
 	proc->src = MemoryContextStrdup(proc->mcxt, msrc);
-	crv = PyRun_String(msrc, Py_file_input, proc->globals, NULL);
+	code0 = Py_CompileString(msrc, "<string>", Py_file_input);
+	if (code0)
+		crv = PyEval_EvalCode(code0, proc->globals, NULL);
 	pfree(msrc);
 
 	if (crv != NULL)
diff --git a/src/pl/plpython/plpy_resultobject.c b/src/pl/plpython/plpy_resultobject.c
index 95acce65493..f2628205669 100644
--- a/src/pl/plpython/plpy_resultobject.c
+++ b/src/pl/plpython/plpy_resultobject.c
@@ -10,7 +10,7 @@
 #include "plpy_resultobject.h"
 #include "plpython.h"
 
-static void PLy_result_dealloc(PyObject *arg);
+static void PLy_result_dealloc(PLyResultObject *self);
 static PyObject *PLy_result_colnames(PyObject *self, PyObject *unused);
 static PyObject *PLy_result_coltypes(PyObject *self, PyObject *unused);
 static PyObject *PLy_result_coltypmods(PyObject *self, PyObject *unused);
@@ -24,17 +24,6 @@ static int	PLy_result_ass_subscript(PyObject *arg, PyObject *item, PyObject *val
 
 static char PLy_result_doc[] = "Results of a PostgreSQL query";
 
-static PySequenceMethods PLy_result_as_sequence = {
-	.sq_length = PLy_result_length,
-	.sq_item = PLy_result_item,
-};
-
-static PyMappingMethods PLy_result_as_mapping = {
-	.mp_length = PLy_result_length,
-	.mp_subscript = PLy_result_subscript,
-	.mp_ass_subscript = PLy_result_ass_subscript,
-};
-
 static PyMethodDef PLy_result_methods[] = {
 	{"colnames", PLy_result_colnames, METH_NOARGS, NULL},
 	{"coltypes", PLy_result_coltypes, METH_NOARGS, NULL},
@@ -44,23 +33,55 @@ static PyMethodDef PLy_result_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_ResultType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyResult",
-	.tp_basicsize = sizeof(PLyResultObject),
-	.tp_dealloc = PLy_result_dealloc,
-	.tp_as_sequence = &PLy_result_as_sequence,
-	.tp_as_mapping = &PLy_result_as_mapping,
-	.tp_str = &PLy_result_str,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_result_doc,
-	.tp_methods = PLy_result_methods,
+static PyType_Slot PLyResult_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_result_dealloc
+	},
+	{
+		Py_sq_length, PLy_result_length
+	},
+	{
+		Py_sq_item, PLy_result_item
+	},
+	{
+		Py_mp_length, PLy_result_length
+	},
+	{
+		Py_mp_subscript, PLy_result_subscript
+	},
+	{
+		Py_mp_ass_subscript, PLy_result_ass_subscript
+	},
+	{
+		Py_tp_str, PLy_result_str
+	},
+	{
+		Py_tp_doc, (char *) PLy_result_doc
+	},
+	{
+		Py_tp_methods, PLy_result_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyResult_spec =
+{
+	.name = "PLyResult",
+		.basicsize = sizeof(PLyResultObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyResult_slots,
+};
+
+static PyTypeObject *PLy_ResultType;
+
 void
 PLy_result_init_type(void)
 {
-	if (PyType_Ready(&PLy_ResultType) < 0)
+	PLy_ResultType = (PyTypeObject *) PyType_FromSpec(&PLyResult_spec);
+	if (!PLy_ResultType)
 		elog(ERROR, "could not initialize PLy_ResultType");
 }
 
@@ -69,7 +90,7 @@ PLy_result_new(void)
 {
 	PLyResultObject *ob;
 
-	if ((ob = PyObject_New(PLyResultObject, &PLy_ResultType)) == NULL)
+	if ((ob = PyObject_New(PLyResultObject, PLy_ResultType)) == NULL)
 		return NULL;
 
 	/* ob->tuples = NULL; */
@@ -89,20 +110,21 @@ PLy_result_new(void)
 }
 
 static void
-PLy_result_dealloc(PyObject *arg)
+PLy_result_dealloc(PLyResultObject *self)
 {
-	PLyResultObject *ob = (PLyResultObject *) arg;
+	PyTypeObject *tp = Py_TYPE(self);
 
-	Py_XDECREF(ob->nrows);
-	Py_XDECREF(ob->rows);
-	Py_XDECREF(ob->status);
-	if (ob->tupdesc)
+	Py_XDECREF(self->nrows);
+	Py_XDECREF(self->rows);
+	Py_XDECREF(self->status);
+	if (self->tupdesc)
 	{
-		FreeTupleDesc(ob->tupdesc);
-		ob->tupdesc = NULL;
+		FreeTupleDesc(self->tupdesc);
+		self->tupdesc = NULL;
 	}
 
-	arg->ob_type->tp_free(arg);
+	PyObject_Free(self);
+	Py_DECREF(tp);
 }
 
 static PyObject *
@@ -125,7 +147,7 @@ PLy_result_colnames(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PLyUnicode_FromString(NameStr(attr->attname)));
+		PyList_SetItem(list, i, PLyUnicode_FromString(NameStr(attr->attname)));
 	}
 
 	return list;
@@ -151,7 +173,7 @@ PLy_result_coltypes(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypid));
+		PyList_SetItem(list, i, PyLong_FromLong(attr->atttypid));
 	}
 
 	return list;
@@ -177,7 +199,7 @@ PLy_result_coltypmods(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypmod));
+		PyList_SetItem(list, i, PyLong_FromLong(attr->atttypmod));
 	}
 
 	return list;
@@ -227,7 +249,7 @@ PLy_result_str(PyObject *arg)
 	PLyResultObject *ob = (PLyResultObject *) arg;
 
 	return PyUnicode_FromFormat("<%s status=%S nrows=%S rows=%S>",
-								Py_TYPE(ob)->tp_name,
+								"PLyResult",
 								ob->status,
 								ob->nrows,
 								ob->rows);
diff --git a/src/pl/plpython/plpy_subxactobject.c b/src/pl/plpython/plpy_subxactobject.c
index 5c92a0e089a..cc7ff3f9df7 100644
--- a/src/pl/plpython/plpy_subxactobject.c
+++ b/src/pl/plpython/plpy_subxactobject.c
@@ -15,7 +15,6 @@
 List	   *explicit_subtransactions = NIL;
 
 
-static void PLy_subtransaction_dealloc(PyObject *subxact);
 static PyObject *PLy_subtransaction_enter(PyObject *self, PyObject *unused);
 static PyObject *PLy_subtransaction_exit(PyObject *self, PyObject *args);
 
@@ -31,21 +30,35 @@ static PyMethodDef PLy_subtransaction_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_SubtransactionType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLySubtransaction",
-	.tp_basicsize = sizeof(PLySubtransactionObject),
-	.tp_dealloc = PLy_subtransaction_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_subtransaction_doc,
-	.tp_methods = PLy_subtransaction_methods,
+static PyType_Slot PLySubtransaction_slots[] =
+{
+	{
+		Py_tp_doc, (char *) PLy_subtransaction_doc
+	},
+	{
+		Py_tp_methods, PLy_subtransaction_methods
+	},
+	{
+		0, NULL
+	}
+};
+
+static PyType_Spec PLySubtransaction_spec =
+{
+	.name = "PLySubtransaction",
+		.basicsize = sizeof(PLySubtransactionObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLySubtransaction_slots,
 };
 
+static PyTypeObject *PLy_SubtransactionType;
+
 
 void
 PLy_subtransaction_init_type(void)
 {
-	if (PyType_Ready(&PLy_SubtransactionType) < 0)
+	PLy_SubtransactionType = (PyTypeObject *) PyType_FromSpec(&PLySubtransaction_spec);
+	if (!PLy_SubtransactionType)
 		elog(ERROR, "could not initialize PLy_SubtransactionType");
 }
 
@@ -55,7 +68,7 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 {
 	PLySubtransactionObject *ob;
 
-	ob = PyObject_New(PLySubtransactionObject, &PLy_SubtransactionType);
+	ob = PyObject_New(PLySubtransactionObject, PLy_SubtransactionType);
 
 	if (ob == NULL)
 		return NULL;
@@ -66,12 +79,6 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 	return (PyObject *) ob;
 }
 
-/* Python requires a dealloc function to be defined */
-static void
-PLy_subtransaction_dealloc(PyObject *subxact)
-{
-}
-
 /*
  * subxact.__enter__() or subxact.enter()
  *
diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c
index db14c5f8dae..27d9b2f1af6 100644
--- a/src/pl/plpython/plpy_typeio.c
+++ b/src/pl/plpython/plpy_typeio.c
@@ -723,7 +723,7 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 
 			sublist = PLyList_FromArray_recurse(elm, dims, ndim, dim + 1,
 												dataptr_p, bitmap_p, bitmask_p);
-			PyList_SET_ITEM(list, i, sublist);
+			PyList_SetItem(list, i, sublist);
 		}
 	}
 	else
@@ -742,14 +742,14 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 			if (bitmap && (*bitmap & bitmask) == 0)
 			{
 				Py_INCREF(Py_None);
-				PyList_SET_ITEM(list, i, Py_None);
+				PyList_SetItem(list, i, Py_None);
 			}
 			else
 			{
 				Datum		itemvalue;
 
 				itemvalue = fetch_att(dataptr, elm->typbyval, elm->typlen);
-				PyList_SET_ITEM(list, i, elm->func(elm, itemvalue));
+				PyList_SetItem(list, i, elm->func(elm, itemvalue));
 				dataptr = att_addlength_pointer(dataptr, elm->typlen, dataptr);
 				dataptr = (char *) att_align_nominal(dataptr, elm->typalign);
 			}
-- 
2.47.1

v2-0003-Activate-Python-Limited-API-in-PL-Python.patchtext/plain; charset=UTF-8; name=v2-0003-Activate-Python-Limited-API-in-PL-Python.patchDownload
From e059a1653a47f1ca9155a84c8f9a9f122a970ace Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 15 Jan 2025 22:22:59 +0100
Subject: [PATCH v2 3/3] Activate Python "Limited API" in PL/Python

This allows building PL/Python against any Python 3.x version and
using another Python 3.x version at run time.

Discussion: https://www.postgresql.org/message-id/flat/ee410de1-1e0b-4770-b125-eeefd4726a24@eisentraut.org
---
 src/pl/plpython/plpython.h | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/pl/plpython/plpython.h b/src/pl/plpython/plpython.h
index ec822577260..06fc1a5440f 100644
--- a/src/pl/plpython/plpython.h
+++ b/src/pl/plpython/plpython.h
@@ -19,6 +19,15 @@
 #error Python.h must be included via plpython.h
 #endif
 
+/*
+ * Enable Python Limited API
+ *
+ * XXX currently not enabled on MSVC because of build failures
+ */
+#if !defined(_MSC_VER)
+#define Py_LIMITED_API 0x03020000
+#endif
+
 /*
  * Pull in Python headers via a wrapper header, to control the scope of
  * the system_header pragma therein.
-- 
2.47.1

#6Peter Eisentraut
peter@eisentraut.org
In reply to: Peter Eisentraut (#5)
Re: Use Python "Limited API" in PL/Python

On 15.01.25 23:20, Peter Eisentraut wrote:

On 15.01.25 12:28, Jakob Egger wrote:

On 14.01.2025, at 16:51, Jakob Egger <jakob@eggerapps.at> wrote:

I've tried to create a patch with this change. I'm attaching it to
this message so that cfbot picks it up. (I was unable to reproduce
the issue locally)

Apologies, please disregard my last patch. It does not work.

It looks like meson just can't build libraries that link with Python
that use the limited API.

My understanding of Meson is limited, so I am unsure how to best work
around this limitation.

I've spent two days trying to figure it out; I'm at the end of my wits.

Thanks for checking.  I've also been poking at this, without success.

What we could do is just not activate the limited API on Windows
(actually MSVC, since it works on MinGW).  If someone later finds a way
to make it work on Windows/MSVC, then it's easy to change.

So here is an updated patch set with that change.  It should pass cfbot
like this.

Update for the hackers list: This patch set was briefly committed but
had to be reverted because it crashed on some older Python versions; see
[0]: /messages/by-id/E1tnJNu-000CZW-0X@gemulon.postgresql.org

I have locally reproduced the problem with Python 3.6 and 3.7; Python
3.8 is ok. This matches the results from the buildfarm.

I have poked at this a bit more but haven't gotten a good idea where or
how to fix it so far. Help welcome.

[0]: /messages/by-id/E1tnJNu-000CZW-0X@gemulon.postgresql.org
/messages/by-id/E1tnJNu-000CZW-0X@gemulon.postgresql.org

#7Peter Eisentraut
peter@eisentraut.org
In reply to: Peter Eisentraut (#6)
2 attachment(s)
Re: Use Python "Limited API" in PL/Python

On 03.03.25 11:17, Peter Eisentraut wrote:

Update for the hackers list: This patch set was briefly committed but
had to be reverted because it crashed on some older Python versions; see
[0].

I have locally reproduced the problem with Python 3.6 and 3.7; Python
3.8 is ok.  This matches the results from the buildfarm.

I have poked at this a bit more but haven't gotten a good idea where or
how to fix it so far.  Help welcome.

I have figured this out. There was a Python API change/bugfix between
3.7 and 3.8 that directly affects this patch. The relevant commit is
<https://github.com/python/cpython/commit/364f0b0f19c&gt;. I have applied
the workarounds described there to my patch set, and now it works for
3.6 and 3.7 as well.

Attachments:

v3-0001-Prepare-for-Python-Limited-API-in-PL-Python.patchtext/plain; charset=UTF-8; name=v3-0001-Prepare-for-Python-Limited-API-in-PL-Python.patchDownload
From d28601d504dff57c49089f3adcaf0dce83893d83 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 5 Mar 2025 17:38:06 +0100
Subject: [PATCH v3 1/2] Prepare for Python "Limited API" in PL/Python

Using the Python Limited API would allow building PL/Python against
any Python 3.x version and using another Python 3.x version at run
time.  This commit does not activate that, but it prepares the code to
only use APIs supported by the Limited API.

Implementation details:

- Convert static types to heap types
  (https://docs.python.org/3/howto/isolating-extensions.html#heap-types).

- Replace PyRun_String() with component functions.

- Replace PyList_SET_ITEM() with PyList_SetItem().

Discussion: https://www.postgresql.org/message-id/flat/ee410de1-1e0b-4770-b125-eeefd4726a24@eisentraut.org
---
 src/pl/plpython/plpy_cursorobject.c  |  84 +++++++++++++++------
 src/pl/plpython/plpy_planobject.c    |  70 +++++++++++------
 src/pl/plpython/plpy_procedure.c     |   5 +-
 src/pl/plpython/plpy_resultobject.c  | 109 +++++++++++++++++----------
 src/pl/plpython/plpy_subxactobject.c |  46 ++++++-----
 src/pl/plpython/plpy_typeio.c        |   6 +-
 6 files changed, 213 insertions(+), 107 deletions(-)

diff --git a/src/pl/plpython/plpy_cursorobject.c b/src/pl/plpython/plpy_cursorobject.c
index bb3fa8a3909..1c6be756120 100644
--- a/src/pl/plpython/plpy_cursorobject.c
+++ b/src/pl/plpython/plpy_cursorobject.c
@@ -20,7 +20,7 @@
 #include "utils/memutils.h"
 
 static PyObject *PLy_cursor_query(const char *query);
-static void PLy_cursor_dealloc(PyObject *arg);
+static void PLy_cursor_dealloc(PLyCursorObject *self);
 static PyObject *PLy_cursor_iternext(PyObject *self);
 static PyObject *PLy_cursor_fetch(PyObject *self, PyObject *args);
 static PyObject *PLy_cursor_close(PyObject *self, PyObject *unused);
@@ -33,22 +33,43 @@ static PyMethodDef PLy_cursor_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_CursorType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyCursor",
-	.tp_basicsize = sizeof(PLyCursorObject),
-	.tp_dealloc = PLy_cursor_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_cursor_doc,
-	.tp_iter = PyObject_SelfIter,
-	.tp_iternext = PLy_cursor_iternext,
-	.tp_methods = PLy_cursor_methods,
+static PyType_Slot PLyCursor_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_cursor_dealloc
+	},
+	{
+		Py_tp_doc, (char *) PLy_cursor_doc
+	},
+	{
+		Py_tp_iter, PyObject_SelfIter
+	},
+	{
+		Py_tp_iternext, PLy_cursor_iternext
+	},
+	{
+		Py_tp_methods, PLy_cursor_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyCursor_spec =
+{
+	.name = "PLyCursor",
+		.basicsize = sizeof(PLyCursorObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyCursor_slots,
+};
+
+static PyTypeObject *PLy_CursorType;
+
 void
 PLy_cursor_init_type(void)
 {
-	if (PyType_Ready(&PLy_CursorType) < 0)
+	PLy_CursorType = (PyTypeObject *) PyType_FromSpec(&PLyCursor_spec);
+	if (!PLy_CursorType)
 		elog(ERROR, "could not initialize PLy_CursorType");
 }
 
@@ -80,8 +101,12 @@ PLy_cursor_query(const char *query)
 	volatile MemoryContext oldcontext;
 	volatile ResourceOwner oldowner;
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
+#if PY_VERSION_HEX < 0x03080000
+	/* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
+	Py_INCREF(PLy_CursorType);
+#endif
 	cursor->portalname = NULL;
 	cursor->closed = false;
 	cursor->mcxt = AllocSetContextCreate(TopMemoryContext,
@@ -177,8 +202,12 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 		return NULL;
 	}
 
-	if ((cursor = PyObject_New(PLyCursorObject, &PLy_CursorType)) == NULL)
+	if ((cursor = PyObject_New(PLyCursorObject, PLy_CursorType)) == NULL)
 		return NULL;
+#if PY_VERSION_HEX < 0x03080000
+	/* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
+	Py_INCREF(PLy_CursorType);
+#endif
 	cursor->portalname = NULL;
 	cursor->closed = false;
 	cursor->mcxt = AllocSetContextCreate(TopMemoryContext,
@@ -272,30 +301,35 @@ PLy_cursor_plan(PyObject *ob, PyObject *args)
 }
 
 static void
-PLy_cursor_dealloc(PyObject *arg)
+PLy_cursor_dealloc(PLyCursorObject *self)
 {
-	PLyCursorObject *cursor;
+#if PY_VERSION_HEX >= 0x03080000
+	PyTypeObject *tp = Py_TYPE(self);
+#endif
 	Portal		portal;
 
-	cursor = (PLyCursorObject *) arg;
-
-	if (!cursor->closed)
+	if (!self->closed)
 	{
-		portal = GetPortalByName(cursor->portalname);
+		portal = GetPortalByName(self->portalname);
 
 		if (PortalIsValid(portal))
 		{
 			UnpinPortal(portal);
 			SPI_cursor_close(portal);
 		}
-		cursor->closed = true;
+		self->closed = true;
 	}
-	if (cursor->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(cursor->mcxt);
-		cursor->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+#if PY_VERSION_HEX >= 0x03080000
+	/* This was not needed before Python 3.8 (Python issue 35810) */
+	Py_DECREF(tp);
+#endif
 }
 
 static PyObject *
diff --git a/src/pl/plpython/plpy_planobject.c b/src/pl/plpython/plpy_planobject.c
index 9427674d2f4..3e385555e5e 100644
--- a/src/pl/plpython/plpy_planobject.c
+++ b/src/pl/plpython/plpy_planobject.c
@@ -12,7 +12,7 @@
 #include "plpython.h"
 #include "utils/memutils.h"
 
-static void PLy_plan_dealloc(PyObject *arg);
+static void PLy_plan_dealloc(PLyPlanObject *self);
 static PyObject *PLy_plan_cursor(PyObject *self, PyObject *args);
 static PyObject *PLy_plan_execute(PyObject *self, PyObject *args);
 static PyObject *PLy_plan_status(PyObject *self, PyObject *args);
@@ -26,20 +26,37 @@ static PyMethodDef PLy_plan_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_PlanType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyPlan",
-	.tp_basicsize = sizeof(PLyPlanObject),
-	.tp_dealloc = PLy_plan_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_plan_doc,
-	.tp_methods = PLy_plan_methods,
+static PyType_Slot PLyPlan_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_plan_dealloc
+	},
+	{
+		Py_tp_doc, (char *) PLy_plan_doc
+	},
+	{
+		Py_tp_methods, PLy_plan_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLyPlan_spec =
+{
+	.name = "PLyPlan",
+		.basicsize = sizeof(PLyPlanObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyPlan_slots,
+};
+
+static PyTypeObject *PLy_PlanType;
+
 void
 PLy_plan_init_type(void)
 {
-	if (PyType_Ready(&PLy_PlanType) < 0)
+	PLy_PlanType = (PyTypeObject *) PyType_FromSpec(&PLyPlan_spec);
+	if (!PLy_PlanType)
 		elog(ERROR, "could not initialize PLy_PlanType");
 }
 
@@ -48,8 +65,12 @@ PLy_plan_new(void)
 {
 	PLyPlanObject *ob;
 
-	if ((ob = PyObject_New(PLyPlanObject, &PLy_PlanType)) == NULL)
+	if ((ob = PyObject_New(PLyPlanObject, PLy_PlanType)) == NULL)
 		return NULL;
+#if PY_VERSION_HEX < 0x03080000
+	/* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
+	Py_INCREF(PLy_PlanType);
+#endif
 
 	ob->plan = NULL;
 	ob->nargs = 0;
@@ -63,25 +84,32 @@ PLy_plan_new(void)
 bool
 is_PLyPlanObject(PyObject *ob)
 {
-	return ob->ob_type == &PLy_PlanType;
+	return ob->ob_type == PLy_PlanType;
 }
 
 static void
-PLy_plan_dealloc(PyObject *arg)
+PLy_plan_dealloc(PLyPlanObject *self)
 {
-	PLyPlanObject *ob = (PLyPlanObject *) arg;
+#if PY_VERSION_HEX >= 0x03080000
+	PyTypeObject *tp = Py_TYPE(self);
+#endif
 
-	if (ob->plan)
+	if (self->plan)
 	{
-		SPI_freeplan(ob->plan);
-		ob->plan = NULL;
+		SPI_freeplan(self->plan);
+		self->plan = NULL;
 	}
-	if (ob->mcxt)
+	if (self->mcxt)
 	{
-		MemoryContextDelete(ob->mcxt);
-		ob->mcxt = NULL;
+		MemoryContextDelete(self->mcxt);
+		self->mcxt = NULL;
 	}
-	arg->ob_type->tp_free(arg);
+
+	PyObject_Free(self);
+#if PY_VERSION_HEX >= 0x03080000
+	/* This was not needed before Python 3.8 (Python issue 35810) */
+	Py_DECREF(tp);
+#endif
 }
 
 
diff --git a/src/pl/plpython/plpy_procedure.c b/src/pl/plpython/plpy_procedure.c
index c35a3b801ab..b494eeb474f 100644
--- a/src/pl/plpython/plpy_procedure.c
+++ b/src/pl/plpython/plpy_procedure.c
@@ -350,6 +350,7 @@ PLy_procedure_compile(PLyProcedure *proc, const char *src)
 {
 	PyObject   *crv = NULL;
 	char	   *msrc;
+	PyObject   *code0;
 
 	proc->globals = PyDict_Copy(PLy_interp_globals);
 
@@ -368,7 +369,9 @@ PLy_procedure_compile(PLyProcedure *proc, const char *src)
 	msrc = PLy_procedure_munge_source(proc->pyname, src);
 	/* Save the mangled source for later inclusion in tracebacks */
 	proc->src = MemoryContextStrdup(proc->mcxt, msrc);
-	crv = PyRun_String(msrc, Py_file_input, proc->globals, NULL);
+	code0 = Py_CompileString(msrc, "<string>", Py_file_input);
+	if (code0)
+		crv = PyEval_EvalCode(code0, proc->globals, NULL);
 	pfree(msrc);
 
 	if (crv != NULL)
diff --git a/src/pl/plpython/plpy_resultobject.c b/src/pl/plpython/plpy_resultobject.c
index 95acce65493..80fa1d7accc 100644
--- a/src/pl/plpython/plpy_resultobject.c
+++ b/src/pl/plpython/plpy_resultobject.c
@@ -10,7 +10,7 @@
 #include "plpy_resultobject.h"
 #include "plpython.h"
 
-static void PLy_result_dealloc(PyObject *arg);
+static void PLy_result_dealloc(PLyResultObject *self);
 static PyObject *PLy_result_colnames(PyObject *self, PyObject *unused);
 static PyObject *PLy_result_coltypes(PyObject *self, PyObject *unused);
 static PyObject *PLy_result_coltypmods(PyObject *self, PyObject *unused);
@@ -24,17 +24,6 @@ static int	PLy_result_ass_subscript(PyObject *arg, PyObject *item, PyObject *val
 
 static char PLy_result_doc[] = "Results of a PostgreSQL query";
 
-static PySequenceMethods PLy_result_as_sequence = {
-	.sq_length = PLy_result_length,
-	.sq_item = PLy_result_item,
-};
-
-static PyMappingMethods PLy_result_as_mapping = {
-	.mp_length = PLy_result_length,
-	.mp_subscript = PLy_result_subscript,
-	.mp_ass_subscript = PLy_result_ass_subscript,
-};
-
 static PyMethodDef PLy_result_methods[] = {
 	{"colnames", PLy_result_colnames, METH_NOARGS, NULL},
 	{"coltypes", PLy_result_coltypes, METH_NOARGS, NULL},
@@ -44,23 +33,55 @@ static PyMethodDef PLy_result_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_ResultType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLyResult",
-	.tp_basicsize = sizeof(PLyResultObject),
-	.tp_dealloc = PLy_result_dealloc,
-	.tp_as_sequence = &PLy_result_as_sequence,
-	.tp_as_mapping = &PLy_result_as_mapping,
-	.tp_str = &PLy_result_str,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_result_doc,
-	.tp_methods = PLy_result_methods,
+static PyType_Slot PLyResult_slots[] =
+{
+	{
+		Py_tp_dealloc, PLy_result_dealloc
+	},
+	{
+		Py_sq_length, PLy_result_length
+	},
+	{
+		Py_sq_item, PLy_result_item
+	},
+	{
+		Py_mp_length, PLy_result_length
+	},
+	{
+		Py_mp_subscript, PLy_result_subscript
+	},
+	{
+		Py_mp_ass_subscript, PLy_result_ass_subscript
+	},
+	{
+		Py_tp_str, PLy_result_str
+	},
+	{
+		Py_tp_doc, (char *) PLy_result_doc
+	},
+	{
+		Py_tp_methods, PLy_result_methods
+	},
+	{
+		0, NULL
+	}
+};
+
+static PyType_Spec PLyResult_spec =
+{
+	.name = "PLyResult",
+		.basicsize = sizeof(PLyResultObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLyResult_slots,
 };
 
+static PyTypeObject *PLy_ResultType;
+
 void
 PLy_result_init_type(void)
 {
-	if (PyType_Ready(&PLy_ResultType) < 0)
+	PLy_ResultType = (PyTypeObject *) PyType_FromSpec(&PLyResult_spec);
+	if (!PLy_ResultType)
 		elog(ERROR, "could not initialize PLy_ResultType");
 }
 
@@ -69,8 +90,12 @@ PLy_result_new(void)
 {
 	PLyResultObject *ob;
 
-	if ((ob = PyObject_New(PLyResultObject, &PLy_ResultType)) == NULL)
+	if ((ob = PyObject_New(PLyResultObject, PLy_ResultType)) == NULL)
 		return NULL;
+#if PY_VERSION_HEX < 0x03080000
+	/* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
+	Py_INCREF(PLy_ResultType);
+#endif
 
 	/* ob->tuples = NULL; */
 
@@ -89,20 +114,26 @@ PLy_result_new(void)
 }
 
 static void
-PLy_result_dealloc(PyObject *arg)
+PLy_result_dealloc(PLyResultObject *self)
 {
-	PLyResultObject *ob = (PLyResultObject *) arg;
-
-	Py_XDECREF(ob->nrows);
-	Py_XDECREF(ob->rows);
-	Py_XDECREF(ob->status);
-	if (ob->tupdesc)
+#if PY_VERSION_HEX >= 0x03080000
+	PyTypeObject *tp = Py_TYPE(self);
+#endif
+
+	Py_XDECREF(self->nrows);
+	Py_XDECREF(self->rows);
+	Py_XDECREF(self->status);
+	if (self->tupdesc)
 	{
-		FreeTupleDesc(ob->tupdesc);
-		ob->tupdesc = NULL;
+		FreeTupleDesc(self->tupdesc);
+		self->tupdesc = NULL;
 	}
 
-	arg->ob_type->tp_free(arg);
+	PyObject_Free(self);
+#if PY_VERSION_HEX >= 0x03080000
+	/* This was not needed before Python 3.8 (Python issue 35810) */
+	Py_DECREF(tp);
+#endif
 }
 
 static PyObject *
@@ -125,7 +156,7 @@ PLy_result_colnames(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PLyUnicode_FromString(NameStr(attr->attname)));
+		PyList_SetItem(list, i, PLyUnicode_FromString(NameStr(attr->attname)));
 	}
 
 	return list;
@@ -151,7 +182,7 @@ PLy_result_coltypes(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypid));
+		PyList_SetItem(list, i, PyLong_FromLong(attr->atttypid));
 	}
 
 	return list;
@@ -177,7 +208,7 @@ PLy_result_coltypmods(PyObject *self, PyObject *unused)
 	{
 		Form_pg_attribute attr = TupleDescAttr(ob->tupdesc, i);
 
-		PyList_SET_ITEM(list, i, PyLong_FromLong(attr->atttypmod));
+		PyList_SetItem(list, i, PyLong_FromLong(attr->atttypmod));
 	}
 
 	return list;
@@ -227,7 +258,7 @@ PLy_result_str(PyObject *arg)
 	PLyResultObject *ob = (PLyResultObject *) arg;
 
 	return PyUnicode_FromFormat("<%s status=%S nrows=%S rows=%S>",
-								Py_TYPE(ob)->tp_name,
+								"PLyResult",
 								ob->status,
 								ob->nrows,
 								ob->rows);
diff --git a/src/pl/plpython/plpy_subxactobject.c b/src/pl/plpython/plpy_subxactobject.c
index 5c92a0e089a..ad24a9fc4b8 100644
--- a/src/pl/plpython/plpy_subxactobject.c
+++ b/src/pl/plpython/plpy_subxactobject.c
@@ -15,7 +15,6 @@
 List	   *explicit_subtransactions = NIL;
 
 
-static void PLy_subtransaction_dealloc(PyObject *subxact);
 static PyObject *PLy_subtransaction_enter(PyObject *self, PyObject *unused);
 static PyObject *PLy_subtransaction_exit(PyObject *self, PyObject *args);
 
@@ -31,21 +30,35 @@ static PyMethodDef PLy_subtransaction_methods[] = {
 	{NULL, NULL, 0, NULL}
 };
 
-static PyTypeObject PLy_SubtransactionType = {
-	PyVarObject_HEAD_INIT(NULL, 0)
-	.tp_name = "PLySubtransaction",
-	.tp_basicsize = sizeof(PLySubtransactionObject),
-	.tp_dealloc = PLy_subtransaction_dealloc,
-	.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
-	.tp_doc = PLy_subtransaction_doc,
-	.tp_methods = PLy_subtransaction_methods,
+static PyType_Slot PLySubtransaction_slots[] =
+{
+	{
+		Py_tp_doc, (char *) PLy_subtransaction_doc
+	},
+	{
+		Py_tp_methods, PLy_subtransaction_methods
+	},
+	{
+		0, NULL
+	}
 };
 
+static PyType_Spec PLySubtransaction_spec =
+{
+	.name = "PLySubtransaction",
+		.basicsize = sizeof(PLySubtransactionObject),
+		.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
+		.slots = PLySubtransaction_slots,
+};
+
+static PyTypeObject *PLy_SubtransactionType;
+
 
 void
 PLy_subtransaction_init_type(void)
 {
-	if (PyType_Ready(&PLy_SubtransactionType) < 0)
+	PLy_SubtransactionType = (PyTypeObject *) PyType_FromSpec(&PLySubtransaction_spec);
+	if (!PLy_SubtransactionType)
 		elog(ERROR, "could not initialize PLy_SubtransactionType");
 }
 
@@ -55,10 +68,13 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 {
 	PLySubtransactionObject *ob;
 
-	ob = PyObject_New(PLySubtransactionObject, &PLy_SubtransactionType);
-
+	ob = PyObject_New(PLySubtransactionObject, PLy_SubtransactionType);
 	if (ob == NULL)
 		return NULL;
+#if PY_VERSION_HEX < 0x03080000
+	/* Workaround for Python issue 35810; no longer necessary in Python 3.8 */
+	Py_INCREF(PLy_SubtransactionType);
+#endif
 
 	ob->started = false;
 	ob->exited = false;
@@ -66,12 +82,6 @@ PLy_subtransaction_new(PyObject *self, PyObject *unused)
 	return (PyObject *) ob;
 }
 
-/* Python requires a dealloc function to be defined */
-static void
-PLy_subtransaction_dealloc(PyObject *subxact)
-{
-}
-
 /*
  * subxact.__enter__() or subxact.enter()
  *
diff --git a/src/pl/plpython/plpy_typeio.c b/src/pl/plpython/plpy_typeio.c
index 51e1d610259..1d127ae3ffe 100644
--- a/src/pl/plpython/plpy_typeio.c
+++ b/src/pl/plpython/plpy_typeio.c
@@ -723,7 +723,7 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 
 			sublist = PLyList_FromArray_recurse(elm, dims, ndim, dim + 1,
 												dataptr_p, bitmap_p, bitmask_p);
-			PyList_SET_ITEM(list, i, sublist);
+			PyList_SetItem(list, i, sublist);
 		}
 	}
 	else
@@ -742,14 +742,14 @@ PLyList_FromArray_recurse(PLyDatumToOb *elm, int *dims, int ndim, int dim,
 			if (bitmap && (*bitmap & bitmask) == 0)
 			{
 				Py_INCREF(Py_None);
-				PyList_SET_ITEM(list, i, Py_None);
+				PyList_SetItem(list, i, Py_None);
 			}
 			else
 			{
 				Datum		itemvalue;
 
 				itemvalue = fetch_att(dataptr, elm->typbyval, elm->typlen);
-				PyList_SET_ITEM(list, i, elm->func(elm, itemvalue));
+				PyList_SetItem(list, i, elm->func(elm, itemvalue));
 				dataptr = att_addlength_pointer(dataptr, elm->typlen, dataptr);
 				dataptr = (char *) att_align_nominal(dataptr, elm->typalign);
 			}

base-commit: ad40644eb8582fc1f537330b76ae7893cacf7b21
-- 
2.48.1

v3-0002-Activate-Python-Limited-API-in-PL-Python.patchtext/plain; charset=UTF-8; name=v3-0002-Activate-Python-Limited-API-in-PL-Python.patchDownload
From dd07e62e041dfae67c1182226b772fe59a71f78e Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter@eisentraut.org>
Date: Wed, 5 Mar 2025 17:38:06 +0100
Subject: [PATCH v3 2/2] Activate Python "Limited API" in PL/Python

This allows building PL/Python against any Python 3.x version and
using another Python 3.x version at run time.

Discussion: https://www.postgresql.org/message-id/flat/ee410de1-1e0b-4770-b125-eeefd4726a24@eisentraut.org
---
 src/pl/plpython/plpython.h | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/src/pl/plpython/plpython.h b/src/pl/plpython/plpython.h
index ec822577260..06fc1a5440f 100644
--- a/src/pl/plpython/plpython.h
+++ b/src/pl/plpython/plpython.h
@@ -19,6 +19,15 @@
 #error Python.h must be included via plpython.h
 #endif
 
+/*
+ * Enable Python Limited API
+ *
+ * XXX currently not enabled on MSVC because of build failures
+ */
+#if !defined(_MSC_VER)
+#define Py_LIMITED_API 0x03020000
+#endif
+
 /*
  * Pull in Python headers via a wrapper header, to control the scope of
  * the system_header pragma therein.
-- 
2.48.1

#8Peter Eisentraut
peter@eisentraut.org
In reply to: Peter Eisentraut (#7)
Re: Use Python "Limited API" in PL/Python

On 05.03.25 17:40, Peter Eisentraut wrote:

On 03.03.25 11:17, Peter Eisentraut wrote:

Update for the hackers list: This patch set was briefly committed but
had to be reverted because it crashed on some older Python versions;
see [0].

I have locally reproduced the problem with Python 3.6 and 3.7; Python
3.8 is ok.  This matches the results from the buildfarm.

I have poked at this a bit more but haven't gotten a good idea where
or how to fix it so far.  Help welcome.

I have figured this out.  There was a Python API change/bugfix between
3.7 and 3.8 that directly affects this patch.  The relevant commit is
<https://github.com/python/cpython/commit/364f0b0f19c&gt;.  I have applied
the workarounds described there to my patch set, and now it works for
3.6 and 3.7 as well.

This has all been committed now.

#9vignesh C
vignesh21@gmail.com
In reply to: Peter Eisentraut (#8)
Re: Use Python "Limited API" in PL/Python

On Fri, 14 Mar 2025 at 14:11, Peter Eisentraut <peter@eisentraut.org> wrote:

On 05.03.25 17:40, Peter Eisentraut wrote:

On 03.03.25 11:17, Peter Eisentraut wrote:

Update for the hackers list: This patch set was briefly committed but
had to be reverted because it crashed on some older Python versions;
see [0].

I have locally reproduced the problem with Python 3.6 and 3.7; Python
3.8 is ok. This matches the results from the buildfarm.

I have poked at this a bit more but haven't gotten a good idea where
or how to fix it so far. Help welcome.

I have figured this out. There was a Python API change/bugfix between
3.7 and 3.8 that directly affects this patch. The relevant commit is
<https://github.com/python/cpython/commit/364f0b0f19c&gt;. I have applied
the workarounds described there to my patch set, and now it works for
3.6 and 3.7 as well.

This has all been committed now.

I felt we can mark this commitfest entry at [1]https://commitfest.postgresql.org/patch/5416/ to commited now.
[1]: https://commitfest.postgresql.org/patch/5416/

Regards,
Vignesh

#10Bryan Green
dbryan.green@gmail.com
In reply to: Jakob Egger (#4)
1 attachment(s)
Re: Use Python "Limited API" in PL/Python

On 1/15/2025 5:28 AM, Jakob Egger wrote:

On 14.01.2025, at 16:51, Jakob Egger <jakob@eggerapps.at> wrote:

I've tried to create a patch with this change. I'm attaching it to this message so that cfbot picks it up. (I was unable to reproduce the issue locally)

Apologies, please disregard my last patch. It does not work.

It looks like meson just can't build libraries that link with Python that use the limited API.

My understanding of Meson is limited, so I am unsure how to best work around this limitation.

I've spent two days trying to figure it out; I'm at the end of my wits.

Best regards,
Jakob

Hi Jakob and Peter,

I've been working on enabling the Python Limited API on MSVC, which was
disabled in the recent commit due to build failures.

The issue is that Meson doesn't automatically link against python3.lib
when using the Limited API. Meson's Python dependency always returns the
version-specific library (python3XX.lib) regardless of whether
limited api is defined. This appears to be a known Meson limitation:
https://github.com/mesonbuild/meson/issues/13824

Initially, I thought that maybe the library wasn't being installed, but
that is not the case and the patch to our CI Windows MSVC image is not
needed.

I have a patch that works around this by manually constructing the
Python dependency on MSVC to use python3.lib instead of python3XX.lib,
using cc.find_library('python3') and building the dependency with the
appropriate include directory. It passes CI when I push. I'll see what
cfbot thinks.

Still testing, but wanted to share progress and get feedback on the
approach.

--
Bryan Green
EDB: https://www.enterprisedb.com

Attachments:

v1-0001-Enable-Python-Limited-API-for-PL-Python-on-MSVC.patchtext/plain; charset=UTF-8; name=v1-0001-Enable-Python-Limited-API-for-PL-Python-on-MSVC.patchDownload
From 52efe8af1377e8adbac3533537f15ad20f7af1cf Mon Sep 17 00:00:00 2001
From: Bryan Green <dbryan.green@gmail.com>
Date: Fri, 2 Jan 2026 10:48:54 -0600
Subject: [PATCH v1] Enable Python Limited API for PL/Python on MSVC

Previously, the Python Limited API was disabled on MSVC due to build
failures caused by Meson not knowing to link against python3.lib
instead of python3XX.lib when using the Limited API.

This commit works around the Meson limitation by explicitly finding
and linking against python3.lib on MSVC, and removes the preprocessor
guard that was disabling the Limited API on MSVC in plpython.h.

This requires python3.lib to be present in the Python installation,
which is included when Python is installed.

Discussion: https://www.postgresql.org/message-id/flat/ee410de1-1e0b-4770-b125-eeefd4726a24%40eisentraut.org
---
 meson.build                | 12 +++++++++++-
 src/pl/plpython/plpython.h |  4 ----
 2 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/meson.build b/meson.build
index d7c5193d4c..e4ecd78a7a 100644
--- a/meson.build
+++ b/meson.build
@@ -1329,7 +1329,17 @@ if not pyopt.disabled()
   pm = import('python')
   python3_inst = pm.find_installation(python.full_path(), required: pyopt)
   if python3_inst.found()
-    python3_dep = python3_inst.dependency(embed: true, required: pyopt)
+    # For Limited API on MSVC, link against python3.lib instead of python3XX.lib
+    if host_system == 'windows' and cc.get_id() == 'msvc'
+      python3_libdir = python3_inst.get_variable('prefix') / 'libs'
+      python3_lib = cc.find_library('python3', dirs: python3_libdir, required: pyopt)
+      python3_dep = declare_dependency(
+        include_directories: include_directories(python3_inst.get_variable('prefix') / 'include'),
+        dependencies: python3_lib,
+      )
+    else
+      python3_dep = python3_inst.dependency(embed: true, required: pyopt)
+    endif
     # Remove this check after we depend on Meson >= 1.1.0
     if not cc.check_header('Python.h', dependencies: python3_dep, required: pyopt, include_directories: postgres_inc)
       python3_dep = not_found_dep
diff --git a/src/pl/plpython/plpython.h b/src/pl/plpython/plpython.h
index 118b310084..445f7bc31a 100644
--- a/src/pl/plpython/plpython.h
+++ b/src/pl/plpython/plpython.h
@@ -25,12 +25,8 @@
 
 /*
  * Enable Python Limited API
- *
- * XXX currently not enabled on MSVC because of build failures
  */
-#if !defined(_MSC_VER)
 #define Py_LIMITED_API 0x03020000
-#endif
 
 /*
  * Pull in Python headers via a wrapper header, to control the scope of
-- 
2.52.0.windows.1

#11Bryan Green
dbryan.green@gmail.com
In reply to: Bryan Green (#10)
Re: Use Python "Limited API" in PL/Python

On 1/2/2026 11:40 AM, Bryan Green wrote:

On 1/15/2025 5:28 AM, Jakob Egger wrote:

On 14.01.2025, at 16:51, Jakob Egger <jakob@eggerapps.at> wrote:

I've tried to create a patch with this change. I'm attaching it to this message so that cfbot picks it up. (I was unable to reproduce the issue locally)

Apologies, please disregard my last patch. It does not work.

It looks like meson just can't build libraries that link with Python that use the limited API.

My understanding of Meson is limited, so I am unsure how to best work around this limitation.

I've spent two days trying to figure it out; I'm at the end of my wits.

Best regards,
Jakob

Hi Jakob and Peter,

I've been working on enabling the Python Limited API on MSVC, which was
disabled in the recent commit due to build failures.

The issue is that Meson doesn't automatically link against python3.lib
when using the Limited API. Meson's Python dependency always returns the
version-specific library (python3XX.lib) regardless of whether
limited api is defined. This appears to be a known Meson limitation:
https://github.com/mesonbuild/meson/issues/13824

Initially, I thought that maybe the library wasn't being installed, but
that is not the case and the patch to our CI Windows MSVC image is not
needed.

I have a patch that works around this by manually constructing the
Python dependency on MSVC to use python3.lib instead of python3XX.lib,
using cc.find_library('python3') and building the dependency with the
appropriate include directory. It passes CI when I push. I'll see what
cfbot thinks.

Still testing, but wanted to share progress and get feedback on the
approach.

cfbot seems to be happy:
https://cirrus-ci.com/github/postgresql-cfbot/postgresql/cf%2F6369
--
Bryan Green
EDB: https://www.enterprisedb.com

#12Peter Eisentraut
peter@eisentraut.org
In reply to: Bryan Green (#10)
Re: Use Python "Limited API" in PL/Python

On 02.01.26 18:40, Bryan Green wrote:

I have a patch that works around this by manually constructing the
Python dependency on MSVC to use python3.lib instead of python3XX.lib,
using cc.find_library('python3') and building the dependency with the
appropriate include directory. It passes CI when I push. I'll see what
cfbot thinks.

Still testing, but wanted to share progress and get feedback on the
approach.

This approach seems fine to me. I suppose one question would be whether
it would be worth using that same approach on all platforms rather than
just on windows/msvc. That way we only have one code path to maintain.
But I'm not sure how easy that would be to achieve.

#13Peter Eisentraut
peter@eisentraut.org
In reply to: Bryan Green (#11)
Re: Use Python "Limited API" in PL/Python

On 02.01.26 19:12, Bryan Green wrote:

I have a patch that works around this by manually constructing the
Python dependency on MSVC to use python3.lib instead of python3XX.lib,
using cc.find_library('python3') and building the dependency with the
appropriate include directory. It passes CI when I push. I'll see what
cfbot thinks.

Still testing, but wanted to share progress and get feedback on the
approach.

cfbot seems to be happy:
https://cirrus-ci.com/github/postgresql-cfbot/postgresql/cf%2F6369

Another thing you could test is whether this actually makes the ABI
stable. So for example build PL/Python against Python 3.13 and then
install Python 3.14 (and uninstall 3.13 just to be sure) and then start
a server and run some PL/Python stuff.