diff --git a/.gitignore b/.gitignore index 378eac2..5990836 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ build +node_modules +.DS_Store +.cproject +.project +.settings 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/index.js b/index.js index e205f21..2c6d5fc 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,66 @@ +var binding = require('bindings')('binding.node'); +var debug = require('debug')('node-python'); +var warn = require('debug')('node-python:warn'); +var util = require('util'); -module.exports = require('bindings')('binding.node') +function PythonError (message, value) { + if (arguments[0] instanceof Error) { + var error = arguments[0]; + this.message = error.message; + this.stack = error.stack; + this.value = error.value; + Error.apply(this, arguments); + } else { + Error.call(this, message, value); + } +} +util.inherits(PythonError, Error); + +module.exports.PythonError = PythonError; + +var finalized = false; +function pythonFinalized () { + if (finalized) { + warn('node-python\'s python interpreter has already been finalized. python code cannot be executed.'); + } + return finalized; +} + +module.exports.eval = function (string) { + if (pythonFinalized()) throw new PythonError('node-python\'s python interpreter has already been finalized. python code cannot be executed.'); + return binding.eval(string); +} + +module.exports.finalize = function () { + if ( ! pythonFinalized()) { + binding.finalize(); + finalized = true; + return finalized; + } + return false; +} + +var _import = module.exports.import = function (string) { + if (pythonFinalized()) throw new PythonError('node-python\'s python interpreter has already been finalized. python code cannot be executed.'); + + var result = null; + + try { + result = binding.import(string); + } catch (e) { + e = new PythonError(e); + debug(e); + throw e; + } + + return result; +} + +var os = _import('os'); + +var pythonPath = os.environ.get('PYTHONPATH'); + +if (pythonPath === undefined) { + debug('WARNING: PYTHONPATH environment variable is undefined. This may cause problems finding python modules. see: https://docs.python.org/2/tutorial/modules.html#the-module-search-path'); +} \ No newline at end of file diff --git a/package.json b/package.json index 06f0bad..46b4829 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-python", - "version": "0.0.4", + "version": "v0.0.5-rc4", "description": "Call python stuff from nodejs", "main": "index.js", "repository": { @@ -24,12 +24,18 @@ } ], "scripts": { - "test": "node test/linker.js" + "test": "make test" }, "license": "BSD", "gypfile": true, "readmeFilename": "README.md", "dependencies": { - "bindings": "~1.1.1" + "bindings": "~1.1.1", + "debug": "^1.0.4" + }, + "devDependencies": { + "mocha": "^1.21.3", + "should": "^4.0.4", + "sinon": "^1.10.3" } } diff --git a/src/binding.cc b/src/binding.cc index cae0aab..0ee4e2b 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -1,7 +1,6 @@ - #include #include - +#include #include "py_object_wrapper.h" #include "utils.h" @@ -9,16 +8,54 @@ 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()) { + 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); - if(!module) { + + 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); @@ -26,13 +63,26 @@ Handle import(const Arguments& args) { return scope.Close(PyObjectWrapper::New(module)); } - void init (Handle exports) { HandleScope scope; - Py_Initialize(); + 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"), @@ -47,4 +97,4 @@ void init (Handle exports) { } -NODE_MODULE(binding, init) +NODE_MODULE(binding, init) \ No newline at end of file diff --git a/src/py_object_wrapper.cc b/src/py_object_wrapper.cc index 370b391..a8f472f 100644 --- a/src/py_object_wrapper.cc +++ b/src/py_object_wrapper.cc @@ -1,11 +1,15 @@ - +#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(); @@ -35,6 +39,28 @@ Handle PyObjectWrapper::New(PyObject* obj) { 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); @@ -74,7 +100,6 @@ Handle PyObjectWrapper::New(PyObject* obj) { else { Py_XDECREF(obj); } - return scope.Close(jsVal); } @@ -177,37 +202,72 @@ Handle PyObjectWrapper::ValueOf(const Arguments& args) { PyObject* PyObjectWrapper::ConvertToPython(const Handle& value) { int len; HandleScope scope; + if(value->IsString()) { return PyString_FromString(*String::Utf8Value(value->ToString())); - } else if(value->IsNumber()) { + } 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()) { - 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; - } + 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; + 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; } @@ -219,12 +279,21 @@ Handle PyObjectWrapper::InstanceCall(const Arguments& args) { 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 { @@ -243,6 +312,6 @@ 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/utils.cc b/src/utils.cc index f4a8274..dd395e1 100644 --- a/src/utils.cc +++ b/src/utils.cc @@ -7,26 +7,71 @@ 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(!ptype) { + if(pvalue == NULL) { return ThrowException( Exception::Error(String::New("No exception found")) ); } // handle exception message - Local msg; - if(pvalue && PyObject_TypeCheck(pvalue, &PyString_Type)) { - msg = String::New(PyString_AsString(pvalue)); + 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)) { + if (PyErr_GivenExceptionMatches(ptype, PyExc_ReferenceError)) { err = Exception::ReferenceError(msg); } - else if(PyErr_GivenExceptionMatches(ptype, PyExc_SyntaxError)) { + else if (PyErr_GivenExceptionMatches(ptype, PyExc_SyntaxError)) { err = Exception::SyntaxError(msg); } - else if(PyErr_GivenExceptionMatches(ptype, PyExc_TypeError)) { + else if (PyErr_GivenExceptionMatches(ptype, PyExc_TypeError)) { err = Exception::TypeError(msg); } else { 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/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