Skip to content

Commit 544bcc8

Browse files
author
João Eiras
committed
Report stacktraces for remote_exec() (#101)
1 parent e1b11d6 commit 544bcc8

File tree

5 files changed

+53
-13
lines changed

5 files changed

+53
-13
lines changed

execnet/gateway.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import os
88
import inspect
99
import types
10-
import linecache
1110
import textwrap
1211
import execnet
1312
from execnet.gateway_base import Message
@@ -111,22 +110,27 @@ def remote_exec(self, source, **kwargs):
111110
executing code.
112111
"""
113112
call_name = None
113+
file_name = None
114114
if isinstance(source, types.ModuleType):
115-
linecache.updatecache(inspect.getsourcefile(source))
116-
source = inspect.getsource(source)
115+
file_name = inspect.getsourcefile(source)
116+
if not file_name:
117+
source = inspect.getsource(source)
118+
else:
119+
source = None
117120
elif isinstance(source, types.FunctionType):
118121
call_name = source.__name__
122+
file_name = inspect.getsourcefile(source)
119123
source = _source_of_function(source)
120124
else:
121125
source = textwrap.dedent(str(source))
122126

123-
if call_name is None and kwargs:
127+
if not call_name and kwargs:
124128
raise TypeError("can't pass kwargs to non-function remote_exec")
125129

126130
channel = self.newchannel()
127131
self._send(Message.CHANNEL_EXEC,
128132
channel.id,
129-
gateway_base.dumps_internal((source, call_name, kwargs)))
133+
gateway_base.dumps_internal((source, file_name, call_name, kwargs)))
130134
return channel
131135

132136
def remote_init_threads(self, num=None):
@@ -186,7 +190,7 @@ def _source_of_function(function):
186190
args = inspect.getargspec(function)[0]
187191
else:
188192
args = sig.args
189-
if args[0] != 'channel':
193+
if not args or args[0] != 'channel':
190194
raise ValueError('expected first function argument to be `channel`')
191195

192196
if gateway_base.ISPY3:
@@ -213,4 +217,5 @@ def _source_of_function(function):
213217
used_globals,
214218
)
215219

216-
return source
220+
leading_ws = "\n" * (codeobj.co_firstlineno - 1)
221+
return leading_ws + source

execnet/gateway_base.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"""
1414
from __future__ import with_statement
1515
import sys
16+
import linecache
1617
import os
1718
import weakref
1819
import traceback
@@ -25,8 +26,9 @@
2526
ISPY3 = sys.version_info >= (3, 0)
2627
if ISPY3:
2728
from io import BytesIO
28-
exec("def do_exec(co, loc): exec(co, loc)\n"
29-
"def reraise(cls, val, tb): raise val\n")
29+
exec("do_exec = exec")
30+
def reraise(cls, val, tb):
31+
raise val.with_traceback(tb)
3032
unicode = str
3133
_long_type = int
3234
from _thread import interrupt_main
@@ -1044,7 +1046,7 @@ def trace(msg):
10441046

10451047
def executetask(self, item):
10461048
try:
1047-
channel, (source, call_name, kwargs) = item
1049+
channel, (source, file_name, call_name, kwargs) = item
10481050
if not ISPY3 and kwargs:
10491051
# some python2 versions do not accept unicode keyword params
10501052
# note: Unserializer generally turns py2-str to py3-str objects
@@ -1054,12 +1056,15 @@ def executetask(self, item):
10541056
name = name.encode('ascii')
10551057
newkwargs[name] = value
10561058
kwargs = newkwargs
1059+
if source is None:
1060+
assert file_name, file_name
1061+
source = "".join(linecache.updatecache(file_name))
10571062
loc = {'channel': channel, '__name__': '__channelexec__'}
10581063
self._trace("execution starts[%s]: %s" %
10591064
(channel.id, repr(source)[:50]))
10601065
channel._executing = True
10611066
try:
1062-
co = compile(source+'\n', '<remote exec>', 'exec')
1067+
co = compile(source+'\n', file_name or '<remote exec>', 'exec')
10631068
do_exec(co, loc) # noqa
10641069
if call_name:
10651070
self._trace('calling %s(**%60r)' % (call_name, kwargs))

execnet/script/socketserver.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def exec_from_one_connection(serversock):
6363
}
6464
source = eval(source)
6565
if source:
66-
co = compile(source+'\n', source, 'exec')
66+
co = compile(source+'\n', '<socket server>', 'exec')
6767
print_(progname, 'compiled source, executing')
6868
try:
6969
exec_(co, g) # noqa

testing/test_basics.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ def test_source_of_nested_function(self):
326326
def working(channel):
327327
pass
328328

329-
send_source = gateway._source_of_function(working)
329+
send_source = gateway._source_of_function(working).lstrip("\r\n")
330330
expected = 'def working(channel):\n pass\n'
331331
assert send_source == expected
332332

testing/test_gateway.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,36 @@ def test_remote_exec_module(self, tmpdir, gw):
110110
name = channel.receive()
111111
assert name == 2
112112

113+
def test_remote_exec_module_with_traceback(self, gw, tmpdir):
114+
remotetest = tmpdir.join("remotetest.py")
115+
remotetest.write(
116+
"\ndef run_me(channel=None):\n raise ValueError('me')\n\n" +
117+
"if __name__ == '__channelexec__':\n run_me()\n")
118+
119+
try:
120+
sys.path.insert(0, str(tmpdir))
121+
module = __import__("remotetest")
122+
finally:
123+
sys.path.pop(0)
124+
125+
ch = gw.remote_exec(module)
126+
try:
127+
ch.receive()
128+
except execnet.gateway_base.RemoteError as e:
129+
assert 'remotetest.py", line 3, in run_me' in str(e)
130+
assert "ValueError: me" in str(e)
131+
finally:
132+
ch.close()
133+
134+
ch = gw.remote_exec(module.run_me)
135+
try:
136+
ch.receive()
137+
except execnet.gateway_base.RemoteError as e:
138+
assert 'remotetest.py", line 3, in run_me' in str(e)
139+
assert "ValueError: me" in str(e)
140+
finally:
141+
ch.close()
142+
113143
def test_correct_setup_no_py(self, gw):
114144
channel = gw.remote_exec("""
115145
import sys

0 commit comments

Comments
 (0)