Commit 519f7480 authored by Arnaud Fontaine's avatar Arnaud Fontaine

ZODB Components: Like Python Script, use Ace Editor annotations to check...

ZODB Components: Like Python Script, use Ace Editor annotations to check source code without saving.

Also, switch to Pylint for Python Script source code checking to use the same
code for both.
parent 659d9ac5
...@@ -414,15 +414,36 @@ ...@@ -414,15 +414,36 @@
ace_editor.getSession().setMode(new PythonMode());\n ace_editor.getSession().setMode(new PythonMode());\n
ace_editor.getSession().setUseSoftTabs(true);\n ace_editor.getSession().setUseSoftTabs(true);\n
ace_editor.getSession().setTabSize(2);\n ace_editor.getSession().setTabSize(2);\n
\n
ace.require(\'ace/ext/language_tools\');\n
ace_editor.setOptions({ enableBasicAutocompletion: true, enableSnippets: true });\n
\n
timer = 0;\n
function checkPythonSourceCode() {\n
if (timer) {\n
window.clearTimeout(timer);\n
timer = 0;\n
}\n
timer = window.setTimeout(function() {\n
$.post(\'${portal_url}/ERP5Site_checkPythonSourceCodeAsJSON\',\n
{\'data\': JSON.stringify(\n
{ code: ace_editor.getSession().getValue() })},\n
function(data){\n
ace_editor.getSession().setAnnotations(data.annotations);\n
}\n
)\n
}, 500);\n
}\n
\n
checkPythonSourceCode();\n
\n \n
var textarea = $(\'#${id}\');\n var textarea = $(\'#${id}\');\n
ace_editor.getSession().on(\'change\', function() {\n ace_editor.getSession().on(\'change\', function() {\n
changed = true; // This is the dirty flag for onbeforeunload warning in erp5.js\n changed = true; // This is the dirty flag for onbeforeunload warning in erp5.js\n
textarea.val(ace_editor.getSession().getValue());\n textarea.val(ace_editor.getSession().getValue());\n
checkPythonSourceCode();\n
});\n });\n
ace.require(\'ace/ext/language_tools\');\n \n
ace_editor.setOptions({ enableBasicAutocompletion: true, enableSnippets: true });\n
\n
/* Only display the source code saving button if the main save button is\n /* Only display the source code saving button if the main save button is\n
* displayed. This specific save button allows to save without reloading the\n * displayed. This specific save button allows to save without reloading the\n
* page (and thus keep the cursor position and mode (maximize/fullscreen)\n * page (and thus keep the cursor position and mode (maximize/fullscreen)\n
......
2014-02-24 arnaud.fontaine
* Add annotations support to check source code without saving.
2013-08-22 jerome 2013-08-22 jerome
* Update to 45d3068aa7190f08396bcfe134e505fe144c1ccb ( package 10.28.2013 ) * Update to 45d3068aa7190f08396bcfe134e505fe144c1ccb ( package 10.28.2013 )
......
19 20
\ No newline at end of file \ No newline at end of file
...@@ -136,16 +136,12 @@ def checkConversionToolAvailability(self): ...@@ -136,16 +136,12 @@ def checkConversionToolAvailability(self):
result.edit(severity=severity) result.edit(severity=severity)
active_process.activateResult(result) active_process.activateResult(result)
def runPyflakes(script_code, script_path): from Products.ERP5Type.Utils import checkPythonSourceCode
# TODO: reuse _runPyflakes ...
from pyflakes.api import check def checkPythonSourceCodeAsJSON(self, data):
from pyflakes import reporter """
from StringIO import StringIO Check Python source suitable for Ace Editor and return a JSON object
stream = StringIO() """
check(script_code, script_path, reporter.Reporter(stream, stream))
return stream.getvalue()
def runPyflakesOnPythonScript(self, data):
import json import json
# XXX data is encoded as json, because jQuery serialize lists as [] # XXX data is encoded as json, because jQuery serialize lists as []
...@@ -153,56 +149,35 @@ def runPyflakesOnPythonScript(self, data): ...@@ -153,56 +149,35 @@ def runPyflakesOnPythonScript(self, data):
data = json.loads(data) data = json.loads(data)
# data contains the code, the bound names and the script params. From this # data contains the code, the bound names and the script params. From this
# we reconstruct a function that can be parsed with pyflakes. # we reconstruct a function that can be checked
code = data
def indent(text): def indent(text):
return ''.join((" " + line) for line in text.splitlines(True)) return ''.join((" " + line) for line in text.splitlines(True))
bound_names = data['bound_names'] is_python_script = 'bound_names' in data
signature_parts = data['bound_names'] if is_python_script:
if data['params']: signature_parts = data['bound_names']
signature_parts += [data['params']] if data['params']:
signature = ", ".join(signature_parts) signature_parts += [data['params']]
signature = ", ".join(signature_parts)
function_name = "function_name"
body = "def %s(%s):\n%s" % (function_name,
signature,
indent(data['code']) or " pass")
else:
body = data['code']
function_name = "function_name" message_list = checkPythonSourceCode(body)
body = "def %s(%s):\n%s" % (function_name, for message_dict in message_list:
signature, if is_python_script:
indent(data['code']) or " pass") message_dict['row'] = message_dict['row'] - 2
else:
message_dict['row'] = message_dict['row'] - 1
error_list = _runPyflakes(body, lineno_offset=-1) if message_dict['type'] in ('E', 'F'):
message_dict['type'] = 'error'
else:
message_dict['type'] = 'warning'
self.REQUEST.RESPONSE.setHeader('content-type', 'application/json') self.REQUEST.RESPONSE.setHeader('content-type', 'application/json')
return json.dumps(dict(annotations=error_list)) return json.dumps(dict(annotations=message_list))
\ No newline at end of file
def _runPyflakes(code, lineno_offset=0):
import pyflakes.api
error_list = []
class Reporter(object):
def unexpectedError(self, filename, msg):
error_list.append(
{ 'row': 0,
'column': 0,
'text': msg,
'type': 'error' }
)
def syntaxError(self, filename, msg, lineno, offset, text):
error_list.append(
{ 'row': lineno - 1 + lineno_offset,
'column': offset,
'text': msg + (text and ": " + text or ''),
'type': 'error' }
)
def flake(self, message):
error_list.append(
{ 'row': message.lineno - 1 + lineno_offset,
'column': getattr(message, 'col', 0),
'text': message.message % message.message_args,
'type': 'warning' }
)
pyflakes.api.check(code, '', Reporter())
return error_list
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
<dictionary> <dictionary>
<item> <item>
<key> <string>_function</string> </key> <key> <string>_function</string> </key>
<value> <string>runPyflakesOnPythonScript</string> </value> <value> <string>checkPythonSourceCodeAsJSON</string> </value>
</item> </item>
<item> <item>
<key> <string>_module</string> </key> <key> <string>_module</string> </key>
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
</item> </item>
<item> <item>
<key> <string>id</string> </key> <key> <string>id</string> </key>
<value> <string>ERP5Site_checkPythonScriptAsJSON</string> </value> <value> <string>ERP5Site_checkPythonSourceCodeAsJSON</string> </value>
</item> </item>
<item> <item>
<key> <string>title</string> </key> <key> <string>title</string> </key>
......
2014-02-24 arnaud.fontaine
* Use Pylint to check source code to unify ZODB Components and Python Script source code checking.
2013-09-03 arnaud.fontaine 2013-09-03 arnaud.fontaine
* ZODB Components: Workflow History must always be kept, so avoid an extra step for developers. * ZODB Components: Workflow History must always be kept, so avoid an extra step for developers.
......
158 159
\ No newline at end of file \ No newline at end of file
...@@ -52,7 +52,19 @@ ...@@ -52,7 +52,19 @@
<key> <string>_body</string> </key> <key> <string>_body</string> </key>
<value> <string>obj = state_change[\'object\']\n <value> <string>obj = state_change[\'object\']\n
\n \n
error_list, warning_list = obj.checkSourceCode()\n error_list = []\n
warning_list = []\n
for message_dict in obj.checkSourceCode():\n
message = \'%s:%3d,%3d: %s\' % (message_dict[\'type\'],\n
message_dict[\'row\'],\n
message_dict[\'column\'],\n
message_dict[\'text\'])\n
\n
if message_dict[\'type\'] in (\'F\', \'E\'):\n
error_list.append(message)\n
else:\n
warning_list.append(message)\n
\n
obj.setTextContentWarningMessageList(warning_list)\n obj.setTextContentWarningMessageList(warning_list)\n
obj.setTextContentErrorMessageList(error_list)\n obj.setTextContentErrorMessageList(error_list)\n
\n \n
......
2014-02-24 arnaud.fontaine
* Follow API changes to unify ZODB Components and Python Script source code checking.
2014-02-19 Arnaud Fontaine 2014-02-19 Arnaud Fontaine
* ZODB Components: Remove ClassTool and DocumentationHelper relying on it. * ZODB Components: Remove ClassTool and DocumentationHelper relying on it.
......
41152 41153
\ No newline at end of file \ No newline at end of file
...@@ -419,6 +419,116 @@ def fill_args_from_request(*optional_args): ...@@ -419,6 +419,116 @@ def fill_args_from_request(*optional_args):
return decorator(function) return decorator(function)
return decorator return decorator
_pylint_message_re = re.compile(
'^(?P<type>[CRWEF]):\s*(?P<row>\d+),\s*(?P<column>\d+):\s*(?P<message>.*)$')
def checkPythonSourceCode(source_code_str):
"""
Check source code with pylint or compile() builtin if not available.
TODO-arnau: Get rid of NamedTemporaryFile (require a patch on pylint to
allow passing a string) and this should probably return a proper
ERP5 object rather than a dict...
"""
if not source_code_str:
return []
try:
from pylint.lint import Run
from pylint.reporters.text import TextReporter
except ImportError, error:
try:
compile(source_code_str, '<string>', 'exec')
return []
except Exception, error:
if isinstance(error, SyntaxError):
message = {'type': 'F',
'row': error.lineno,
'column': error.offset,
'text': error.message}
else:
message = {'type': 'F',
'row': -1,
'column': -1,
'text': str(error)}
return [message]
import cStringIO
import tempfile
import sys
#import time
#started = time.time()
message_list = []
output_file = cStringIO.StringIO()
# pylint prints directly on stderr/stdout (only reporter content matters)
stderr = sys.stderr
stdout = sys.stdout
try:
sys.stderr = cStringIO.StringIO()
sys.stdout = cStringIO.StringIO()
with tempfile.NamedTemporaryFile() as input_file:
input_file.write(source_code_str)
input_file.seek(0)
Run([input_file.name, '--reports=n', '--indent-string=" "', '--zope=y',
# Disable Refactoring and Convention messages which are too verbose
# TODO-arnau: Should perphaps check ERP5 Naming Conventions?
'--disable=R,C',
# 'String statement has no effect': eg docstring at module level
'--disable=W0105',
# 'Using possibly undefined loop variable %r': Spurious warning
# (loop variables used after the loop)
'--disable=W0631',
# 'fixme': No need to display TODO/FIXME entry in warnings
'--disable=W0511',
# 'Unused argument %r': Display for readability or when defining abstract methods
'--disable=W0613',
# 'Catching too general exception %s': Too coarse
# TODO-arnau: Should consider raise in except
'--disable=W0703',
# 'Used * or ** magic': commonly used in ERP5
'--disable=W0142',
# 'Class has no __init__ method': Spurious warning
'--disable=W0232',
# 'Attribute %r defined outside __init__': Spurious warning
'--disable=W0201',
# Dynamic class generation so some attributes may not be found
# TODO-arnau: Enable it properly would require inspection API
# '%s %r has no %r member'
'--disable=E1101,E1103',
# 'No name %r in module %r'
'--disable=E0611',
# map and filter should not be considered bad as in some cases
# map is faster than its recommended replacement (list
# comprehension)
'--bad-functions=apply,input',
# 'Access to a protected member %s of a client class'
'--disable=W0212',
# string module does not only contain deprecated functions...
'--deprecated-modules=regsub,TERMIOS,Bastion,rexec'],
reporter=TextReporter(output_file), exit=False)
output_file.reset()
for line in output_file:
match_obj = _pylint_message_re.match(line)
if match_obj:
message_list.append({'type': match_obj.group('type'),
'row': int(match_obj.group('row')),
'column': int(match_obj.group('column')),
'text': match_obj.group('message')})
finally:
output_file.close()
sys.stderr = stderr
sys.stdout = stdout
#LOG('Utils', INFO, 'Checking time (pylint): %.2f' % (time.time() - started))
return message_list
##################################################### #####################################################
# Globals initialization # Globals initialization
##################################################### #####################################################
......
...@@ -41,10 +41,7 @@ from Products.ERP5Type.ConsistencyMessage import ConsistencyMessage ...@@ -41,10 +41,7 @@ from Products.ERP5Type.ConsistencyMessage import ConsistencyMessage
from zLOG import LOG, INFO from zLOG import LOG, INFO
from ExtensionClass import ExtensionClass from ExtensionClass import ExtensionClass
from Products.ERP5Type.Utils import convertToUpperCase from Products.ERP5Type.Utils import convertToUpperCase, checkPythonSourceCode
import re
pylint_message_re = re.compile('^(?P<type>[CRWEF]):\s*\d+,\s*\d+:\s*.*$')
class RecordablePropertyMetaClass(ExtensionClass): class RecordablePropertyMetaClass(ExtensionClass):
""" """
...@@ -279,109 +276,10 @@ class ComponentMixin(PropertyRecordableMixin, Base): ...@@ -279,109 +276,10 @@ class ComponentMixin(PropertyRecordableMixin, Base):
security.declareProtected(Permissions.ModifyPortalContent, 'checkSourceCode') security.declareProtected(Permissions.ModifyPortalContent, 'checkSourceCode')
def checkSourceCode(self): def checkSourceCode(self):
""" """
Check source code with pylint Check Component source code through Pylint or compile() builtin if not
available
TODO-arnau: Get rid of NamedTemporaryFile (require a patch on pylint to
allow passing a string)
""" """
source_code = self.getTextContent() return checkPythonSourceCode(self.getTextContent())
# checkConsistency() ensures that it cannot happen once validated/modified
if not source_code:
return [], []
try:
from pylint.lint import Run
from pylint.reporters.text import TextReporter
except ImportError, error:
try:
compile(source_code, '<string>', 'exec')
return [], []
except Exception, error:
if isinstance(error, SyntaxError):
error = '%4d, %4d: %s' % (error.lineno,
error.offset,
error.message)
return ['F: %s' % error], []
import cStringIO
import tempfile
import sys
#import time
#started = time.time()
error_list = []
warning_list = []
output_file = cStringIO.StringIO()
# pylint prints directly on stderr/stdout (only reporter content matters)
stderr = sys.stderr
stdout = sys.stdout
try:
sys.stderr = cStringIO.StringIO()
sys.stdout = cStringIO.StringIO()
with tempfile.NamedTemporaryFile() as input_file:
input_file.write(source_code)
input_file.seek(0)
Run([input_file.name, '--reports=n', '--indent-string=" "', '--zope=y',
# Disable Refactoring and Convention messages which are too verbose
# TODO-arnau: Should perphaps check ERP5 Naming Conventions?
'--disable=R,C',
# 'String statement has no effect': eg docstring at module level
'--disable=W0105',
# 'Using possibly undefined loop variable %r': Spurious warning
# (loop variables used after the loop)
'--disable=W0631',
# 'fixme': No need to display TODO/FIXME entry in warnings
'--disable=W0511',
# 'Unused argument %r': Display for readability or when defining abstract methods
'--disable=W0613',
# 'Catching too general exception %s': Too coarse
# TODO-arnau: Should consider raise in except
'--disable=W0703',
# 'Used * or ** magic': commonly used in ERP5
'--disable=W0142',
# 'Class has no __init__ method': Spurious warning
'--disable=W0232',
# 'Attribute %r defined outside __init__': Spurious warning
'--disable=W0201',
# Dynamic class generation so some attributes may not be found
# TODO-arnau: Enable it properly would require inspection API
# '%s %r has no %r member'
'--disable=E1101,E1103',
# 'No name %r in module %r'
'--disable=E0611',
# map and filter should not be considered bad as in some cases
# map is faster than its recommended replacement (list
# comprehension)
'--bad-functions=apply,input',
# 'Access to a protected member %s of a client class'
'--disable=W0212',
# string module does not only contain deprecated functions...
'--deprecated-modules=regsub,TERMIOS,Bastion,rexec'],
reporter=TextReporter(output_file), exit=False)
output_file.reset()
for line in output_file:
message_obj = pylint_message_re.match(line)
if message_obj:
line = line.strip()
if line[0] in ('E', 'F'):
error_list.append(line)
else:
warning_list.append(line)
finally:
output_file.close()
sys.stderr = stderr
sys.stdout = stdout
#LOG('component', INFO, 'Checking time (pylint): %.2f' % (time.time() -
# started))
return error_list, warning_list
security.declareProtected(Permissions.ModifyPortalContent, 'PUT') security.declareProtected(Permissions.ModifyPortalContent, 'PUT')
def PUT(self, REQUEST, RESPONSE): def PUT(self, REQUEST, RESPONSE):
......
...@@ -98,7 +98,7 @@ $(document).ready(function() { ...@@ -98,7 +98,7 @@ $(document).ready(function() {
timer = 0; timer = 0;
} }
timer = window.setTimeout(function() { timer = window.setTimeout(function() {
$.post('%(portal_url)s/ERP5Site_checkPythonScriptAsJSON', $.post('%(portal_url)s/ERP5Site_checkPythonSourceCodeAsJSON',
{'data': JSON.stringify( {'data': JSON.stringify(
{ code: editor.getSession().getValue(), { code: editor.getSession().getValue(),
bound_names: %(bound_names)s, bound_names: %(bound_names)s,
......
...@@ -1627,17 +1627,17 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1627,17 +1627,17 @@ class _TestZodbComponent(SecurityTestCase):
self.assertEqual(component.checkConsistency(), []) self.assertEqual(component.checkConsistency(), [])
self.assertEqual(component.getTextContentErrorMessageList(), []) self.assertEqual(component.getTextContentErrorMessageList(), [])
self.assertEqual(component.getTextContentWarningMessageList(), self.assertEqual(component.getTextContentWarningMessageList(),
["W: 1, 0: Unused import sys (unused-import)"]) ["W: 1, 0: Unused import sys (unused-import)"])
component.setTextContent('import unexistent_module') component.setTextContent('import unexistent_module')
self.tic() self.tic()
self.assertEqual( self.assertEqual(
[m.getMessage().translate() for m in component.checkConsistency()], [m.getMessage().translate() for m in component.checkConsistency()],
["Error in Source Code: F: 1, 0: Unable to import 'unexistent_module' (import-error)"]) ["Error in Source Code: F: 1, 0: Unable to import 'unexistent_module' (import-error)"])
self.assertEqual(component.getTextContentErrorMessageList(), self.assertEqual(component.getTextContentErrorMessageList(),
["F: 1, 0: Unable to import 'unexistent_module' (import-error)"]) ["F: 1, 0: Unable to import 'unexistent_module' (import-error)"])
self.assertEqual(component.getTextContentWarningMessageList(), self.assertEqual(component.getTextContentWarningMessageList(),
["W: 1, 0: Unused import unexistent_module (unused-import)"]) ["W: 1, 0: Unused import unexistent_module (unused-import)"])
valid_code = 'def foobar():\n return 42' valid_code = 'def foobar():\n return 42'
ComponentTool.reset = assertResetCalled ComponentTool.reset = assertResetCalled
...@@ -1669,15 +1669,15 @@ class _TestZodbComponent(SecurityTestCase): ...@@ -1669,15 +1669,15 @@ class _TestZodbComponent(SecurityTestCase):
[], [],
[]), []),
('def foobar(*args, **kwargs)\n return 42', ('def foobar(*args, **kwargs)\n return 42',
["Error in Source Code: E: 1, 0: invalid syntax (syntax-error)"], ["Error in Source Code: E: 1, 0: invalid syntax (syntax-error)"],
["E: 1, 0: invalid syntax (syntax-error)"], ["E: 1, 0: invalid syntax (syntax-error)"],
[]), []),
# Make sure that foobar NameError is at the end to make sure that after # Make sure that foobar NameError is at the end to make sure that after
# defining foobar function, it is not available at all # defining foobar function, it is not available at all
('foobar', ('foobar',
["Error in Source Code: E: 1, 0: Undefined variable 'foobar' (undefined-variable)"], ["Error in Source Code: E: 1, 0: Undefined variable 'foobar' (undefined-variable)"],
["E: 1, 0: Undefined variable 'foobar' (undefined-variable)"], ["E: 1, 0: Undefined variable 'foobar' (undefined-variable)"],
["W: 1, 0: Statement seems to have no effect (pointless-statement)"])) ["W: 1, 0: Statement seems to have no effect (pointless-statement)"]))
for (invalid_code, for (invalid_code,
check_consistency_list, check_consistency_list,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment