Skip to content

Commit 695bfa3

Browse files
authored
Report stacktraces for remote_exec() (#102)
Report stacktraces for remote_exec()
2 parents 8cae029 + 83a3ecf commit 695bfa3

File tree

8 files changed

+66
-18
lines changed

8 files changed

+66
-18
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
1.6.3 (08-08-2019)
2+
------------------
3+
4+
* `#102 <https://github.com/pytest-dev/execnet/pull/102>`__: Show paths in stack traces
5+
generated by ``remote_exec()``.
6+
17
1.6.1 (2019-07-22)
28
------------------
39

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/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,8 @@ def gw(request, execmodel, group):
186186
def execmodel(request):
187187
if request.param != "thread":
188188
pytest.importorskip(request.param)
189-
if sys.platform == "win32":
190-
pytest.xfail("eventlet/gevent do not work onwin32")
189+
if request.param in ("eventlet", "gevent") and sys.platform == "win32":
190+
pytest.xfail(request.param + " does not work on win32")
191191
return get_execmodel(request.param)
192192

193193

testing/test_basics.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,6 @@ def test_stdouterrin_setnull(execmodel):
218218
cap = py.io.StdCaptureFD()
219219
gateway_base.init_popen_io(execmodel)
220220
os.write(1, "hello".encode('ascii'))
221-
if os.name == "nt":
222-
os.write(2, "world")
223221
os.read(0, 1)
224222
out, err = cap.reset()
225223
assert not out
@@ -326,7 +324,7 @@ def test_source_of_nested_function(self):
326324
def working(channel):
327325
pass
328326

329-
send_source = gateway._source_of_function(working)
327+
send_source = gateway._source_of_function(working).lstrip("\r\n")
330328
expected = 'def working(channel):\n pass\n'
331329
assert send_source == expected
332330

testing/test_gateway.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
mostly functional tests of gateways.
33
"""
44
import os
5+
from textwrap import dedent
6+
57
import py
68
import pytest
79
import socket
@@ -112,6 +114,38 @@ def test_remote_exec_module(self, tmpdir, gw):
112114
name = channel.receive()
113115
assert name == 2
114116

117+
def test_remote_exec_module_with_traceback(self, gw, tmpdir, monkeypatch):
118+
remotetest = tmpdir.join("remotetest.py")
119+
remotetest.write(dedent("""
120+
def run_me(channel=None):
121+
raise ValueError('me')
122+
123+
if __name__ == '__channelexec__':
124+
run_me()
125+
""")
126+
)
127+
128+
monkeypatch.syspath_prepend(tmpdir)
129+
import remotetest
130+
131+
ch = gw.remote_exec(remotetest)
132+
try:
133+
ch.receive()
134+
except execnet.gateway_base.RemoteError as e:
135+
assert 'remotetest.py", line 3, in run_me' in str(e)
136+
assert "ValueError: me" in str(e)
137+
finally:
138+
ch.close()
139+
140+
ch = gw.remote_exec(remotetest.run_me)
141+
try:
142+
ch.receive()
143+
except execnet.gateway_base.RemoteError as e:
144+
assert 'remotetest.py", line 3, in run_me' in str(e)
145+
assert "ValueError: me" in str(e)
146+
finally:
147+
ch.close()
148+
115149
def test_correct_setup_no_py(self, gw):
116150
channel = gw.remote_exec("""
117151
import sys

testing/test_rsync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ def filter(self, x):
232232
assert len(dest.listdir()) == 1
233233
assert len(source.listdir()) == 1
234234

235-
@py.test.mark.skip_if('sys.version_info >= (3)')
235+
@py.test.mark.skipif('sys.version_info >= (3,)')
236236
def test_2_to_3_bridge_can_send_binary_files(self, tmpdir, makegateway):
237237
python = _find_version('3')
238238
gw = makegateway('popen//python=%s' % python)

0 commit comments

Comments
 (0)