Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-86542: New C-APIs to simplify module attribute declaration #23286

Closed
wants to merge 13 commits into from
145 changes: 145 additions & 0 deletions Doc/c-api/module.rst
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,151 @@ state:

.. versionadded:: 3.9

.. c:function:: PyTypeObject * PyModule_AddNewTypeFromSpec(PyObject *module, PyType_Spec *spec, PyObject *base)

Initialize a new type and add it to *module*.
The function is equivalent to :c:func:`PyType_FromModuleAndSpec` followed
by :c:func:`PyModule_AddType`. *base* must be either ``NULL``, a single
type object, or a tuple of types.
Return ``NULL`` on error; otherwise a ``PyTypeObject *``, which can
be assigned to a module state object.

.. versionadded:: 3.10

.. c:function:: PyObject * PyModule_AddNewException(PyObject *module, const char *name, const char *doc, PyObject *base, PyObject *dict)

Create a new exception and add it to *module*.
The function is equivalent to :c:func:`PyErr_NewExceptionWithDoc` followed
by :c:func:`PyModule_AddObjectRef`. The name of the exception object is
taken from the last component of *name* after dot.
Return ``NULL`` on error; otherwise ``PyObject *``, which can be assigned
to a module state object.

.. versionadded:: 3.10

.. c:function:: int PyModule_AddConstants(PyObject *module, PyModuleConst_Def *def)

Initialize module constants from a PyModuleConst_Def array. The function
provides a convenient way to declare module-level constants.
Return ``-1`` on error, ``0`` on success.

Example::

static PyObject*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: static PyObject * in here.

example_call(PyObject *module)
{
return PyBytes_FromString("23");
}

#define EXAMPLE_INT 23
#define EXAMPLE_STRING "world"

static PyModuleConst_Def example_constants[] = {
PyModuleConst_None("none_value"),
PyModuleConst_Long("integer", 42),
PyModuleConst_ULong("unsigned", 42UL),
PyModuleConst_Bool("false_value", 0),
PyModuleConst_Bool("true_value", 1),
#ifdef Py_MATH_PI
PyModuleConst_Double("pi", Py_MATH_PI),
#endif
PyModuleConst_String("somestring", "Hello"),
PyModuleConst_Call("call", example_call),
PyModuleConst_LongMacro(EXAMPLE_INT),
PyModuleConst_StringMacro(EXAMPLE_STRING),
{NULL},
}

static int
example_init_constants(PyObject *module)
{
return PyModule_AddConstants(module, example_constants);
}

static PyModuleDef_Slot example_slots[] = {
{Py_mod_exec, example_init_constants},
{0, NULL}
};


.. c:type:: PyModuleConst_Def

The values for *type* and the definition of the *value* union are
internal implementation details. Use any of the ``PyModuleConst_`` macros
to define entries. The array must be terminated by an entry with name
set to ``NULL``.

.. c:member:: const char *name

Attribute name.

.. c:member:: int type

Attribute type.

.. c:member:: void *value

Value of the module constant definition, whose meaning depends on
*type*.
Comment on lines +643 to +659
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The values for *type* and the definition of the *value* union are
internal implementation details. Use any of the ``PyModuleConst_`` macros
to define entries. The array must be terminated by an entry with name
set to ``NULL``.
.. c:member:: const char *name
Attribute name.
.. c:member:: int type
Attribute type.
.. c:member:: void *value
Value of the module constant definition, whose meaning depends on
*type*.
Definition of a module constant.
The members of this struct are internal implementation details.
To define entries, use only the ``PyModuleConst_`` macros below,
and ``{NULL}`` to mark the end of the array.


.. versionadded:: 3.10

.. c:macro:: PyModuleConst_None(name)

Add an entry for None.

.. versionadded:: 3.10

.. c:macro:: PyModuleConst_Long(name, value)

Add an entry for an integer constant.

.. versionadded:: 3.10

.. c:macro:: PyModuleConst_ULong(name, value)

