Skip to content

Commit 597652b

Browse files
committed
Comments & cleanups
1 parent 323695f commit 597652b

File tree

3 files changed

+112
-43
lines changed

3 files changed

+112
-43
lines changed

Lib/test/test_xxlimited.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ def test_xxo_demo(self, module):
7272
self.assertEqual(xxo.demo("abc"), "abc")
7373
self.assertEqual(xxo.demo(0), None)
7474
self.assertEqual(xxo.__module__, module.__name__)
75+
with self.assertRaises(TypeError):
76+
module.Xxo('arg')
77+
with self.assertRaises(TypeError):
78+
module.Xxo(kwarg='arg')
7579

7680
@test_with_xxlimited_modules(since=(3, 13))
7781
def test_xxo_demo_extra(self, module):
@@ -80,6 +84,16 @@ def test_xxo_demo_extra(self, module):
8084
self.assertEqual(xxo.demo(xxo), xxo)
8185
self.assertEqual(xxo.demo(other), other)
8286

87+
@test_with_xxlimited_modules(since=(3, 15))
88+
def test_xxo_subclass(self, module):
89+
class Sub(module.Xxo):
90+
pass
91+
sub = Sub()
92+
sub.a = 123
93+
self.assertEqual(sub.a, 123)
94+
with self.assertRaisesRegex(AttributeError, "cannot set 'reserved'"):
95+
sub.reserved = 123
96+
8397
@test_with_xxlimited_modules(since=(3, 13))
8498
def test_error(self, module):
8599
with self.assertRaises(module.Error):

Modules/xxlimited.c

