1 module python.conv.python_to_d;
2 
3 
4 import python.raw: PyObject, pyListCheck, pyTupleCheck, PyTuple_Size, pyCallableCheck;
5 import python.type: isUserAggregate, isTuple;
6 import std.traits: Unqual, isIntegral, isFloatingPoint, isAggregateType, isArray,
7     isStaticArray, isAssociativeArray, isPointer, PointerTarget, isSomeChar, isSomeString,
8     isDelegate, isFunctionPointer;
9 import std.range: isInputRange;
10 import std.datetime: DateTime, Date;
11 import core.time: Duration;
12 
13 
14 T to(T)(PyObject* value) @trusted if(isIntegral!T && !is(T == enum)) {
15     import python.raw: PyLong_AsLong;
16 
17     const ret = PyLong_AsLong(value);
18     if(ret > T.max || ret < T.min) throw new Exception("Overflow");
19 
20     return cast(T) ret;
21 }
22 
23 
24 T to(T)(PyObject* value) @trusted if(isFloatingPoint!T) {
25     import python.raw: PyFloat_AsDouble;
26     auto ret = PyFloat_AsDouble(value);
27     return cast(T) ret;
28 }
29 
30 
31 private template UserAggregateReturnType(T) {
32     import std.traits: isCopyable;
33 
34     static if(isCopyable!T)
35         alias UserAggregateReturnType = T;
36      else
37         alias UserAggregateReturnType = T*;
38 }
39 
40 // Returns T if T is copyable, T* otherwise
41 UserAggregateReturnType!T to(T)(PyObject* value)
42     @trusted
43     if(isUserAggregate!T && (is(T == struct) || is(T == union)))
44 {
45     import std.traits: Unqual, isCopyable;
46 
47     alias RetType = UserAggregateReturnType!T;
48 
49     static if(isCopyable!T) {
50         Unqual!T ret;
51         toStructImpl(value, &ret);
52     } else {
53         auto ret = new Unqual!T;
54         toStructImpl(value, ret);
55     }
56 
57     return maybeCast!RetType(ret);
58 }
59 
60 
61 private void toStructImpl(T)(PyObject* value, T* ret) {
62     import autowrap.common: AlwaysTry;
63     import python.raw: Py_None;
64     import python.type: PythonClass;
65     import std.traits: fullyQualifiedName, isCopyable, isPointer;
66 
67     if(value == Py_None)
68         throw new Exception("Cannot convert None to " ~ T.stringof);
69 
70     auto pyclass = cast(PythonClass!T*) value;
71 
72     static foreach(i; 0 .. typeof(*ret).tupleof.length) {{
73         static if(AlwaysTry || __traits(compiles, pyclass.getField!i.to!(typeof(T.tupleof[i])))) {
74 
75             auto pythonField = pyclass.getField!i;
76             auto converted = pythonField.to!(typeof(T.tupleof[i]));
77 
78             static if(isCopyable!(typeof((*ret).tupleof[i])))
79                 (*ret).tupleof[i] = converted;
80             else {
81                 static assert(isPointer!(typeof(converted)));
82                 toStructImpl(pyclass.getField!i, &((*ret).tupleof[i]));
83             }
84         } else
85             pragma(msg, "WARNING: cannot convert struct field #", i, " of ", fullyQualifiedName!T);
86     }}
87 }
88 
89 
90 T to(T)(PyObject* value) @trusted if(isUserAggregate!T && !is(T == struct) && !is(T == union))
91 {
92     import python.type: PythonClass, userAggregateInit, gFactory;
93     import std.traits: Unqual;
94     import std..string: fromStringz;
95     import std.conv: text;
96 
97     assert(value !is null, "Cannot convert null PyObject to `" ~ T.stringof ~ "`");
98 
99     auto pyclass = cast(PythonClass!T*) value;
100     const runtimeType = value.ob_type.tp_name.fromStringz.text;
101     auto creator = runtimeType in gFactory;
102     return creator
103         ? cast(T) (*creator)(value)
104         : userAggregateInit!T;
105 }
106 
107 
108 T to(T)(PyObject* value) if(isPointer!T && isUserAggregate!(PointerTarget!T)) {
109     auto ret = new Unqual!(PointerTarget!T);
110     toStructImpl(value, ret);
111     return ret;
112 }
113 
114 
115 // FIXME - not sure why a separate implementation is needed for non user aggregates
116 T to(T)(PyObject* value)
117     if(isPointer!T && !isUserAggregate!(PointerTarget!T) &&
118        !isFunctionPointer!T && !isDelegate!T &&
119        !is(Unqual!(PointerTarget!T) == void))
120 {
121     import python.raw: pyUnicodeCheck;
122     import std.traits: Unqual, PointerTarget;
123     import std..string: toStringz;
124 
125     enum isStringz = is(PointerTarget!T == const(char)) || is(PointerTarget!T == immutable(char));
126 
127     if(isStringz && pyUnicodeCheck(value))
128         return value.to!string.toStringz.maybeCast!T;
129     else {
130         auto ret = new Unqual!(PointerTarget!T);
131         *ret = value.to!(Unqual!(PointerTarget!T));
132         return ret.maybeCast!T;
133     }
134 }
135 
136 
137 T to(T)(PyObject* value) if(isPointer!T && is(Unqual!(PointerTarget!T) == void))
138 {
139     import python.raw: pyBytesCheck, PyBytes_AsString;
140     import std.exception: enforce;
141 
142     enforce(pyBytesCheck(value), "Can only convert Python bytes object to void*");
143     return cast(void*) PyBytes_AsString(value);
144 }
145 
146 
147 T to(T)(PyObject* value) if(is(Unqual!T == DateTime)) {
148     import python.raw;
149 
150     return DateTime(pyDateTimeYear(value),
151                     pyDateTimeMonth(value),
152                     pyDateTimeDay(value),
153                     pyDateTimeHour(value),
154                     pyDateTimeMinute(value),
155                     pyDateTimeSecond(value));
156 
157 }
158 
159 
160 T to(T)(PyObject* value) if(is(Unqual!T == Date)) {
161     import python.raw;
162 
163     return Date(pyDateTimeYear(value),
164                 pyDateTimeMonth(value),
165                 pyDateTimeDay(value));
166 }
167 
168 
169 T to(T)(PyObject* value) if(isArray!T && !isSomeString!T)
170     in(pyListCheck(value) || pyTupleCheck(value))
171 {
172     import python.raw: PyList_Size, PyList_GetItem, PyTuple_Size, PyTuple_GetItem;
173     import std.range: ElementEncodingType;
174     import std.traits: Unqual, isDynamicArray;
175     import std.exception: enforce;
176     import std.conv: text;
177 
178     alias ElementType = Unqual!(ElementEncodingType!T);
179 
180     // This is needed to deal with array of const or immutable
181     static if(isDynamicArray!T)
182         alias ArrayType = ElementType[];
183     else
184         alias ArrayType = Unqual!T;
185 
186     // deal with void[] here since otherwise we won't be able to iterate over it
187     static if(is(ElementType == void))
188         alias RetType = ubyte[];
189     else
190         alias RetType = ArrayType;
191 
192     RetType ret;
193 
194     static if(__traits(compiles, ret.length = 1)) {
195         const valueLength = {
196             if(pyListCheck(value))
197                 return PyList_Size(value);
198             else if(pyTupleCheck(value))
199                 return PyTuple_Size(value);
200             else
201                 assert(0);
202         }();
203         assert(valueLength >= 0, text("Invalid length ", valueLength));
204         ret.length = valueLength;
205     }
206 
207     foreach(i, ref elt; ret) {
208         auto pythonItem = {
209             if(pyListCheck(value))
210                 return PyList_GetItem(value, i);
211             else if(pyTupleCheck(value))
212                 return PyTuple_GetItem(value, i);
213             else
214                 assert(0);
215         }();
216         elt = pythonItem.to!(typeof(elt));
217     }
218 
219     return maybeCast!T(ret);
220 }
221 
222 
223 T to(T)(PyObject* value) if(isSomeString!T) {
224 
225     import python.raw: pyUnicodeGetSize, pyUnicodeCheck,
226         pyBytesAsString, pyObjectUnicode, pyUnicodeAsUtf8String, Py_ssize_t;
227     import std.conv: to;
228 
229     value = pyObjectUnicode(value);
230 
231     const length = pyUnicodeGetSize(value);
232     if(length == 0) return T.init;
233 
234     auto str = pyUnicodeAsUtf8String(value);
235     if(str is null) throw new Exception("Tried to convert a non-string Python value to D string");
236 
237     auto ptr = pyBytesAsString(str);
238     assert(length == 0 || ptr !is null);
239 
240     auto slice = ptr[0 .. length];
241 
242     return slice.to!T;
243 }
244 
245 
246 T to(T)(PyObject* value) if(is(Unqual!T == bool)) {
247     import python.raw: pyTrue;
248     return value is pyTrue;
249 }
250 
251 
252 T to(T)(PyObject* value) if(isAssociativeArray!T)
253 {
254     import python.raw: pyDictCheck, PyDict_Keys, PyList_Size, PyList_GetItem, PyDict_GetItem;
255 
256     assert(pyDictCheck(value));
257 
258     // this enum is to get K and V whilst avoiding auto-decoding, which is why we're not using
259     // std.traits
260     enum _ = is(T == V[K], V, K);
261     alias KeyType = Unqual!K;
262     alias ValueType = Unqual!V;
263 
264     ValueType[KeyType] ret;
265 
266     auto keys = PyDict_Keys(value);
267 
268     foreach(i; 0 .. PyList_Size(keys)) {
269         auto k = PyList_GetItem(keys, i);
270         auto v = PyDict_GetItem(value, k);
271         auto dk = k.to!KeyType;
272         auto dv = v.to!ValueType;
273 
274         ret[dk] = dv;
275     }
276 
277     return ret;
278 }
279 
280 
281 T to(T)(PyObject* value) if(isTuple!T)
282     in(pyTupleCheck(value))
283     in(PyTuple_Size(value) == T.length)
284     do
285 {
286     import python.raw: pyTupleCheck, PyTuple_Size, PyTuple_GetItem;
287 
288     T ret;
289 
290     static foreach(i; 0 .. T.length) {
291         ret[i] = PyTuple_GetItem(value, i).to!(typeof(ret[i]));
292     }
293 
294     return ret;
295 }
296 
297 
298 T to(T)(PyObject* value) if(is(Unqual!T == char) || is(Unqual!T == wchar) || is(Unqual!T == dchar)) {
299     auto str = value.to!string;
300     return str[0];
301 }
302 
303 
304 T to(T)(PyObject* value) if(isDelegate!T) {
305     return value.toDlangFunction!T;
306 }
307 
308 T to(T)(PyObject* value) if(isFunctionPointer!T) {
309     throw new Exception("Conversion of Python functions to D function pointers not yet implemented");
310 }
311 
312 
313 private T toDlangFunction(T)(PyObject* value)
314     in(pyCallableCheck(value))
315     do
316 {
317     import python.raw: PyObject_CallObject;
318     import python.conv.d_to_python: toPython;
319     import python.conv.python_to_d: to;
320     import std.traits: ReturnType, Unqual;
321     static import std.traits;
322     import std.meta: staticMap;
323     import std.typecons: Tuple;
324     import std.format: format;
325     import std.conv: text;
326 
327     alias UnqualParams = staticMap!(Unqual, std.traits.Parameters!T);
328 
329     enum code =
330         q{
331             // FIXME: the @trusted here is due to conversions to @safe
332             // D delegates
333             return (%s) @trusted {
334                 try {
335                     Tuple!UnqualParams dArgsTuple;
336                     static foreach(i; 0 .. UnqualParams.length) {
337                         dArgsTuple[i] = mixin(`arg` ~ i.text);
338                     }
339                     auto pyArgs = dArgsTuple.toPython;
340                     auto pyResult = PyObject_CallObject(value, pyArgs);
341                     static if(!is(ReturnType!T == void))
342                         return pyResult.to!(ReturnType!T);
343                     else
344                         return;
345                 } catch(Exception e) {
346                     import python.raw: PyErr_SetString, PyExc_RuntimeError;
347                     import std..string: toStringz;
348                     PyErr_SetString(PyExc_RuntimeError, e.msg.toStringz);
349                 }
350                 assert(0);
351 
352             };
353         }.format(
354             parametersRecipe!T("T"),
355         )
356     ;
357 
358     // pragma(msg, code);
359     mixin(code);
360 }
361 
362 private string parametersRecipe(alias F)(in string symbol)
363     in(__ctfe)
364     do
365 {
366 
367     import std.array: join;
368     import std.traits: Parameters;
369 
370     string[] parameters;
371 
372     static foreach(i; 0 .. Parameters!F.length) {
373         parameters ~= parameterRecipe!(F, i)(symbol);
374     }
375 
376     return parameters.join(", ");
377 }
378 
379 
380 private string parameterRecipe(alias F, size_t i)(in string symbol)
381     in(__ctfe)
382     do
383 {
384     import std.array: join;
385     import std.conv: text;
386     import std.traits: ParameterDefaults;
387 
388     const string[] storageClasses = [ __traits(getParameterStorageClasses, F, i) ];
389 
390     static string defaultValue(alias default_)() {
391         static if(is(default_ == void))
392             return "";
393         else
394             return text(" = ", default_);
395     }
396 
397 
398     return
399         text(storageClasses.join(" "), " ",
400              `std.traits.Parameters!(`, symbol, `)[`, i, `] `,
401              `arg`, i,
402              defaultValue!(ParameterDefaults!F[i]),
403             );
404 }
405 
406 
407 
408 T to(T)(PyObject* value) if(is(Unqual!T == Duration)) {
409     import python.raw: PyDateTime_Delta;
410     import core.time: days, seconds, usecs;
411 
412     const delta = cast(PyDateTime_Delta*) value;
413 
414     return delta.days.days + delta.seconds.seconds + delta.microseconds.usecs;
415 }
416 
417 
418 T to(T)(PyObject* value) if(is(T == enum)) {
419     import std.traits: OriginalType;
420     return cast(T) value.to!(OriginalType!T);
421 }
422 
423 
424 // Usually this is needed in the presence of `const` or `immutable`
425 private auto maybeCast(Wanted, Actual)(auto ref Actual value) {
426     static if(is(Wanted == Actual))
427         return value;
428     // the presence of `opCast` might mean this isn't
429     // always possible
430     else static if(__traits(compiles, cast(Wanted) value))
431         return cast(Wanted) value;
432     else {
433         Wanted ret = value;
434         return ret;
435     }
436 }