Repository: flightaware/tohil (language: python)

Issue:
There is currently no way to pass None to a Python function when using `::tohil::call`
This may not have a satisfactory solution, but I think it's something worth thinking about (special ::tohil::NONE sentinel value perhaps?). This is typically easy to work around as Python functions most often treat None as a default value rather than it having a specific meaning, so we can just not pass the arg at all.


Discussion / hints:

Fix patch:
diff --git a/Doc/reference/tohil_tcl_functions.rst b/Doc/reference/tohil_tcl_functions.rst
index 4b3eb17..69a7cb6 100644
--- a/Doc/reference/tohil_tcl_functions.rst
+++ b/Doc/reference/tohil_tcl_functions.rst
@@ -22,6 +22,11 @@ from Tcl interpreter, the following commands are available:
    If *-kwlist list* is specified, *list* contains key-value pairs
    that will be passed to the function as named parameters.
 
+   When you use tohil::call, Tohil converts all of your arguments
+   to Python Unicode, unless an argument is comprised of the special
+   sentinel `tohil::NONE`, in which case the Python "None" data type
+   is substituted in place of that argument.
+
 .. function:: tohil::eval evalString
 
    *evalString* contains a valid Python expression.  Tohil
diff --git a/Doc/tutorial/tohil_tcl.rst b/Doc/tutorial/tohil_tcl.rst
index 9e9fd5e..4de42d5 100644
--- a/Doc/tutorial/tohil_tcl.rst
+++ b/Doc/tutorial/tohil_tcl.rst
@@ -141,8 +141,8 @@ or whatever, that unless you are very careful, various characters
 can cause your Python not to parse properly.  For example, a single
 quote in a name, quotes in general, and other stuff.
 
-Tohil provides *tohil::call* to make it possible to call a Python
-function and make sure that the arguments you pass to the function
+Tohil provides *tohil::call* to provide a way to call a Python
+function while making sure that the arguments you pass to the function
 are not interpreted by Python along the way.
 
 
@@ -151,9 +151,9 @@ arguments, without having to pass it through Python's eval or exec and running
 the risk that Python metacharacters appearing in the data will cause quoting
 problems, accidental code execution, etc.
 
-tohil::import provides a way to import Python modules, although it's not much
-different from doing a tohil::exec "import module"
-
+When you use tohil::call, Tohil converts all of your arguments to Python
+Unicode, unless an argument is comprised of the special sentinel `tohil::NONE`,
+in which case that argument is replaced by the Python "None" data type.
 
 ***************
 tohil::interact
diff --git a/generic/tohil.c b/generic/tohil.c
index 80b7fa6..9dc22a3 100644
--- a/generic/tohil.c
+++ b/generic/tohil.c
@@ -74,6 +74,17 @@ typedef struct {
 
 // TCL library begins here
 
+#define TOHIL_NONE_SENTINEL "tohil::NONE"
+
+// tohil_TclObjIsNoneSentinel - return true if a Tcl object matches the
+// tohil "none" sentinel, else false.
+static int
+tohil_TclObjIsNoneSentinel(Tcl_Obj *obj)
+{
+    char *tclString = Tcl_GetString(obj);
+    return (STREQU(tclString, TOHIL_NONE_SENTINEL));
+}
+
 //
 // tohil_TclObjToUTF8DString - convert a Tcl object (string in WTF-8) to real UTF-8
 // for Python. Use a DString for buffering.
@@ -704,7 +715,7 @@ TohilCall_Cmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *cons
     const char *objandfn = tohil_TclObjToUTF8DString(interp, objv[1], &ds);
     int objStart = 2;
 
-    if (*objandfn == '-' && STREQU(objandfn, "-kwlist")) {
+    if (STREQU(objandfn, "-kwlist")) {
         if (objc < 4)
             goto wrongargs;
         kwObj = tclListObjToPyDictObject(interp, objv[2]);
@@ -764,15 +775,19 @@ TohilCall_Cmd(ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *cons
 
     // if there are no positional arguments, we will
     // call PyTuple_New with a 0 argument, producing
-    // a 0-length tuple.  whil PyObject_Call's kwargs
+    // a 0-length tuple.  while PyObject_Call's kwargs
     // argument can be NULL, args must not; an empty
-    // tuple should be used
+    // tuple should be used.
     int i;
     PyObject *pArgs = PyTuple_New(objc - objStart);
     PyObject *curarg = NULL;
     for (i = objStart; i < objc; i++) {
-        curarg = PyUnicode_FromString(tohil_TclObjToUTF8DString(interp, objv[i], &ds));
-        Tcl_DStringFree(&ds);
+        if (tohil_TclObjIsNoneSentinel(objv[i])) {
+            curarg = Py_None;
+        } else {
+            curarg = PyUnicode_FromString(tohil_TclObjToUTF8DString(interp, objv[i], &ds));
+            Tcl_DStringFree(&ds);
+        }
         if (curarg == NULL) {
             Py_DECREF(pArgs);
             Py_DECREF(pFn);


Test patch:
diff --git a/tests/tohil.test b/tests/tohil.test
index 7d9df77..7fa08b7 100755
--- a/tests/tohil.test
+++ b/tests/tohil.test
@@ -195,6 +195,27 @@ test tohil_call-1.12 {call of nonexistent object methods} \
 	-returnCodes error \
 	-result {invalid syntax (<string>, line 1)}
 
+test tohil_call-1.13 {call with tohil::NONE sentinel for None type} \
+	-body {
+		tohil::exec {def a(x): return x == None}
+		tohil::call a tohil::NONE
+	} \
+	-result 1
+
+test tohil_call-1.14 {call with not quite tohil::NONE sentinel} \
+	-body {
+		tohil::exec {def a(x): return x == None}
+		tohil::call a tohil::NONEzee
+	} \
+	-result 0
+
+test tohil_call-1.15 {call with extra tohil::NONE sentinel action} \
+	-body {
+		tohil::exec {def a(x, y, z): return str(x) + ' ' + str(y) + ' ' + str(z)}
+		tohil::call a tohil::NONE 42 tohil::NONE
+	} \
+	-result "None 42 None"
+
 # =========
 # TYPES
 # =========