Lines changed: 97 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@
1111
other files, you'll have to create a file "foobarobject.h"; see
1212
floatobject.h for an example.
1313
14-
This module roughly corresponds to::
14+
This module uses Limited API 3.15.
15+
See ``xxlimited_3_13.c`` if you want to support older CPython versions.
16+
17+
This module roughly corresponds to the following.
18+
(All underscore-prefixed attributes are not accessible from Python.)
19+
20+
::
1521
1622
class Xxo:
1723
"""A class that explicitly stores attributes in an internal dict
@@ -27,6 +33,8 @@
2733
return self._x_attr[name]
2834
2935
def __setattr__(self, name, value):
36+
if name == "reserved":
37+
raise AttributeError("cannot set 'reserved'")
3038
self._x_attr[name] = value
3139
3240
def __delattr__(self, name):
@@ -68,8 +76,10 @@
6876
#define Py_LIMITED_API 0x030f0000
6977

7078
// experimental: free-threaded build compatibility
79+
// (for internal tests; this should only appear here in CPython alpha builds)
7180
#define _Py_OPAQUE_PYOBJECT 0x030f0000
7281

82+
7383
#include "Python.h"
7484
#include <string.h>
7585

@@ -82,11 +92,49 @@ typedef struct {
8292
} xx_state;
8393

8494

85-
/* Xxo objects */
95+
/* Xxo objects.
96+
*
97+
* A non-trivial extension type, intentionally showing a number of features
98+
* that aren't easy to implement in the Limited API.
99+
*/
86100

101+
// Forward declaration
87102
static PyType_Spec Xxo_Type_spec;
88103

89-
// Instance state
104+
// Get the module state (xx_state*) from a given type object 'type', which
105+
// must be a subclass of Xxo (the type we're defining).
106+
// This is complicated by the fact that the Xxo type is dynamically allocated,
107+
// and there may be several such types in a given Python process -- for
108+
// example, in different subinterpreters, or through loading this
109+
// extension module several times.
110+
// So, we don't have a "global" pointer to the type, or to the module, etc.;
111+
// instead we search based on `Xxo_Type_spec` (which is static, immutable,
112+
// and process-global).
113+
//
114+
// When possible, it's better to avoid `PyType_GetBaseByToken` -- for an
115+
// example, see the `demo` method (Xxo_demo C function), which uses a
116+
// "defining class". But, in many cases it's the best solution.
117+
static xx_state *
118+
Xxo_state_from_type(PyTypeObject *type)
119+
{
120+
PyTypeObject *base;
121+
// Search all superclasses of 'type' for one that was defined using
122+
// "Xxo_Type_spec". That must be our 'Xxo' class.
123+
if (PyType_GetBaseByToken(type, &Xxo_Type_spec, &base) < 0) {
124+
return NULL;
125+
}
126+
if (base == NULL) {
127+
PyErr_SetString(PyExc_TypeError, "need Xxo subclass");
128+
return NULL;
129+
}
130+
// From this type, get the associated module. That must be the
131+
// relevant `xxlimited` module.
132+
xx_state *state = PyType_GetModuleState(base);
133+
Py_DECREF(base);
134+
return state;
135+
}
136+
137+
// Structure for data needed by the XxoObject type.
90138
typedef struct {
91139
PyObject *x_attr; /* Attributes dictionary.
92140
* May be NULL, which acts as an
@@ -96,23 +144,41 @@ typedef struct {
96144
Py_ssize_t x_exports; /* how many buffer are exported */
97145
} XxoObject_Data;
98146

99-
#define XxoObject_CAST(self) ((XxoObject *)(self))
100-
// TODO: full support for type-checking was added in 3.14 (Py_tp_token)
101-
// #define XxoObject_Check(v) Py_IS_TYPE(v, Xxo_Type)
147+
// Get the `XxoObject_Data` structure for a given instance of our type.
148+
static XxoObject_Data *
149+
Xxo_get_data(PyObject *self)
150+
{
151+
xx_state *state = Xxo_state_from_type(Py_TYPE(self));
152+
if (!state) {
153+
return NULL;
154+
}
155+
XxoObject_Data *data = PyObject_GetTypeData(self, state->Xxo_Type);
156+
return data;
157+
}
102158

159+
// Xxo initialization
160+
// This is the implementation of Xxo.__new__; it takes arbitrary positional
161+
// and keyword arguments.
103162
static PyObject *
104-
newXxoObject(PyObject *module)
105-
{
106-
xx_state *state = PyModule_GetState(module);
107-
if (state == NULL) {
163+
Xxo_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) {
164+
// Validate that we did not get any arguments.
165+
if ((args != NULL && PyObject_Length(args))
166+
|| (kwargs != NULL && PyObject_Length(kwargs)))
167+
{
168+
PyErr_SetString(PyExc_TypeError, "Xxo.__new__() takes no arguments");
108169
return NULL;
109170
}
110-
allocfunc alloc = PyType_GetSlot(state->Xxo_Type, Py_tp_alloc);
111-
PyObject *self = alloc(state->Xxo_Type, 0);
171+
// Create an instance of *type* (which may be a subclass)
172+
allocfunc alloc = PyType_GetSlot(type, Py_tp_alloc);
173+
PyObject *self = alloc(type, 0);
112174
if (self == NULL) {
113175
return NULL;
114176
}
115-
XxoObject_Data *xxo_data = PyObject_GetTypeData(self, state->Xxo_Type);
177+
178+
// Initialize the C members on the instance.
179+
// This is only included for the sake of example. The default alloc
180+
// function zeroes instance memory; we don't need to do it again.
181+
XxoObject_Data *xxo_data = Xxo_get_data(self);
116182
if (xxo_data == NULL) {
117183
return NULL;
118184
}
@@ -122,33 +188,6 @@ newXxoObject(PyObject *module)
122188
return self;
123189
}
124190

125-
static xx_state *
126-
Xxo_state_from_type(PyTypeObject *type, void *token)
127-
{
128-
PyTypeObject *base;
129-
if (PyType_GetBaseByToken(type, &Xxo_Type_spec, &base) < 0) {
130-
return NULL;
131-
}
132-
if (base == NULL) {
133-
return NULL;
134-
}
135-
xx_state *state = PyType_GetModuleState(base);
136-
return state;
137-
}
138-
139-
#include <stdio.h>
140-
141-
static XxoObject_Data *
142-
Xxo_get_data(PyObject *self)
143-
{
144-
xx_state *state = Xxo_state_from_type(Py_TYPE(self), &Xxo_Type_spec);
145-
if (!state) {
146-
return NULL;
147-
}
148-
XxoObject_Data *data = PyObject_GetTypeData(self, state->Xxo_Type);
149-
return data;
150-
}
151-
152191
/* Xxo finalization.
153192
*
154193
* Types that store references to other PyObjects generally need to implement
@@ -236,6 +275,16 @@ Xxo_getattro(PyObject *self, PyObject *name)
236275
static int
237276
Xxo_setattro(PyObject *self, PyObject *name, PyObject *v)
238277
{
278+
// filter a specific attribute name
279+
int is_reserved = PyUnicode_EqualToUTF8(name, "reserved");
280+
if (is_reserved < 0) {
281+
return -1;
282+
}
283+
else if (is_reserved) {
284+
PyErr_Format(PyExc_AttributeError, "cannot set %R", name);
285+
return -1;
286+
}
287+
239288
XxoObject_Data *data = Xxo_get_data(self);
240289
if (data == NULL) {
241290
return -1;
@@ -355,6 +404,7 @@ static PyGetSetDef Xxo_getsetlist[] = {
355404

356405
static PyType_Slot Xxo_Type_slots[] = {
357406
{Py_tp_doc, (char *)Xxo_doc},
407+
{Py_tp_new, Xxo_new},
358408
{Py_tp_traverse, Xxo_traverse},
359409
{Py_tp_clear, Xxo_clear},
360410
{Py_tp_finalize, Xxo_finalize},
@@ -372,7 +422,7 @@ static PyType_Slot Xxo_Type_slots[] = {
372422
static PyType_Spec Xxo_Type_spec = {
373423
.name = "xxlimited.Xxo",
374424
.basicsize = -(Py_ssize_t)sizeof(XxoObject_Data),
375-
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC,
425+
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_BASETYPE,
376426
.slots = Xxo_Type_slots,
377427
};
378428

@@ -419,7 +469,9 @@ xx_foo(PyObject *module, PyObject *args)
419469
static PyObject *
420470
xx_new(PyObject *module, PyObject *Py_UNUSED(unused))
421471
{
422-
return newXxoObject(module);
472+
xx_state *state = PyModule_GetState(module);
473+
474+
return Xxo_new(state->Xxo_Type, NULL, NULL);
423475
}
424476

425477

@@ -538,6 +590,9 @@ static PyModuleDef_Slot xx_slots[] = {
538590
* Without this slot, free-threaded builds of CPython will enable
539591
* the GIL when this module is loaded.
540592
*/
593+
/* TODO: This is not quite true yet: there is a race in Xxo_setattro
594+
* for example.
595+
*/
541596
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
542597

543598

Modules/xxlimited_35.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ xx_modexec(PyObject *m)
305305
static PyModuleDef_Slot xx_slots[] = {
306306
{Py_mod_exec, xx_modexec},
307307
#ifdef Py_GIL_DISABLED
308-
// These definitions are in the limited API, but not until 3.13.
308+
// In a free-threaded build, we don't use Limited API.
309309
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
310310
#endif
311311
{0, NULL}

0 commit comments

Comments
 (0)