Skip to content

Commit bd99348

Browse files
authored
Show Python Script source code in tracebacks (#65)
Expose a __loader__ in globals so that linecache module is able to use it to display the source code. This requires changing the "filename" used when compiling function, because linecache uses code.co_filename as a cache key, so it's necessary that each python script use a different filename.
1 parent 554f8e5 commit bd99348

File tree

3 files changed

+68
-4
lines changed

3 files changed

+68
-4
lines changed

CHANGES.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Changelog
1010

1111
- Drop support for Python 3.7.
1212

13+
- Show Python Scripts source code in tracebacks.
14+
`#64 <https://github.com/zopefoundation/Products.PythonScripts/issues/64>`_
1315

1416
5.0 (2023-02-01)
1517
----------------

src/Products/PythonScripts/PythonScript.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
Python code.
1717
"""
1818

19+
import importlib.abc
1920
import importlib.util
21+
import linecache
2022
import marshal
2123
import os
2224
import re
@@ -56,7 +58,7 @@
5658
Python_magic = importlib.util.MAGIC_NUMBER
5759

5860
# This should only be incremented to force recompilation.
59-
Script_magic = 4
61+
Script_magic = 5
6062
_log_complaint = (
6163
'Some of your Scripts have stale code cached. Since Zope cannot'
6264
' use this code, startup will be slightly slower until these Scripts'
@@ -97,6 +99,17 @@ def manage_addPythonScript(self, id, title='', file=None, REQUEST=None,
9799
return ''
98100

99101

102+
class PythonScriptLoader(importlib.abc.Loader):
103+
"""PEP302 loader to display source code in tracebacks
104+
"""
105+
106+
def __init__(self, source):
107+
self._source = source
108+
109+
def get_source(self, name):
110+
return self._source
111+
112+
100113
class PythonScript(Script, Historical, Cacheable):
101114
"""Web-callable scripts written in a safe subset of Python.
102115
@@ -234,7 +247,7 @@ def _compile(self):
234247
self._params,
235248
body=self._body or 'pass',
236249
name=self.id,
237-
filename=self.meta_type,
250+
filename=getattr(self, '_filepath', None) or self.get_filepath(),
238251
globalize=bind_names)
239252

240253
code = compile_result.code
@@ -261,6 +274,7 @@ def _compile(self):
261274
fc.co_argcount)
262275
self.Python_magic = Python_magic
263276
self.Script_magic = Script_magic
277+
linecache.clearcache()
264278
self._v_change = 0
265279

266280
def _newfun(self, code):
@@ -331,6 +345,8 @@ def _exec(self, bound_names, args, kw):
331345
PythonScriptTracebackSupplement, self, -1)
332346
safe_globals['__file__'] = getattr(
333347
self, '_filepath', None) or self.get_filepath()
348+
safe_globals['__loader__'] = PythonScriptLoader(self._body)
349+
334350
function = types.FunctionType(
335351
function_code, safe_globals, None, function_argument_definitions)
336352

src/Products/PythonScripts/tests/testPythonScript.py

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import io
1616
import os
1717
import sys
18+
import traceback
1819
import unittest
1920
import warnings
2021
from urllib.error import HTTPError
@@ -241,7 +242,8 @@ def test_manage_DAVget(self):
241242
self.assertEqual(ps.read(), ps.manage_DAVget())
242243

243244
def test_PUT_native_string(self):
244-
ps = makerequest(self._filePS('complete'))
245+
container = DummyFolder('container')
246+
ps = makerequest(self._filePS('complete').__of__(container))
245247
self.assertEqual(ps.title, 'This is a title')
246248
self.assertEqual(ps.body(), 'print(foo+bar+baz)\nreturn printed\n')
247249
self.assertEqual(ps.params(), 'foo, bar, baz=1')
@@ -265,7 +267,8 @@ def test_PUT_native_string(self):
265267
self.assertEqual(ps.params(), 'oops')
266268

267269
def test_PUT_bytes(self):
268-
ps = makerequest(self._filePS('complete'))
270+
container = DummyFolder('container')
271+
ps = makerequest(self._filePS('complete').__of__(container))
269272
self.assertEqual(ps.title, 'This is a title')
270273
self.assertEqual(ps.body(), 'print(foo+bar+baz)\nreturn printed\n')
271274
self.assertEqual(ps.params(), 'foo, bar, baz=1')
@@ -588,3 +591,46 @@ def test_PythonScript_proxyroles_nonmanager(self):
588591

589592
# Cleanup
590593
noSecurityManager()
594+
595+
596+
class TestTraceback(FunctionalTestCase, PythonScriptTestBase):
597+
598+
def _format_exception(self):
599+
return "".join(traceback.format_exception(*sys.exc_info()))
600+
601+
def test_source_code_in_traceback(self):
602+
ps = self._newPS("1 / 0")
603+
try:
604+
ps()
605+
except ZeroDivisionError:
606+
formatted_exception = self._format_exception()
607+
self.assertIn("1 / 0", formatted_exception)
608+
609+
ps.write("2 / 0")
610+
try:
611+
ps()
612+
except ZeroDivisionError:
613+
formatted_exception = self._format_exception()
614+
self.assertIn("2 / 0", formatted_exception)
615+
616+
def test_multiple_scripts_in_traceback(self):
617+
from Products.PythonScripts.PythonScript import manage_addPythonScript
618+
619+
script1_body = "container.script2()"
620+
manage_addPythonScript(
621+
self.folder,
622+
"script1",
623+
file=script1_body,
624+
)
625+
script2_body = "1 / 0"
626+
manage_addPythonScript(
627+
self.folder,
628+
"script2",
629+
file=script2_body,
630+
)
631+
try:
632+
self.folder.script1()
633+
except ZeroDivisionError:
634+
formatted_exception = self._format_exception()
635+
self.assertIn(script1_body, formatted_exception)
636+
self.assertIn(script2_body, formatted_exception)

0 commit comments

Comments
 (0)