Add an entry for an unsigned integer constant.

.. versionadded:: 3.10

.. c:macro:: PyModuleConst_Bool(name, value)

Add an entry for a bool constant. ``0`` is false, ``1`` is true.

.. versionadded:: 3.10

.. c:macro:: PyModuleConst_Double(name, value)

Add an entry for a float constant.

.. versionadded:: 3.10

.. c:macro:: PyModuleConst_String(name, value)

Add an entry for a string constant.

.. versionadded:: 3.10

.. c:macro:: PyModuleConst_Call(name, func)

Add an entry for a constant as returned by callback with signature
``PyObject* (*func)(PyObject *module)``.

.. versionadded:: 3.10

.. c:macro:: PyModuleConst_LongMacro(macro)

Add an entry for an int constant. The name and the value are taken from
*macro*.

.. versionadded:: 3.10

.. c:macro:: PyModuleConst_StringMacro(macro)

Add an entry for a string constant. The name and the value are taken from
*macro*.

.. versionadded:: 3.10

Module lookup
^^^^^^^^^^^^^
Expand Down
16 changes: 16 additions & 0 deletions Doc/data/refcounts.dat
Original file line number Diff line number Diff line change
Expand Up @@ -1325,6 +1325,10 @@ PyMethod_New:PyObject*:class:0:
PyMethod_Self:PyObject*::0:
PyMethod_Self:PyObject*:im:0:

PyModule_AddConstants:int:::
PyModule_AddConstants:PyObject*:module:0:
PyModule_AddConstants:PyModuleConst_Def*:def::

PyModule_AddFunctions:int:::
PyModule_AddFunctions:PyObject*:module:0:
PyModule_AddFunctions:PyMethodDef*:functions::
Expand All @@ -1343,6 +1347,18 @@ PyModule_AddObject:PyObject*:module:0:
PyModule_AddObject:const char*:name::
PyModule_AddObject:PyObject*:value:+1:

PyModule_AddNewException:PyObject*::+1:
PyModule_AddNewException:PyObject*:module:0:
PyModule_AddNewException:const char*:name::
PyModule_AddNewException:const char*:doc::
PyModule_AddNewException:PyObject*:base:0:
PyModule_AddNewException:PyObject*:dict:0:

PyModule_AddNewTypeFromSpec:PyObject*::+1:
PyModule_AddNewTypeFromSpec:PyObject*:module:0:
PyModule_AddNewTypeFromSpec:PyType_spec*:spec::
PyModule_AddNewTypeFromSpec:PyObject*:base:0:

PyModule_AddStringConstant:int:::
PyModule_AddStringConstant:PyObject*:module:0:
PyModule_AddStringConstant:const char*:name::
Expand Down
9 changes: 9 additions & 0 deletions Include/modsupport.h
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@ PyAPI_FUNC(int) PyModule_AddType(PyObject *module, PyTypeObject *type);
#define PyModule_AddIntMacro(m, c) PyModule_AddIntConstant(m, #c, c)
#define PyModule_AddStringMacro(m, c) PyModule_AddStringConstant(m, #c, c)

#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03100000
/* New in 3.9 */
PyAPI_FUNC(PyTypeObject *) PyModule_AddNewTypeFromSpec(
PyObject *module, PyType_Spec *spec, PyObject *base);
PyAPI_FUNC(PyObject *) PyModule_AddNewException(
PyObject *module, const char *name, const char *doc,
PyObject *base, PyObject *dict);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you start by adding these two functions in a separated PR, to make this PR shorter?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really think that's necessary; the PR is not that big.

#endif

