Commit a4136edf authored by Jérome Perrin's avatar Jérome Perrin

monaco_editor: increase debounce timeout for pylint checks XXX

on very large python files (>1000 lines) sometimes they queue up and we
have to wait for all requests that were queued by zope.

XXX maybe this does not happen when accessing through haproxy/apache, I
am observing this when hitting zope directly
parent e0d2316e
import json
import sys
from threading import RLock
import logging
import jedi
# increase default cache duration
jedi.settings.call_signatures_validity = 30 # XXX needed ?
# map jedi type to the name of monaco.languages.CompletionItemKind
# This mapping and the functions below (_format_completion, _label, _detail, _sort_text )
# are copied/inspired by jedi integration in python-language-server
# https://github.com/palantir/python-language-server/blob/19b10c47988df504872a4fe07c421b0555b3127e/pyls/plugins/jedi_completion.py
# python-language-server is Copyright 2017 Palantir Technologies, Inc. and distributed under MIT License.
# https://github.com/palantir/python-language-server/blob/19b10c47988df504872a4fe07c421b0555b3127e/LICENSE
_TYPE_MAP = {
'none': 'Value',
'type': 'Class',
'tuple': 'Class',
'dict': 'Class',
'dictionary': 'Class',
'function': 'Function',
'lambda': 'Function',
'generator': 'Function',
'class': 'Class',
'instance': 'Reference',
'method': 'Method',
'builtin': 'Class',
'builtinfunction': 'Function',
'module': 'Module',
'file': 'File',
'xrange': 'Class',
'slice': 'Class',
'traceback': 'Class',
'frame': 'Class',
'buffer': 'Class',
'dictproxy': 'Class',
'funcdef': 'Function',
'property': 'Property',
'import': 'Module',
'keyword': 'Keyword',
'constant': 'Variable',
'variable': 'Variable',
'value': 'Value',
'param': 'Variable',
'statement': 'Keyword',
}
def _label(definition):
if definition.type in ('function', 'method') and hasattr(definition, 'params'):
params = ', '.join([param.name for param in definition.params])
return '{}({})'.format(definition.name, params)
return definition.name
def _detail(definition):
try:
return definition.parent().full_name or ''
except AttributeError:
return definition.full_name or ''
def _sort_text(definition):
""" Ensure builtins appear at the bottom.
Description is of format <type>: <module>.<item>
"""
# If its 'hidden', put it next last
prefix = 'z{}' if definition.name.startswith('_') else 'a{}'
return prefix.format(definition.name)
def _format_docstring(docstring):
return docstring
def _format_completion(d):
completion = {
'label': _label(d),
'_kind': _TYPE_MAP.get(d.type),
'detail': _detail(d),
'documentation': _format_docstring(d.docstring()),
'sortText': _sort_text(d),
'insertText': d.name
}
return completion
def _guessType(name):
"""guess the type of python script parameters based on naming conventions.
TODO: depend on the script name, for Person_getSomething, context is a erp5.portal_type.Person
"""
name = name.split('=')[0] # support also assigned names ( like REQUEST=None in params)
if name in ('context', 'container',):
return 'Products.ERP5Type.Core.Folder.Folder'
if name == 'script':
return 'Products.PythonScripts.PythonScript'
if name == 'REQUEST':
return 'ZPublisher.HTTPRequest.HTTPRequest'
if name == 'RESPONSE':
return 'ZPublisher.HTTPRequest.HTTPResponse'
return 'str' # assume string by default
#jedi_lock = RLock() # jedi is not thread safe
import Products.ERP5Type.Utils
logger = logging.getLogger("erp5.extension.Jedi")
# Jedi is not thread safe
jedi_lock = getattr(Products.ERP5Type.Utils, 'jedi_lock', None)
if jedi_lock is None:
logger.critical("There was no lock, making a new one")
jedi_lock = Products.ERP5Type.Utils.jedi_lock = RLock()
logger.info("Jedi locking with %s (%s)", jedi_lock, id(jedi_lock))
def ERP5Site_getPythonSourceCodeCompletionList(self, data, REQUEST=None):
"""Complete source code with jedi.
"""
logger.info('jedi get lock %s (%s)', jedi_lock, id(jedi_lock))
with jedi_lock:
if isinstance(data, basestring):
data = json.loads(data)
# data contains the code, the bound names and the script params. From this
# we reconstruct a function that can be checked
def indent(text):
return ''.join((" " + line) for line in text.splitlines(True))
is_python_script = 'bound_names' in data
if is_python_script:
signature_parts = data['bound_names']
if data['params']:
signature_parts += [data['params']]
signature = ", ".join(signature_parts)
imports = "import Products.ERP5Type.Core.Folder; import ZPublisher.HTTPRequest; import Products.PythonScripts"
function_name = "function_name"
type_annotation = " # type: (%s) -> None" % (
', '.join([_guessType(part) for part in signature_parts]))
body = "%s\ndef %s(%s):\n%s\n%s" % (
imports,
function_name,
signature,
type_annotation,
indent(data['code']) or " pass")
data['position']['line'] = data['position']['line'] + 3 # imports, fonction header + type annotation line
data['position']['column'] = data['position']['column'] + 2 # " " from indent(text)
else:
body = data['code']
logger.info("jedi getting completions....")
script = jedi.Script(
body,
data['position']['line'],
data['position']['column'] - 1,
'example.py', # TODO name
sys_path=list(sys.path),
)
completions = [_format_completion(c) for c in script.completions()]
logger.info("jedi got completion")
if REQUEST is not None:
REQUEST.RESPONSE.setHeader('content-type', 'application/json')
return json.dumps(completions)
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Extension Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>Jedi</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>extension.erp5.Jedi</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Extension Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData>
from yapf.yapflib import yapf_api
import json
import tempfile
import textwrap
def ERP5Site_formatPythonSourceCode(self, data, REQUEST=None):
if isinstance(data, basestring):
data = json.loads(data)
try:
extra = {}
if data['range']:
extra['lines'] = (
(data['range']['startLineNumber'], data['range']['endLineNumber']),)
with tempfile.NamedTemporaryFile(mode='w', suffix='.style.yapf') as f:
f.write(
textwrap.dedent(
'''
[style]
based_on_style = chromium
#SPLIT_ALL_TOP_LEVEL_COMMA_SEPARATED_VALUES = true
#SPLIT_ARGUMENTS_WHEN_COMMA_TERMINATED = true
SPLIT_BEFORE_EXPRESSION_AFTER_OPENING_PAREN = true
BLANK_LINE_BEFORE_NESTED_CLASS_OR_DEF = false
ALLOW_SPLIT_BEFORE_DICT_VALUE = true
SPLIT_BEFORE_FIRST_ARGUMENT = true
SPLIT_BEFORE_LOGICAL_OPERATOR = true
SPLIT_BEFORE_DOT = true
'''))
f.flush()
formatted_code, changed = yapf_api.FormatCode(
data['code'], style_config=f.name, **extra)
except SyntaxError as e:
return json.dumps(dict(error=True, error_line=e.lineno))
if REQUEST is not None:
REQUEST.RESPONSE.setHeader('content-type', 'application/json')
return json.dumps(dict(formatted_code=formatted_code, changed=changed))
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Extension Component" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_recorded_property_dict</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>default_reference</string> </key>
<value> <string>YAPF</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>extension.erp5.YAPF</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Extension Component</string> </value>
</item>
<item>
<key> <string>sid</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>text_content_error_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>text_content_warning_message</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>version</string> </key>
<value> <string>erp5</string> </value>
</item>
<item>
<key> <string>workflow_history</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAM=</string> </persistent>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary/>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="3" aka="AAAAAAAAAAM=">
<pickle>
<global name="PersistentMapping" module="Persistence.mapping"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>data</string> </key>
<value>
<dictionary>
<item>
<key> <string>component_validation_workflow</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAQ=</string> </persistent>
</value>
</item>
</dictionary>
</value>
</item>
</dictionary>
</pickle>
</record>
<record id="4" aka="AAAAAAAAAAQ=">
<pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/>
</pickle>
<pickle>
<tuple>
<none/>
<list>
<dictionary>
<item>
<key> <string>action</string> </key>
<value> <string>validate</string> </value>
</item>
<item>
<key> <string>validation_state</string> </key>
<value> <string>validated</string> </value>
</item>
</dictionary>
</list>
</tuple>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>ERP5Site_formatPythonSourceCode</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>YAPF</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_formatPythonSourceCode</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="ExternalMethod" module="Products.ExternalMethod.ExternalMethod"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_function</string> </key>
<value> <string>ERP5Site_getPythonSourceCodeCompletionList</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>Jedi</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getPythonSourceCodeCompletionList</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
...@@ -47,6 +47,7 @@ ...@@ -47,6 +47,7 @@
<script tal:content='python: "var textarea_selector=" + modules["json"].dumps(options.get("textarea_selector"))'> <script tal:content='python: "var textarea_selector=" + modules["json"].dumps(options.get("textarea_selector"))'>
</script> </script>
<script tal:content='python: "var bound_names=" + modules["json"].dumps(options.get("bound_names"))'></script> <script tal:content='python: "var bound_names=" + modules["json"].dumps(options.get("bound_names"))'></script>
<script tal:content='python: "var script_name=" + modules["json"].dumps(options.get("script_name"))'></script>
<script <script
tal:content='python: "window.monacoEditorWebPackResourceBaseUrl = " + modules["json"].dumps(options["portal_url"]) + " + \"/monaco-editor/\""'> tal:content='python: "window.monacoEditorWebPackResourceBaseUrl = " + modules["json"].dumps(options["portal_url"]) + " + \"/monaco-editor/\""'>
...@@ -257,10 +258,99 @@ $script.onload = function() { ...@@ -257,10 +258,99 @@ $script.onload = function() {
function makeTimeoutFunction(ac){ function makeTimeoutFunction(ac){
return () => checkPythonSourceCode(ac) return () => checkPythonSourceCode(ac)
} }
timeout = setTimeout(makeTimeoutFunction(controller), 300); timeout = setTimeout(makeTimeoutFunction(controller), 3000);
} }
}); });
yapfDocumentFormattingProvider = {
_provideFormattingEdits: function(model, range, options, token) {
const controller = new AbortController();
token.onCancellationRequested(() => {controller.abort()})
const data = new FormData();
data.append("data", JSON.stringify({code: model.getValue(), range:range}));
return fetch(portal_url + "/ERP5Site_formatPythonSourceCode", {
method: "POST",
body: data,
signal: controller.signal
})
.then(response => response.json())
.then(data => {
if (data.error){
editor.revealLine(data.error_line);
return;
}
if (data.changed) {
return [
{
range: model.getFullModelRange(),
text: data.formatted_code,
},
];
};
}, e => {
if (!e instanceof DOMException /* AbortError */ ) {
throw e;
}
/* ignore aborted requests */
});
},
provideDocumentRangeFormattingEdits: function(model, range, options, token){
return this._provideFormattingEdits(model, range, options, token);
},
provideDocumentFormattingEdits: function(model, options, token) {
return this._provideFormattingEdits(model, null, options, token);
}
}
monaco.languages.registerDocumentFormattingEditProvider(
'python',
yapfDocumentFormattingProvider)
monaco.languages.registerDocumentRangeFormattingEditProvider(
'python',
yapfDocumentFormattingProvider)
monaco.languages.registerCompletionItemProvider('python', {
provideCompletionItems: async function(model, position, context, token) {
const controller = new AbortController();
token.onCancellationRequested(() => {controller.abort()})
const data = new FormData();
const complete_parameters = {
code: model.getValue(),
position: {line: position.lineNumber, column: position.column}
};
// ZMI python scripts pass extra parameters to linter
if (bound_names) {
complete_parameters["script_name"] = script_name;
complete_parameters["bound_names"] = JSON.parse(bound_names);
complete_parameters["params"] = document.querySelector(
'input[name="params"]'
).value;
}
data.append("data", JSON.stringify(complete_parameters));
return fetch(portal_url + "/ERP5Site_getPythonSourceCodeCompletionList", {
method: "POST",
body: data,
signal: controller.signal
})
.then(response => response.json())
.then(data => {
return {suggestions: data.map(c => {
c.kind = monaco.languages.CompletionItemKind[c._kind];
// this makes monaco render documentation as markdown.
c.documentation = {value: c.documentation};
return c
})};
}, e => {
if (!e instanceof DOMException /* AbortError */ ) {
throw e;
}
/* ignore aborted requests */
});
}
});
if (mode === "python") { if (mode === "python") {
// Perform a first check when loading document. // Perform a first check when loading document.
checkPythonSourceCode(new AbortController()); checkPythonSourceCode(new AbortController());
......
extension.erp5.Jedi
extension.erp5.YAPF
\ No newline at end of file
...@@ -108,6 +108,7 @@ def manage_page_footer(self): ...@@ -108,6 +108,7 @@ def manage_page_footer(self):
textarea_selector=textarea_selector, textarea_selector=textarea_selector,
portal_url=portal_url, portal_url=portal_url,
bound_names=bound_names, bound_names=bound_names,
script_name=document.getId(),
mode=mode).encode('utf-8')) mode=mode).encode('utf-8'))
return default return default
......
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