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 }