diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5990836 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build +node_modules +.DS_Store +.cproject +.project +.settings diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c2cb8ce --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: node_js +node_js: + - "0.11" + - "0.10" + - "0.8" + - "0.6" \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fe41d5b --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +DEBUG=node-python* +PYTHONPATH=./test/support + +test: + $(MAKE) DEBUG= test-debug + +test-debug: + DEBUG=$(DEBUG) PYTHONPATH=$(PYTHONPATH) ./node_modules/.bin/mocha -R spec + +.PHONY: test test-debug diff --git a/README.md b/README.md index 09bdcc2..349fb24 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,70 @@ -node-python binding -====================== +# node-python binding -I bumped this up from "playground" to "binding" on account of it starting to feel like the -right thing to do. +python bridge for nodejs! -This is a binding between Node.js and Python; unfortunately as written it actually embeds a -python process inside of Node. It's of extremely alpha quality and was originally written with -the intent of getting a better understanding of the internals of both V8 and CPython. +Hyper-beta, don't hesitate to try and report bugs! -But, yeah, okay. So the cool things: +[![Build Status](https://travis-ci.org/JeanSebTr/node-python.png)](https://travis-ci.org/JeanSebTr/node-python) - var sys = require('sys'); - var python = require('./binding'); - var pysys = python.import('sys'); - sys.puts(pysys.toString()); +## Installation -Will output python's `sys.path`. And passing in arguments works, too: +For the module to compile, you need cpython's development headers. +Example for Ubuntu : run ```sudo apt-get install python-dev``` - var python = require('./binding'), - os = python.import('os'), - cwd = os.getcwd(), - basename = os.path.basename(cwd); +Then simply: +```npm install node-python``` - var sys = require('sys'); +## Usage - sys.puts(basename.toString()); +```javascript -Unfortunately Python objects are not really fully translated into native Javascript objects yet; -you have to cast them from whatever they are into whatever you want them to be. At the moment, the -only provided cast is "toString", but that should change in the near future (hopefully). +// python stuff +var python = require('node-python'); +var os = python.import('os'); -Passing python objects that you get from calling python functions from javascript can seamlessly -be passed back into python functions (no casting required). Currently there's what I assume to be -a passable argument translation implementation for simple Objects (ones that act like dicts), -Arrays, Numbers (maybe?), and Strings. +// nodejs stuff +var path = require('path'); -You can slap together a tiny WSGI hosting thing on it, as well, which is provided in `wsgi.js`. -It's half implemented, but it's midnight on a Sunday and I should probably sleep. +assert(os.path.basename(os.getcwd()) == path.basename(process.cwd())) -Installation ------------- +``` -I've only tested this out on my computer so no huge promises can be made about other platforms. -Running OSX 10.6.3: +You should now go have fun with that and make it brokes :) - node-waf configure build +## Current status -In theory this is all you need in the entire world. +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 diff --git a/binding.cc b/binding.cc deleted file mode 100644 index 799274e..0000000 --- a/binding.cc +++ /dev/null @@ -1,275 +0,0 @@ -// binding.cc -#include -#include -#include -#define RETURN_NEW_PYOBJ(scope,pyobject) \ - Local jsobject = python_function_template_->GetFunction()->NewInstance(); \ - PyObjectWrapper* py_object_wrapper = new PyObjectWrapper(pyobject); \ - py_object_wrapper->Wrap(jsobject);\ - return scope.Close(jsobject); - -using namespace v8; -using std::string; - -#include -#include -using namespace node; - -class PyObjectWrapper : public ObjectWrap { - PyObject* mPyObject; - public: - static Persistent python_function_template_; - PyObjectWrapper(PyObject* obj) : mPyObject(obj), ObjectWrap() { } - virtual ~PyObjectWrapper() { - Py_XDECREF(mPyObject); - mPyObject = NULL; - } - - static void - Initialize(Handle target) { - HandleScope scope; - Local fn_tpl = FunctionTemplate::New(); - Local obj_tpl = fn_tpl->InstanceTemplate(); - - obj_tpl->SetInternalFieldCount(1); - - // this has first priority. see if the properties already exist on the python object - obj_tpl->SetNamedPropertyHandler(Get, Set); - - // If we're calling `toString`, delegate to our version of ToString - obj_tpl->SetAccessor(String::NewSymbol("toString"), ToStringAccessor); - - // likewise for valueOf - obj_tpl->SetAccessor(String::NewSymbol("valueOf"), ValueOfAccessor); - - // Python objects can be called as functions. - obj_tpl->SetCallAsFunctionHandler(Call, Handle()); - - python_function_template_ = Persistent::New(fn_tpl); - // let's also export "import" - Local import = FunctionTemplate::New(Import); - target->Set(String::New("import"), import->GetFunction()); - }; - - static Handle - 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_NEW_PYOBJ(scope, result); - } - return Handle(); - } - - static Handle - Set(Local key, Local value, const AccessorInfo& info) { - // we don't know what to do. - return Undefined(); - }; - - static Handle - CallAccessor(Local property, const AccessorInfo& info) { - HandleScope scope; - Local func = FunctionTemplate::New(Call); - return scope.Close(func->GetFunction()); - }; - - static Handle - ToStringAccessor(Local property, const AccessorInfo& info) { - HandleScope scope; - Local func = FunctionTemplate::New(ToString); - return scope.Close(func->GetFunction()); - }; - - static Handle - ValueOfAccessor(Local property, const AccessorInfo& info) { - HandleScope scope; - Local func = FunctionTemplate::New(ValueOf); - return scope.Close(func->GetFunction()); - } - - static Handle - Call(const Arguments& args) { - HandleScope scope; - Local this_object = args.This(); - PyObjectWrapper* pyobjwrap = ObjectWrap::Unwrap(args.This()); - Handle result = pyobjwrap->InstanceCall(args); - return scope.Close(result); - } - - static Handle - ToString(const Arguments& args) { - HandleScope scope; - Local this_object = args.This(); - PyObjectWrapper* pyobjwrap = ObjectWrap::Unwrap(args.This()); - Local result = String::New(pyobjwrap->InstanceToString(args).c_str()); - return scope.Close(result); - } - static Handle ValueOf(const Handle& obj) { - }; - - static Handle - ValueOf(const Arguments& args) { - HandleScope scope; - Local this_object = args.This(); - PyObjectWrapper* pyobjwrap = ObjectWrap::Unwrap(args.This()); - PyObject* py_obj = pyobjwrap->InstanceGetPyObject(); - if(PyCallable_Check(py_obj)) { - Local call = FunctionTemplate::New(Call); - return scope.Close(call->GetFunction()); - } else if (PyNumber_Check(py_obj)) { - long long_result = PyLong_AsLong(py_obj); - return scope.Close(Integer::New(long_result)); - } else if (PySequence_Check(py_obj)) { - int len = PySequence_Length(py_obj); - Local array = Array::New(len); - for(int i = 0; i < len; ++i) { - Handle jsobj = python_function_template_->GetFunction()->NewInstance(); - PyObject* py_obj_out = PySequence_GetItem(py_obj, i); - PyObjectWrapper* obj_out = new PyObjectWrapper(py_obj_out); - obj_out->Wrap(jsobj); - array->Set(i, jsobj); - } - return scope.Close(array); - } else if (PyMapping_Check(py_obj)) { - int len = PyMapping_Length(py_obj); - Local object = Object::New(); - 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); - - Local jsobj = python_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); - return scope.Close(object); - } - return Undefined(); - } - - static Handle - Import(const Arguments& args) { - HandleScope scope; - if(args.Length() < 1 || !args[0]->IsString()) { - return ThrowException( - Exception::Error(String::New("I don't know how to import that.")) - ); - } - PyObject* module_name = PyString_FromString(*String::Utf8Value(args[0]->ToString())); - PyObject* module = PyImport_Import(module_name); - Py_XDECREF(module_name); - - PyObjectWrapper* pyobject_wrapper = new PyObjectWrapper(module); - RETURN_NEW_PYOBJ(scope, module); - } - - static PyObject* - ConvertToPython(const Handle& value) { - int len; - HandleScope scope; - if(value->IsString()) { - return PyString_FromString(*String::Utf8Value(value->ToString())); - } else if(value->IsNumber()) { - return PyFloat_FromDouble(value->NumberValue()); - } else if(value->IsObject()) { - Local obj = value->ToObject(); - if(!obj->FindInstanceInPrototypeChain(python_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()) { - Py_INCREF(Py_None); - return Py_None; - } - return NULL; - } - - PyObject* - InstanceGetPyObject() { - return mPyObject; - } - - Handle 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]); - PyTuple_SET_ITEM(args_tuple, i, py_arg); - } - PyObject* result = PyObject_CallObject(mPyObject, args_tuple); - Py_XDECREF(args_tuple); - if(!result) { - PyErr_Clear(); - return ThrowException( - Exception::Error( - String::New("Python exception") - ) - ); - } else { - RETURN_NEW_PYOBJ(scope, result); - } - return Undefined(); - } - - string InstanceToString(const Arguments& args) { - PyObject* as_string = PyObject_Str(mPyObject); - string native_string(PyString_AsString(as_string)); - Py_XDECREF(as_string); - return native_string; - } - - PyObject* InstanceGet(const string& key) { - if(PyObject_HasAttrString(mPyObject, key.c_str())) { - PyObject* attribute = PyObject_GetAttrString(mPyObject, key.c_str()); - return attribute; - } - return (PyObject*)NULL; - } -}; - -Persistent PyObjectWrapper::python_function_template_; -// all v8 plugins must emit -// a "init" function -extern "C" void -init (Handle target) { - HandleScope scope; - Py_Initialize(); - PyObjectWrapper::Initialize(target); -} - diff --git a/binding.gyp b/binding.gyp new file mode 100644 index 0000000..ebeaa63 --- /dev/null +++ b/binding.gyp @@ -0,0 +1,31 @@ +{ + "targets": [ + { + "target_name": "binding", + "sources": [ + "src/binding.cc", + "src/utils.cc", + "src/py_object_wrapper.cc" + ], + "conditions": [ + ['OS=="mac"', { + "xcode_settings": { + "OTHER_CFLAGS": [ + " +#include +#include +#include "py_object_wrapper.h" +#include "utils.h" + +using namespace v8; +using namespace node; +using std::string; + +Handle eval(const Arguments& args) { + HandleScope scope; + if (args.Length() < 1 || !args[0]->IsString()) { + return ThrowException( + Exception::Error(String::New("A string expression must be provided.")) + ); + } + + PyCodeObject* code = (PyCodeObject*) Py_CompileString(*String::Utf8Value(args[0]->ToString()), "eval", Py_eval_input); + PyObject* main_module = PyImport_AddModule("__main__"); + PyObject* global_dict = PyModule_GetDict(main_module); + PyObject* local_dict = PyDict_New(); + PyObject* obj = PyEval_EvalCode(code, global_dict, local_dict); + PyObject* result = PyObject_Str(obj); + + Py_XDECREF(code); + Py_XDECREF(global_dict); + Py_XDECREF(local_dict); + Py_XDECREF(obj); + + return scope.Close(PyObjectWrapper::New(result)); +} + +Handle finalize(const Arguments& args) { + HandleScope scope; + Py_Finalize(); + return scope.Close(Undefined()); +} + +Handle import(const Arguments& args) { + HandleScope scope; + if (args.Length() < 1 || !args[0]->IsString()) { + return ThrowException( + Exception::Error(String::New("I don't know how to import that.")) + ); + } + + PyObject* module_name; + PyObject* module; + + module_name = PyUnicode_FromString(*String::Utf8Value(args[0]->ToString())); + module = PyImport_Import(module_name); + + if (PyErr_Occurred()) { + return ThrowPythonException(); + } + + if (!module) { + return ThrowPythonException(); + } + Py_XDECREF(module_name); + + return scope.Close(PyObjectWrapper::New(module)); +} + +void init (Handle exports) { + HandleScope scope; + + Py_Initialize(); + PyObjectWrapper::Initialize(); + + // how to schedule Py_Finalize(); to be called when process exits? + + // module.exports.eval + exports->Set( + String::NewSymbol("eval"), + FunctionTemplate::New(eval)->GetFunction() + ); + + // module.exports.finalize + exports->Set( + String::NewSymbol("finalize"), + FunctionTemplate::New(finalize)->GetFunction() + ); + + // module.exports.import + exports->Set( + String::NewSymbol("import"), + FunctionTemplate::New(import)->GetFunction() + ); + + // module.exports.PyObject + exports->Set( + String::NewSymbol("PyObject"), + PyObjectWrapper::py_function_template->GetFunction() + ); + +} + +NODE_MODULE(binding, init) \ No newline at end of file diff --git a/src/py_object_wrapper.cc b/src/py_object_wrapper.cc new file mode 100644 index 0000000..a8f472f --- /dev/null +++ b/src/py_object_wrapper.cc @@ -0,0 +1,317 @@ +#include +#include "py_object_wrapper.h" +#include "utils.h" +#include "datetime.h" + +Persistent PyObjectWrapper::py_function_template; + +void PyObjectWrapper::Initialize() { + HandleScope scope; + + PyDateTime_IMPORT; + + Local fn_tpl = FunctionTemplate::New(); + Local proto = fn_tpl->PrototypeTemplate(); + Local obj_tpl = fn_tpl->InstanceTemplate(); + + obj_tpl->SetInternalFieldCount(1); + + // this has first priority. see if the properties already exist on the python object + obj_tpl->SetNamedPropertyHandler(Get, Set); + + // If we're calling `toString`, delegate to our version of ToString + proto->SetAccessor(String::NewSymbol("toString"), ToStringAccessor); + + // likewise for valueOf + obj_tpl->SetAccessor(String::NewSymbol("valueOf"), ValueOfAccessor); + + // Python objects can be called as functions. + obj_tpl->SetCallAsFunctionHandler(Call, Handle()); + + py_function_template = Persistent::New(fn_tpl); +} + +Handle PyObjectWrapper::New(PyObject* obj) { + HandleScope scope; + Local jsVal; + + // 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)); + } + } + + if(PyErr_Occurred()) { + Py_XDECREF(obj); + 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); +} + +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 Handle(); +} + +Handle PyObjectWrapper::Set(Local key, Local value, const AccessorInfo& info) { + // we don't know what to do. + return Undefined(); +} + +Handle PyObjectWrapper::CallAccessor(Local property, const AccessorInfo& info) { + HandleScope scope; + Local func = FunctionTemplate::New(Call); + return scope.Close(func->GetFunction()); +} + +Handle PyObjectWrapper::ToStringAccessor(Local property, const AccessorInfo& info) { + HandleScope scope; + Local func = FunctionTemplate::New(ToString); + return scope.Close(func->GetFunction()); +} + +Handle PyObjectWrapper::ValueOfAccessor(Local property, const AccessorInfo& info) { + HandleScope scope; + Local func = FunctionTemplate::New(ValueOf); + return scope.Close(func->GetFunction()); +} + +Handle PyObjectWrapper::Call(const Arguments& args) { + HandleScope scope; + PyObjectWrapper* pyobjwrap = ObjectWrap::Unwrap(args.This()); + Handle result = pyobjwrap->InstanceCall(args); + return scope.Close(result); +} + +Handle PyObjectWrapper::ToString(const Arguments& args) { + HandleScope scope; + PyObjectWrapper* pyobjwrap = ObjectWrap::Unwrap(args.This()); + Local result = String::New(pyobjwrap->InstanceToString(args).c_str()); + return scope.Close(result); +} + +Handle PyObjectWrapper::ValueOf(const Arguments& args) { + HandleScope scope; + PyObjectWrapper* pyobjwrap = ObjectWrap::Unwrap(args.This()); + PyObject* py_obj = pyobjwrap->InstanceGetPyObject(); + if(PyCallable_Check(py_obj)) { + Local call = FunctionTemplate::New(Call); + return scope.Close(call->GetFunction()); + } else if (PyNumber_Check(py_obj)) { + long long_result = PyLong_AsLong(py_obj); + return scope.Close(Integer::New(long_result)); + } else if (PySequence_Check(py_obj)) { + int len = PySequence_Length(py_obj); + Local array = Array::New(len); + for(int i = 0; i < len; ++i) { + Handle jsobj = PyObjectWrapper::py_function_template->GetFunction()->NewInstance(); + PyObject* py_obj_out = PySequence_GetItem(py_obj, i); + PyObjectWrapper* obj_out = new PyObjectWrapper(py_obj_out); + obj_out->Wrap(jsobj); + array->Set(i, jsobj); + } + return scope.Close(array); + } else if (PyMapping_Check(py_obj)) { + int len = PyMapping_Length(py_obj); + Local object = Object::New(); + 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); + + 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); + return scope.Close(object); + } + return Undefined(); +} + +PyObject* PyObjectWrapper::ConvertToPython(const Handle& value) { + int len; + HandleScope scope; + + 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; + } + } else if (value->IsNull() || value->IsUndefined()) { + 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; + } + } + 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()) { + 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); + } + PyObject* result = PyObject_CallObject(mPyObject, args_tuple); + if (PyErr_Occurred()) { + return ThrowPythonException(); + } + + Py_XDECREF(args_tuple); + + if(result) { + return scope.Close(PyObjectWrapper::New(result)); + } else { + return ThrowPythonException(); + } +} + +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; +} + +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*)NULL; +} diff --git a/src/py_object_wrapper.h b/src/py_object_wrapper.h new file mode 100644 index 0000000..9020fa0 --- /dev/null +++ b/src/py_object_wrapper.h @@ -0,0 +1,59 @@ + +#ifndef PY_OBJECT_WRAPPER_H +#define PY_OBJECT_WRAPPER_H + +#include + +#include +#include + +#include "utils.h" + +using namespace v8; +using std::string; + +class PyObjectWrapper : public node::ObjectWrap { + PyObject* mPyObject; + public: + static Persistent py_function_template; + PyObjectWrapper(PyObject* obj) : node::ObjectWrap(), mPyObject(obj) {}; + virtual ~PyObjectWrapper() { + Py_XDECREF(mPyObject); + mPyObject = NULL; + } + + static void Initialize(); + + static Handle New(PyObject* obj); + static Handle New(const Arguments& args); + + static Handle Get(Local key, const AccessorInfo& info); + static Handle Set(Local key, Local value, const AccessorInfo& info); + + static Handle CallAccessor(Local property, const AccessorInfo& info); + + static Handle ToStringAccessor(Local property, const AccessorInfo& info); + + static Handle ValueOfAccessor(Local property, const AccessorInfo& info); + + static Handle Call(const Arguments& args); + + static Handle ToString(const Arguments& args); + + static Handle ValueOf(const Arguments& args); + + static PyObject* ConvertToPython(const Handle& value); + + PyObject* InstanceGetPyObject() { + return mPyObject; + }; + + Handle InstanceCall(const Arguments& args); + + string InstanceToString(const Arguments& args); + + PyObject* InstanceGet(const string& key); +}; + + +#endif diff --git a/src/utils.cc b/src/utils.cc new file mode 100644 index 0000000..dd395e1 --- /dev/null +++ b/src/utils.cc @@ -0,0 +1,88 @@ + +#include +#include + +#include "utils.h" + +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: "); + + if (ptype != NULL) { + msg = v8::String::Concat(msg, v8::String::New(PyString_AsString(PyObject_Str(PyObject_GetAttrString(ptype, "__name__"))))); + msg = v8::String::Concat(msg, v8::String::New(": ")); + + if (pvalue != NULL) { + msg = v8::String::Concat(msg, v8::String::New(PyString_AsString(PyObject_Str(pvalue)))); + } + + msg = v8::String::Concat(msg, v8::String::New("\n")); + } + + if (ptraceback != NULL) { + + 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); + + if (pyth_func) { + PyObject *pyth_val, *pystr, *ret; + char *str; + + char *full_backtrace; + + pyth_val = PyObject_CallFunctionObjArgs(pyth_func, ptype, pvalue, ptraceback, NULL); + ret = PyUnicode_Join(PyUnicode_FromString(""), pyth_val); + pystr = PyObject_Str(ret); + str = PyString_AsString(pystr); + full_backtrace = strdup(str); + + Py_DECREF(pyth_func); + Py_DECREF(pyth_val); + Py_DECREF(pystr); + Py_DECREF(str); + + msg = v8::String::Concat(msg, v8::String::New("\n")); + msg = v8::String::Concat(msg, v8::String::New(full_backtrace)); + } else { + msg = v8::String::Concat(msg, v8::String::New("\n")); + msg = v8::String::Concat(msg, v8::String::New(PyString_AsString(PyObject_Str(ptraceback)))); + } + + } + + Local err; + if (PyErr_GivenExceptionMatches(ptype, PyExc_ReferenceError)) { + err = Exception::ReferenceError(msg); + } + else if (PyErr_GivenExceptionMatches(ptype, PyExc_SyntaxError)) { + err = Exception::SyntaxError(msg); + } + else if (PyErr_GivenExceptionMatches(ptype, PyExc_TypeError)) { + err = Exception::TypeError(msg); + } + else { + err = Exception::Error(msg); + } + + // @TODO : handle stacktrace + + Py_XDECREF(ptype); + Py_XDECREF(pvalue); + Py_XDECREF(ptraceback); + + return ThrowException(err); +} diff --git a/src/utils.h b/src/utils.h new file mode 100644 index 0000000..34139e3 --- /dev/null +++ b/src/utils.h @@ -0,0 +1,6 @@ + +#include + +using namespace v8; + +Handle ThrowPythonException(); diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..5d878d8 --- /dev/null +++ b/test/index.js @@ -0,0 +1,112 @@ +var python = require('../'); +var PythonError = python.PythonError; +var should = require('should'); + +describe('node-python', function () { + describe('eval', function () { + it('should return resulting value from python statement executed', function () { + var value = python.eval('"1"'); + value.should.equal("1"); + }); + it('should return resulting value from python statement executed, converting to string with complex types', function () { + var decimal = python.import('decimal'); + var smallNum = decimal.Decimal('0.0000000001'); + smallNum.toString().should.equal('1E-10'); + }); + }); + describe('import', function () { + it('should return object representing module imported, containing functions from imported module', function () { + var value = python.import('decimal'); + value.should.have.property('valueOf'); + }); + it('should throw a PythonError when importing a module that does not exist', function () { + should(function () { + python.import('jibberish'); + }).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/) + }); + }); + it('should convert javascript null to python NoneType', function () { + test = python.import('test2'); + var type = test.getPythonTypeName(null); + type.should.equal('NoneType'); + }); + it('should convert javascript undefined to python NoneType', function () { + test = python.import('test2'); + var type = test.getPythonTypeName(undefined); + type.should.equal('NoneType'); + }); + it('should convert javascript booleans to python booleans', function () { + test = python.import('test2'); + var type = test.getPythonTypeName(true); + type.should.equal('bool'); + }); + it('should convert javascript date to python date', function () { + test = python.import('test2'); + var type = test.getPythonTypeName(new Date()); + type.should.equal('datetime'); + }); + it('should convert javascript numbers to python floats', function () { + test = python.import('test2'); + var type = test.getPythonTypeName(1); + type.should.equal('float'); + }); + it('should convert javascript arrays to python list', function () { + test = python.import('test2'); + var type = test.getPythonTypeName([]); + type.should.equal('list'); + }); + it('should convert javascript objects to python dictionaries', function () { + test = python.import('test2'); + var type = test.getPythonTypeName({}); + type.should.equal('dict'); + }); + it('should convert javascript nested objects correctly', function () { + test = python.import('test2'); + var type = test.getPythonTypeName2({ + value: 1 + }, 'value'); + type.should.equal('float'); + var type = test.getPythonTypeName2({ + value: true + }, 'value'); + type.should.equal('bool'); + var type = test.getPythonTypeName2({ + value: new Date() + }, 'value'); + type.should.equal('datetime'); + var type = test.getPythonTypeName2({ + value: {} + }, 'value'); + type.should.equal('dict'); + var type = test.getPythonTypeName2({ + value: ['one', 'two', 'three'] + }, 'value'); + type.should.equal('list'); + var i = 0, arr = []; + while (i < 10000) { + arr.push(Math.random().toString()) + i++; + } + var type = test.getPythonTypeName(arr); + type.should.equal('list'); + }); + it('should convert python dicts to javascript objects', function () { + test = python.import('test2'); + var value = test.getPythonValue({ + value: 1 + }); + value.should.have.property('value', 1); + }); + it('should convert python lists to javascript arrays', function () { + test = python.import('test2'); + var value = test.getPythonValue([ 1, 2, 3]); + value.should.containEql(1); + value.should.containEql(2); + value.should.containEql(3); + }); +}); \ No newline at end of file diff --git a/test/linker.js b/test/linker.js new file mode 100644 index 0000000..a8f5aed --- /dev/null +++ b/test/linker.js @@ -0,0 +1,2 @@ + +require('../index.js'); diff --git a/test/support/__init__.py b/test/support/__init__.py new file mode 100644 index 0000000..ec7dc75 --- /dev/null +++ b/test/support/__init__.py @@ -0,0 +1 @@ +__author__ = 'matt walters' diff --git a/test/support/test.py b/test/support/test.py new file mode 100644 index 0000000..5fbea7f --- /dev/null +++ b/test/support/test.py @@ -0,0 +1,7 @@ +class Good(): + def good(self): + return 0; + +class Bad(): + def bad(self): + should cause parse error \ No newline at end of file diff --git a/test/support/test2.py b/test/support/test2.py new file mode 100644 index 0000000..7d8fe71 --- /dev/null +++ b/test/support/test2.py @@ -0,0 +1,7 @@ +def getPythonTypeName(value): + return type(value).__name__ +def getPythonTypeName2(value, index): + item = value[index] + return type(item).__name__ +def getPythonValue(value): + return value \ No newline at end of file diff --git a/test/support/test2.pyc b/test/support/test2.pyc new file mode 100644 index 0000000..6c6520c Binary files /dev/null and b/test/support/test2.pyc differ diff --git a/wscript b/wscript deleted file mode 100644 index 1b1f060..0000000 --- a/wscript +++ /dev/null @@ -1,34 +0,0 @@ -import Options -from os import unlink, symlink, popen -from os.path import exists - -srcdir = '.' -blddir = 'build' -VERSION = '0.0.1' - -def set_options(opt): - opt.tool_options('compiler_cxx') - opt.tool_options('python') - -def configure(conf): - conf.check_tool('compiler_cxx') - conf.check_tool('node_addon') - conf.check_tool('osx') - conf.check_tool('python') - -def build(bld): - obj = bld.new_task_gen('cxx', 'shlib', 'node_addon', 'py', 'pyembed', 'pyext') - obj.env['FRAMEWORK'] = 'python' - obj.target = 'binding' - obj.source = "binding.cc" - obj.init_py() - obj.init_pyembed() - -def shutdown(): - # HACK to get binding.node out of build directory. - # better way to do this? - if Options.commands['clean']: - if exists('binding.node'): unlink('binding.node') - else: - if exists('build/default/binding.node') and not exists('binding.node'): - symlink('build/default/binding.node', 'binding.node') diff --git a/wsgi.js b/wsgi.js deleted file mode 100644 index 1a2a2cb..0000000 --- a/wsgi.js +++ /dev/null @@ -1,62 +0,0 @@ -var sys = require('sys'), - puts = sys.puts, - binding = require('./binding'), - path_additions = require('./path_additions'), - http = require('http'), - url = require('url'), - stdin = process.openStdin(); - -var sys = binding.import('sys'); -var os = binding.import('os'); - -os.environ.update({ - 'DJANGO_SETTINGS_MODULE':'project.development', -}); - -/* -var gary_busey = binding.import("gary_busey"); -var result = gary_busey.say_hey("man i suck"); -*/ -var django_wsgi = binding.import('django.core.handlers.wsgi'); - -var wsgi_handler = django_wsgi.WSGIHandler() -wsgi_handler.load_middleware(); - -var server = http.createServer(function (req, res) { - var path_and_query = url.parse(req.url); - if(!path_and_query.pathname.match(/^\/media/)) { - var wsgi_request = django_wsgi.WSGIRequest({ - 'PATH_INFO':path_and_query.pathname, - 'QUERY_STRING':path_and_query.query, - 'HTTP_VERSION':req.httpVersion, - 'HTTP_ACCEPT':req.headers['http-accept'], - 'HTTP_ACCEPT_CHARSET':req.headers['http-accept-charset'], - 'HTTP_ACCEPT_ENCODING':req.headers['http-accept-encoding'], - 'HTTP_ACCEPT_LANGUAGE':req.headers['http-accept-language'], - 'HTTP_CACHE_CONTROL':req.headers['http-cache-control'], - 'REQUEST_METHOD':req.method, - 'HTTP_HOST':req.headers['http-host'] - }); - var response = wsgi_handler.get_response(wsgi_request), - headers = response._headers.valueOf(), - content = response.content.toString(), - headers_out = {}, - status_code = response.status_code.valueOf(); - - for(var i in headers) { - var as_array = headers[i].valueOf(); - headers_out[as_array[0]] = as_array[1].toString(); - }; - res.writeHead(status_code, headers_out); - res.write(content); - res.end(); - } else { - res.writeHead(200, {"Content-Type":"text/html"}); - res.write("

sorry, no images.

"); - res.end(); - } -}).listen(8000); -process.addListener('SIGINT', function () { - server.close(); - puts("Shutting down..."); -});