1 /**
2    A D API for dealing with Python's PyTypeObject
3  */
4 module python.type;
5 
6 
7 import python.raw: PyObject;
8 import std.traits: isArray, isIntegral, isBoolean, isFloatingPoint, isAggregateType;
9 import std.datetime: DateTime, Date;
10 
11 
12 /**
13    A wrapper for `PyTypeObject`.
14 
15    This struct does all of the necessary boilerplate to intialise
16    a `PyTypeObject` for a Python extension type that mimics the D
17    type `T`.
18  */
19 struct PythonType(T) {
20     import python.raw: PyTypeObject;
21     import std.traits: FieldNameTuple, Fields;
22 
23     alias fieldNames = FieldNameTuple!T;
24     alias fieldTypes = Fields!T;
25 
26     static PyTypeObject _pyType;
27     static bool failedToReady;
28 
29     static PyObject* pyObject() {
30         initialise;
31         return failedToReady ? null : cast(PyObject*) &_pyType;
32     }
33 
34     static PyTypeObject* pyType() {
35         initialise;
36         return failedToReady ? null : &_pyType;
37     }
38 
39     private static void initialise() {
40         import python.raw: PyType_GenericNew, PyType_Ready, TypeFlags,
41             PyErr_SetString, PyExc_TypeError;
42 
43         if(_pyType != _pyType.init) return;
44 
45         _pyType.tp_name = T.stringof;
46         _pyType.tp_basicsize = PythonClass!T.sizeof;
47         _pyType.tp_flags = TypeFlags.Default;
48         _pyType.tp_new = &PyType_GenericNew;
49         _pyType.tp_getset = getsetDefs;
50         _pyType.tp_methods = methodDefs;
51         _pyType.tp_repr = &repr;
52         _pyType.tp_init = &init;
53         _pyType.tp_new = &new_;
54 
55         if(PyType_Ready(&_pyType) < 0) {
56             PyErr_SetString(PyExc_TypeError, &"not ready"[0]);
57             failedToReady = true;
58         }
59     }
60 
61     private static auto getsetDefs() {
62         import python.raw: PyGetSetDef;
63 
64         // +1 due to the sentinel
65         static PyGetSetDef[fieldNames.length + 1] getsets;
66 
67         if(getsets != getsets.init) return &getsets[0];
68 
69         static foreach(i; 0 .. fieldNames.length) {
70             getsets[i].name = cast(typeof(PyGetSetDef.name))fieldNames[i];
71             getsets[i].get = &PythonClass!T.get!i;
72             getsets[i].set = &PythonClass!T.set!i;
73         }
74 
75         return &getsets[0];
76     }
77 
78     private static auto methodDefs()() {
79         import python.raw: PyMethodDef;
80         import python.cooked: pyMethodDef;
81         import std.meta: AliasSeq, Alias, staticMap, Filter;
82         import std.traits: isSomeFunction;
83 
84         alias memberNames = AliasSeq!(__traits(allMembers, T));
85         enum ispublic(string name) = isPublic!(T, name);
86         alias publicMemberNames = Filter!(ispublic, memberNames);
87         alias Member(string name) = Alias!(__traits(getMember, T, name));
88         alias members = staticMap!(Member, publicMemberNames);
89         alias memberFunctions = Filter!(isSomeFunction, members);
90 
91         // +1 due to sentinel
92         static PyMethodDef[memberFunctions.length + 1] methods;
93 
94         if(methods != methods.init) return &methods[0];
95 
96         static foreach(i, memberFunction; memberFunctions) {
97             methods[i] = pyMethodDef!(__traits(identifier, memberFunction))
98                                      (&PythonMethod!(T, memberFunction).impl);
99         }
100 
101         return &methods[0];
102     }
103 
104     private static extern(C) PyObject* repr(PyObject* self_) {
105         import python: pyUnicodeDecodeUTF8;
106         import python.conv: to;
107         import std.conv: text;
108 
109         assert(self_ !is null);
110         auto ret = text(self_.to!T);
111         return pyUnicodeDecodeUTF8(ret.ptr, ret.length, null /*errors*/);
112     }
113 
114     private static extern(C) int init(PyObject* self_, PyObject* args, PyObject* kwargs) {
115         // nothing to do
116         return 0;
117     }
118 
119     private static extern(C) PyObject* new_(PyTypeObject *type, PyObject* args, PyObject* kwargs) {
120         import python.conv: toPython, to;
121         import python.raw: PyTuple_Size, PyTuple_GetItem;
122         import std.traits: hasMember;
123         import std.meta: AliasSeq;
124 
125         const numArgs = PyTuple_Size(args);
126 
127         if(numArgs == 0)
128             return toPython(T());
129 
130         // TODO: parameters
131         static if(hasMember!(T, "__ctor"))
132             alias constructors = AliasSeq!(__traits(getOverloads, T, "__ctor"));
133         else
134             alias constructors = AliasSeq!();
135 
136         static if(constructors.length == 0) {
137 
138             import std.typecons: Tuple;
139 
140             Tuple!fieldTypes dArgs;
141 
142             static foreach(i; 0 .. fieldTypes.length) {
143                 dArgs[i] = PyTuple_GetItem(args, i).to!(fieldTypes[i]);
144             }
145 
146             return toPython(T(dArgs.expand));
147 
148         } else {
149             import python.raw: PyErr_SetString, PyExc_TypeError;
150             import std.traits: Parameters;
151             import std.typecons: Tuple;
152 
153             static foreach(constructor; constructors) {
154                 if(Parameters!constructor.length == numArgs) {
155 
156                     Tuple!(Parameters!constructor) dArgs;
157 
158                     static foreach(i; 0 .. Parameters!constructor.length) {
159                         dArgs[i] = PyTuple_GetItem(args, i).to!(Parameters!constructor[i]);
160                     }
161 
162                     return toPython(T(dArgs.expand));
163                 }
164             }
165 
166             PyErr_SetString(PyExc_TypeError, "Could not find a suitable constructor");
167             return null;
168         }
169 
170 
171     }
172 }
173 
174 private alias Type(alias A) = typeof(A);
175 
176 
177 /**
178    The C API implementation of a Python method F of aggregate type T
179  */
180 struct PythonMethod(T, alias F) {
181     static extern(C) PyObject* impl(PyObject* self_, PyObject* args, PyObject* kwargs) {
182         import python.raw: PyTuple_Size, PyTuple_GetItem, pyIncRef, pyNone, pyDecRef;
183         import python.conv: toPython, to;
184         import std.traits: Parameters, ReturnType, FunctionAttribute, functionAttributes, Unqual;
185         import std.typecons: Tuple;
186         import std.meta: staticMap;
187 
188         assert(PyTuple_Size(args) == Parameters!F.length);
189 
190         Tuple!(staticMap!(Unqual, Parameters!F)) dArgs;
191 
192         static foreach(i; 0 .. Parameters!F.length) {
193             dArgs[i] = PyTuple_GetItem(args, i).to!(Parameters!F[i]);
194         }
195 
196         assert(self_ !is null);
197         auto dAggregate = self_.to!(Unqual!T);
198 
199         static if(is(ReturnType!F == void))
200             enum dret = "";
201         else
202             enum dret = "auto dRet = ";
203 
204         // e.g. `auto dRet = dAggregate.myMethod(dArgs[0], dArgs[1]);`
205         mixin(dret, `dAggregate.`, __traits(identifier, F), `(dArgs.expand);`);
206 
207         // The member function could have side-effects, we need to copy the changes
208         // back to the Python object.
209         static if(!(functionAttributes!F & FunctionAttribute.const_)) {
210             auto newSelf = toPython(dAggregate);
211             scope(exit) {
212                 pyDecRef(newSelf);
213             }
214             auto pyClassSelf = cast(PythonClass!T*) self_;
215             auto pyClassNewSelf = cast(PythonClass!T*) newSelf;
216 
217             static foreach(i; 0 .. PythonClass!T.fieldNames.length) {
218                 pyClassSelf.set!i(self_, pyClassNewSelf.get!i(newSelf));
219             }
220         }
221 
222         static if(!is(ReturnType!F == void))
223             return dRet.toPython;
224         else {
225             pyIncRef(pyNone);
226             return pyNone;
227         }
228     }
229 }
230 
231 
232 /**
233    Creates an instance of a Python class that is equivalent to the D type `T`.
234    Return PyObject*.
235  */
236 PyObject* pythonClass(T)(auto ref T dobj) {
237 
238     import python.conv: toPython;
239     import python.raw: pyObjectNew;
240     import std.traits: FieldNameTuple;
241 
242     auto ret = pyObjectNew!(PythonClass!T)(PythonType!T.pyType);
243 
244     static foreach(fieldName; FieldNameTuple!T) {
245         static if(isPublic!(T, fieldName))
246             mixin(`ret.`, fieldName, ` = dobj.`, fieldName, `.toPython;`);
247     }
248 
249     return cast(PyObject*) ret;
250 }
251 
252 
253 private template isPublic(T, string memberName) {
254     enum protection = __traits(getProtection, __traits(getMember, T, memberName));
255     enum isPublic = protection == "public" || protection == "export";
256 }
257 
258 
259 private enum isDateOrDateTime(T) = is(Unqual!T == DateTime) || is(Unqual!T == Date);
260 
261 
262 /**
263    A Python class that mirrors the D type `T`.
264    For instance, this struct:
265    ----------
266    struct Foo {
267        int i;
268        string s;
269    }
270    ----------
271 
272    Will generate a Python class called `Foo` with two members, and trying to
273    assign anything but an integer to `Foo.i` or a string to `Foo.s` in Python
274    will raise `TypeError`.
275  */
276 struct PythonClass(T) if(isAggregateType!T && !isDateOrDateTime!T) {
277     import python.raw: PyObjectHead, PyGetSetDef;
278     import std.traits: FieldNameTuple, Fields;
279 
280     alias fieldNames = FieldNameTuple!T;
281     alias fieldTypes = Fields!T;
282 
283     // +1 for the sentinel
284     static PyGetSetDef[fieldNames.length + 1] getsets;
285 
286     /// Field members
287     // Every python object must have this
288     mixin PyObjectHead;
289     // Generate a python object field for every field in T
290     static foreach(fieldName; fieldNames) {
291         mixin(`PyObject* `, fieldName, `;`);
292     }
293 
294     // The function pointer for PyGetSetDef.get
295     private static extern(C) PyObject* get(int FieldIndex)(PyObject* self_, void* closure = null) {
296         import python.raw: pyIncRef;
297 
298         assert(self_ !is null);
299         auto self = cast(PythonClass*) self_;
300 
301         auto field = self.getField!FieldIndex;
302         assert(field !is null, "Cannot increase reference count on null field");
303         pyIncRef(field);
304 
305         return field;
306     }
307 
308     // The function pointer for PyGetSetDef.set
309     static extern(C) int set(int FieldIndex)(PyObject* self_, PyObject* value, void* closure = null) {
310         import python.raw: pyIncRef, pyDecRef, PyErr_SetString, PyExc_TypeError;
311 
312         if(value is null) {
313             enum deleteErrStr = "Cannot delete " ~ fieldNames[FieldIndex];
314             PyErr_SetString(PyExc_TypeError, deleteErrStr);
315             return -1;
316         }
317 
318         // FIXME
319         // if(!checkPythonType!(fieldTypes[FieldIndex])(value)) {
320         //     return -1;
321         // }
322 
323         assert(self_ !is null);
324         auto self = cast(PythonClass!T*) self_;
325         auto tmp = self.getField!FieldIndex;
326 
327         pyIncRef(value);
328         self.setField!FieldIndex(value);
329         pyDecRef(tmp);
330 
331         return 0;
332     }
333 
334     PyObject* getField(int FieldIndex)() {
335         mixin(`return this.`, fieldNames[FieldIndex], `;`);
336     }
337 
338     private void setField(int FieldIndex)(PyObject* value) {
339         mixin(`this.`, fieldNames[FieldIndex], ` = value;`);
340     }
341 }
342 
343 
344 private bool checkPythonType(T)(PyObject* value) if(isArray!T) {
345     import python.raw: pyListCheck;
346     const ret = pyListCheck(value);
347     if(!ret) setPyErrTypeString!"list";
348     return ret;
349 }
350 
351 
352 private bool checkPythonType(T)(PyObject* value) if(isIntegral!T) {
353     import python.raw: pyIntCheck, pyLongCheck;
354     const ret = pyLongCheck(value) || pyIntCheck(value);
355     if(!ret) setPyErrTypeString!"long";
356     return ret;
357 }
358 
359 
360 private bool checkPythonType(T)(PyObject* value) if(isFloatingPoint!T) {
361     import python.raw: pyFloatCheck;
362     const ret = pyFloatCheck(value);
363     if(!ret) setPyErrTypeString!"float";
364     return ret;
365 }
366 
367 
368 private bool checkPythonType(T)(PyObject* value) if(is(T == DateTime)) {
369     import python.raw: pyDateTimeCheck;
370     const ret = pyDateTimeCheck(value);
371     if(!ret) setPyErrTypeString!"DateTime";
372     return ret;
373 }
374 
375 
376 private bool checkPythonType(T)(PyObject* value) if(is(T == Date)) {
377     import python.raw: pyDateCheck;
378     const ret = pyDateCheck(value);
379     if(!ret) setPyErrTypeString!"Date";
380     return ret;
381 }
382 
383 
384 
385 private void setPyErrTypeString(string type)() @trusted @nogc nothrow {
386     import python.raw: PyErr_SetString, PyExc_TypeError;
387     enum str = "must be a " ~ type;
388     PyErr_SetString(PyExc_TypeError, &str[0]);
389 }