#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03050000
/* New in 3.5 */
PyAPI_FUNC(int) PyModule_SetDocString(PyObject *, const char *);
Expand Down
47 changes: 47 additions & 0 deletions Include/moduleobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,53 @@ typedef struct PyModuleDef_Slot{

#endif /* New in 3.5 */

struct PyModuleConst_Def;
#if !defined(Py_LIMITED_API) || Py_LIMITED_API+0 >= 0x03100000
/* New in 3.10 */
enum _PyModuleConst_type {
_PyModuleConst_none_type = 1,
_PyModuleConst_long_type = 2,
_PyModuleConst_ulong_type = 3,
_PyModuleConst_bool_type = 4,
_PyModuleConst_double_type = 5,
_PyModuleConst_string_type = 6,
_PyModuleConst_call_type = 7,
};

typedef struct PyModuleConst_Def {
const char *name;
enum _PyModuleConst_type type;
union {
const char *m_str;
long m_long;
unsigned long m_ulong;
double m_double;
PyObject* (*m_call)(PyObject *module);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how to make this member future-proof in term of stable ABI.

We should try to put a max_align_t inside, but this type requires C11. GCC defines it with:

typedef struct {
  long long __max_align_ll __attribute__((__aligned__(__alignof__(long long))));
  long double __max_align_ld __attribute__((__aligned__(__alignof__(long double))));
  /* _Float128 is defined as a basic type, so max_align_t must be
     sufficiently aligned for it.  This code must work in C++, so we
     use __float128 here; that is only available on some
     architectures, but only on i386 is extra alignment needed for
     __float128.  */
#ifdef __i386__
  __float128 __max_align_f128 __attribute__((__aligned__(__alignof(__float128))));
#endif
} max_align_t;

Maybe we can at least put long long and long double:

// Unused members added to make PyModuleConst_Def large enough
// to get a stable ABI and support future additions.
long long m_long_long;
long double m_long_double;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only issue I found here :)
The stable ABI doesn't have a good story around evolving structs, and I think we should design a general mechanism for that rather than try to future-proof individual structs.
To move this PR forward, could we exclude PyModule_AddConstants & co. from the limited API for the time being?

} value;
} PyModuleConst_Def;

PyAPI_FUNC(int) PyModule_AddConstants(PyObject *, PyModuleConst_Def *);

#define PyModuleConst_None(name) \
{(name), _PyModuleConst_none_type, {.m_long=0}}
#define PyModuleConst_Long(name, value) \
{(name), _PyModuleConst_long_type, {.m_long=(value)}}
#define PyModuleConst_ULong(name, value) \
{(name), _PyModuleConst_ulong_type, {.m_ulong=(value)}}
#define PyModuleConst_Bool(name, value) \
{(name), _PyModuleConst_bool_type, {.m_long=(value)}}
#define PyModuleConst_Double(name, value) \
{(name), _PyModuleConst_double_type, {.m_double=(value)}}
#define PyModuleConst_String(name, value) \
{(name), _PyModuleConst_string_type, {.m_str=(value)}}
#define PyModuleConst_Call(name, value) \
{(name), _PyModuleConst_call_type, {.m_call=(value)}}

#define PyModuleConst_LongMacro(m) PyModuleConst_Long(#m, m)
#define PyModuleConst_StringMacro(m) PyModuleConst_String(#m, m)

#endif /* New in 3.10 */

