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