diff --git a/.gitignore b/.gitignore index 5990836..023e130 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,7 @@ node_modules .cproject .project .settings +npm-debug.log +*.swp +*/**.swp +test-report.xml diff --git a/Makefile b/Makefile index fe41d5b..11819d4 100644 --- a/Makefile +++ b/Makefile @@ -1,10 +1,19 @@ +LD_PRELOAD=/usr/lib64/libpython2.7.so DEBUG=node-python* +LD_PRELOAD=/usr/lib64/libpython2.7.so PYTHONPATH=./test/support +REPORTER=spec test: $(MAKE) DEBUG= test-debug - + test-debug: - DEBUG=$(DEBUG) PYTHONPATH=$(PYTHONPATH) ./node_modules/.bin/mocha -R spec + DEBUG=$(DEBUG) \ + LD_PRELOAD=$(LD_PRELOAD) \ + PYTHONPATH=$(PYTHONPATH) \ + ./node_modules/.bin/mocha -R $(REPORTER) + +test-jenkins: + $(MAKE) JUNIT_REPORT_PATH=test-report.xml JUNIT_REPORT_STACK=1 REPORTER=mocha-jenkins-reporter test .PHONY: test test-debug diff --git a/README.md b/README.md index 887f4a7..1b58c62 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,6 @@ # node-python binding -python bridge for nodejs! - -Hyper-beta, don't hesitate to try and report bugs! +Python bridge for nodejs! Call your python code from node! [![Build Status](https://travis-ci.org/JeanSebTr/node-python.png)](https://travis-ci.org/JeanSebTr/node-python) @@ -29,38 +27,30 @@ You should now go have fun with that and make it brokes :) ## Current status -What should work: - -* Conversion between None and Undefined -* Conversion between Python's and Node's Boolean -* Conversion between Python's and Node's String -* Calling python functions from node -* Conversion from Python's Array to Node's Array - -What may be broken: - -* Losing precision from Python's 64 bits Integer to Node's Number -* If you're using node v0.6.x (please upgrade) you'll have to manually compile with node-gyp - -What's to be done: - -* Conversion from Node's Array to Python's Array -* Pass javascript object to python -* Call javascript function from python - -What would be realy awesome: - -* Proper object introspection - - -## History - -* **v0.0.4** : 2013-10-09 - - use the bindings module to load the native extension -* **v0.0.3** : 2013-07-06 - - Refactor - - Better type conversion & error handling - - Compilation now properly working on both OSX and Linux. Windows compilation _may_ work too -* **v0.0.2** : 2012-12-21 - - Forked from [chrisdickinson/node-python](https://github.com/chrisdickinson/node-python) - - Compilation with node-gyp +What works: + + * ✓ should convert javascript null to python NoneType + * ✓ should convert javascript undefined to python NoneType + * ✓ should convert javascript booleans to python booleans + * ✓ should convert javascript date to python date + * ✓ should convert javascript numbers to python floats + * ✓ should convert javascript arrays to python list + * ✓ should convert javascript objects to python dictionaries + * ✓ should convert javascript nested objects correctly + * ✓ should convert python dicts to javascript objects + * ✓ should convert python lists to javascript arrays + * ✓ should convert python strings to javascript strings + * ✓ should convert python boolean to javascript booleans + * ✓ should convert python numbers to javascript numbers + * ✓ should convert python dates to javascript dates + * ✓ should convert basic python dict to javascript object + * ✓ should convert basic python list to javascript array + * ✓ should return resulting value from python statement executed + * ✓ should return resulting value from python statement executed, converting to string with complex types + * ✓ should return object representing module imported, containing functions from imported module + * ✓ should throw a PythonError when importing a module that does not exist + * ✓ should throw an Error when importing a module that includes bad syntax + +What to watch out for: + +* Losing precision from Python's 64 bits Integer to Node's Number (we hande this by converting decimals to string and using bignumber.js to consume them. better solutions to come) \ No newline at end of file diff --git a/binding.gyp b/binding.gyp index ebeaa63..5d7313f 100644 --- a/binding.gyp +++ b/binding.gyp @@ -8,22 +8,27 @@ "src/py_object_wrapper.cc" ], "conditions": [ - ['OS=="mac"', { - "xcode_settings": { - "OTHER_CFLAGS": [ - " #include "py_object_wrapper.h" #include "utils.h" +#include using namespace v8; using namespace node; @@ -97,4 +98,4 @@ void init (Handle exports) { } -NODE_MODULE(binding, init) \ No newline at end of file +NODE_MODULE(binding, init) diff --git a/src/py_object_wrapper.cc b/src/py_object_wrapper.cc index a8f472f..d7f1d7a 100644 --- a/src/py_object_wrapper.cc +++ b/src/py_object_wrapper.cc @@ -1,7 +1,9 @@ #include +#include #include "py_object_wrapper.h" #include "utils.h" #include "datetime.h" +#include "time.h" Persistent PyObjectWrapper::py_function_template; @@ -33,86 +35,31 @@ void PyObjectWrapper::Initialize() { Handle PyObjectWrapper::New(PyObject* obj) { HandleScope scope; - Local jsVal; + Local jsObj; - // undefined - if(obj == Py_None) { - jsVal = Local::New(Undefined()); - } - else if(PyDict_Check(obj)) { - Local dict = v8::Object::New(); - PyObject *key, *value; - Py_ssize_t pos = 0; - while (PyDict_Next(obj, &pos, &key, &value)) { - Handle jsKey = PyObjectWrapper::New(key); - Handle jsValue = PyObjectWrapper::New(value); - dict->Set(jsKey, jsValue); - } - jsVal = dict; - } - else if(PyList_CheckExact(obj)) { - int size = PyList_Size(obj); - Local array = v8::Array::New(size); - PyObject* value; - for(int i = 0; i < size; i++ ){ - value = PyList_GetItem(obj, i); - Handle jsValue = PyObjectWrapper::New(value); - array->Set(i, jsValue); - } - jsVal = array; - } - // double - else if(PyFloat_CheckExact(obj)) { - double d = PyFloat_AsDouble(obj); - jsVal = Local::New(Number::New(d)); - } - // integer (can be 64b) - else if(PyInt_CheckExact(obj)) { - long i = PyInt_AsLong(obj); - jsVal = Local::New(Number::New((double) i)); - } - // string - else if(PyString_CheckExact(obj)) { - // ref to internal representation: no need to dealloc - char *str = PyString_AsString(obj); - if(str) { - jsVal = Local::New(String::New(str)); - } - } - else if(PyBool_Check(obj)) { - int b = PyObject_IsTrue(obj); - if(b != -1) { - jsVal = Local::New(Boolean::New(b)); - } + try { + jsObj = ConvertToJavaScript(obj); + } catch (int e) { + if (PyErr_Occurred()) { + return ThrowPythonException(); + } } - if(PyErr_Occurred()) { - Py_XDECREF(obj); + if (PyErr_Occurred()) { return ThrowPythonException(); - } - - if(jsVal.IsEmpty()) { - Local jsObj = py_function_template->GetFunction()->NewInstance(); - PyObjectWrapper* wrapper = new PyObjectWrapper(obj); - wrapper->Wrap(jsObj); - jsVal = Local::New(jsObj); - } - else { - Py_XDECREF(obj); - } - return scope.Close(jsVal); + } + + return scope.Close(jsObj); } Handle PyObjectWrapper::Get(Local key, const AccessorInfo& info) { - // returning an empty Handle object signals V8 that we didn't - // find the property here, and we should check the "NamedAccessor" functions HandleScope scope; PyObjectWrapper* wrapper = ObjectWrap::Unwrap(info.Holder()); String::Utf8Value utf8_key(key); string value(*utf8_key); PyObject* result = wrapper->InstanceGet(value); if(result) { - return PyObjectWrapper::New(result); + return scope.Close(PyObjectWrapper::New(result)); } return Handle(); } @@ -181,16 +128,11 @@ Handle PyObjectWrapper::ValueOf(const Arguments& args) { PyObject* keys = PyMapping_Keys(py_obj); PyObject* values = PyMapping_Values(py_obj); for(int i = 0; i < len; ++i) { - PyObject *key = PySequence_GetItem(keys, i), - *value = PySequence_GetItem(values, i), - *key_as_string = PyObject_Str(key); - char* cstr = PyString_AsString(key_as_string); - + PyObject *key = PySequence_GetItem(keys, i); + PyObject *value = PySequence_GetItem(values, i); Local jsobj = PyObjectWrapper::py_function_template->GetFunction()->NewInstance(); PyObjectWrapper* obj_out = new PyObjectWrapper(value); obj_out->Wrap(jsobj); - Py_XDECREF(key); - Py_XDECREF(key_as_string); } Py_XDECREF(keys); Py_XDECREF(values); @@ -199,103 +141,218 @@ Handle PyObjectWrapper::ValueOf(const Arguments& args) { return Undefined(); } +Local PyObjectWrapper::ConvertToJavaScript(PyObject* obj) { + Local jsVal; + // undefined + if(obj == Py_None) { + jsVal = Local::New(Undefined()); + } + // integer (can be 64b) + else if(PyInt_CheckExact(obj)) { + long i = PyInt_AsLong(obj); + jsVal = Local::New(Number::New((double) i)); + } + // double + else if(PyFloat_CheckExact(obj)) { + double d = PyFloat_AsDouble(obj); + jsVal = Local::New(Number::New(d)); + } + // long + else if(PyLong_CheckExact(obj)) { + double d = PyLong_AsDouble(obj); + jsVal = Local::New(Number::New(d)); + } + // string + else if(PyString_CheckExact(obj)) { + jsVal = Local::New(String::New(PyString_AsString(obj))); + } + // bool + else if(PyBool_Check(obj)) { + int b = PyObject_IsTrue(obj); + jsVal = Local::New(Boolean::New(b)); + } + // date + else if (PyDate_Check(obj)) { + time_t rawtime = 0; + + struct tm * timeinfo = { 0 }; + timeinfo = gmtime ( &rawtime ); + + int year = PyDateTime_GET_YEAR(obj); + int month = PyDateTime_GET_MONTH(obj); + int day = PyDateTime_GET_DAY(obj); + + if ((day == 28) && (month == 2) && (year % 4 == 0) && (year % 100 == 0 && year % 400 != 0)) { + timeinfo->tm_mday = 29; + } + + timeinfo->tm_year = year - 1900; + timeinfo->tm_mon = month - 1; + timeinfo->tm_mday = day; + + int microseconds = 0; + if ( PyDateTime_Check(obj) ) { + PyObject *utcoffset = PyObject_CallMethod(obj, "utcoffset", NULL); + if(utcoffset != Py_None){ + obj = PyNumber_Subtract(obj, utcoffset); + } + + timeinfo->tm_hour = PyDateTime_DATE_GET_HOUR(obj); + timeinfo->tm_min = PyDateTime_DATE_GET_MINUTE(obj); + timeinfo->tm_sec = PyDateTime_DATE_GET_SECOND(obj); + timeinfo->tm_isdst = -1; + microseconds = PyDateTime_DATE_GET_MICROSECOND(obj); + } else if (PyTime_Check(obj) ) { + timeinfo->tm_hour = PyDateTime_TIME_GET_HOUR(obj); + timeinfo->tm_min = PyDateTime_TIME_GET_MINUTE(obj); + timeinfo->tm_sec = PyDateTime_TIME_GET_SECOND(obj); + timeinfo->tm_isdst = -1; + microseconds = PyDateTime_TIME_GET_MICROSECOND(obj); + } else { + timeinfo->tm_hour = 0; + timeinfo->tm_min = 0; + timeinfo->tm_sec = 0; + timeinfo->tm_isdst = -1; + } + + time_t result = mktime(timeinfo) * 1000 + microseconds; + + jsVal = v8::Date::New(result); + } + // dict + else if(PyDict_CheckExact(obj)) { + Local dict = v8::Object::New(); + PyObject *key, *value; + Py_ssize_t pos = 0; + + while (PyDict_Next(obj, &pos, &key, &value)) { + Handle jsKey = ConvertToJavaScript(key); + Handle jsValue = ConvertToJavaScript(value); + + dict->Set(jsKey, jsValue); + } + jsVal = dict; + } + // list + else if(PyList_CheckExact(obj)) { + int size = PyList_Size(obj); + Local array = v8::Array::New(size); + PyObject* value; + for(int i = 0; i < size; i++ ){ + value = PyList_GetItem(obj, i); + array->Set(i, ConvertToJavaScript(value)); + } + jsVal = array; + } else { + Py_XINCREF(obj); + Local jsObj = py_function_template->GetFunction()->NewInstance(); + PyObjectWrapper* wrapper = new PyObjectWrapper(obj); + wrapper->Wrap(jsObj); + jsVal = Local::New(jsObj); + } + + return jsVal; +} + PyObject* PyObjectWrapper::ConvertToPython(const Handle& value) { int len; - HandleScope scope; - - if(value->IsString()) { + if (value->IsString()) { return PyString_FromString(*String::Utf8Value(value->ToString())); } else if (value->IsBoolean()) { - if (value->ToBoolean()->IsTrue()) { - return Py_True; - } else { - return Py_False; - } + if (value->ToBoolean()->IsTrue()) { + return Py_True; + } else { + return Py_False; + } } else if (value->IsNull() || value->IsUndefined()) { - return Py_None; - } else if(value->IsNumber()) { + return Py_None; + } else if (value->IsNumber()) { return PyFloat_FromDouble(value->NumberValue()); - } else if(value->IsDate()) { - Handle date = Handle::Cast(value); - PyObject* floatObj = PyFloat_FromDouble(date->NumberValue() / 1000.0 ); // javascript returns milliseconds since epoch. python wants seconds since epoch - PyObject* timeTuple = Py_BuildValue("(O)", floatObj); - Py_DECREF(floatObj); - PyObject* dateTime = PyDateTime_FromTimestamp(timeTuple); - Py_DECREF(timeTuple); - return dateTime; - } else if(value->IsObject()) { - if(value->IsArray()) { - Local array = Array::Cast(*value); - len = array->Length(); - PyObject* py_list = PyList_New(len); - for(int i = 0; i < len; ++i) { - Local obj = array->Get(i)->ToObject(); - if (!obj->FindInstanceInPrototypeChain(PyObjectWrapper::py_function_template).IsEmpty()) { - PyObjectWrapper* python_object = ObjectWrap::Unwrap(obj); - PyObject* pyobj = python_object->InstanceGetPyObject(); - PyList_SET_ITEM(py_list, i, pyobj); - } else { - Local js_val = array->Get(i); - PyList_SET_ITEM(py_list, i, ConvertToPython(js_val)); - } - } - return py_list; - } else { - Local obj = value->ToObject(); - if(!obj->FindInstanceInPrototypeChain(PyObjectWrapper::py_function_template).IsEmpty()) { - PyObjectWrapper* python_object = ObjectWrap::Unwrap(value->ToObject()); - PyObject* pyobj = python_object->InstanceGetPyObject(); - return pyobj; - } else { - Local property_names = obj->GetPropertyNames(); - len = property_names->Length(); - PyObject* py_dict = PyDict_New(); - for(int i = 0; i < len; ++i) { - Local str = property_names->Get(i)->ToString(); - Local js_val = obj->Get(str); - PyDict_SetItemString(py_dict, *String::Utf8Value(str), ConvertToPython(js_val)); - } - return py_dict; - } - } + } else if (value->IsDate()) { + Handle dt = Handle::Cast(value); + long sinceEpoch = dt->NumberValue(); + long milliseconds = (sinceEpoch / 1000); + time_t timestamp = (time_t)(milliseconds); + + struct tm* tmp = localtime(×tamp); + + return PyDateTime_FromDateAndTime( + tmp->tm_year + 1900, + tmp->tm_mon + 1, + tmp->tm_mday, + tmp->tm_hour, + tmp->tm_min, + tmp->tm_sec, + sinceEpoch % 1000 + ); + } else if (value->IsObject()) { + if (value->IsArray()) { + Local array = Array::Cast(*value); + len = array->Length(); + PyObject* py_list = PyList_New(len); + for (int i = 0; i < len; ++i) { + Local js_val = array->Get(i); + PyList_SET_ITEM(py_list, i, ConvertToPython(js_val)); + } + return py_list; + } else { + Local obj = value->ToObject(); + + if(!obj->FindInstanceInPrototypeChain(PyObjectWrapper::py_function_template).IsEmpty()) { + PyObjectWrapper* python_object = ObjectWrap::Unwrap(value->ToObject()); + PyObject* pyobj = python_object->InstanceGetPyObject(); + return pyobj; + } else { + Local property_names = obj->GetPropertyNames(); + len = property_names->Length(); + PyObject* py_dict = PyDict_New(); + + for (int i = 0; i < len; ++i) { + Local str = property_names->Get(i)->ToString(); + Local js_val = obj->Get(str); + PyDict_SetItemString(py_dict, *String::Utf8Value(str), ConvertToPython(js_val)); + } + return py_dict; + } + } return NULL; - } else if(value->IsArray()) { - Local array = Array::Cast(*value); - len = array->Length(); - PyObject* py_list = PyList_New(len); - for(int i = 0; i < len; ++i) { - Local js_val = array->Get(i); - PyList_SET_ITEM(py_list, i, ConvertToPython(js_val)); - } - return py_list; - } else if(value->IsUndefined()) { + } else if (value->IsArray()) { + Local array = Array::Cast(*value); + len = array->Length(); + PyObject* py_list = PyList_New(len); + for (int i = 0; i < len; ++i) { + Local js_val = array->Get(i); + PyList_SET_ITEM(py_list, i, ConvertToPython(js_val)); + } + return py_list; + } else if (value->IsUndefined()) { Py_RETURN_NONE; } + return NULL; } Handle PyObjectWrapper::InstanceCall(const Arguments& args) { // for now, we don't do anything. HandleScope scope; + int len = args.Length(); PyObject* args_tuple = PyTuple_New(len); - - for(int i = 0; i < len; ++i) { - PyObject* py_arg = ConvertToPython(args[i]); - if (PyErr_Occurred()) { - return ThrowPythonException(); - } - PyTuple_SET_ITEM(args_tuple, i, py_arg); + for (int i = 0; i < len; ++i) { + PyTuple_SET_ITEM(args_tuple, i, ConvertToPython(args[i])); } + PyObject* result = PyObject_CallObject(mPyObject, args_tuple); + //Py_XDECREF(args_tuple); + if (PyErr_Occurred()) { return ThrowPythonException(); } - Py_XDECREF(args_tuple); - - if(result) { - return scope.Close(PyObjectWrapper::New(result)); + if (result) { + Handle val = PyObjectWrapper::New(result); + Py_XDECREF(result); + return scope.Close(val); } else { return ThrowPythonException(); } @@ -303,15 +360,12 @@ Handle PyObjectWrapper::InstanceCall(const Arguments& args) { string PyObjectWrapper::InstanceToString(const Arguments& args) { PyObject* as_string = PyObject_Str(mPyObject); - string native_string(PyString_AsString(as_string)); - Py_XDECREF(as_string); - return native_string; + return PyString_AsString(as_string); } PyObject* PyObjectWrapper::InstanceGet(const string& key) { if(PyObject_HasAttrString(mPyObject, key.c_str())) { - PyObject* attribute = PyObject_GetAttrString(mPyObject, key.c_str()); - return attribute; - } + return PyObject_GetAttrString(mPyObject, key.c_str()); + } return (PyObject*)NULL; } diff --git a/src/py_object_wrapper.h b/src/py_object_wrapper.h index 9020fa0..5824495 100644 --- a/src/py_object_wrapper.h +++ b/src/py_object_wrapper.h @@ -43,6 +43,8 @@ class PyObjectWrapper : public node::ObjectWrap { static Handle ValueOf(const Arguments& args); static PyObject* ConvertToPython(const Handle& value); + + static Local ConvertToJavaScript(PyObject* obj); PyObject* InstanceGetPyObject() { return mPyObject; diff --git a/src/utils.cc b/src/utils.cc index dd395e1..b1d49a6 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -8,12 +8,14 @@ Handle ThrowPythonException() { PyObject *ptype, *pvalue, *ptraceback; PyErr_Fetch(&ptype, &pvalue, &ptraceback); PyErr_NormalizeException(&ptype, &pvalue, &ptraceback); + // maybe useless to protect against bad use of ThrowPythonException ? if(pvalue == NULL) { return ThrowException( Exception::Error(String::New("No exception found")) ); } + // handle exception message Local msg = String::New("Python Error: "); @@ -33,8 +35,8 @@ Handle ThrowPythonException() { PyObject *module_name, *pyth_module, *pyth_func; module_name = PyString_FromString("traceback"); pyth_module = PyImport_Import(module_name); - Py_DECREF(module_name); + pyth_func = PyObject_GetAttrString(pyth_module, "format_exception"); Py_DECREF(pyth_module); diff --git a/test/index.js b/test/index.js index 5d878d8..7c82d12 100644 --- a/test/index.js +++ b/test/index.js @@ -25,9 +25,9 @@ describe('node-python', function () { }).throw(/No module named jibberish/); }); it('should throw an Error when importing a module that includes bad syntax', function () { - should(function () { - python.import('test'); - }).throw(/Python Error: SyntaxError/) + should(function () { + python.import('test'); + }).throw(/Python Error: SyntaxError/); }); }); it('should convert javascript null to python NoneType', function () { @@ -52,7 +52,7 @@ describe('node-python', function () { }); it('should convert javascript numbers to python floats', function () { test = python.import('test2'); - var type = test.getPythonTypeName(1); + var type = test.getPythonTypeName(1.1); type.should.equal('float'); }); it('should convert javascript arrays to python list', function () { @@ -89,7 +89,7 @@ describe('node-python', function () { type.should.equal('list'); var i = 0, arr = []; while (i < 10000) { - arr.push(Math.random().toString()) + arr.push(Math.random().toString()); i++; } var type = test.getPythonTypeName(arr); @@ -104,9 +104,75 @@ describe('node-python', function () { }); it('should convert python lists to javascript arrays', function () { test = python.import('test2'); - var value = test.getPythonValue([ 1, 2, 3]); - value.should.containEql(1); + var value = test.getPythonValue([ '1', 2, '3']); + value.should.containEql('1'); value.should.containEql(2); - value.should.containEql(3); + value.should.containEql('3'); + }); + it('should convert python strings to javascript strings', function () { + test = python.import('test2'); + var value = test.getPythonValue('str'); + value.should.equal('str'); + }); + it('should convert python boolean to javascript booleans', function () { + test = python.import('test2'); + var value = test.getPythonValue(true); + value.should.equal(true); + }); + it('should convert python numbers to javascript numbers', function () { + test = python.import('test2'); + var value = test.getPythonValue(1); + value.should.equal(1); + }); + it('should convert python dates to javascript dates', function () { + test = python.import('test2'); + var date = new Date('2014-12-01'); + console.log(date); + var value = test.getPythonValue(date); + value.should.eql(date); + }); + it('should convert daylight savings dates from python to javascript', function () { + test = python.import('test2'); + var date = new Date('2014-06-01'); + console.log(date); + var value = test.getPythonValue(date); + value.should.eql(date); + }); + it('should convert basic python dict to javascript object', function () { + test = python.import('test3'); + var obj = test.getPythonDict(); + obj.should.have.property('one', 1); + obj.should.have.property('two', '2'); + obj.should.have.property('three', 3); + }); + it('should convert nested python dict to javascript object', function () { + + test = python.import('test3'); + + var dict = test.getPythonNestedDict; + + for(var i = 0; i < 10000; i++) { + var obj = dict(); + obj.should.have.property('one'); + obj.one.should.have.property('four', 4); + obj.one.should.have.property('five', 5); + obj.should.have.property('two', '2'); + obj.should.have.property('three', 3); + } + + }); + it('should convert basic python list to javascript array', function () { + test = python.import('test3'); + var obj = test.getPythonList(); + obj.should.have.containEql(1); + obj.should.have.containEql('2'); + obj.should.have.containEql('three'); + }); + it('should convert nested python list to javascript array', function () { + test = python.import('test3'); + var obj = test.getPythonNestedList(); + obj.should.have.containEql([4, 5, 6]); + obj.should.have.containEql('2'); + obj.should.have.containEql('three'); }); -}); \ No newline at end of file +}); diff --git a/test/support/test2.pyc b/test/support/test2.pyc index 6c6520c..fd78960 100644 Binary files a/test/support/test2.pyc and b/test/support/test2.pyc differ diff --git a/test/support/test3.py b/test/support/test3.py new file mode 100644 index 0000000..0e4d6e2 --- /dev/null +++ b/test/support/test3.py @@ -0,0 +1,13 @@ +from decimal import * + +def getPythonDict(): + return dict(one=1, two=str(Decimal(2)), three=3) + +def getPythonNestedDict(): + return dict(one=dict(four=4, five=5), two=str(Decimal(2)), three=3) + +def getPythonList(): + return [1, str(Decimal(2)), 'three'] + +def getPythonNestedList(): + return [[4, 5, 6], str(Decimal(2)), 'three'] \ No newline at end of file diff --git a/test/support/test3.pyc b/test/support/test3.pyc new file mode 100644 index 0000000..7bd718f Binary files /dev/null and b/test/support/test3.pyc differ