typedef struct PyModuleDef{
PyModuleDef_Base m_base;
const char* m_name;
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_capi.py
Original file line number Diff line number Diff line change
Expand Up @@ -971,5 +971,19 @@ def test_state_access(self):
increment_count(1, 2, 3)


class Test_PyModuleConst_Def(unittest.TestCase):
def test_constants(self):
self.assertIs(_testcapi.const_none, None)
self.assertEqual(_testcapi.const_int, 42)
self.assertEqual(_testcapi.const_uint, _testcapi.ULONG_MAX)
self.assertIs(_testcapi.const_true, True)
self.assertIs(_testcapi.const_false, False)
self.assertEqual(_testcapi.const_almost_tau, 6.2831)
self.assertEqual(_testcapi.const_str, "Hello")
self.assertEqual(_testcapi.const_call, b"23")
self.assertEqual(_testcapi.CONST_INT, 7)
self.assertEqual(_testcapi.CONST_STRING, "world")


if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add new functions :c:func:`PyModule_AddConstants`,
:c:func:`PyModule_AddNewTypeFromSpec`, :c:func:`PyModule_AddNewException` to
simplify the declaration of attribute in modules.
37 changes: 10 additions & 27 deletions Modules/_hashopenssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -2025,14 +2025,9 @@ hashlib_init_evptype(PyObject *module)
{
_hashlibstate *state = get_hashlib_state(module);

state->EVPtype = (PyTypeObject *)PyType_FromSpec(&EVPtype_spec);
if (state->EVPtype == NULL) {
return -1;
}
if (PyModule_AddType(module, state->EVPtype) < 0) {
return -1;
}
return 0;
state->EVPtype = PyModule_AddNewTypeFromSpec(
module, &EVPtype_spec, NULL);
return state->EVPtype == NULL ? -1 : 0;
}

static int
Expand All @@ -2044,33 +2039,21 @@ hashlib_init_evpxoftype(PyObject *module)
if (state->EVPtype == NULL) {
return -1;
}

state->EVPXOFtype = (PyTypeObject *)PyType_FromSpecWithBases(
&EVPXOFtype_spec, (PyObject *)state->EVPtype
);
if (state->EVPXOFtype == NULL) {
return -1;
}
if (PyModule_AddType(module, state->EVPXOFtype) < 0) {
return -1;
}
state->EVPXOFtype = PyModule_AddNewTypeFromSpec(
module, &EVPXOFtype_spec, (PyObject *)state->EVPtype);
return state->EVPXOFtype == NULL ? -1 : 0;
#endif
return 0;
}

static int
hashlib_init_hmactype(PyObject *module)
{
_hashlibstate *state = get_hashlib_state(module);
_hashlibstate *state = get_hashlib_state(module);

state->HMACtype = (PyTypeObject *)PyType_FromSpec(&HMACtype_spec);
if (state->HMACtype == NULL) {
return -1;
}
if (PyModule_AddType(module, state->HMACtype) < 0) {
return -1;
}
return 0;
state->HMACtype = PyModule_AddNewTypeFromSpec(
module, &HMACtype_spec, NULL);
return state->HMACtype == NULL ? -1 : 0;
}

static int
Expand Down
17 changes: 4 additions & 13 deletions Modules/_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -5459,39 +5459,30 @@ static PyMethodDef PySSL_methods[] = {
static int
sslmodule_init_types(PyObject *module)
{
PySSLContext_Type = (PyTypeObject *)PyType_FromModuleAndSpec(
PySSLContext_Type = PyModule_AddNewTypeFromSpec(
module, &PySSLContext_spec, NULL
);
if (PySSLContext_Type == NULL)
return -1;

PySSLSocket_Type = (PyTypeObject *)PyType_FromModuleAndSpec(
PySSLSocket_Type = PyModule_AddNewTypeFromSpec(
module, &PySSLSocket_spec, NULL
);
if (PySSLSocket_Type == NULL)
return -1;

PySSLMemoryBIO_Type = (PyTypeObject *)PyType_FromModuleAndSpec(
PySSLMemoryBIO_Type = PyModule_AddNewTypeFromSpec(
module, &PySSLMemoryBIO_spec, NULL
);
if (PySSLMemoryBIO_Type == NULL)
return -1;

PySSLSession_Type = (PyTypeObject *)PyType_FromModuleAndSpec(
PySSLSession_Type = PyModule_AddNewTypeFromSpec(
module, &PySSLSession_spec, NULL
);
if (PySSLSession_Type == NULL)
return -1;

if (PyModule_AddType(module, PySSLContext_Type))
return -1;
if (PyModule_AddType(module, PySSLSocket_Type))
return -1;
if (PyModule_AddType(module, PySSLMemoryBIO_Type))
return -1;
if (PyModule_AddType(module, PySSLSession_Type))
return -1;

return 0;
}

Expand Down
Loading