├── .gitignore ├── ChangeLog ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── generic └── tclpy.c ├── notes.md ├── pkgIndex.tcl.in └── tests └── tclpy.test /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | 3 | # Intermediate build files 4 | tclpy.o 5 | 6 | # Build output 7 | libtclpy*.so 8 | pkgIndex.tcl 9 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 0.4 2 | 3 | * Expose the tcl interpreter in a Python module. 4 | * Allow importing libtclpy in a parent Python interpreter. 5 | * Added 'tclpy.eval' command for Python. 6 | * Updated to Python 3 7 | 8 | 0.3 - 2014-08-13 9 | 10 | * Use tcl stubs for forwards compatibility of tcl versions 11 | * C extensions should work properly 12 | * None, True and False are converted to sane tcl values 13 | * Allow null bytes returned in python strings 14 | * Define unicode behaviour 15 | * Python sequences are converted to tcl lists 16 | * Python mapping objects are converted to tcl dicts 17 | * Python number objects are now supported explicitly 18 | 19 | 0.2 - 2014-05-30 20 | 21 | * Added 'py import' command 22 | * Added 'py call' command 23 | * Handle exceptions in a sane way 24 | * Remove autotools 25 | 26 | 0.1 - 2014-01-05 27 | 28 | * Initial release with 'py eval' command 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tianon/centos:5.8 2 | 3 | #ADD http://pkgs.repoforge.org/rpmforge-release/rpmforge-release-0.5.3-1.el5.rf.x86_64.rpm /root/ 4 | #RUN rpm --import http://apt.sw.be/RPM-GPG-KEY.dag.txt 5 | #RUN rpm -K rpmforge-release-0.5.3-1.el5.rf.*.rpp 6 | #RUN rpm -i rpmforge-release-0.5.3-1.el5.rf.*.rpm 7 | 8 | RUN yum update -y 9 | RUN yum install -y make gcc44 10 | RUN ln -s /usr/bin/gcc44 /usr/bin/gcc 11 | 12 | ADD http://prdownloads.sourceforge.net/tcl/tcl8.5.15-src.tar.gz /root/ 13 | RUN cd /root && tar xf tcl8.5.15-src.tar.gz 14 | RUN cd /root/tcl8.5.15/unix && ./configure --prefix=/opt/tcl && make && make install 15 | ENV PATH /opt/tcl/bin:$PATH 16 | ENV LD_LIBRARY_PATH /opt/tcl/lib 17 | ENV LIBRARY_PATH /opt/tcl/lib 18 | 19 | RUN yum install -y zlib-devel sqlite-devel 20 | ADD https://www.python.org/ftp/python/2.7.8/Python-2.7.8.tgz /root/ 21 | RUN cd /root && tar xf /root/Python-2.7.8.tgz 22 | RUN cd /root/Python-2.7.8 && ./configure --enable-shared --prefix=/opt/python && make && make install 23 | ENV PATH /opt/python/bin:$PATH 24 | ENV LD_LIBRARY_PATH /opt/python/lib 25 | ENV LIBRARY_PATH /opt/python/lib 26 | 27 | ADD . /root/libtclpy 28 | RUN ln -s /opt/tcl/bin/tclsh8.5 /opt/tcl/bin/tclsh 29 | RUN cd /root/libtclpy && make TCLCONFIG=/opt/tcl/lib/tclConfig.sh && make test 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Aidan Hobson Sayers 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. The name of the author may not be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PACKAGE_VERSION = 0.4 2 | 3 | DFLAGS = -DPACKAGE_VERSION='"$(PACKAGE_VERSION)"' 4 | 5 | # Flags to improve security 6 | CFLAGS_SEC = \ 7 | -fstack-protector \ 8 | --param=ssp-buffer-size=4 \ 9 | -Wformat \ 10 | -Werror=format-security \ 11 | -D_FORTIFY_SOURCE=2\ 12 | -Wl,-z,relro,-z,now 13 | # Protect against my own poor programming 14 | CFLAGS_SAFE = -fno-strict-overflow 15 | # Tell me when I'm doing something wrong 16 | CFLAGS_WARN = \ 17 | -Wall -Wextra \ 18 | -Wstrict-aliasing -Wstrict-overflow -Wstrict-prototypes 19 | # Not interested in these warnings 20 | CFLAGS_NOWARN = -Wno-unused-parameter 21 | # Speed things up 22 | CFLAGS_FAST = -O2 23 | 24 | CFLAGS = \ 25 | $(CFLAGS_SEC) $(CFLAGS_SAFE) \ 26 | $(CFLAGS_WARN) $(CFLAGS_NOWARN) \ 27 | $(CFLAGS_FAST) 28 | 29 | # TODO: 30 | # - check python-config works 31 | # - check stubs are supported (TCL_SUPPORTS_STUBS) 32 | 33 | TCL_STUBS ?= 1 34 | TCLCONFIG ?= $(shell \ 35 | (X=/usr/lib/tcl8.6/tclConfig.sh; test -f $$X && echo $$X || exit 1) || \ 36 | (X=/usr/lib64/tclConfig.sh; test -f $$X && echo $$X || exit 1) || \ 37 | (X=/usr/lib/tclConfig.sh; test -f $$X && echo $$X || exit 1) || \ 38 | echo "" \ 39 | ) 40 | TCLCONFIG_TEST = test -f "$(TCLCONFIG)" || (echo "Couldn't find tclConfig.sh" && exit 1) 41 | 42 | TCL_LIB = $(shell . "$(TCLCONFIG)"; \ 43 | if [ "$(TCL_STUBS)" = 1 ]; then \ 44 | echo "$$TCL_STUB_LIB_SPEC -DUSE_TCL_STUBS"; \ 45 | else \ 46 | echo "$$TCL_LIB_SPEC"; \ 47 | fi \ 48 | ) 49 | TCL_INCLUDE = $(shell . "$(TCLCONFIG)"; echo $$TCL_INCLUDE_SPEC) 50 | PY_LIB = $(shell python3-config --libs) 51 | PY_INCLUDE = $(shell python3-config --includes) 52 | 53 | PY_LIBFILE = $(shell python3 -c 'import distutils.sysconfig; print(distutils.sysconfig.get_config_var("LDLIBRARY"))') 54 | CFLAGS += -DPY_LIBFILE='"$(PY_LIBFILE)"' 55 | 56 | default: libtclpy$(PACKAGE_VERSION).so 57 | 58 | libtclpy$(PACKAGE_VERSION).so: tclpy.o pkgIndex.tcl 59 | @$(TCLCONFIG_TEST) 60 | rm -f libtclpy.so tclpy.so 61 | gcc -shared -fPIC $(CFLAGS) $< -o $@ -Wl,--export-dynamic $(TCL_LIB) $(PY_LIB) 62 | ln -s $@ libtclpy.so 63 | ln -s libtclpy.so tclpy.so 64 | 65 | tclpy.o: generic/tclpy.c 66 | @$(TCLCONFIG_TEST) 67 | gcc -fPIC $(CFLAGS) $(DFLAGS) $(PY_INCLUDE) $(TCL_INCLUDE) -c $< -o $@ 68 | 69 | pkgIndex.tcl: pkgIndex.tcl.in 70 | sed "s/PACKAGE_VERSION/$(PACKAGE_VERSION)/g" $< > $@ 71 | 72 | clean: 73 | rm -f *.so *.o pkgIndex.tcl 74 | 75 | test: default 76 | TCLLIBPATH=. tclsh tests/tclpy.test 77 | 78 | .PHONY: clean test default 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | libtclpy 2 | ======== 3 | 4 | This is a Tcl extension to effortlessly to call bidirectionally between Tcl and 5 | Python, targeting Tcl >= 8.5 and Python 3.6+ 6 | 7 | The extension is available under the 3-clause BSD license (see "LICENSE"). 8 | 9 | Tcl users may also want to consider using 10 | [pyman](http://chiselapp.com/user/gwlester/repository/pyman/home), a Tcl package 11 | that provides a higher level of abstraction on top of tclpy. 12 | 13 | USAGE 14 | ----- 15 | 16 | You can import the libtclpy in either a Tcl or Python parent interpreter. Doing 17 | so will initialise an interpreter for the other language and insert all 18 | libtclpy methods. This means you can call backwards and forwards between 19 | interpreters. 20 | 21 | *FROM TCL* 22 | 23 | General notes: 24 | - Unless otherwise noted, 'interpreter' refers to the python interpreter. 25 | - All commands are run in the context of a single interpreter session. Imports, 26 | function definitions and variables persist. 27 | - Exceptions in the python interpreter will return a stack trace of the python 28 | code that was executing. If the exception continues up the stack, the tcl 29 | stack trace will be appended to it. 30 | They may be masked (as per tcl stack traces) with catch. 31 | 32 | Reference: 33 | - `py call ?obj.?func ?arg ...?` 34 | - `takes: name of a python function` 35 | - `returns: return value of function with the first appropriate conversion 36 | applied from the list below:` 37 | - `None is converted to an empty string` 38 | - `True is converted to 1` 39 | - `False is converted to 0` 40 | - `Python 'str' objects are considered to be byte arrays` 41 | - `Python 'unicode' objects are considered to be unicode strings` 42 | - `Python 'number' objects are converted to base 10 if necessary` 43 | - `Python mapping objects (supporting key-val mapping, e.g. python dicts) 44 | are converted to tcl dicts` 45 | - `Python sequence objects (supporting indexing, e.g. python lists) are 46 | converted to tcl lists` 47 | - `Otherwise, the str function is applied to the python object` 48 | - `side effects: executes function` 49 | - `func` may be a dot qualified name (i.e. object or module method) 50 | - `py eval evalString` 51 | - `takes: string of valid python code` 52 | - `returns: nothing` 53 | - `side effects: executes code in the python interpreter` 54 | - **Do not use with substituted input** 55 | - `evalString` may be any valid python code, including semicolons for single 56 | line statements or (non-indented) multiline blocks 57 | - errors reaching the python interpreter top level are printed to stderr 58 | - `py import module` 59 | - `takes: name of a python module` 60 | - `returns: nothing` 61 | - `side effects: imports named module into globals of the python interpreter` 62 | - the name of the module may be of the form module.submodule 63 | 64 | example tclsh session: 65 | 66 | ``` 67 | % load ./libtclpy.so 68 | % 69 | % py eval {def mk(dir): os.mkdir(dir)} 70 | % py eval {def rm(dir): os.rmdir(dir); return 15} 71 | % py import os 72 | % set a [py eval {print "creating 'testdir'"; mk('testdir')}] 73 | creating 'testdir' 74 | % set b [py call rm testdir] 75 | 15 76 | % 77 | % py import StringIO 78 | % py eval {sio = StringIO.StringIO()} 79 | % py call sio.write someinput 80 | % set c [py call sio.getvalue] 81 | someinput 82 | % 83 | % py eval {divide = lambda x: 1.0/int(x)} 84 | % set d [py call divide 16] 85 | 0.0625 86 | % list [catch {py call divide 0} err] $err 87 | 1 {ZeroDivisionError: float division by zero 88 | File "", line 1, in 89 | ----- tcl -> python interface -----} 90 | % 91 | % py import json 92 | % py eval { 93 | def jobj(*args): 94 | d = {} 95 | for i in range(len(args)/2): 96 | d[args[2*i]] = args[2*i+1] 97 | return json.dumps(d) 98 | } 99 | % set e [dict create] 100 | % dict set e {t"est} "11{24" 101 | t\"est 11\{24 102 | % dict set e 6 5 103 | t\"est 11\{24 6 5 104 | % set e [py call jobj {*}$e] 105 | {"t\"est": "11{24", "6": "5"} 106 | % 107 | % py import sqlite3 108 | % py eval {b = sqlite3.connect(":memory:").cursor()} 109 | % py eval {def exe(sql, *args): b.execute(sql, args)} 110 | % py call exe "create table x(y integer, z integer)" 111 | % py call exe "insert into x values (?,?)" 1 5 112 | % py call exe "insert into x values (?,?)" 7 9 113 | % py call exe "select avg(y), min(z) from x" 114 | % py call b.fetchone 115 | 4.0 5 116 | % py call exe "select * from x" 117 | % set f [py call b.fetchall] 118 | {1 5} {7 9} 119 | % 120 | % puts "a: $a, b: $b, c: $c, d: $d, e: $e, f: $f" 121 | a: , b: 15, c: someinput, d: 0.0625, e: {"t\"est": "11{24", "6": "5"}, f: {1 5} {7 9} 122 | ``` 123 | 124 | *FROM PYTHON* 125 | 126 | Reference: 127 | - `tclpy.eval(evalstring)` 128 | - `takes: string of valid Tcl code` 129 | - `returns: the final return value` 130 | - `side effects: executes code in the Tcl interpreter` 131 | - **Do not use with substituted input** 132 | - `evalString` may be any valid Tcl code, including semicolons for single 133 | line statements or multiline blocks 134 | - errors reaching the Tcl interpreter top level are raised as an exception 135 | 136 | example python session: 137 | 138 | ``` 139 | >>> import tclpy 140 | >>> a = tclpy.eval('list 1 [list 2 4 5] 3') 141 | >>> print a 142 | 1 {2 4 5} 3 143 | ``` 144 | 145 | UNIX BUILD 146 | ---------- 147 | 148 | It is assumed that you 149 | - have got the repo (either by `git clone` or a tar.gz from the releases page). 150 | - have updated your package lists. 151 | 152 | The build process fairly simple: 153 | - make sure `make` and `gcc` are installed. 154 | - make sure you can run `python-config` and have the Python headers available 155 | (usually installed by the Python development package for your distro). 156 | - locate the tclConfig.sh file and make sure you have the Tcl headers available 157 | (usually installed by the Tcl development package for your distro). 158 | - run `make` 159 | - specifying the tclConfig.sh path if not `/usr/lib/tclConfig.sh` 160 | (`TCLCONFIG=/path/to/tclConfig.sh`). 161 | - disabling tcl stubs if you wish to use Python as the parent interpreter 162 | (`TCL_STUBS=0`). Note this then requires compilation per Tcl interpreter. 163 | 164 | On Ubuntu the default tclConfig.sh path is correct: 165 | 166 | $ sudo apt-get install -y python-dev tcl-dev 167 | $ cd libtclpy 168 | $ make 169 | 170 | For other distros you may need give the path of tclConfig.sh. E.g. CentOS 6.5: 171 | 172 | $ sudo yum install -y python-devel tcl-devel make gcc 173 | $ cd libtclpy 174 | $ make TCLCONFIG=/usr/lib64/tclConfig.sh 175 | 176 | Now try it out: 177 | 178 | $ TCLLIBPATH=. tclsh 179 | % package require tclpy 180 | 0.3 181 | % py import random 182 | % py call random.random 183 | 0.507094977417 184 | 185 | TESTS 186 | ----- 187 | 188 | Run the tests with 189 | 190 | $ make test 191 | 192 | GOTCHAS 193 | ------- 194 | 195 | 1. Be very careful when putting unicode characters into a inside a `py eval` 196 | call - they are decoded by the tcl parser and passed as literal bytes 197 | to the python interpreter. So if we directly have the character "ಠ", it is 198 | decoded to a utf-8 byte sequence and becomes u"\xe0\xb2\xa0" (where the \xXY are 199 | literal bytes) as seen by the Python interpreter. 200 | 2. Escape sequences (e.g. `\x00`) inside py eval may be interpreted by tcl - use 201 | {} quoting to avoid this. 202 | 203 | TODO 204 | ---- 205 | 206 | In order of priority: 207 | 208 | - allow python to call back into tcl 209 | - allow compiling on Windows 210 | - `py call -types [list t1 ...] func ?arg ...? : ?t1 ...? -> multi` 211 | (polymorphic args, polymorphic return) 212 | - unicode handling (exception messages, fn param, returns from calls...AS\_STRING is bad) 213 | - allow statically compiling python into tclpy 214 | - http://pkaudio.blogspot.co.uk/2008/11/notes-for-embedding-python-in-your-cc.html 215 | - https://github.com/albertz/python-embedded 216 | - https://github.com/zeha/python-superstatic 217 | - http://www.velocityreviews.com/forums/t741756-embedded-python-static-modules.html 218 | - http://christian.hofstaedtler.name/blog/2013/01/embedding-python-on-win32.html 219 | - http://stackoverflow.com/questions/1150373/compile-the-python-interpreter-statically 220 | - allow statically compiling 221 | - check threading compatibility 222 | - let `py eval` work with indented multiline blocks 223 | - `py import ?-from module? module : -> nil` 224 | - return the short error line in the catch err variable and put the full stack 225 | trace in errorInfo 226 | - py call of non-existing function says raises attribute err, should be a 227 | NameError 228 | - make `py call` look in the builtins module - http://stackoverflow.com/a/11181607 229 | - all TODOs 230 | -------------------------------------------------------------------------------- /generic/tclpy.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | /* TCL LIBRARY BEGINS HERE */ 7 | 8 | // Need an integer we can use for detecting python errors, assume we'll never 9 | // use TCL_BREAK 10 | #define PY_ERROR TCL_BREAK 11 | 12 | static PyObject *pFormatException = NULL; 13 | static PyObject *pFormatExceptionOnly = NULL; 14 | 15 | static Tcl_Obj * 16 | pyObjToTcl(Tcl_Interp *interp, PyObject *pObj) 17 | { 18 | Tcl_Obj *tObj; 19 | PyObject *pBytesObj; 20 | PyObject *pStrObj; 21 | 22 | Py_ssize_t i, len; 23 | PyObject *pVal = NULL; 24 | Tcl_Obj *tVal; 25 | 26 | PyObject *pItems = NULL; 27 | PyObject *pItem = NULL; 28 | PyObject *pKey = NULL; 29 | Tcl_Obj *tKey; 30 | 31 | /* 32 | * The ordering must always be more 'specific' types first. E.g. a 33 | * string also obeys the sequence protocol...but we probably want it 34 | * to be a string rather than a list. Suggested order below: 35 | * - None -> {} 36 | * - True -> 1, False -> 0 37 | * - bytes -> tcl byte string 38 | * - unicode -> tcl unicode string 39 | * - number protocol -> tcl number 40 | * - sequence protocol -> tcl list 41 | * - mapping protocol -> tcl dict 42 | * - other -> error (currently converts to string) 43 | * 44 | * Note that the sequence and mapping protocol are both determined by __getitem__, 45 | * the only difference is that dict subclasses are excluded from sequence. 46 | */ 47 | 48 | if (pObj == Py_None) { 49 | tObj = Tcl_NewObj(); 50 | } else if (pObj == Py_True || pObj == Py_False) { 51 | tObj = Tcl_NewBooleanObj(pObj == Py_True); 52 | } else if (PyBytes_Check(pObj)) { 53 | tObj = Tcl_NewByteArrayObj( 54 | (const unsigned char *)PyBytes_AS_STRING(pObj), 55 | PyBytes_GET_SIZE(pObj) 56 | ); 57 | } else if (PyUnicode_Check(pObj)) { 58 | pBytesObj = PyUnicode_AsUTF8String(pObj); 59 | if (pBytesObj == NULL) 60 | return NULL; 61 | tObj = Tcl_NewStringObj( 62 | PyBytes_AS_STRING(pBytesObj), PyBytes_GET_SIZE(pBytesObj) 63 | ); 64 | Py_DECREF(pBytesObj); 65 | } else if (PyNumber_Check(pObj)) { 66 | /* We go via string to support arbitrary length numbers */ 67 | if (PyLong_Check(pObj)) { 68 | pStrObj = PyNumber_ToBase(pObj, 10); 69 | } else { 70 | assert(PyComplex_Check(pObj) || PyFloat_Check(pObj)); 71 | pStrObj = PyObject_Str(pObj); 72 | } 73 | if (pStrObj == NULL) 74 | return NULL; 75 | pBytesObj = PyUnicode_AsUTF8String(pStrObj); 76 | Py_DECREF(pStrObj); 77 | if (pBytesObj == NULL) 78 | return NULL; 79 | tObj = Tcl_NewStringObj( 80 | PyBytes_AS_STRING(pBytesObj), PyBytes_GET_SIZE(pBytesObj) 81 | ); 82 | Py_DECREF(pBytesObj); 83 | } else if (PySequence_Check(pObj)) { 84 | tObj = Tcl_NewListObj(0, NULL); 85 | len = PySequence_Length(pObj); 86 | if (len == -1) 87 | return NULL; 88 | 89 | for (i = 0; i < len; i++) { 90 | pVal = PySequence_GetItem(pObj, i); 91 | if (pVal == NULL) 92 | return NULL; 93 | tVal = pyObjToTcl(interp, pVal); 94 | Py_DECREF(pVal); 95 | if (tVal == NULL) 96 | return NULL; 97 | Tcl_ListObjAppendElement(interp, tObj, tVal); 98 | } 99 | } else if (PyMapping_Check(pObj)) { 100 | tObj = Tcl_NewDictObj(); 101 | len = PyMapping_Length(pObj); 102 | if (len == -1) 103 | return NULL; 104 | pItems = PyMapping_Items(pObj); 105 | if (pItems == NULL) 106 | return NULL; 107 | #define ONERR(VAR) if (VAR == NULL) { Py_DECREF(pItems); return NULL; } 108 | for (i = 0; i < len; i++) { 109 | pItem = PySequence_GetItem(pItems, i); 110 | ONERR(pItem) 111 | pKey = PySequence_GetItem(pItem, 0); 112 | ONERR(pKey) 113 | pVal = PySequence_GetItem(pItem, 1); 114 | ONERR(pVal) 115 | tKey = pyObjToTcl(interp, pKey); 116 | Py_DECREF(pKey); 117 | ONERR(tKey); 118 | tVal = pyObjToTcl(interp, pVal); 119 | Py_DECREF(pVal); 120 | ONERR(tVal); 121 | Tcl_DictObjPut(interp, tObj, tKey, tVal); 122 | } 123 | #undef ONERR 124 | Py_DECREF(pItems); 125 | /* Broke out of loop because of error */ 126 | if (i != len) { 127 | Py_XDECREF(pItem); 128 | return NULL; 129 | } 130 | } else { 131 | /* Get python string representation of other objects */ 132 | pStrObj = PyObject_Str(pObj); 133 | if (pStrObj == NULL) 134 | return NULL; 135 | pBytesObj = PyUnicode_AsUTF8String(pStrObj); 136 | Py_DECREF(pStrObj); 137 | if (pBytesObj == NULL) 138 | return NULL; 139 | tObj = Tcl_NewStringObj( 140 | PyBytes_AS_STRING(pBytesObj), PyBytes_GET_SIZE(pBytesObj) 141 | ); 142 | Py_DECREF(pBytesObj); 143 | } 144 | 145 | return tObj; 146 | } 147 | 148 | // Returns a string that must be 'free'd containing an error and traceback, or 149 | // NULL if there was no Python error 150 | static char * 151 | pyTraceAsStr(void) 152 | { 153 | // Shouldn't call this function unless Python has excepted 154 | if (PyErr_Occurred() == NULL) 155 | return NULL; 156 | 157 | /* USE PYTHON TRACEBACK MODULE */ 158 | 159 | // TODO: save the error and reraise in python if we have no idea 160 | // TODO: prefix everything with 'PY:'? 161 | // TODO: use extract_tb to get stack, print custom traceback myself 162 | 163 | PyObject *pType = NULL, *pVal = NULL, *pTrace = NULL; 164 | PyObject *pTraceList = NULL, *pTraceStr = NULL, *pTraceDesc = NULL, *pTraceBytes = NULL; 165 | PyObject *pNone = NULL, *pEmptyStr = NULL; 166 | char *traceStr = NULL; 167 | Py_ssize_t traceLen = 0; 168 | 169 | PyErr_Fetch(&pType, &pVal, &pTrace); /* Clears exception */ 170 | PyErr_NormalizeException(&pType, &pVal, &pTrace); 171 | 172 | /* Get traceback as a python list */ 173 | if (pTrace != NULL) { 174 | pTraceList = PyObject_CallFunctionObjArgs( 175 | pFormatException, pType, pVal, pTrace, NULL); 176 | } else { 177 | pTraceList = PyObject_CallFunctionObjArgs( 178 | pFormatExceptionOnly, pType, pVal, NULL); 179 | } 180 | if (pTraceList == NULL) 181 | return strdup("[Failed to get python exception details (#e_ltp01)]\n"); 182 | 183 | /* Put the list in tcl order (top stack level at top) */ 184 | pNone = PyObject_CallMethod(pTraceList, "reverse", NULL); 185 | if (pNone == NULL) { 186 | Py_DECREF(pTraceList); 187 | return strdup("[Failed to get python exception details (#e_ltp02)]\n"); 188 | } 189 | assert(pNone == Py_None); 190 | Py_DECREF(pNone); 191 | 192 | /* Remove "Traceback (most recent call last):" if the trace len > 1 */ 193 | /* TODO: this feels like a hack, there must be a better way */ 194 | traceLen = PyObject_Length(pTraceList); 195 | if (traceLen > 1) { 196 | pTraceDesc = PyObject_CallMethod(pTraceList, "pop", NULL); 197 | } 198 | if (traceLen <= 0 || (pTraceDesc == NULL && traceLen > 1)) { 199 | Py_DECREF(pTraceList); 200 | return strdup("[Failed to get python exception details (#e_ltp03)]\n"); 201 | } 202 | Py_XDECREF(pTraceDesc); 203 | 204 | /* Turn the python list into a python string */ 205 | pEmptyStr = PyUnicode_FromString(""); 206 | if (pEmptyStr == NULL) { 207 | Py_DECREF(pTraceList); 208 | return strdup("[Failed to get python exception details (#e_ltp04)]\n"); 209 | } 210 | pTraceStr = PyObject_CallMethod(pEmptyStr, "join", "O", pTraceList); 211 | Py_DECREF(pTraceList); 212 | Py_DECREF(pEmptyStr); 213 | if (pTraceStr == NULL) 214 | return strdup("[Failed to get python exception details (#e_ltp05)]\n"); 215 | 216 | /* Turn the python string into a string */ 217 | pTraceBytes = PyUnicode_AsASCIIString(pTraceStr); 218 | Py_DECREF(pTraceStr); 219 | if (pTraceBytes == NULL) 220 | return strdup("[Failed to convert python exception details to ascii bytes (#e_ltp06)]\n"); 221 | traceStr = strdup(PyBytes_AS_STRING(pTraceBytes)); 222 | Py_DECREF(pTraceBytes); 223 | 224 | return traceStr; 225 | } 226 | 227 | static int 228 | PyCall_Cmd( 229 | ClientData clientData, /* Not used. */ 230 | Tcl_Interp *interp, /* Current interpreter */ 231 | int objc, /* Number of arguments */ 232 | Tcl_Obj *const objv[] /* Argument strings */ 233 | ) 234 | { 235 | if (objc < 3) { 236 | Tcl_WrongNumArgs(interp, 2, objv, "func ?arg ...?"); 237 | return TCL_ERROR; 238 | } 239 | 240 | const char *objandfn = Tcl_GetString(objv[2]); 241 | 242 | /* Borrowed ref, do not decrement */ 243 | PyObject *pMainModule = PyImport_AddModule("__main__"); 244 | if (pMainModule == NULL) 245 | return PY_ERROR; 246 | 247 | /* So we don't have to special case the decref in the following loop */ 248 | Py_INCREF(pMainModule); 249 | PyObject *pObjParent = NULL; 250 | PyObject *pObj = pMainModule; 251 | PyObject *pObjStr = NULL; 252 | char *dot = index(objandfn, '.'); 253 | while (dot != NULL) { 254 | pObjParent = pObj; 255 | 256 | pObjStr = PyUnicode_FromStringAndSize(objandfn, dot-objandfn); 257 | if (pObjStr == NULL) { 258 | Py_DECREF(pObjParent); 259 | return PY_ERROR; 260 | } 261 | 262 | pObj = PyObject_GetAttr(pObjParent, pObjStr); 263 | Py_DECREF(pObjStr); 264 | Py_DECREF(pObjParent); 265 | if (pObj == NULL) 266 | return PY_ERROR; 267 | 268 | objandfn = dot + 1; 269 | dot = index(objandfn, '.'); 270 | } 271 | 272 | PyObject *pFn = PyObject_GetAttrString(pObj, objandfn); 273 | Py_DECREF(pObj); 274 | if (pFn == NULL) 275 | return PY_ERROR; 276 | 277 | if (!PyCallable_Check(pFn)) { 278 | Py_DECREF(pFn); 279 | return PY_ERROR; 280 | } 281 | 282 | int i; 283 | PyObject *pArgs = PyTuple_New(objc-3); 284 | PyObject* curarg = NULL; 285 | for (i = 0; i < objc-3; i++) { 286 | curarg = PyUnicode_FromString(Tcl_GetString(objv[i+3])); 287 | if (curarg == NULL) { 288 | Py_DECREF(pArgs); 289 | Py_DECREF(pFn); 290 | return PY_ERROR; 291 | } 292 | /* Steals a reference */ 293 | PyTuple_SET_ITEM(pArgs, i, curarg); 294 | } 295 | 296 | PyObject *pRet = PyObject_Call(pFn, pArgs, NULL); 297 | Py_DECREF(pFn); 298 | Py_DECREF(pArgs); 299 | if (pRet == NULL) 300 | return PY_ERROR; 301 | 302 | Tcl_Obj *tRet = pyObjToTcl(interp, pRet); 303 | Py_DECREF(pRet); 304 | if (tRet == NULL) 305 | return PY_ERROR; 306 | 307 | Tcl_SetObjResult(interp, tRet); 308 | return TCL_OK; 309 | } 310 | 311 | static int 312 | PyEval_Cmd( 313 | ClientData clientData, /* Not used. */ 314 | Tcl_Interp *interp, /* Current interpreter */ 315 | int objc, /* Number of arguments */ 316 | Tcl_Obj *const objv[] /* Argument strings */ 317 | ) 318 | { 319 | if (objc != 3) { 320 | Tcl_WrongNumArgs(interp, 2, objv, "evalString"); 321 | return TCL_ERROR; 322 | } 323 | 324 | const char *cmd = Tcl_GetString(objv[2]); 325 | 326 | if (PyRun_SimpleString(cmd) == 0) { 327 | return TCL_OK; 328 | } else { 329 | return PY_ERROR; 330 | }; 331 | } 332 | 333 | static int 334 | PyImport_Cmd( 335 | ClientData clientData, /* Not used. */ 336 | Tcl_Interp *interp, /* Current interpreter */ 337 | int objc, /* Number of arguments */ 338 | Tcl_Obj *const objv[] /* Argument strings */ 339 | ) 340 | { 341 | const char *modname, *topmodname; 342 | PyObject *pMainModule, *pTopModule; 343 | int ret = -1; 344 | 345 | if (objc != 3) { 346 | Tcl_WrongNumArgs(interp, 2, objv, "module"); 347 | return TCL_ERROR; 348 | } 349 | 350 | modname = Tcl_GetString(objv[2]); 351 | 352 | /* Borrowed ref, do not decrement */ 353 | pMainModule = PyImport_AddModule("__main__"); 354 | if (pMainModule == NULL) 355 | return PY_ERROR; 356 | 357 | // We don't use PyImport_ImportModule so mod.submod works 358 | pTopModule = PyImport_ImportModuleEx(modname, NULL, NULL, NULL); 359 | if (pTopModule == NULL) 360 | return PY_ERROR; 361 | 362 | topmodname = PyModule_GetName(pTopModule); 363 | if (topmodname != NULL) { 364 | ret = PyObject_SetAttrString(pMainModule, topmodname, pTopModule); 365 | } 366 | Py_DECREF(pTopModule); 367 | 368 | if (ret != -1) { 369 | return TCL_OK; 370 | } else { 371 | return PY_ERROR; 372 | } 373 | } 374 | 375 | /* The two static variables below are related by order, keep alphabetical */ 376 | static const char *cmdnames[] = { 377 | "call", "eval", "import", NULL 378 | }; 379 | static int (*cmds[]) ( 380 | ClientData clientData, Tcl_Interp *interp, int objc, Tcl_Obj *const objv[] 381 | ) = { 382 | PyCall_Cmd, PyEval_Cmd, PyImport_Cmd 383 | }; 384 | 385 | static int 386 | Py_Cmd( 387 | ClientData clientData, /* Not used. */ 388 | Tcl_Interp *interp, /* Current interpreter */ 389 | int objc, /* Number of arguments */ 390 | Tcl_Obj *const objv[] /* Argument strings */ 391 | ) 392 | { 393 | if (objc < 2) { 394 | Tcl_WrongNumArgs(interp, 1, objv, "subcommand ?arg ...?"); 395 | return TCL_ERROR; 396 | } 397 | 398 | int cmdindex; 399 | if (Tcl_GetIndexFromObj(interp, objv[1], cmdnames, "command", TCL_EXACT, 400 | &cmdindex) != TCL_OK) 401 | return TCL_ERROR; 402 | 403 | /* Actually call the command */ 404 | int ret = (*(cmds[cmdindex]))(clientData, interp, objc, objv); 405 | 406 | if (ret == PY_ERROR) { 407 | ret = TCL_ERROR; 408 | // Not entirely sure if this is the correct way of doing things. Should 409 | // I be calling Tcl_AddErrorInfo instead? 410 | char *traceStr = pyTraceAsStr(); // clears exception 411 | if (traceStr == NULL) { 412 | // TODO: something went wrong in traceback 413 | PyErr_Clear(); 414 | return TCL_ERROR; 415 | } 416 | Tcl_AppendResult(interp, traceStr, NULL); 417 | Tcl_AppendResult(interp, "----- tcl -> python interface -----", NULL); 418 | free(traceStr); 419 | } 420 | 421 | return ret; 422 | } 423 | 424 | /* PYTHON LIBRARY BEGINS HERE */ 425 | 426 | static PyObject * 427 | tclpy_eval(PyObject *self, PyObject *args, PyObject *kwargs) 428 | { 429 | static char *kwlist[] = {"tcl_code", NULL}; 430 | char *tclCode = NULL; 431 | 432 | if (!PyArg_ParseTupleAndKeywords(args, kwargs, "s", kwlist, &tclCode)) 433 | return NULL; 434 | 435 | Tcl_Interp *interp = PyCapsule_Import("tclpy.interp", 0); 436 | 437 | int result = Tcl_Eval(interp, tclCode); 438 | Tcl_Obj *tResult = Tcl_GetObjResult(interp); 439 | 440 | int tclStringSize; 441 | char *tclString; 442 | tclString = Tcl_GetStringFromObj(tResult, &tclStringSize); 443 | 444 | if (result == TCL_ERROR) { 445 | PyErr_SetString(PyExc_RuntimeError, tclString); 446 | return NULL; 447 | } 448 | return Py_BuildValue("s#", tclString, tclStringSize); 449 | } 450 | 451 | static PyMethodDef TclPyMethods[] = { 452 | {"eval", (PyCFunction)tclpy_eval, 453 | METH_VARARGS | METH_KEYWORDS, 454 | "Evaluate some tcl code"}, 455 | {NULL, NULL, 0, NULL} /* Sentinel */ 456 | }; 457 | 458 | // TODO: there should probably be some tcl deinit in the clear/free code 459 | static struct PyModuleDef TclPyModule = { 460 | PyModuleDef_HEAD_INIT, 461 | "tclpy", 462 | "A module to permit interop with Tcl", 463 | -1, 464 | TclPyMethods, 465 | NULL, // m_slots 466 | NULL, // m_traverse 467 | NULL, // m_clear 468 | NULL, // m_free 469 | }; 470 | 471 | /* SHARED INITIALISATION BEGINS HERE */ 472 | 473 | /* Keep track of the top level interpreter */ 474 | typedef enum { 475 | NO_PARENT, 476 | TCL_PARENT, 477 | PY_PARENT 478 | } ParentInterp; 479 | static ParentInterp parentInterp = NO_PARENT; 480 | 481 | int Tclpy_Init(Tcl_Interp *interp); 482 | PyObject *init_python_tclpy(Tcl_Interp* interp); 483 | 484 | int 485 | Tclpy_Init(Tcl_Interp *interp) 486 | { 487 | /* TODO: all TCL_ERRORs should set an error return */ 488 | 489 | if (parentInterp == TCL_PARENT) 490 | return TCL_ERROR; 491 | if (parentInterp == NO_PARENT) 492 | parentInterp = TCL_PARENT; 493 | 494 | if (Tcl_InitStubs(interp, "8.5", 0) == NULL) 495 | return TCL_ERROR; 496 | if (Tcl_PkgRequire(interp, "Tcl", "8.5", 0) == NULL) 497 | return TCL_ERROR; 498 | if (Tcl_PkgProvide(interp, "tclpy", PACKAGE_VERSION) != TCL_OK) 499 | return TCL_ERROR; 500 | 501 | Tcl_Command cmd = Tcl_CreateObjCommand(interp, "py", 502 | (Tcl_ObjCmdProc *) Py_Cmd, (ClientData)NULL, (Tcl_CmdDeleteProc *)NULL); 503 | if (cmd == NULL) 504 | return TCL_ERROR; 505 | 506 | /* Hack to fix Python C extensions not linking to libpython*.so */ 507 | /* http://bugs.python.org/issue4434 */ 508 | dlopen(PY_LIBFILE, RTLD_LAZY | RTLD_GLOBAL); 509 | 510 | if (parentInterp != PY_PARENT) { 511 | Py_Initialize(); /* void */ 512 | if (init_python_tclpy(interp) == NULL) 513 | return TCL_ERROR; 514 | } 515 | 516 | /* Get support for full tracebacks */ 517 | PyObject *pTraceModStr, *pTraceMod; 518 | 519 | pTraceModStr = PyUnicode_FromString("traceback"); 520 | if (pTraceModStr == NULL) 521 | return TCL_ERROR; 522 | pTraceMod = PyImport_Import(pTraceModStr); 523 | Py_DECREF(pTraceModStr); 524 | if (pTraceMod == NULL) 525 | return TCL_ERROR; 526 | pFormatException = PyObject_GetAttrString(pTraceMod, "format_exception"); 527 | pFormatExceptionOnly = PyObject_GetAttrString(pTraceMod, "format_exception_only"); 528 | Py_DECREF(pTraceMod); 529 | if (pFormatException == NULL || pFormatExceptionOnly == NULL || 530 | !PyCallable_Check(pFormatException) || 531 | !PyCallable_Check(pFormatExceptionOnly)) { 532 | Py_XDECREF(pFormatException); 533 | Py_XDECREF(pFormatExceptionOnly); 534 | return TCL_ERROR; 535 | } 536 | 537 | return TCL_OK; 538 | } 539 | 540 | PyObject * 541 | init_python_tclpy(Tcl_Interp* interp) 542 | { 543 | if (parentInterp == PY_PARENT) 544 | return NULL; 545 | if (parentInterp == NO_PARENT) 546 | parentInterp = PY_PARENT; 547 | if (parentInterp == TCL_PARENT) 548 | assert(interp != NULL); 549 | 550 | if (interp == NULL) 551 | interp = Tcl_CreateInterp(); 552 | if (Tcl_Init(interp) != TCL_OK) 553 | return NULL; 554 | if (parentInterp == PY_PARENT && Tclpy_Init(interp) == TCL_ERROR) 555 | return NULL; 556 | 557 | PyObject *m = PyModule_Create(&TclPyModule); 558 | if (m == NULL) 559 | return NULL; 560 | PyObject *pCap = PyCapsule_New(interp, "tclpy.interp", NULL); 561 | if (PyObject_SetAttrString(m, "interp", pCap) == -1) 562 | return NULL; 563 | Py_DECREF(pCap); 564 | 565 | return m; 566 | } 567 | 568 | PyMODINIT_FUNC 569 | inittclpy(void) 570 | { 571 | return init_python_tclpy(NULL); 572 | } 573 | -------------------------------------------------------------------------------- /notes.md: -------------------------------------------------------------------------------- 1 | http://www.tcl.tk/man/tcl/TclLib/contents.htm 2 | 3 | tclsh <(echo -e "load libtclpy0.1.so\npy import hashlib}\nx") 4 | crashes badly 5 | 6 | # Exceptions 7 | 8 | Alternative approaches 9 | ``` 10 | // http://stackoverflow.com/questions/16733425/how-to-retrieve-filename-and-lineno-attribute-of-syntaxerror 11 | // http://stackoverflow.com/questions/1796510/accessing-a-python-traceback-from-the-c-api 12 | // http://www.gossamer-threads.com/lists/python/python/150924 13 | // https://mail.python.org/pipermail/capi-sig/2009-January/000197.html 14 | // http://docs.python.org/2/library/traceback.html 15 | 16 | #if 0 17 | /* WALK FRAMES BACKWARDS */ 18 | 19 | PyThreadState *tstate = PyThreadState_GET(); 20 | if (tstate != NULL && tstate->frame != NULL) { 21 | PyFrameObject *frame = tstate->frame; 22 | 23 | printf("Python stack trace:\n"); 24 | while (frame != NULL) { 25 | int line = frame->f_lineno; 26 | const char *filename = PyString_AsString(frame->f_code->co_filename); 27 | const char *funcname = PyString_AsString(frame->f_code->co_name); 28 | printf(" %s(%d): %s\n", filename, line, funcname); 29 | frame = frame->f_back; 30 | } 31 | } 32 | #endif 33 | 34 | #if 0 35 | /* USE EXCEPTION STRUCT */ 36 | 37 | PyObject *pType, *pVal, *pTrace; 38 | 39 | PyErr_Fetch(&pType, &pVal, &pTrace); 40 | PyErr_NormalizeException(&pType, &pVal, &pTrace); 41 | 42 | PyTracebackObject* traceback = (PyTracebackObject *)pTrace; 43 | // Advance to the last frame (python puts the most-recent call at the end) 44 | while (traceback->tb_next != NULL) 45 | traceback = traceback->tb_next; 46 | 47 | int line = traceback->tb_lineno; 48 | const char* filename = PyString_AsString(traceback->tb_frame->f_code->co_filename) 49 | #endif 50 | ``` 51 | 52 | -------------------------------------------------------------------------------- /pkgIndex.tcl.in: -------------------------------------------------------------------------------- 1 | # 2 | # Tcl package index file 3 | # 4 | package ifneeded tclpy PACKAGE_VERSION \ 5 | [list load [file join $dir libtclpyPACKAGE_VERSION.so] tclpy] 6 | -------------------------------------------------------------------------------- /tests/tclpy.test: -------------------------------------------------------------------------------- 1 | if {[lsearch [namespace children] ::tcltest] == -1} { 2 | package require tcltest 3 | namespace import ::tcltest::* 4 | } 5 | 6 | package require tclpy 7 | 8 | # ========= 9 | # PY 10 | # ========= 11 | test py-1.1 {incorrect usage} { 12 | list [catch {py} errMsg] $errMsg 13 | } {1 {wrong # args: should be "py subcommand ?arg ...?"}} 14 | 15 | # ========= 16 | # PY EVAL 17 | # ========= 18 | test py_eval-1.1 {incorrect eval usage} { 19 | list [catch {py eval} errMsg1] $errMsg1\ 20 | [catch {py eval {print 1} {print 2}} errMsg2] $errMsg2 21 | } {1 {wrong # args: should be "py eval evalString"}\ 22 | 1 {wrong # args: should be "py eval evalString"}} 23 | 24 | test py_eval-1.2 {eval returns nothing} { 25 | list [py eval {1+1}] 26 | } {{}} 27 | 28 | test py_eval-1.2 {basic eval} -setup { 29 | set randdir tmp_[expr {rand()}] 30 | } -body { 31 | py eval "def mk(dir): os.mkdir(dir)" 32 | py eval "import os; mk('$randdir')" 33 | file isdirectory $randdir 34 | } -result {1} -cleanup { 35 | file delete $randdir 36 | } 37 | 38 | # ========= 39 | # PY IMPORT 40 | # ========= 41 | test py_import-1.1 {incorrect import usage} { 42 | list [catch {py import} errMsg1] $errMsg1\ 43 | [catch {py import -from os} errMsg2] $errMsg2 44 | } {1 {wrong # args: should be "py import module"}\ 45 | 1 {wrong # args: should be "py import module"}} 46 | 47 | test py_import-1.2 {basic import} -body { 48 | py import re 49 | py eval "import sys; assert 're' in sys.modules; assert 're' in globals()" 50 | } -result {} 51 | 52 | test py_import-1.3 {submodule import} -body { 53 | py import xml.dom 54 | py eval "assert 'dom' in dir(xml)" 55 | } -result {} 56 | 57 | test py_import-1.4 {non-existent import} -body { 58 | list [catch {py import aosidas} err] $err 59 | } -result {1 {ModuleNotFoundError: No module named 'aosidas' 60 | ----- tcl -> python interface -----}} 61 | 62 | test py_import-1.5 {non-existent import with full trace} -body { 63 | proc aaa {} {py import aosidas} 64 | list [catch {aaa} err] $err $::errorInfo 65 | } -result {1 {ModuleNotFoundError: No module named 'aosidas' 66 | ----- tcl -> python interface -----} {ModuleNotFoundError: No module named 'aosidas' 67 | ----- tcl -> python interface ----- 68 | while executing 69 | "py import aosidas" 70 | (procedure "aaa" line 1) 71 | invoked from within 72 | "aaa"}} 73 | 74 | # ========= 75 | # PY CALL 76 | # ========= 77 | test py_call-1.1 {incorrect call usage} { 78 | list [catch {py call} errMsg1] $errMsg1 79 | } {1 {wrong # args: should be "py call func ?arg ...?"}} 80 | 81 | test py_call-1.2 {basic call} { 82 | py eval {def a(): return 5**2} 83 | py call a 84 | } {25} 85 | 86 | test py_call-1.3 {basic call with args} { 87 | py eval {def a(x,y): return x+y} 88 | py call a string1 string2 89 | } {string1string2} 90 | 91 | test py_call-1.4 {call of module function} { 92 | py import base64 93 | py call base64.b64decode YXRlc3Q= 94 | } {atest} 95 | 96 | test py_call-1.5 {call of object methods} { 97 | py import io 98 | py eval {a = io.StringIO(); a.write('btest'); a.seek(0)} 99 | py call a.read 100 | } {btest} 101 | 102 | test py_call-1.6 {simple call exception} { 103 | py eval {a = lambda: 1/0} 104 | list [catch {py call a} err] $err 105 | } {1 {ZeroDivisionError: division by zero 106 | File "", line 1, in 107 | ----- tcl -> python interface -----}} 108 | 109 | test py_call-1.7 {stacked call exception} { 110 | py eval {def a(): return 5 + dict()} 111 | py eval {def b(): return a()} 112 | py eval {def c(): return b()} 113 | proc d {} {py call c} 114 | proc e {} {d} 115 | list [catch {e} err] $err $::errorInfo 116 | } {1 {TypeError: unsupported operand type(s) for +: 'int' and 'dict' 117 | File "", line 1, in a 118 | File "", line 1, in b 119 | File "", line 1, in c 120 | ----- tcl -> python interface -----} {TypeError: unsupported operand type(s) for +: 'int' and 'dict' 121 | File "", line 1, in a 122 | File "", line 1, in b 123 | File "", line 1, in c 124 | ----- tcl -> python interface ----- 125 | while executing 126 | "py call c" 127 | (procedure "d" line 1) 128 | invoked from within 129 | "d" 130 | (procedure "e" line 1) 131 | invoked from within 132 | "e"}} 133 | 134 | # TODO: this error message is terrible 135 | test py_call-1.8 {call of nonexistent functions} { 136 | list [catch {py call aosdin} err] $err 137 | } {1 {AttributeError: module '__main__' has no attribute 'aosdin' 138 | ----- tcl -> python interface -----}} 139 | 140 | # TODO: this error message could be improved ("has no method") 141 | test py_call-1.9 {call of nonexistent object methods} { 142 | py eval {a = "aaa"} 143 | list [catch {py call a.aosdin} err] $err 144 | } {1 {AttributeError: 'str' object has no attribute 'aosdin' 145 | ----- tcl -> python interface -----}} 146 | 147 | # ========= 148 | # TYPES 149 | # ========= 150 | test types-1.1 {return True} { 151 | py eval {def a(): return True} 152 | py call a 153 | } {1} 154 | 155 | test types-1.2 {return False} { 156 | py eval {def a(): return False} 157 | py call a 158 | } {0} 159 | 160 | test types-1.3 {return None} { 161 | py eval {def a(): return None} 162 | py call a 163 | } {} 164 | 165 | test types-1.4 {return null byte} { 166 | py eval {def a(): return '\0'} 167 | set a [py call a] 168 | list [string length $a] [expr {$a == "\0"}] 169 | } {1 1} 170 | 171 | # See gotcha 1 for explanation of roundabout way of getting a unicode object. 172 | test types-1.5 {return unicode object} { 173 | py eval {def a(): return b'\xe0\xb2\xa0'.decode('utf-8')} 174 | expr {[py call a] == "ಠ"} 175 | } {1} 176 | 177 | test types-1.6 {return literal bytes} { 178 | py eval {def a(): return '\xe0\xb2\xa0'} 179 | expr {[py call a] == "\xe0\xb2\xa0"} 180 | } {1} 181 | 182 | test types-1.7 {return nested lists and dictionaries} { 183 | py eval {def a(): return [ 184 | (1,2), 185 | [u"a",["b",7]], 186 | {"x":[(3,4),{'a':{'b':'c'}}],"y":(4,5,6)}]} 187 | set a [py call a] 188 | set ad [lindex $a 2] 189 | set ada [lindex [dict get $ad x] 1] 190 | list \ 191 | [lindex $a 0] [lindex $a 1] [lindex [dict get $ad x] 0]\ 192 | [dict get $ad y] [dict get [dict get $ada a] b] 193 | } {{1 2} {a {b 7}} {3 4} {4 5 6} c} 194 | 195 | test types-1.8 {return float} { 196 | py eval {def a(): return 1.0/3} 197 | py call a 198 | } {0.3333333333333333} 199 | 200 | test types-1.9 {return large integer} { 201 | py eval {def a(): return 3 << 5000} 202 | expr {[py call a] == 3 << 5000} 203 | } {1} 204 | 205 | # ========= 206 | # MODULES 207 | # ========= 208 | test modules-1.1 {hashlib module} { 209 | py import hashlib 210 | py eval {def a(): return hashlib.sha1('password'.encode('utf8')).hexdigest()} 211 | py call a 212 | } {5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8} 213 | 214 | test modules-1.2 {zlib module} { 215 | py import zlib 216 | py eval {def a(): return zlib.decompress(b'x\x9cKLL\x04\x00\x02I\x01$')} 217 | py call a 218 | } {aaa} 219 | 220 | test modules-1.3 {datetime module} { 221 | py import datetime 222 | py eval {def a(): return datetime.datetime.utcfromtimestamp(0).isoformat()} 223 | py call a 224 | } {1970-01-01T00:00:00} 225 | 226 | test module-1.4 {sqlite3 module} { 227 | py import sqlite3 228 | py eval {def a(): global b; b = sqlite3.connect(":memory:").cursor()} 229 | py call a 230 | py call b.execute "create table x(y integer)" 231 | py call b.execute "insert into x values (1)" 232 | py call b.execute "insert into x values (18)" 233 | py call b.execute "select avg(y) from x" 234 | py call b.fetchone 235 | } {9.5} 236 | 237 | # ========= 238 | # cleanup 239 | # ========= 240 | ::tcltest::cleanupTests 241 | --------------------------------------------------------------------------------