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

core,monaco_editor: python language support 🚧

some work in progress changes to improve developer experience

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

jedi: generate stubs WIP

ERP5: "quick and dirty" type annotations XXX

IIRC the only thing needed is that ERP5TypeTestCase.getPortal is an
ERP5Site

monaco_editor: also enable jedi for codelens ( WIP: ZMI only )

core: pass "language support url" to text editors XXX

For now this is just the portal_url, but I'm thinking it could be a
proper tool.

monaco_editor: enable pylint in gadget version

because pylint is a bit slow on large components, debounce every 2
seconds. TODO: this is too slow.

monaco_editor: enable formatting provider for python

This makes "Format Document" / "Format Selection" work.

monaco_editor: enable completion provider for python

this makes completions works when using Ctrl+space

monaco_editor: pass portal_type to checkPythonSourceCode

python_support: new business template to act as a language server for python

checkPythonSourceCode: add a cache

When using checkPythonSourceCode integrated in the source code editor,
for a scenario where developer edit a component and save we can benefit
from caching the check message for the source code content, because the
same check that the one happening in the editor will happen when the
component is saved.

This cache varies on:
 - "component_packages" cache cookie which is reset every time some
component code is edited.
 - zope startup time to take into account editions of file system code.
This assumes that after reseting file system code zope will be
restarted.
 - portal_type, because the checks performed by this function also
depend on portal type.

jedi wip

administration: keep using pylint only for now

monaco_editor: jedi WIP

core: use mypy to check python code ( WIP experiment )

Revert "core: use mypy to check python code ( WIP experiment )"

This reverts commit cfa27232.

ERP5TypeTestCase: jedi workarounds

monaco_editor: WIP reference provider for python

monaco_editor: jedi wip (no longer use /tmp/)

yapf: adjust config following up Gabriel feedback
parent 3cb786cc
# coding: utf-8
# TODO: drop this ? it confuse type checking this file
from __future__ import unicode_literals
import json
import sys
import inspect
# pylint: disable=unused-import
from typing import List, Type, Optional, Dict, Tuple, Sequence, TYPE_CHECKING
import typing
import logging
from threading import RLock
from Products.ERP5Type.Cache import transactional_cached
logger = logging.getLogger("erp5.extension.Jedi")
logger.setLevel(logging.DEBUG)
import os
import jedi
import time
import erp5.portal_type
import tempfile
STUBS_BASE_PATH = os.path.join(tempfile.gettempdir(), 'erp5-stubs')
last_reload_time = time.time()
# increase default cache duration
# XXX I'm really not sure this is needed, jedi seems fast enough.
jedi.settings.call_signatures_validity = 30
if True:
# monkey patch to disable buggy sys.path addition based on buildout.
# https://github.com/davidhalter/jedi/issues/1325
# rdiff-backup seem to trigger a bug, but it's generally super slow and not correct for us.
try:
# in jedi 0.15.1 it's here
from jedi.evaluate import sys_path as jedi_inference_sys_path # pylint: disable=import-error,unused-import,no-name-in-module
except ImportError:
# but it's beeing moved. Next release (0.15.2) will be here
# https://github.com/davidhalter/jedi/commit/3b4f2924648eafb9660caac9030b20beb50a83bb
from jedi.inference import sys_path as jedi_inference_sys_path # pylint: disable=import-error,unused-import,no-name-in-module
_ = jedi_inference_sys_path.discover_buildout_paths # make sure it's here
def dont_discover_buildout_paths(*args, **kw):
return set()
jedi_inference_sys_path.discover_buildout_paths = dont_discover_buildout_paths
from jedi.api import project as jedi_api_project
jedi_api_project.discover_buildout_paths = dont_discover_buildout_paths
from jedi.evaluate.context.instance import TreeInstance
from jedi.evaluate.gradual.typing import InstanceWrapper
from jedi.evaluate.lazy_context import LazyKnownContexts
from jedi.evaluate.base_context import ContextSet, NO_CONTEXTS
def executeJediXXX(callback, context, arguments):
# XXX function for relaodability
def call():
return callback(context, arguments=arguments)
def makeFilterFunc(class_from_portal_type, arguments):
def filter_func(val):
if isinstance(val, TreeInstance) and val.tree_node.type == 'classdef':
logger.info(
"classdef cool => %s == %s", val.tree_node.name.value,
class_from_portal_type)
return val.tree_node.name.value == class_from_portal_type
if isinstance(val, LazyKnownContexts) and filter_func(val.infer()):
return True
if isinstance(val, ContextSet):
return val.filter(filter_func) != NO_CONTEXTS
if isinstance(val, InstanceWrapper):
for wrapped in val.iterate():
if filter_func(wrapped):
return True
return False
## annotation_classes = val.gather_annotation_classes()
## import pdb; pdb.set_trace()
## return val.gather_annotation_classes().filter(filter_func)
logger.info("not found in %s", val)
return False
return filter_func
# methods returning portal types
if context.is_function():
# and 1 or context.get_function_execution(
#).function_context.name.string_name == 'newContent':
if not arguments.argument_node:
return call() # no portal_type, we'll use what's defined in the stub
original = call()
#logger.info('re-evaluating %s ...', original)
# look for a "portal_type=" argument
for arg_name, arg_value in arguments.unpack():
if arg_name == 'portal_type':
try:
portal_type = iter(arg_value.infer()).next().get_safe_value()
except Exception:
logger.exception("error infering")
continue
if not isinstance(portal_type, str):
continue
logger.info(
'ahah portal_type based method with portal type=%s ...',
portal_type)
# XXX this is really horrible
original = call()
filtered = original.filter(
makeFilterFunc(portal_type.replace(' ', ''), arguments))
#original._set = frozenset(
# {x for x in original._set if class_from_portal_type in str(x)})
logger.info(
'portal_type based method, returning\n %s instead of\n %s',
filtered, original)
return filtered
# methods returning List of portal types
# methods returning List of Brain of portal types
return call()
def makeERP5Plugin():
logger.info('making erp5 plugin')
class JediERP5Plugin(object):
_cache = {}
def _getPortalObject(self): # XXX needed ?
# type: () -> erp5.portal_type.ERP5Site
from Products.ERP5.ERP5Site import getSite
from Products.ERP5Type.Globals import get_request
from ZPublisher.BaseRequest import RequestContainer
request = get_request()
assert request
return getSite().__of__(RequestContainer(REQUEST=request))
def execute(self, callback):
"""Handle dynamic methods accepting portal_type= arguments
"""
logger.info("JediERP5Plugin registering execute")
def wrapper(context, arguments):
# XXX call an external function that will be reloaded
from erp5.component.extension.Jedi import executeJediXXX as _execute
return _execute(callback, context, arguments)
return wrapper
return JediERP5Plugin()
# 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):
# type: (jedi.api.classes.Completion,) -> str
#if definition.type == 'param':
# return '{}='.format(definition.name)
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 _insertText(definition):
# type: (jedi.api.classes.Completion,) -> str
# XXX
#if definition.type == 'param':
# return '{}='.format(definition.name)
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(d):
try:
return d.docstring()
except Exception as e:
logger.exception('error getting completions from %s', d)
return "```{}```".format(repr(e))
def _format_completion(d):
# type: (jedi.api.classes.Completion,) -> Dict[str, Optional[str]]
completion = {
'label': _label(d),
'_kind': _TYPE_MAP.get(d.type),
'detail': _detail(d),
'documentation': _format_docstring(d),
'sortText': _sort_text(d),
'insertText': _insertText(d),
}
return completion
def _guessType(name, context_type=None):
"""guess the type of python script parameters based on naming conventions.
"""
# TODO: `state_change` arguments for workflow scripts
name = name.split('=')[
0] # support also assigned names ( like REQUEST=None in params)
if name == 'context' and context_type:
return context_type
if name in (
'context',
'container',
):
return 'erp5.portal_type.ERP5Site'
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 is not thread safe
import Products.ERP5Type.Utils
jedi_lock = getattr(Products.ERP5Type.Utils, 'jedi_lock', None) # type: RLock
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.
"""
portal = self.getPortalObject()
logger.debug('jedi get lock %s (%s)', jedi_lock, id(jedi_lock))
for _ in range(10):
locked = not jedi_lock.acquire(False)
if locked:
time.sleep(.5)
else:
jedi_lock.release()
break
else:
raise RuntimeError('jedi is locked')
with jedi_lock:
# register our erp5 plugin
from jedi.plugins import plugin_manager
if not getattr(plugin_manager, '_erp5_plugin_registered', None):
plugin_manager.register(makeERP5Plugin())
plugin_manager._erp5_plugin_registered = True
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))
script_name = data.get('script_name', 'unknown.py') # TODO name
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)
# guess type of `context`
context_type = None
if '_' in script_name:
context_type = script_name.split('_')[0]
if context_type not in [ti.replace(' ', '')
for ti in portal.portal_types.objectIds()] + [
'ERP5Site',
]:
logger.warning(
"context_type %s has no portal type, using ERP5Site", context_type)
context_type = None
else:
context_type = 'erp5.portal_type.{}'.format(context_type)
imports = "import erp5.portal_type; import Products.ERP5Type.Core.Folder; import ZPublisher.HTTPRequest; import Products.PythonScripts"
type_annotation = " # type: ({}) -> None".format(
', '.join([_guessType(part, context_type) for part in signature_parts]))
body = "%s\ndef %s(%s):\n%s\n%s" % (
imports, script_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']
with jedi_lock:
logger.debug("jedi getting completions for %s ...", script_name)
start = time.time()
script = jedi.Script(
body,
data['position']['line'],
data['position']['column'] - 1,
script_name,
sys_path=[STUBS_BASE_PATH] + list(sys.path),
)
def _get_param_name(p):
if (p.name.startswith('param ')):
return p.name[6:] # drop leading 'param '
return p.name
def _get_param_value(p):
pair = p.description.split('=')
if (len(pair) > 1):
return pair[1]
return None
completions = []
signature_completions = set()
try:
signatures = []
call_signatures = script.call_signatures()
logger.info(
"jedi first got %d call signatures in %.2fs", len(call_signatures),
(time.time() - start))
for signature in call_signatures:
for pos, param in enumerate(signature.params):
if not param.name:
continue
name = _get_param_name(param)
if param.name == 'self' and pos == 0:
continue
if name.startswith('*'):
continue
value = _get_param_value(param)
signatures.append((signature, name, value))
for signature, name, value in signatures:
completion = {
'label': '{}='.format(name),
'_kind': 'Variable',
'detail': value,
#'documentation': value,
'sortText': 'aaaaa_{}'.format(name),
'insertText': '{}='.format(name),
}
completions.append(completion)
signature_completions.add(name)
except Exception:
logger.exception("Error getting call signatures")
completions.extend(
_format_completion(c)
for c in script.completions()
if c.name not in signature_completions)
logger.info(
"jedi got %d completions in %.2fs", len(completions),
(time.time() - start))
if data.get('xxx_definition'):
completions = [] # XXX this is not "completions" ...
# TODO: only follow imports for classes, methods or functions ?
for definition in script.goto_definitions(
# follow_imports=True,
# only_stubs=False,
# prefer_stubs=False,
):
definition_line = definition.line
definition_column = definition.column
definition_uri = definition.module_path
definition_code = None
if script.path == definition.module_path:
definition_uri = None # client side will understand this as "current file"
if is_python_script:
# If we are in a python script and symbol is defined in the python script,
# we have to account for the 3 lines headers we added and the 2 columns of
# indentation
definition_line = definition_line - 3
definition_column = definition_column - 2
else:
if os.path.exists(definition.module_path):
with open(definition.module_path) as f:
definition_code = f.read()
# strip common prefix from Products or eggs directory
products_base_dir = os.path.join(
Products.ERP5Type.__file__, '..', '..')
prefix = os.path.commonprefix(
(products_base_dir, definition.module_path))
if prefix:
definition_uri = definition.module_path[len(prefix):]
if definition_uri.startswith('eggs/'):
definition_uri = definition_uri[len('eggs/'):]
elif definition_uri.startswith('develop-eggs/'):
definition_uri = definition_uri[len('develop-eggs/'):]
completions.append(
{
"range":
{
'startColumn':
1 + definition_column,
'endColumn':
1 + definition_column + len(definition.name),
'startLineNumber':
definition_line,
'endLineNumber':
definition_line,
},
'uri': definition_uri,
'code': definition_code,
})
if data.get('xxx_hover'):
completions = '' # XXX this is not "completions" ...
for definition in script.goto_definitions():
documentation_lines = definition.docstring().splitlines()
# reformat this in nicer markdown
completions = textwrap.dedent(
'''\
`{}`
---
{}
''').format(
documentation_lines[0],
'\n'.join(documentation_lines[1:]),
)
logger.info('hover: %s', completions)
if REQUEST is not None:
REQUEST.RESPONSE.setHeader('content-type', 'application/json')
return json.dumps(completions)
import textwrap
def safe_python_identifier(name):
# type: (str) -> str
"""Format this name as a python idenfier.
For example "Person Module" becomes "PersonModule"
"""
return name.replace(" ", "")
def safe_docstring(docstring):
# type: (str) -> str
"""Formats a docstring to include in generated stub.
"""
if not docstring:
return '...'
return "'''{}\n'''".format(docstring.replace("'''", r"\'\'\'"))
from Products.ERP5Type.Accessor import Constant
from Products.ERP5Type.Accessor import WorkflowState
from Products.ERP5Type.Base import WorkflowMethod
from Products.ERP5Type.ERP5Type import ERP5TypeInformation # pylint: disable=unused-import
from Products.PythonScripts.PythonScript import PythonScript # pylint: disable=unused-import
from collections import namedtuple, defaultdict
def SkinsTool_getClassSet(self):
portal = self.getPortalObject()
class_set = set([])
# TODO: sort by default skin selection and use the ones registered in skin selections
for skin_folder in portal.portal_skins.objectValues():
for script in skin_folder.objectValues(spec=('Script (Python)',
'External Method')):
if not '_' in script.getId():
logger.debug('Skipping wrongly named script %s', script.getId())
continue
type_ = script.getId().split('_')[0]
class_set.add(type_)
return class_set
def SkinsTool_getStubForClass(self, class_name):
portal = self.getPortalObject()
line_list = []
SkinDefinition = namedtuple(
'SkinDefinition', 'id,docstring,type_comment,skin_folder,params')
import parso
grammar = parso.load_grammar()
import re
type_coment_re = re.compile(r"\s*#\s*type:.*")
# collect skins by type
skin_by_type = defaultdict(list)
# TODO: sort by default skin selection and use only the ones registered in skin selections
# TODO: don't make this silly loop for all classes ? or maybe keep it - it could be useful
# when we are able to regenerate only what was changed.
for skin_folder in portal.portal_skins.objectValues():
for script in skin_folder.objectValues(spec=('Script (Python)',
'External Method')):
if not '_' in script.getId():
logger.debug('Skipping script without prefix %s', script.getId())
continue
# TODO: understand more invalid characters (use a regex)
if " " in script.getId() or "." in script.getId():
logger.debug(
'Skipping script with invalid characters %s', script.getId())
continue
type_ = script.getId().split('_')[0]
if type_ != class_name:
continue
docstring = '"""External method"""'
params = ''
type_comment = ''
if script.meta_type == 'Script (Python)':
body = script.body()
params = script.params()
if params:
params = ', {}'.format(params)
icon_path = script.om_icons()[0]['path']
icon_alt = script.om_icons()[0]['alt']
docstring_first_line = (
"![{icon_alt}]({portal_url}/{icon_path}) "
"[`{script_id}`]({portal_url}/portal_skins/{skin_folder_id}/{script_id}/manage_main)\n"
).format(
icon_alt=icon_alt,
portal_url=portal.absolute_url(),
icon_path=icon_path,
script_id=script.getId(),
skin_folder_id=skin_folder.getId())
docstring = '"""{}"""'.format(docstring_first_line)
module = grammar.parse(body)
if next(iter(grammar.iter_errors(module)), None) is not None:
first_leaf = module.get_first_leaf()
type_comment = first_leaf.prefix.strip()
# TODO: adjust type comment ?
if not type_coment_re.match(type_comment):
type_comment = ''
else:
# make sure docstring is indented
type_comment = '{}\n '.format(type_comment)
if first_leaf.type == 'string':
original_docstring = first_leaf.value
if original_docstring.startswith("'''"):
docstring = "'''{}\n{}".format(
docstring_first_line, original_docstring[3:])
elif original_docstring.startswith("'"):
docstring = "'''{}\n{}''".format(
docstring_first_line, original_docstring[1:])
elif original_docstring.startswith('"""'):
docstring = '"""{}\n{}'.format(
docstring_first_line, original_docstring[3:])
elif original_docstring.startswith('"'):
docstring = '"""{}\n{}""'.format(
docstring_first_line, original_docstring[1:])
skin_by_type[type_].append(
SkinDefinition(
script.getId(), docstring, type_comment, skin_folder.getId(),
params))
# TODO: this loop is nonsense.
for type_, skins in skin_by_type.items():
line_list.append(
textwrap.dedent(
"""\
# coding: utf-8
import erp5.portal_type
import typing
class {class_name}:
{docstring}
""").format(
class_name=safe_python_identifier(type_),
docstring=safe_docstring("Skins for {}".format(type_))))
# TODO: we just ignore duplicated scripts, but it would be better to use @typing.overload
defined_methods = set([])
for skin in skins:
skin = skin # type: SkinDefinition
if skin.id in defined_methods:
logger.debug(
"Skipping duplicated skin %s while defining erp5.skins_tool.%s",
skin.id, type_)
continue
defined_methods.add(skin.id)
line_list.append(
# the comment is also here so that dedent keep indentation, because this method block needs
# more indentation than class block
textwrap.dedent(
"""\
# {skin_id} in {skin_folder}
def {skin_id}(self{params}):
{type_comment}{docstring}
""").format(
skin_id=skin.id,
skin_folder=skin.skin_folder,
params=skin.params,
type_comment=skin.type_comment,
docstring=skin.docstring))
return "\n".join(line_list)
@WorkflowMethod.disable
def makeTempClass(portal, portal_type):
# type: (erp5.portal_type.ERP5Site, str) -> Type[Products.ERP5Type.Base.Base]
return portal.newContent(
portal_type=portal_type,
temp_object=True,
id='?',
title='?',
).__class__
def _getPythonTypeFromPropertySheetType(prop):
# type: (erp5.portal_type.StandardProperty,) -> str
property_sheet_type = prop.getElementaryType()
if property_sheet_type in ('content', 'object'):
# TODO
return 'Any'
mapped_type = {
'string': 'str',
'boolean': 'bool',
'data': 'bytes',
# XXX jedi does not understand DateTime dynamic name, so use "real name"
'date': 'DateTime.DateTime',
'int': 'int',
'long': 'int', # ???
'lines': 'Sequence[str]',
'tokens': 'Sequence[str]',
'float': 'float',
'text': 'str',
}.get(property_sheet_type, 'Any')
if prop.isMultivalued() \
and property_sheet_type not in ('lines', 'token'):
# XXX see Resource/p_variation_base_category_property, we can have multivalued lines properties
return 'Sequence[{}]'.format(mapped_type)
return mapped_type
def _isMultiValuedProperty(prop):
# type: (erp5.portal_type.StandardProperty,) -> bool
"""If this is a multi valued property, we have to generate list accessor.
"""
if prop.isMultivalued():
return True
return prop.getElementaryType() in ('lines', 'tokens')
@transactional_cached()
def TypeInformation_getEditParameterDict(self):
# type: (ERP5TypeInformation) -> Dict[str, Tuple[str, str]]
"""returns a mapping of properties that can be set on this type by edit or newContent
The returned data format is tuples containing documentation and type annotations,
keyed by parameter, like:
{ "title": ("The title of the document", "str") }
Python has a limitation on the number of arguments in a function, to prevent
SyntaxError: more than 255 arguments
we only generate the most common ones.
"""
portal = self.getPortalObject()
property_dict = {} # type: Dict[str, Tuple[str, str]]
temp_class = makeTempClass(portal, self.getId())
for property_sheet_id in [
parent_class.__name__
for parent_class in temp_class.mro()
if parent_class.__module__ == 'erp5.accessor_holder.property_sheet'
]:
property_sheet = portal.portal_property_sheets[property_sheet_id]
for prop in property_sheet.contentValues():
if not prop.getReference():
continue
if prop.getPortalType() in ('Standard Property', 'Acquired Property'):
property_dict[('{}_list' if _isMultiValuedProperty(prop) else
'{}').format(prop.getReference())] = (
prop.getDescription(),
_getPythonTypeFromPropertySheetType(prop))
elif prop.getPortalType() in (
'Category Property',
'Dynamic Category Property',
):
# XXX only generate a few
# property_dict['{}'.format(
# prop.getReference())] = (prop.getDescription(), 'str')
# property_dict['{}_list'.format(
# prop.getReference())] = (prop.getDescription(), 'Sequence[str]')
property_dict['{}_value'.format(prop.getReference())] = (
prop.getDescription(), '"erp5.portal_type.Type_AnyPortalType"')
# property_dict['{}_value_list'.format(prop.getReference())] = (
# prop.getDescription(),
# 'Sequence["erp5.portal_type.Type_AnyPortalType"]')
elif prop.getPortalType() == 'Dynamic Category Property':
# TODO
pass
return property_dict
def XXX_skins_class_exists(name):
# type: (str) -> bool
"""Returns true if a skin class exists for this name.
"""
return os.path.exists(
"{STUBS_BASE_PATH}/erp5/skins_tool/{name}.pyi".format(STUBS_BASE_PATH=STUBS_BASE_PATH, name=name))
def TypeInformation_getStub(self):
# type: (ERP5TypeInformation) -> str
"""returns a .pyi stub file for this portal type
https://www.python.org/dev/peps/pep-0484/
"""
portal = self.getPortalObject()
portal_url = portal.absolute_url()
# TODO: getParentValue
temp_class = makeTempClass(portal, self.getId())
# mro() of temp objects is like :
# (<class 'erp5.temp_portal_type.Temporary Person Module'>,
# <class 'Products.ERP5Type.mixin.temporary.TemporaryDocumentMixin'>,
# <class 'erp5.portal_type.Person Module'>, <-- this is the generated class.
# <class 'Products.ERP5.Document.Person.Person'>, <-- this is the source code of the "real" class,
# extending other "real" classes
# ...
temp_class = temp_class.mro()[2]
parent_class = temp_class.mro()[1]
parent_class_module = parent_class.__module__
imports = set(
[
'from Products.ERP5Type.Base import Base as Products_ERP5Type_Base_Base',
'import erp5.portal_type',
# TODO use "" style type definition without importing
# 'from erp5.portal_type import Type_CatalogBrain',
# 'from erp5.portal_type import Type_AnyPortalTypeList',
# 'from erp5.portal_type import Type_AnyPortalTypeCatalogBrainList',
'from typing import Union, List, Optional, Any, overload, Literal, TypeVar, Generic',
'from DateTime.DateTime import DateTime as DateTime # XXX help jedi',
# 'TranslatedMessage = str # TODO: this is type for translations ( Products.ERP5Type.Message.translateString should return this )'
])
header = ""
methods = []
debug = ""
method_template_template = """ {decorator}\n def {method_name}({method_args}) -> {return_type}:\n {docstring}"""
methods.append(
method_template_template.format(
decorator='',
method_name='getPortalType',
method_args="self",
return_type='Literal[b"{}"]'.format(self.getId()),
# We want to be able to infer based on the portal type named returned by x.getPortalType()
# jedi does not support Literal in this context, so add a method implementation.
docstring="{}\n return b'{}'".format(
safe_docstring(self.getId()), self.getId())))
methods.append(
method_template_template.format(
decorator='',
method_name='getPortalObject',
method_args="self",
return_type='"ERP5Site"',
docstring=safe_docstring(
getattr(temp_class.getPortalObject, '__doc__', None) or '...')))
# first class contain workflow and some constraints.
for property_name in sorted(vars(temp_class)):
if property_name[0] == '_':
continue
property_value = getattr(temp_class, property_name)
if isinstance(property_value, Constant.Getter):
# XXX skipped for now, too many methods that are not so useful
# TODO: add an implementation returning the value so that jedi can infer
if 0:
methods.append(
method_template_template.format(
decorator='',
method_name=safe_python_identifier(property_name),
method_args="self",
return_type=type(property_value.value).__name__,
docstring=safe_docstring('TODO %s' % property_value)))
elif isinstance(
property_value,
(
# we don't generate for TitleGetter and TranslatedGetter because they are useless
WorkflowState.TranslatedTitleGetter,
WorkflowState.Getter,
)):
workflow_id = property_value._key
workflow_url = '{portal_url}/portal_workflow/{workflow_id}'.format(
portal_url=portal_url,
workflow_id=workflow_id,
)
docstring = "State on [{workflow_id}]({workflow_url}/manage_main)\n".format(
workflow_id=workflow_id,
workflow_url=workflow_url,
)
if isinstance(property_value, WorkflowState.Getter):
docstring += "\n---\n"
docstring += " | State ID | State Name |\n"
docstring += " | --- | --- |\n"
for state in portal.portal_workflow[workflow_id].states.objectValues():
docstring += " | {state_id} | [{state_title}]({workflow_url}/states/{state_id}/manage_properties) |\n".format(
state_id=state.getId(),
state_title=state.title_or_id(),
workflow_url=workflow_url,
)
methods.append(
method_template_template.format(
decorator='',
method_name=safe_python_identifier(property_name),
method_args="self",
return_type="str",
docstring=safe_docstring(docstring)))
elif isinstance(property_value, WorkflowMethod):
docstring = ""
method_args = "self, comment:TranslatedMessage=None, **kw:Any"
return_type = 'None'
if hasattr(parent_class, property_name):
parent_method = getattr(parent_class, property_name)
docstring = inspect.getdoc(parent_method) + "\n\n--\n"
method_args = "self, *args:Any, **kw:Any"
return_type = 'Any'
if (property_name.startswith("manage_")
or property_name.startswith("set")
or property_name.startswith("get")):
logger.debug(
"Skipping workflow method %s wrapping existing %s (types: %s)",
property_name, parent_method,
typing.get_type_hints(parent_method))
continue
# TODO: also docstring for interaction methods (and maybe something clever so that if we
# have an interaction on _setSomething the docstring of setSomething mention it).
# or maybe not because:
# TODO: only do this for REAL workflow method, not interaction workflow wrap?
# issue is that we loose the type information of wrapped method
for workflow_id, transition_list in property_value._invoke_always.get(
temp_class.__name__, {}).items():
workflow_url = '{portal_url}/portal_workflow/{workflow_id}'.format(
portal_url=portal_url,
workflow_id=workflow_id,
)
for transition_id in transition_list:
docstring += "Transition [{transition_id}]({workflow_url}/transitions/{transition_id}/manage_properties) on [{workflow_id}]({workflow_url}/manage_main)\n\n".format(
transition_id=transition_id,
workflow_id=workflow_id,
workflow_url=workflow_url,
)
methods.append(
method_template_template.format(
decorator='',
method_name=safe_python_identifier(property_name),
method_args=method_args,
return_type=return_type,
docstring=safe_docstring(docstring)))
elif property_name.startswith(
'serialize'
): # isinstance(property_value, WorkflowState.SerializeGetter): XXX not a class..
methods.append(
method_template_template.format(
decorator='',
method_name=safe_python_identifier(property_name),
method_args="self",
return_type='None',
docstring=safe_docstring(
getattr(property_value, '__doc__', None))))
# TODO: generated methods for categories.
else:
debug += "\n # not handled property: {} -> {} {}".format(
property_name, property_value,
getattr(property_value, '__dict__', ''))
# for folderish contents, generate typed contentValues and other folderish methods
allowed_content_types = self.getTypeAllowedContentTypeList()
multiple_allowed_content_types = len(allowed_content_types) > 1
# TODO generate contentValues() without portal_type argument
for allowed_content_type in allowed_content_types:
if multiple_allowed_content_types:
new_content_portal_type_type_annotation = 'Literal[b"{allowed_content_type}"]'.format(
allowed_content_type=allowed_content_type)
else:
new_content_portal_type_type_annotation = 'str="{allowed_content_type}"'.format(
allowed_content_type=allowed_content_type)
subdocument_type = '"{}"'.format(
safe_python_identifier(allowed_content_type))
new_content_method_arg = "self, portal_type:{new_content_portal_type_type_annotation}".format(
new_content_portal_type_type_annotation=new_content_portal_type_type_annotation
)
parameters_by_parameter_name = defaultdict(list)
for prop, prop_def in TypeInformation_getEditParameterDict(
portal.portal_types[allowed_content_type]).items():
parameters_by_parameter_name[prop].append(
(allowed_content_type, prop_def))
if parameters_by_parameter_name:
new_content_method_arg += ',\n'
for prop, prop_defs in sorted(parameters_by_parameter_name.items()):
# XXX we could build a better documentation with this prop_def, but no tools seems to understand this.
# XXX can we assume that all properties have same types ? shouldn't we build unions ?
param_type = prop_defs[0][1][1]
new_content_method_arg += ' {}:{} = None,\n'.format(
safe_python_identifier(prop),
param_type,
)
methods.append(
method_template_template.format(
decorator='@overload' if multiple_allowed_content_types else '',
method_name='newContent',
method_args=new_content_method_arg,
return_type=subdocument_type,
docstring=safe_docstring(
getattr(temp_class.newContent, '__doc__', None))))
# TODO: getParentValue
method_args = 'self, portal_type:str="{allowed_content_type}"'.format(
allowed_content_type=allowed_content_type,)
if multiple_allowed_content_types:
method_args = 'self, portal_type:Literal[b"{allowed_content_type}"]'.format(
allowed_content_type=allowed_content_type,)
for method_name in ('contentValues', 'objectValues', 'searchFolder'):
return_type = 'Sequence[{}]'.format(subdocument_type)
if 0 and method_name == 'searchFolder': # TODO searchFolder is different, it returns brain and accepts **kw
return_type = 'Sequence[Type_CatalogBrain[{}]]'.format(subdocument_type)
if multiple_allowed_content_types:
# not correct but it makes jedi complete well when portal_type='one'
return_type = 'Union[{}]'.format(
', '.join(
(
'Sequence[Type_CatalogBrain["erp5.portal_type.{}"]]'
# TODO
.format(t) for t in 'allowed_content_types_classes')))
methods.append(
method_template_template.format(
decorator='@overload' if multiple_allowed_content_types else '',
method_name=method_name,
method_args=method_args,
return_type=return_type,
docstring=safe_docstring(
getattr(getattr(temp_class, method_name), '__doc__', None))))
subdocument_type = 'None'
if allowed_content_types:
subdocument_type = '"{}"'.format(
safe_python_identifier(allowed_content_types[0]))
if multiple_allowed_content_types:
subdocument_type = 'Union[{}]'.format(
', '.join(
'"{}"'.format(safe_python_identifier(allowed_content_type))
for allowed_content_type in allowed_content_types))
# getattr, getitem and other Zope.OFS alias returns an instance of allowed content types.
# so that portal.person_module['1'] is a person
for method_name in (
'__getattr__',
'__getitem__',
'_getOb',
'get',
):
# TODO: some accept default=None !
methods.append(
method_template_template.format(
decorator='',
method_name=method_name,
method_args="self, attribute:str, default:Any=None",
return_type=subdocument_type,
docstring='...'))
# TODO not true for __of__(context) and asContent(**kw)
for identity_method in (
'getObject',
'asContext',
'__of__',
):
method = getattr(temp_class, identity_method, None)
if method is not None:
methods.append(
method_template_template.format(
decorator='',
method_name=identity_method,
method_args="self",
return_type='"{}"'.format(
safe_python_identifier(temp_class.__name__)),
docstring=safe_docstring(getattr(method, '__doc__', None))))
# the parent class is imported in a name that should not clash
parent_class_alias = '{class_name}_parent_{parent_class}'.format(
class_name=safe_python_identifier(temp_class.__name__),
parent_class=safe_python_identifier(parent_class.__name__))
base_classes = [parent_class_alias]
for pc in temp_class.mro():
if pc.__module__ == 'erp5.accessor_holder.property_sheet':
# Fake name for property sheets
prefixed_class_name = 'property_sheet_{}'.format(
safe_python_identifier(pc.__name__))
imports.add(
'from erp5.accessor_holder import {} as {}'.format(
safe_python_identifier(pc.__name__), prefixed_class_name))
base_classes.append(prefixed_class_name)
# Fake name for skins
if XXX_skins_class_exists(pc.__name__):
class_name = safe_python_identifier(pc.__name__)
prefixed_class_name = 'skins_tool_{class_name}'.format(
class_name=class_name)
imports.add(
'from erp5.skins_tool.{class_name} import {class_name} as {prefixed_class_name}'
.format(
class_name=class_name, prefixed_class_name=prefixed_class_name))
if prefixed_class_name not in base_classes:
base_classes.append(prefixed_class_name)
# everything can use ERP5Site_ skins
if 'skins_tool_ERP5Site' not in base_classes:
imports.add(
'from erp5.skins_tool.ERP5Site import ERP5Site as skins_tool_ERP5Site')
base_classes.append('skins_tool_ERP5Site')
class_template = textwrap.dedent(
"""\
{header}
{imports}
from {parent_class_module} import {parent_class} as {parent_class_alias}
class {class_name}(
{base_classes}):
{docstring}
{methods}
{debug}
""")
docstring = textwrap.dedent(
'''
## [{type_title_or_id}](type_url)
---
{type_description}
''').format(
type_title_or_id=self.getTitleOrId(),
type_description=self.getDescription(),
type_url=self.absolute_url())
return class_template.format(
imports="\n".join(sorted(imports)),
header=header,
docstring=safe_docstring(docstring),
class_name=safe_python_identifier(temp_class.__name__),
base_classes=',\n '.join(base_classes),
parent_class=safe_python_identifier(parent_class.__name__),
parent_class_alias=parent_class_alias,
parent_class_module=safe_python_identifier(parent_class_module),
methods="\n".join(methods),
debug=debug)
from Products.ERP5Type.Core.PropertySheet import PropertySheet # pylint: disable=unused-import
def PropertySheetTool_getStub(self):
print('PropertySheetTool_getStub start')
sources = []
for ps in self.getPortalObject().portal_property_sheets.contentValues():
try:
sources.append(PropertySheet_getStub(ps))
except Exception:
logger.exception('error generating stub for %s', ps.getId())
print('PropertySheetTool_getStub end')
return "\n".join(sources)
def PropertySheet_getStub(self):
# type: (PropertySheet) -> str
"""returns a .pyi stub file for this property sheet
https://www.python.org/dev/peps/pep-0484/
"""
portal_categories = self.getPortalObject().portal_categories
class_template = textwrap.dedent(
"""\
class {class_name}:
'''{property_sheet_id}
{property_sheet_description}
'''
{methods}
{debug}
""")
debug = ''
methods = []
method_template_template = """ def {method_name}({method_args}) -> {return_type}:\n {docstring}"""
from Products.ERP5Type.Utils import convertToUpperCase
from Products.ERP5Type.Utils import evaluateExpressionFromString
from Products.ERP5Type.Utils import createExpressionContext
expression_context = createExpressionContext(self)
for prop in self.contentValues():
# XXX skip duplicate property
# TODO: how about just removing this from business templates ?
if self.getId() == 'Resource' and prop.getReference() in (
'destination_title', 'source_title'):
logger.debug(
"Skipping Resource duplicate property %s", prop.getRelativeUrl())
continue
if prop.getPortalType() in ('Standard Property', 'Acquired Property'):
docstring = safe_docstring(
textwrap.dedent(
"""\
[{property_sheet_title} {property_reference}]({property_url})
{property_description}
""").format(
property_description=prop.getDescription(),
property_sheet_title=self.getTitle(),
property_reference=prop.getReference(),
property_url=prop.absolute_url()))
methods.append(
method_template_template.format(
method_name='get{}{}'.format(
convertToUpperCase(prop.getReference()),
'List' if _isMultiValuedProperty(prop) else '',
),
method_args='self',
return_type=_getPythonTypeFromPropertySheetType(prop),
docstring=docstring))
if prop.getElementaryType() == 'boolean':
methods.append(
method_template_template.format(
method_name='is{}'.format(
convertToUpperCase(prop.getReference())),
method_args='self',
return_type=_getPythonTypeFromPropertySheetType(prop),
docstring=docstring))
methods.append(
method_template_template.format(
method_name='set{}{}'.format(
convertToUpperCase(prop.getReference()),
'List' if _isMultiValuedProperty(prop) else '',
),
method_args='self, value:{}'.format(
_getPythonTypeFromPropertySheetType(prop)),
return_type='None',
docstring=docstring))
elif prop.getPortalType() in ('Category Property',
'Dynamic Category Property'):
if prop.getPortalType() == 'Dynamic Category Property':
category_id_list = evaluateExpressionFromString(
expression_context, prop.getCategoryExpression())
else:
category_id_list = [prop.getReference()]
for category in category_id_list:
category_value = portal_categories._getOb(category, None)
if category_value is None:
continue
# XXX size category clashes with size accessor from Data propertysheet
if category in ('size',):
continue
docstring = safe_docstring(
textwrap.dedent(
"""\
[{property_sheet_title} {property_reference}]({property_url})
[{category_title}]({category_url})
{property_description}
{category_description}
""").format(
property_description=prop.getDescription(),
property_sheet_title=self.getTitle(),
property_reference=prop.getReference() or '',
property_url=prop.absolute_url(),
category_title=category_value.getTitle(),
category_url=category_value.absolute_url(),
category_description=category_value.getDescription(),
))
methods.append(
method_template_template.format(
method_name='get{}'.format(
convertToUpperCase(category_value.getId())),
method_args='self',
return_type='str',
docstring=docstring))
methods.append(
method_template_template.format(
method_name='get{}Title'.format(
convertToUpperCase(category_value.getId())),
method_args='self',
return_type='str',
docstring=docstring))
methods.append(
method_template_template.format(
method_name='get{}TranslatedTitle'.format(
convertToUpperCase(category_value.getId())),
method_args='self',
return_type='str',
docstring=docstring))
methods.append(
method_template_template.format(
method_name='get{}Value'.format(
convertToUpperCase(category_value.getId())),
method_args='self',
return_type='"erp5.portal_type.Type_AnyPortalType"',
docstring=docstring))
methods.append(
method_template_template.format(
method_name='get{}ValueList'.format(
convertToUpperCase(category_value.getId())),
method_args='self',
return_type='"erp5.portal_type.Type_AnyPortalTypeList"',
docstring=docstring))
methods.append(
method_template_template.format(
method_name='set{}'.format(
convertToUpperCase(category_value.getId())),
method_args='self, value: str',
return_type='None',
docstring=docstring))
methods.append(
method_template_template.format(
method_name='set{}Value'.format(
convertToUpperCase(category_value.getId())),
method_args='self, value: "erp5.portal_type.Type_AnyPortalType"',
return_type='None',
docstring=docstring))
methods.append(
method_template_template.format(
method_name='set{}ValueList'.format(
convertToUpperCase(category_value.getId())),
method_args='self, value_list: "erp5.portal_type.Type_AnyPortalTypeList"',
return_type='None',
docstring=docstring))
return class_template.format(
class_name=safe_python_identifier(self.getId()),
property_sheet_id=self.getId(),
property_sheet_description=self.getDescription().replace(
"'''", r"\\'\\'\\'"),
methods='\n'.join(methods),
debug=debug,
)
def ERP5Site_getPortalStub(self):
# type: (erp5.portal_type.ERP5Site,) -> erp5.portal_type.ERP5Site
module_stub_template = textwrap.dedent(
'''
@property
def {module_id}(self) -> '{module_class_name}':
...
#return {module_class_name}()
''')
tool_stub_template = textwrap.dedent(
'''
@property
def {tool_id}(self) -> 'tool_{tool_id}_{tool_class}':
...
#return tool_{tool_id}_{tool_class}()
''')
source = []
imports = []
from Acquisition import aq_base
for m in self.objectValues():
if hasattr(aq_base(m), 'getPortalType'):
source.extend(
module_stub_template.format(
module_id=m.getId(),
module_class_name=safe_python_identifier(
m.getPortalType())).splitlines())
else:
tool_class = safe_python_identifier(m.__class__.__name__)
tool_import = 'from {tool_module} import {tool_class} as tool_{tool_id}_{tool_class}'.format(
tool_module=m.__class__.__module__,
tool_class=tool_class,
tool_id=m.getId(),
)
if 0:
if m.getId() == 'portal_catalog':
tool_class = 'ICatalogTool' # XXX these I-prefix are stupid
tool_import = 'from erp5.portal_type import ICatalogTool'
elif m.getId() == 'portal_simulation':
tool_class = 'ISimulationTool' # XXX these I-prefix are stupid
tool_import = 'from erp5.portal_type import ISimulationTool'
imports.append(tool_import)
source.extend(
tool_stub_template.format(
tool_id=m.getId(),
tool_class=tool_class,
).splitlines())
# TODO: tools with at least base categories for CategoryTool
return textwrap.dedent(
'''
from Products.ERP5.ERP5Site import ERP5Site as ERP5Site_parent_ERP5Site
from erp5.skins_tool.ERP5Site import ERP5Site as skins_tool_ERP5Site
from erp5.skins_tool.Base import Base as skins_tool_Base
{imports}
class ERP5Site(ERP5Site_parent_ERP5Site, skins_tool_ERP5Site, skins_tool_Base):
{source}
def getPortalObject(self) -> 'ERP5Site':
return self
''').format(
imports='\n'.join(imports), source='\n '.join(source))
def ERP5Site_dumpModuleCode(self, component_or_script=None):
# type: (erp5.portal_type.ERP5Site,) -> None
"""Save code in filesystem for jedi to use it.
Generate stubs for erp5.* dynamic modules and copy the in-ZODB modules
to files.
"""
def mkdir_p(path):
# type: (str) -> None
if not os.path.exists(path):
os.mkdir(path, 0o700)
def writeFile(path, content):
# type: (str, str) -> None
"""Write file at `path` with `content`, only if content is different.
"""
if os.path.exists(path):
with open(path) as existing_f:
if content == existing_f.read():
return
with open(path, 'w') as f:
f.write(content)
portal = self.getPortalObject()
mkdir_p(STUBS_BASE_PATH)
module_dir = os.path.join(STUBS_BASE_PATH, 'erp5')
mkdir_p(module_dir)
# generate erp5/__init__.py
# mypy wants __init__.pyi jedi wants __init__.py so we generate both
writeFile(
os.path.join(module_dir, '__init__.py'),
"# empty __init__ for jedi ... mypy will use __init__.pyi")
with open(
os.path.join(module_dir, '__init__.pyi'),
'w',
) as erp5__init__f:
for module in (
'portal_type',
'accessor_holder',
'skins_tool',
'component',
):
erp5__init__f.write('from . import {module}\n'.format(module=module))
mkdir_p(os.path.join(module_dir, module))
if module == 'portal_type':
# portal types
all_portal_type_class_names = []
with open(
os.path.join(
module_dir,
module,
'__init__.pyi',
),
'w',
) as module_f:
# header
module_f.write("# coding: utf-8\n")
module_f.write(
'TranslatedMessage = str # TODO: this is type for translations ( Products.ERP5Type.Message.translateString should return this )\n'
)
# ERP5Site
module_f.write(ERP5Site_getPortalStub(self.getPortalObject()))
for ti in portal.portal_types.contentValues():
class_name = safe_python_identifier(ti.getId())
all_portal_type_class_names.append(class_name)
module_f.write(
'# from {class_name} import {class_name}\n'.format(
class_name=class_name))
try:
stub_code = ti.TypeInformation_getStub().encode('utf-8')
except Exception as e:
logger.exception("Could not generate code for %s", ti.getId())
stub_code = """class {class_name}:\n {error}""".format(
class_name=class_name,
error=safe_docstring(
"Error trying to create {}: {} {}".format(
ti.getId(), e.__class__, e)))
module_f.write(stub_code)
if 0:
with open(
os.path.join(
module_dir,
module,
'{class_name}.pyi'.format(class_name=class_name),
),
'w',
) as type_information_f:
try:
stub_code = ti.TypeInformation_getStub().encode('utf-8')
except Exception as e:
logger.exception("Could not generate code for %s", ti.getId())
stub_code = """class {class_name}:\n {error}""".format(
class_name=class_name,
error=safe_docstring(
"Error trying to create {}: {} {}".format(
ti.getId(), e.__class__, e)))
type_information_f.write(stub_code)
# generate missing classes without portal type
for class_name, klass in inspect.getmembers(
erp5.portal_type,
inspect.isclass,
):
if class_name not in portal.portal_types:
# TODO: use a better base class from klass mro
del klass
stub_code = textwrap.dedent(
"""
class {safe_class_name}(Products_ERP5Type_Base_Base):
'''Warning: {class_name} has no portal type.
'''
""").format(
safe_class_name=safe_python_identifier(class_name),
class_name=class_name,
)
module_f.write(stub_code)
# portal type groups ( useful ? used in Simulation Tool only )
if 0:
portal_types_by_group = defaultdict(list)
for ti_for_group in portal.portal_types.contentValues():
for group in ti_for_group.getTypeGroupList():
portal_types_by_group[group].append(
safe_python_identifier(ti_for_group.getId()))
for group, portal_type_class_list in portal_types_by_group.items():
group_class = 'Group_{}'.format(group)
module_f.write(
'from {} import {}\n'.format(group_class, group_class))
with open(
os.path.join(
module_dir,
module,
'{}.pyi'.format(group_class),
),
'w',
) as group_f:
group_f.write(
textwrap.dedent(
'''
import erp5.portal_type
class {group_class}({bases}):
"""All portal types of group {group}.
"""
''').format(
group_class=group_class,
bases=', \n'.join(
'erp5.portal_type.{}'.format(c)
for c in portal_type_class_list),
group=group))
# tools with extra type annotations
module_f.write('from ICatalogTool import ICatalogTool\n')
with open(
os.path.join(
module_dir,
module,
'ICatalogTool.pyi',
),
'w',
) as portal_f:
portal_f.write(
textwrap.dedent(
'''
from Products.ERP5Catalog.Tool.ERP5CatalogTool import ERP5CatalogTool
# XXX CatalogTool itself has a portal type
# from erp5.portal_type import Type_AnyPortalTypeCatalogBrainList
from typing import Any
class ICatalogTool(ERP5CatalogTool):
def searchResults(self) -> Any: #Type_AnyPortalTypeCatalogBrainList:
"""Search Catalog"""
def __call__(self) -> Any: #Type_AnyPortalTypeCatalogBrainList:
"""Search Catalog"""
'''))
if 0: # TODO
module_f.write('from ISimulationTool import ISimulationTool\n')
with open(
os.path.join(
module_dir,
module,
'ISimulationTool.pyi',
),
'w',
) as portal_f:
portal_f.write(
textwrap.dedent(
'''
from erp5.portal_type import SimulationTool
from erp5.portal_type import Type_AnyPortalTypeInventoryListBrainList
class ISimulationTool(SimulationTool):
def getInventoryList() -> Type_AnyPortalTypeInventoryListBrainList:
...
'''))
# portal object
module_f.write('from ERP5Site import ERP5Site\n')
with open(
os.path.join(
module_dir,
module,
'ERP5Site.pyi',
),
'w',
) as portal_f:
portal_f.write(ERP5Site_getPortalStub(self.getPortalObject()))
# some type helpers
if 0:
module_f.write('from Type_CatalogBrain import Type_CatalogBrain\n')
with open(
os.path.join(
module_dir,
module,
'Type_CatalogBrain.pyi',
),
'w',
) as catalog_brain_f:
catalog_brain_f.write(
textwrap.dedent(
'''
from typing import TypeVar, Generic
T = TypeVar('T')
class Type_CatalogBrain(Generic[T]):
id: str
path: str
def getObject(self) -> T:
...
'''))
module_f.write(
'from Type_InventoryListBrain import Type_InventoryListBrain\n')
with open(
os.path.join(
module_dir,
module,
'Type_InventoryListBrain.pyi',
),
'w',
) as catalog_brain_f:
catalog_brain_f.write(
textwrap.dedent(
'''
from typing import TypeVar, Generic
from erp5.component.extension.InventoryBrain import InventoryListBrain
from DateTime.DateTime import DateTime as DateTime
import erp5.portal_type
T = TypeVar('T')
class Type_InventoryListBrain(Generic[T], InventoryListBrain):
node_uid: int
mirror_node_uid: int
section_uid: int
mirror_section_uid: int
function_uid: int
project_uid: int
funding_uid: int
ledger_uid: int
payment_request_uid: int
node_value: 'erp5.portal_type.Organisation' # TODO
mirror_node_value: 'erp5.portal_type.Organisation'
section_value: 'erp5.portal_type.Organisation'
mirror_section_value: 'erp5.portal_type.Organisation'
resource_value: 'erp5.portal_type.Product' # TODO
date: DateTime
mirror_date: DateTime
variation_text: str
sub_variation_text: str
simulation_state: str
inventory: float
total_price: float
path: str
stock_uid: int
def getObject(self) -> T:
...
'''))
module_f.write('from typing import Sequence, Union\n')
module_f.write(
'Type_AnyPortalType = Union[\n {}]\n'.format(
',\n '.join(
'{}'.format(portal_type_class)
for portal_type_class in all_portal_type_class_names),))
# TODO: Union[Sequence] or Sequence[Union] ?
module_f.write(
'Type_AnyPortalTypeList = Union[\n {}]\n'.format(
',\n '.join(
'Sequence[{}]'.format(portal_type_class)
for portal_type_class in all_portal_type_class_names)))
if 0:
module_f.write(
'Type_AnyPortalTypeCatalogBrainList = Union[\n {}]\n'.format(
',\n '.join(
'List[Type_CatalogBrain[{}]]'.format(portal_type_class)
for portal_type_class in all_portal_type_class_names),))
module_f.write(
'Type_AnyPortalTypeInventoryListBrainList = Union[\n {}]\n'
.format(
',\n '.join(
'List[Type_InventoryListBrain[{}]]'.format(
portal_type_class)
for portal_type_class in all_portal_type_class_names),))
elif module == 'accessor_holder':
# TODO: real path is accessor_holder.something !?
with open(
os.path.join(module_dir, module, '__init__.pyi'),
'w',
) as accessor_holder_f:
accessor_holder_f.write(
textwrap.dedent(
"""\
# coding: utf-8\n
from typing import Optional, List, Any, Sequence
from Products.ERP5Type.Base import Base as Products_ERP5Type_Base_Base
import erp5.portal_type
from DateTime import DateTime
"""))
for ps in portal.portal_property_sheets.contentValues():
class_name = safe_python_identifier(ps.getId())
accessor_holder_f.write(ps.PropertySheet_getStub().encode('utf-8'))
if 0:
with open(
os.path.join(
module_dir,
module,
'{class_name}.pyi'.format(class_name=class_name),
),
'w',
) as property_sheet_f:
property_sheet_f.write(
ps.PropertySheet_getStub().encode('utf-8'))
elif module == 'skins_tool':
skins_tool = portal.portal_skins
with open(
os.path.join(module_dir, module, '__init__.pyi'),
'w',
) as skins_tool_f:
skins_tool_f.write("# coding: utf-8\n")
for class_name in SkinsTool_getClassSet(skins_tool):
skins_tool_f.write(
'from {class_name} import {class_name}\n'.format(
class_name=class_name))
writeFile(
os.path.join(
module_dir,
module,
'{}.pyi'.format(class_name),
),
SkinsTool_getStubForClass(
skins_tool,
class_name,
).encode('utf-8'))
elif module == 'component':
module_to_component_portal_type_mapping = {
'test': 'Test Component',
'document': 'Document Component',
'extension': 'Extension Component',
'tool': 'Tool Component',
'module': 'Module Component',
'interface': 'Interface Component',
}
with open(
os.path.join(module_dir, module, '__init__.py'),
'w',
) as component_module__init__f:
for sub_module, portal_type in module_to_component_portal_type_mapping.items(
):
component_module__init__f.write(
'from . import {}\n'.format(sub_module))
mkdir_p(os.path.join(module_dir, module, sub_module))
# TODO: write actual version, not always erp5_version !
mkdir_p(
os.path.join(
module_dir,
module,
sub_module,
'erp5_version',
))
with open(
os.path.join(module_dir, module, sub_module, '__init__.py'),
'w',
) as component_sub_module_init_f:
for brain in portal.portal_catalog(
portal_type=portal_type, validation_state=('validated',)):
component = brain.getObject()
# TODO write __init__ for erp5_version as well
component_sub_module_init_f.write(
"from {component_reference} import {component_reference}\n"
.format(component_reference=component.getReference()))
writeFile(
os.path.join(
module_dir,
module,
sub_module,
'{}.py'.format(component.getReference()),
), component.getTextContent())
writeFile(
os.path.join(
module_dir,
module,
sub_module,
'erp5_version',
'{}.py'.format(component.getReference()),
), component.getTextContent())
# TODO: not like this !
with open(
os.path.join(module_dir, module, sub_module, '__init__.py'),
'r',
) as component_sub_module_init_f:
writeFile(
os.path.join(
module_dir, module, sub_module, 'erp5_version',
'__init__.py'), component_sub_module_init_f.read())
return 'done'
<?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.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<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>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
from yapf.yapflib import yapf_api
import json
import tempfile
import textwrap
import logging
logger = logging.getLogger(__name__)
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 = pep8
indent_width = 2
continuation_indent_width = 2
split_before_expression_after_opening_paren = true
blank_line_before_nested_class_or_def = false
allow_split_before_dict_value = false
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:
logger.exception("Error in source code")
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.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<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>
</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_dumpModuleCode</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>Jedi</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_dumpModuleCode</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_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_getPortalStub</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>Jedi</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>ERP5Site_getPortalStub</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>
<?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>PropertySheetTool_getStub</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>Jedi</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>PropertySheetTool_getStub</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>PropertySheet_getStub</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>Jedi</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>PropertySheet_getStub</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>SkinsTool_getStub</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>Jedi</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>SkinsTool_getStub</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>TypeInformation_getStub</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>Jedi</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>TypeInformation_getStub</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>TypesTool_getStub</string> </value>
</item>
<item>
<key> <string>_module</string> </key>
<value> <string>Jedi</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>TypesTool_getStub</string> </value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -77,6 +77,72 @@
}
});
})
.declareJob('runPyLint', function () {
var context = this;
return (function (controller) {
return new RSVP.Queue()
.push(function () {
return RSVP.delay(2000);
})
.push(function () {
if (
context.state.model_language === 'python' &&
context.state.language_support_url
) {
const data = new FormData();
const checker_parameters = {
code: context.editor.getValue(),
portal_type: context.state.portal_type,
};
data.append('data', JSON.stringify(checker_parameters));
fetch(
context.state.language_support_url +
'/ERP5Site_checkPythonSourceCodeAsJSON',
{
method: 'POST',
body: data,
signal: controller.signal,
}
)
.then((response) => response.json())
.then(
(data) => {
monaco.editor.setModelMarkers(
context.editor.getModel(),
'pylint',
data['annotations'].map((annotation) => {
return {
startLineNumber: annotation.row + 1,
endLineNumber: annotation.row + 1,
startColumn: annotation.col,
endColumn: Infinity,
message: annotation.text,
severity:
annotation.type === 'error'
? monaco.MarkerSeverity.Error
: monaco.MarkerSeverity.Warning,
};
})
);
},
(e) => {
if (!(e instanceof DOMException) /* AbortError */) {
throw e;
}
/* ignore aborted requests */
}
);
}
})
.push(undefined, function (e) {
if (e instanceof RSVP.CancellationError) {
controller.abort();
}
throw e;
});
})(new AbortController());
})
.declareMethod('render', function (options) {
var model_language,
state_dict = {
......@@ -108,12 +174,15 @@
model_language = 'python';
}
state_dict.model_language = model_language;
state_dict.portal_type = options.portal_type;
state_dict.value = options.value || '';
state_dict.language_support_url = options.language_support_url || '';
return this.changeState(state_dict);
})
.onStateChange(function (modification_dict) {
var queue = new RSVP.Queue();
var queue = new RSVP.Queue(),
gadget = this;
if (modification_dict.hasOwnProperty('value')) {
// Do not notify the UI when initializing the value
this.state.ignoredChangeDuringInitialization = true;
......@@ -197,6 +266,197 @@
if (modification_dict.hasOwnProperty('editable')) {
this.editor.updateOptions({ readOnly: !this.state.editable });
}
if (this.state.model_language === 'python') {
this.editor.getModel().onDidChangeContent(this.runPyLint.bind(this));
const 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(
gadget.state.language_support_url +
'/ERP5Site_formatPythonSourceCode',
{
method: 'POST',
body: data,
signal: controller.signal,
}
)
.then((response) => response.json())
.then(
(data) => {
if (data.error) {
this.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,
},
};
data.append('data', JSON.stringify(complete_parameters));
return fetch(
gadget.state.language_support_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 */
}
);
},
});
monaco.languages.registerDefinitionProvider('python', {
provideDefinition: async function (model, position, 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,
},
};
complete_parameters['xxx_definition'] = true;
data.append('data', JSON.stringify(complete_parameters));
// TODO: this should use a proper endpoint ...
return fetch(
gadget.state.language_support_url +
'/ERP5Site_getPythonSourceCodeCompletionList',
{
method: 'POST',
body: data,
signal: controller.signal,
}
)
.then((response) => response.json())
.then(
(data) => {
var definitions = [];
for (let i = 0; i < data.length; i++) {
if (data[i].code) {
// TODO: these models are not refreshed, if the file they refefer is modified,
// they show outdated content.
let definition_uri = monaco.Uri.from({
scheme: 'file',
path: data[i].uri,
});
let definition_model = monaco.editor.getModel(
definition_uri
);
if (!definition_model) {
definition_model = monaco.editor.createModel(
data[i].code,
'python',
definition_uri
);
}
data[i].uri = definition_model.uri;
}
definitions.push({
range: data[i].range,
uri: data[i].uri ? data[i].uri : model.uri,
});
}
return definitions;
},
(e) => {
if (!(e instanceof DOMException) /* AbortError */) {
throw e;
}
/* ignore aborted requests */
}
);
},
});
this.runPyLint();
}
}
return queue;
})
......
......@@ -47,6 +47,7 @@
<script tal:content='python: "var textarea_selector=" + modules["json"].dumps(options.get("textarea_selector"))'>
</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
tal:content='python: "window.monacoEditorWebPackResourceBaseUrl = " + modules["json"].dumps(options["portal_url"]) + " + \"/monaco-editor/\""'>
......@@ -254,10 +255,205 @@ $script.onload = function() {
function makeTimeoutFunction(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.registerDefinitionProvider('python', {
provideDefinition: async function(model, position, 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;
}
complete_parameters['xxx_definition'] = true;
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 => {
var definitions = [];
for (let i = 0; i < data.length; i++) {
if (data[i].code) {
// TODO: these models are not refreshed, if the file they refefer is modified,
// they show outdated content.
let definition_uri = monaco.Uri.from({
scheme: 'file',
path: data[i].uri,
});
let definition_model = monaco.editor.getModel(
definition_uri
);
if (!definition_model) {
definition_model = monaco.editor.createModel(
data[i].code,
'python',
definition_uri
);
}
data[i].uri = definition_model.uri;
}
definitions.push({
range: data[i].range,
uri: data[i].uri ? data[i].uri : model.uri,
});
}
return definitions;
}, e => {
if (!(e instanceof DOMException) /* AbortError */ ) {
throw e;
}
/* ignore aborted requests */
});
}
});
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 */
});
}
});
monaco.languages.registerHoverProvider('python', {
provideHover: function (model, position, 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;
}
complete_parameters['xxx_hover'] = true;
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 {
range: new monaco.Range(position.lineNumber, position.column, position.lineNumber, 0),
contents: [
{ value: data }, // XXX
]
}
}, e => {
if (!e instanceof DOMException /* AbortError */ ) {
throw e;
}
/* ignore aborted requests */
});
}
});
if (mode === "python") {
// Perform a first check when loading document.
checkPythonSourceCode(new AbortController());
......
extension.erp5.Jedi
extension.erp5.YAPF
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Base Type" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>content_icon</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>description</string> </key>
<value> <string>Provide intellisense for python.</string> </value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>Python Support Tool</string> </value>
</item>
<item>
<key> <string>init_script</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>permission</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Base Type</string> </value>
</item>
<item>
<key> <string>type_class</string> </key>
<value> <string>PythonSupportTool</string> </value>
</item>
<item>
<key> <string>type_interface</string> </key>
<value>
<tuple/>
</value>
</item>
<item>
<key> <string>type_mixin</string> </key>
<value>
<tuple/>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
##############################################################################
#
# Copyright (c) 2002-2019 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
import re
import typing
from typing import List # pylint: disable=unused-import
import enum
import textwrap
from collections import namedtuple
# TODO: just import !
# https://microsoft.github.io/monaco-editor/api/classes/monaco.position.html
# lineNumber and column start at 1
Position = namedtuple('Position', 'lineNumber column')
Completion = namedtuple('Completion', 'text description') # TODO
# with ReferenceCompletion, we can use regexs
ReferenceCompletion = namedtuple('ReferenceCompletion', 'text description')
# /TODO
if typing.TYPE_CHECKING:
xScriptType = typing.Union[typing.Literal['Python (Script)']]
import erp5.portal_type # pylint: disable=import-error,unused-import
class ScriptType(enum.Enum):
Component = 0
SkinFolderPythonScript = 1
WorkflowPythonScript = 1
# XXX workaround missing completions from unittest.TestCase
import unittest
class XERP5TypeTestCase(ERP5TypeTestCase, unittest.TestCase):
pass
class PythonSupportTestCase(XERP5TypeTestCase):
"""TestCase for python support
"""
def assertCompletionIn(self, completion, completion_list):
# type: (ReferenceCompletion, List[Completion]) -> None
"""check that `completion` is in `completion_list`
"""
self.fail('TODO')
def assertCompletionNotIn(self, completion, completion_list):
# type: (ReferenceCompletion, List[Completion]) -> None
"""check that `completion` is not in `completion_list`
"""
self.fail('TODO')
class TestCompleteFromScript(PythonSupportTestCase):
"""Test completions from within a python scripts
Check that magic of python scripts with context and params
is properly emulated.
"""
script_name = 'Base_example'
script_params = ''
def getCompletionList(self, code, position=None):
# type: (str, Position) -> List[Completion]
return self.portal.portal_python_support.getCompletionList(code, position)
def test_portal_tools(self):
completion_list = self.getCompletionList(
textwrap.dedent('''
context.getPortalObject().'''))
self.assertCompletionIn(Completion(text="person_module"), completion_list)
self.assertCompletionIn(Completion(text="portal_types"), completion_list)
def test_base_method(self):
completion_list = self.getCompletionList(
textwrap.dedent('''
context.getP'''))
self.assertCompletionIn(Completion(text="getPortalType"), completion_list)
self.assertCompletionIn(Completion(text="getParentValue"), completion_list)
def test_context_name(self):
self.script_name = 'Person_example'
completion_list = self.getCompletionList(
textwrap.dedent('''
context.getFir'''))
self.assertCompletionIn(Completion(text="getFirstName"), completion_list)
def test_params_type_comment(self):
self.script_params = 'person'
completion_list = self.getCompletionList(
textwrap.dedent(
'''
# type: (erp5.portal_type.Person) -> str
person.getFir'''))
self.assertCompletionIn(Completion(text="getFirstName"), completion_list)
class TestCompleteWithScript(PythonSupportTestCase):
"""Check that python scripts are offered as completions items like methods on objects.
"""
def afterSetUp(self):
super(TestCompleteWithScript, self).afterSetUp()
# sanity check that the scripts we are asserting with really exist
self.assertTrue(hasattr(self.portal, 'Account_getFormattedTitle'))
self.assertTrue(hasattr(self.portal, 'Person_getAge'))
self.assertTrue(hasattr(self.portal, 'Base_edit'))
self.assertTrue(hasattr(self.portal, 'ERP5Site_getSearchResultList'))
def test_context(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.'''))
self.assertCompletionIn(Completion(text="Person_getAge"), completion_list)
self.assertCompletionIn(
Completion(text="ERP5Site_getSearchResultList"), completion_list)
self.assertCompletionIn(Completion(text="Base_edit"), completion_list)
self.assertCompletionNotIn(
Completion(text="Account_getFormattedTitle"), completion_list)
def test_docstring(self):
# create python script with docstring
# check docstring from completion contain this docstring + a link to /manage_main on the script
self.fail('TODO')
def test_docstring_plus_type_comment(self):
# create python script with docstring and type comment for parameters
# check docstring from completion contain this docstring + a link to /manage_main on the script
self.fail('TODO')
def test_no_docstring(self):
# create python script with no docstring
# check docstring from completion contain a link to /manage_main on the script
self.fail('TODO')
def test_typevar_in_type_comment(self):
# create a Base_x python script with a type comment returning content same portal_type,
# like Base_createCloneDocument
# type: (X,) -> X
self.fail('TODO')
class TestCompletePortalType(PythonSupportTestCase):
def test_getattr(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person_module = None # type: erp5.portal_type.PersonModule
person_module.person.getFi'''))
self.assertCompletionIn(Completion(text="getFirstName"), completion_list)
def test_getitem(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person_module = None # type: erp5.portal_type.PersonModule
person_module[person].getFi'''))
self.assertCompletionIn(Completion(text="getFirstName"), completion_list)
def test_newContent_return_value(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person_module = None # type: erp5.portal_type.PersonModule
person_module.newContent().getFi'''))
self.assertCompletionIn(Completion(text="getFirstName"), completion_list)
def test_newContent_portal_type_return_value(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.newContent(portal_type="Bank Account").get'''))
self.assertCompletionIn(
Completion(text="getBankAccountHolderName"), completion_list)
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.newContent(portal_type="Address").get'''))
self.assertCompletionNotIn(
Completion(text="getBankAccountHolderName"), completion_list)
def test_searchFolder_return_value(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person_module = None # type: erp5.portal_type.PersonModule
person_module.searchFolder()[0].'''))
self.assertCompletionIn(Completion(text="getObject"), completion_list)
self.assertCompletionNotIn(Completion(text="getFirstName"), completion_list)
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person_module = None # type: erp5.portal_type.PersonModule
person_module.searchFolder()[0].getObject().'''))
self.assertCompletionIn(Completion(text="getFirstName"), completion_list)
def test_searchFolder_portal_type_return_value(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.searchFolder(portal_type="Bank Account")[0].get'''))
self.assertCompletionIn(Completion(text="getObject"), completion_list)
self.assertCompletionNotIn(
Completion(text="getBankAccountHolderName"), completion_list)
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.searchFolder(portal_type="Bank Account")[0].getObject().get'''
))
self.assertCompletionIn(
Completion(text="getBankAccountHolderName"), completion_list)
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.searchFolder(portal_type="Address")[0].getObject().get'''))
self.assertCompletionNotIn(
Completion(text="getBankAccountHolderName"), completion_list)
def test_getPortalType_docstring(self):
# getPortalType docstring has a full description of the portal type and a link
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.getPortalType'''))
self.assertCompletionIn(
Completion(
description=re.compile(
".*Persons capture the contact information.*")),
completion_list)
self.assertCompletionIn(
Completion(description=re.compile(".*portal_types/Person.*")),
completion_list)
def test_getPortalType_literal(self):
# jedi knows what literal getPortalType returns
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
this = "a"
if person.getPortalType() = "Person":
this = []
this.'''))
# jedi understood that `this` is list in this case
self.assertCompletionIn(Completion(text="append"), completion_list)
self.assertCompletionNotIn(Completion(text="capitalize"), completion_list)
def test_getParentValue(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.getParentValue().getPortalType'''))
self.assertCompletionIn(
Completion(description=re.compile(".*Person Module.*")),
completion_list)
def test_workflow_state_getter(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.get'''))
self.assertCompletionIn(
Completion(text="getValidationState"), completion_list)
self.assertCompletionIn(
Completion(text="getTranslatedValidationStateTitle"), completion_list)
self.assertCompletionNotIn(
Completion(text="getSimulationState"), completion_list)
def test_workflow_method(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.validat'''))
self.assertCompletionIn(Completion(text="validate"), completion_list)
def test_edit_argument(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.edit(fir'''))
self.assertCompletionIn(Completion(text="first_name"), completion_list)
def test_new_content_argument(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person_module = None # type: erp5.portal_type.PersonModule
person_module.newContent(fir'''))
self.assertCompletionIn(Completion(text="first_name"), completion_list)
class TestCompletePropertySheet(PythonSupportTestCase):
def test_content_property(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.getDefaultAddre'''))
self.assertCompletionIn(
Completion(text="getDefaultAddressStreetAddress"), completion_list)
self.assertCompletionIn(
Completion(text="getDefaultAddressCity"), completion_list)
self.assertCompletionIn(
Completion(text="getDefaultAddressText"), completion_list)
self.assertCompletionIn(
Completion(text="getDefaultAddressRegionTitle"), completion_list)
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.setDefaultAddre'''))
self.assertCompletionIn(
Completion(text="setDefaultAddressStreetAddress"), completion_list)
self.assertCompletionIn(
Completion(text="setDefaultAddressCity"), completion_list)
self.assertCompletionIn(
Completion(text="setDefaultAddressText"), completion_list)
self.assertCompletionIn(
Completion(text="setDefaultAddressRegionValue"), completion_list)
self.assertCompletionNotIn(
Completion(text="setDefaultAddressRegionTitle"), completion_list)
def test_category(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.get'''))
self.assertCompletionIn(Completion(text="getRegion"), completion_list)
self.assertCompletionIn(Completion(text="getRegionTitle"), completion_list)
self.assertCompletionIn(Completion(text="getRegionValue"), completion_list)
self.assertCompletionIn(
Completion(text="getDefaultRegionTitle"), completion_list)
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.set'''))
self.assertCompletionIn(
Completion(text="setRegion"),
completion_list) # XXX include this accessor ?
self.assertCompletionNotIn(
Completion(text="getRegionTitle"), completion_list)
self.assertCompletionIn(Completion(text="setRegionValue"), completion_list)
self.assertCompletionNotIn(
Completion(text="setDefaultRegionTitle"), completion_list)
def test_category_value_getter_portal_type(self):
# filtered for a portal type
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.getRegionValue(portal_type="Bank Account").'''))
self.assertCompletionIn(
Completion(text="getBankAccountHolderName"), completion_list)
# a list of portal types
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.getRegionValue(portal_type=("Bank Account", "Address")).'''))
self.assertCompletionIn(
Completion(text="getBankAccountHolderName"), completion_list)
# not filter assume any portal type
completion_list = self.getCompletionList(
textwrap.dedent(
'''
person = None # type: erp5.portal_type.Person
person.getRegionValue().'''))
self.assertCompletionIn(
Completion(text="getBankAccountHolderName"), completion_list)
class TestCompleteERP5Site(PythonSupportTestCase):
def test_base_method(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.res'''))
self.assertCompletionIn(Completion(text="restrictedTraverse"), completion_list)
def test_content(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.acl'''))
self.assertCompletionIn(Completion(text="acl_users"), completion_list)
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.acl_users.getUs'''))
self.assertCompletionIn(Completion(text="getUserById"), completion_list)
def test_portal_itself(self):
# non regression for bug, when completing on portal. this cause:
# AttributeError: 'CompiledObject' object has no attribute 'py__get__'
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.'''))
self.assertCompletionIn(Completion(text="getTitle"), completion_list)
class TestCompleteCatalogTool(PythonSupportTestCase):
def test_brain(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_catalog.searchResults().'''))
self.assertCompletionIn(Completion(text="getObject"), completion_list)
self.assertCompletionIn(Completion(text="title"), completion_list)
self.assertCompletionIn(Completion(text="path"), completion_list)
self.assertCompletionNotIn(Completion(text="getTitle"), completion_list)
def test_portal_type(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_catalog.searchResults(
portal_type='Bank Account').getObject().getBank'''))
self.assertCompletionIn(
Completion(text="getBankAccountHolderName"), completion_list)
def test_arguments(self):
# catalog columns
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_catalog(titl'''))
self.assertCompletionIn(
Completion(text="title"), completion_list)
# related keys
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_catalog(tra'''))
self.assertCompletionIn(
Completion(text="translated_simulation_state_title"), completion_list)
# category dynamic related keys
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_catalog(grou'''))
self.assertCompletionIn(
Completion(text="group_uid"), completion_list)
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_catalog(default_grou'''))
self.assertCompletionIn(
Completion(text="default_group_uid"), completion_list)
# scriptable keys
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_catalog(full'''))
self.assertCompletionIn(
Completion(text="full_text"), completion_list)
class TestCompleteSimulationTool(PythonSupportTestCase):
def test_inventory_list_brain(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_simulation.getInventoryList()[0].'''))
self.assertCompletionIn(Completion(text="getObject"), completion_list)
self.assertCompletionIn(Completion(text="section_title"), completion_list)
self.assertCompletionIn(Completion(text="section_uid"), completion_list)
self.assertCompletionIn(Completion(text="quantity"), completion_list)
self.assertCompletionIn(Completion(text="total_price"), completion_list)
self.assertCompletionNotIn(Completion(text="getQuantity"), completion_list)
def test_movement_history_list_brain(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_simulation.getMovementHistoryList()[0].'''))
self.assertCompletionIn(Completion(text="getObject"), completion_list)
self.assertCompletionIn(Completion(text="section_title"), completion_list)
self.assertCompletionIn(Completion(text="section_uid"), completion_list)
self.assertCompletionIn(Completion(text="quantity"), completion_list)
self.assertCompletionIn(Completion(text="total_price"), completion_list)
self.assertCompletionNotIn(Completion(text="getQuantity"), completion_list)
def test_brain_date_is_date_time(self):
for method in (
'getInventoryList',
'getMovementHistoryList',):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_simulation.{}()[0].date.''').format(method))
self.assertCompletionIn(Completion(text="year"), completion_list)
def test_brain_node_value_is_node(self):
for method in (
'getInventoryList',
'getMovementHistoryList',):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_simulation.{}()[0].node_value.''').format(method))
self.assertCompletionIn(Completion(text="getTitle"), completion_list)
def test_portal_type(self):
for method in (
'getInventoryList',
'getMovementHistoryList',):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_simulation.{}(
portal_type='Sale Order Line'
)[0].getObject().getPortalType''').format(method))
self.assertCompletionIn(
Completion(description=re.compile("Sale Order Line")), completion_list)
class TestCompletePreferenceTool(PythonSupportTestCase):
def test_preference_tool_preference_getter(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_preferrences.get'''))
self.assertCompletionIn(Completion(text="getPreferredClientRoleList"), completion_list)
def test_preference_tool_preference_setter(self):
completion_list = self.getCompletionList(
textwrap.dedent(
'''
portal = None # type: erp5.portal_type.ERP5Site
portal.portal_preferrences.set'''))
self.assertCompletionNotIn(Completion(text="setPreferredClientRoleList"), completion_list)
\ No newline at end of file
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Test 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>testPythonSupport</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>test.erp5.testPythonSupport</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Test 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.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<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>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
import sys
from collections import namedtuple
import logging
import json
import functools
import textwrap
import enum
from typing import Union, List, Literal, Dict, NamedTuple, TYPE_CHECKING # pylint: disable=unused-import
from AccessControl import ClassSecurityInfo
from Products.ERP5Type.Tool.BaseTool import BaseTool
from Products.ERP5Type import Permissions
import jedi
logger = logging.getLogger(__name__)
def loadJson(data):
"""Load json in objects (and not dictionaries like json.loads does by default).
"""
return json.loads(
data, object_hook=lambda d: namedtuple('Unknown', d.keys())(*d.values())
)
def dumpsJson(data):
"""symetric of loadJson, dumps to json, with support of simple objects.
"""
def flatten(obj):
if hasattr(obj, '_asdict'): # namedtuple
obj = obj._asdict()
if isinstance(obj, dict):
return {k: flatten(v) for k, v in obj.items()}
if isinstance(obj, (list, tuple)):
return [flatten(x) for x in obj]
if hasattr(obj, '__dict__'):
obj = obj.__dict__
return obj
return json.dumps(flatten(data))
def json_serialized(f):
"""Transparently deserialize `data` parameter and serialize the returned value to/as json.
"""
@functools.wraps(f)
def wrapper(self, data):
return dumpsJson(f(self, loadJson(data)))
return wrapper
Position = namedtuple('Position', 'lineNumber, column')
"""Position in the editor, same as monaco, ie. indexed from 1
"""
if TYPE_CHECKING:
import erp5.portal_type # pylint: disable=import-error,unused-import
# XXX "Context" is bad name
class Context:
code = None # type: str
class PythonScriptContext(Context):
script_name = None # type: str
bound_names = None # type: List[str]
params = None # type: str
class CompletionContext(Context):
position = None # type: Position
class PythonScriptCompletionContext(CompletionContext, PythonScriptContext):
"""completion for portal_skins's Script (Python)
"""
CompletionKind = Union[Literal['Method'],
Literal['Function'],
Literal['Constructor'],
Literal['Field'],
Literal['Variable'],
Literal['Class'],
Literal['Struct'],
Literal['Interface'],
Literal['Module'],
Literal['Property'],
Literal['Event'],
Literal['Operator'],
Literal['Unit'],
Literal['Value'],
Literal['Constant'],
Literal['Enum'],
Literal['EnumMember'],
Literal['Keyword'],
Literal['Text'],
Literal['Color'],
Literal['File'],
Literal['Reference'],
Literal['Customcolor'],
Literal['Folder'],
Literal['TypeParameter'],
Literal['Snippet'],
]
class CompletionKind(enum.Enum): # pylint: disable=function-redefined
Method = 'Method'
Function = 'Function'
Constructor = 'Constructor'
Field = 'Field'
Variable = 'Variable'
Class = 'Class'
Struct = 'Struct'
Interface = 'Interface'
Module = 'Module'
Property = 'Property'
Event = 'Event'
Operator = 'Operator'
Unit = 'Unit'
Value = 'Value'
Constant = 'Constant'
Enum = 'Enum'
EnumMember = 'EnumMember'
Keyword = 'Keyword'
Text = 'Text'
Color = 'Color'
File = 'File'
Reference = 'Reference'
Customcolor = 'Customcolor'
Folder = 'Folder'
TypeParameter = 'TypeParameter'
Snippet = 'Snippet'
# https://microsoft.github.io/monaco-editor/api/interfaces/monaco.imarkdownstring.html
class IMarkdownString(NamedTuple('IMarkdownString', (('value', str),))):
value = None # type: str
class CompletionItem(NamedTuple(
'CompletionItem',
(
('label', str),
('kind', CompletionKind),
('detail', str),
('documentation', Union[str, IMarkdownString]),
('sortText', str),
('insertText', str),
))):
"""A completion item represents a text snippet that is proposed to complete text that is being typed.
https://microsoft.github.io/monaco-editor/api/interfaces/monaco.languages.completionitem.html
"""
label = None # type: str
kind = None # type: CompletionKind
detail = None # type: str
documentation = None # type: Union[str, IMarkdownString]
sortText = None # type: str
insertText = None # type: str
logger = logging.getLogger(__name__)
class PythonSupportTool(BaseTool):
"""Tool to support code editors.
"""
portal_type = 'Python Support Tool'
meta_type = 'ERP5 {}'.format(portal_type)
id = 'portal_python_support'
security = ClassSecurityInfo()
security.declareObjectProtected(Permissions.ManagePortal)
def _getCode(self, completion_context):
# type: (PythonScriptCompletionContext) -> str
return ''
def getStubPath(self):
"""The path where stubs are generated.
"""
return "/tmp/stubs/"
def _convertCompletion(self, completion):
# type: (jedi.api.classes.Completion,) -> CompletionItem
"""Convert a completion from jedi format to the format used by text editors.
"""
# map jedi type to the name of monaco.languages.CompletionItemKind
# This mapping and this method 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': CompletionKind.Value,
'type': CompletionKind.Class,
'tuple': CompletionKind.Class,
'dict': CompletionKind.Class,
'dictionary': CompletionKind.Class,
'function': CompletionKind.Function,
'lambda': CompletionKind.Function,
'generator': CompletionKind.Function,
'class': CompletionKind.Class,
'instance': CompletionKind.Reference,
'method': CompletionKind.Method,
'builtin': CompletionKind.Class,
'builtinfunction': CompletionKind.Function,
'module': CompletionKind.Module,
'file': CompletionKind.File,
'xrange': CompletionKind.Class,
'slice': CompletionKind.Class,
'traceback': CompletionKind.Class,
'frame': CompletionKind.Class,
'buffer': CompletionKind.Class,
'dictproxy': CompletionKind.Class,
'funcdef': CompletionKind.Function,
'property': CompletionKind.Property,
'import': CompletionKind.Module,
'keyword': CompletionKind.Keyword,
'constant': CompletionKind.Variable,
'variable': CompletionKind.Variable,
'value': CompletionKind.Value,
'param': CompletionKind.Variable,
'statement': CompletionKind.Keyword,
} # type: Dict[str, CompletionKind]
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(completion):
# type: (jedi.api.classes.Completion,) -> Union[str, IMarkdownString]
# XXX we could check based on completion.module_path() python's stdlib tend to be rst
# but for now, we assume everything is markdown
return IMarkdownString(completion.docstring())
return {
'label': _label(completion),
'kind': _TYPE_MAP.get(completion.type),
'detail': _detail(completion),
'documentation': _format_docstring(completion),
'sortText': _sort_text(completion),
'insertText': completion.name
}
@json_serialized
def getCompletions(self, completion_context):
# type: (Union[CompletionContext, PythonScriptCompletionContext],) -> List[CompletionItem]
"""Returns completions.
"""
script = JediController(
self,
# fixPythonScriptContext not here !
fixPythonScriptContext(completion_context, self.getPortalObject())
).getScript()
return [self._convertCompletion(c) for c in script.completions()]
@json_serialized
def getCodeLens(self, completion_context):
# type: (Union[CompletionContext, PythonScriptCompletionContext],) -> List[CompletionItem]
"""Returns code lens.
"""
return []
def fixPythonScriptContext(context, portal):
# type: (Union[CompletionContext, PythonScriptCompletionContext], erp5.portal_type.ERP5Site) -> CompletionContext
"""Normalize completion context for python scripts
ie. make a function with params and adjust the line number.
"""
if not getattr(context, "bound_names"):
return context
def _guessParameterType(name, context_type=None):
"""guess the type of python script parameters based on naming conventions.
"""
# TODO: `state_change` arguments for workflow scripts
name = name.split('=')[
0] # support also assigned names (like REQUEST=None in params)
if name == 'context' and context_type:
return context_type
if name in (
'context',
'container',):
return 'erp5.portal_type.ERP5Site'
if name == 'script':
return 'Products.PythonScripts.PythonScript.PythonScript'
if name == 'REQUEST':
return 'ZPublisher.HTTPRequest.HTTPRequest'
if name == 'RESPONSE':
return 'ZPublisher.HTTPRequest.HTTPResponse'
return 'str' # assume string by default
signature_parts = context.bound_names + (
[context.params] if context.params else []
)
# guess type of `context`
context_type = None
if '_' in context:
context_type = context.split('_')[0]
if context_type not in [
ti.replace(' ', '') # XXX "python identifier"
for ti in portal.portal_types.objectIds()
] + [
'ERP5Site',]:
logger.debug(
"context_type %s has no portal type, using ERP5Site", context_type
)
context_type = None
type_comment = " # type: ({}) -> None".format(
', '.join(
[_guessParameterType(part, context_type) for part in signature_parts]
)
)
def indent(text):
return ''.join((" " + line) for line in text.splitlines(True))
context.code = textwrap.dedent(
'''import erp5.portal_type;
import Products.ERP5Type.Core.Folder;
import ZPublisher.HTTPRequest;
import Products.PythonScripts.PythonScript
def {script_name}({signature}):
{type_comment}
{body}
pass
'''
).format(
script_name=context.script_name,
signature=', '.join(signature_parts),
type_comment=type_comment,
body=indent(context.code)
)
context.position.lineNumber += 6 # imports, fonction header + type comment
context.position.column += 2 # re-indentation
return context
class JediController(object):
"""Controls jedi.
"""
def __init__(self, tool, context):
# type: (PythonSupportTool, CompletionContext) -> None
if not self._isEnabled():
self._patchBuildoutSupport()
self._enablePlugin()
self._sys_path = [tool.getStubPath()] + sys.path
self._context = context
def _isEnabled(self):
"""is our plugin already enabled ?
"""
return False
def _enablePlugin(self):
"""Enable our ERP5 jedi plugin.
"""
def _patchBuildoutSupport(self):
"""Patch jedi to disable buggy buildout.cfg support.
"""
# monkey patch to disable buggy sys.path addition based on buildout.
# https://github.com/davidhalter/jedi/issues/1325
# rdiff-backup also seem to trigger a bug, but it's generally super slow and not correct for us.
try:
# in jedi 0.15.1 it's here
from jedi.evaluate import sys_path as jedi_inference_sys_path # pylint: disable=import-error,unused-import,no-name-in-module
except ImportError:
# but it's beeing moved. Next release (0.15.2) will be here
# https://github.com/davidhalter/jedi/commit/3b4f2924648eafb9660caac9030b20beb50a83bb
from jedi.inference import sys_path as jedi_inference_sys_path # pylint: disable=import-error,unused-import,no-name-in-module
_ = jedi_inference_sys_path.discover_buildout_paths # make sure we found it here
def dont_discover_buildout_paths(*args, **kw):
return set()
jedi_inference_sys_path.discover_buildout_paths = dont_discover_buildout_paths
from jedi.api import project as jedi_api_project
jedi_api_project.discover_buildout_paths = dont_discover_buildout_paths
def getScript(self):
# type: () -> jedi.Script
"""Returns a jedi.Script for this code.
"""
# TODO: lock ! (and not only here)
context = self._context
return jedi.Script(
context.code,
context.position.lineNumber,
context.position.column - 1,
context.script_name,
self._sys_path
)
@staticmethod
def jedi_execute(callback, context, arguments):
# type: (Callable[[Any], Any], jedi.Context, Any) -> Any
"""jedi plugin `execute`
XXX
"""
return "jedi executed"
class PythonCodeGenerator(object):
"""Generator python code for static analysis.
"""
# make sure PythonSupportTool is first, this is needed for dynamic components.
__all__ = (
'PythonSupportTool', 'json_serialized', 'PythonCodeGenerator', 'Position'
)
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Tool 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>PythonSupportTool</string> </value>
</item>
<item>
<key> <string>description</string> </key>
<value>
<none/>
</value>
</item>
<item>
<key> <string>id</string> </key>
<value> <string>tool.erp5.PythonSupportTool</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Tool 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.Workflow"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_log</string> </key>
<value>
<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>
</value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
<?xml version="1.0"?>
<ZopeData>
<record id="1" aka="AAAAAAAAAAE=">
<pickle>
<global name="Python Support Tool" module="erp5.portal_type"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>id</string> </key>
<value> <string>portal_python_support</string> </value>
</item>
<item>
<key> <string>portal_type</string> </key>
<value> <string>Python Support Tool</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
Python Support Tool
\ No newline at end of file
test.erp5.testPythonSupport
\ No newline at end of file
tool.erp5.PythonSupportTool
\ No newline at end of file
portal_python_support
\ No newline at end of file
erp5_python_support
\ No newline at end of file
from six import string_types as basestring
import time
import json
import logging
from Products.ERP5Type.Utils import checkPythonSourceCode
logger = logging.getLogger('extension.erp5.PythonCodeUtils')
def checkPythonSourceCodeAsJSON(self, data, REQUEST=None):
"""
Check Python source suitable for Source Code Editor and return a JSON object
"""
import json
# XXX data is encoded as json, because jQuery serialize lists as []
if isinstance(data, basestring):
......@@ -17,6 +22,7 @@ def checkPythonSourceCodeAsJSON(self, data, REQUEST=None):
return ''.join((" " + line) for line in text.splitlines(True))
# don't show 'undefined-variable' errors for {Python,Workflow} Script parameters
script_name = data.get('script_name') or 'unknown.py'
is_script = 'bound_names' in data
if is_script:
signature_parts = data['bound_names']
......@@ -31,7 +37,86 @@ def checkPythonSourceCodeAsJSON(self, data, REQUEST=None):
else:
body = data['code']
message_list = checkPythonSourceCode(body.encode('utf8'), data.get('portal_type'))
start = time.time()
code = body.encode('utf8')
import pyflakes.api
import pyflakes.reporter
import pyflakes.messages
class Reporter(pyflakes.reporter.Reporter):
def __init__(self): # pylint: disable=super-init-not-called
self.message_list = []
def addMessage(self, row, column, level, text):
self.message_list.append(
dict(row=row, column=column, type=level, text=text))
def flake(self, message):
# type: (pyflakes.messages.Message,) -> None
self.addMessage(
row=message.lineno,
column=message.col,
text=message.message % (message.message_args),
level='W')
def syntaxError(self, filename, msg, lineno, offset, text):
self.addMessage(
row=lineno,
column=offset,
text='SyntaxError: {}'.format(text),
level='E')
def unexpectedError(self, filename, msg):
# TODO: extend interface to have range and in this case whole range is wrong ?
# or use parse with python in that case ?
# repro: function(a="b", c)
self.addMessage(
row=0, column=0, text='Unexpected Error: {}'.format(msg), level='E')
start = time.time()
reporter = Reporter()
pyflakes.api.check(code, script_name, reporter)
logger.info(
'pyflake checked %d lines in %.2f',
len(code.splitlines()),
time.time() - start
)
message_list = reporter.message_list
import lib2to3.refactor
import lib2to3.pgen2.parse
refactoring_tool = lib2to3.refactor.RefactoringTool(fixer_names=('lib2to3.fixes.fix_except', ))
old_code = code.decode('utf-8')
try:
new_code = unicode(refactoring_tool.refactor_string(old_code, script_name))
except lib2to3.pgen2.parse.ParseError as e:
message, (row, column) = e.context
message_list.append(
dict(row=row, column=column, type='E', text=message))
else:
if new_code != old_code:
i = 0
for new_line, old_line in zip(new_code.splitlines(), old_code.splitlines()):
i += 1
#print ('new_line', new_line, 'old_line', old_line)
if new_line != old_line:
message_list.append(
dict(row=i, column=0, type='W', text=u'-{}\n+{}'.format(old_line, new_line)))
# import pdb; pdb.set_trace()
pylint_message_list = []
if 1:
start = time.time()
pylint_message_list = checkPythonSourceCode(code, data.get('portal_type'))
logger.info(
'pylint checked %d lines in %.2f',
len(code.splitlines()),
time.time() - start
)
message_list = pylint_message_list
for message_dict in message_list:
if is_script:
message_dict['row'] = message_dict['row'] - 2
......@@ -46,3 +131,4 @@ def checkPythonSourceCodeAsJSON(self, data, REQUEST=None):
if REQUEST is not None:
REQUEST.RESPONSE.setHeader('content-type', 'application/json')
return json.dumps(dict(annotations=message_list))
\ No newline at end of file
......@@ -204,6 +204,16 @@
<key> <string>width</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>renderjs_extra</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
</item>
</dictionary>
</value>
</item>
......@@ -271,6 +281,12 @@
<key> <string>text_editor</string> </key>
<value> <string>text_area</string> </value>
</item>
<item>
<key> <string>renderjs_extra</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Source Code</string> </value>
......
......@@ -10,6 +10,7 @@
<key> <string>delegated_list</string> </key>
<value>
<list>
<string>renderjs_extra</string>
<string>title</string>
</list>
</value>
......@@ -56,6 +57,12 @@
<key> <string>form_id</string> </key>
<value> <string></string> </value>
</item>
<item>
<key> <string>renderjs_extra</string> </key>
<value>
<persistent> <string encoding="base64">AAAAAAAAAAI=</string> </persistent>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string></string> </value>
......@@ -75,6 +82,12 @@
<key> <string>form_id</string> </key>
<value> <string>Base_viewFieldLibrary</string> </value>
</item>
<item>
<key> <string>renderjs_extra</string> </key>
<value>
<list/>
</value>
</item>
<item>
<key> <string>title</string> </key>
<value> <string>Source Code</string> </value>
......@@ -85,4 +98,17 @@
</dictionary>
</pickle>
</record>
<record id="2" aka="AAAAAAAAAAI=">
<pickle>
<global name="TALESMethod" module="Products.Formulator.TALESField"/>
</pickle>
<pickle>
<dictionary>
<item>
<key> <string>_text</string> </key>
<value> <string>python: [(\'editor\', context.Base_getEditorFieldPreferredTextEditor()), (\'portal_type\', context.getPortalType()), (\'maximize\', \'listbox\' not in field.id), (\'content_type\', context.getProperty(\'content_type\')), (\'language_support_url\', context.getPortalObject().portal_url())]</string> </value>
</item>
</dictionary>
</pickle>
</record>
</ZopeData>
......@@ -15,6 +15,8 @@
* @property {string} language the user language, if the editor supports
* localisation it will be displayed in this language
* @property {string} password a password to decrypt the content
* @property {string} language_support_url URL of language support endpoint
* for completion, diagnostics etc in the editor
* @property {boolean} run a hack for jsmd editor
* @property {string} key Key for ERP5 form
*/
......@@ -90,6 +92,7 @@
run: options.run || false,
key: options.key,
password: options.password,
language_support_url: options.language_support_url || "",
// Force calling subfield render
// as user may have modified the input value
render_timestamp: new Date().getTime()
......
......@@ -56,6 +56,11 @@ from Products.ERP5Type.Utils import sortValueList
from Products.ERP5Type import Permissions
from Products.ERP5Type.Globals import InitializeClass
from Products.ERP5Type.Accessor import Base as BaseAccessor
try:
from typing import List
except ImportError:
pass
try:
from Products.CMFCore.CMFBTreeFolder import CMFBTreeFolder
except ImportError:
......@@ -206,6 +211,7 @@ class FolderMixIn(ExtensionClass.Base):
security.declarePublic('newContent')
def newContent(self, id=None, portal_type=None, id_group=None,
default=None, method=None, container=None, temp_object=0, **kw):
# type: (...) -> Folder
"""Creates a new content.
This method is public, since TypeInformation.constructInstance will perform
the security check.
......@@ -421,6 +427,7 @@ class FolderMixIn(ExtensionClass.Base):
# Get the content
security.declareProtected(Permissions.AccessContentsInformation, 'searchFolder')
def searchFolder(self, **kw):
# type: (str) -> List[Folder]
"""
Search the content of a folder by calling
the portal_catalog.
......@@ -1521,6 +1528,7 @@ class Folder(FolderMixIn, CopyContainer, ObjectManager, Base, OFSFolder2, CMFBTr
def objectValues(self, spec=None, meta_type=None, portal_type=None,
sort_on=None, sort_order=None, checked_permission=None,
**kw):
# type: () -> List[Folder]
# Returns list of objects contained in this folder.
# (no docstring to prevent publishing)
if meta_type is not None:
......@@ -1552,6 +1560,7 @@ class Folder(FolderMixIn, CopyContainer, ObjectManager, Base, OFSFolder2, CMFBTr
security.declareProtected( Permissions.AccessContentsInformation,
'contentValues' )
def contentValues(self, *args, **kw):
# type: () -> List[Folder]
# Returns a list of documents contained in this folder.
# ( no docstring to prevent publishing )
portal_type_id_list = self._getTypesTool().listContentTypes()
......
......@@ -431,14 +431,35 @@ def fill_args_from_request(*optional_args):
_pylint_message_re = re.compile(
'^(?P<type>[CRWEF]):\s*(?P<row>\d+),\s*(?P<column>\d+):\s*(?P<message>.*)$')
zope_startup_time = time.time()
def checkPythonSourceCode(source_code_str, portal_type=None):
"""
Check source code with pylint or compile() builtin if not available.
`portal_type` argument can be passed to the checker, to adapt the checks
based on the portal type.
This function is cached, so checkin the same code several times is instant.
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...
"""
# late imports because we have a circular import dependency here.
from Products.ERP5Type.Cache import CachingMethod
from Products.ERP5.ERP5Site import getSite
checkPythonSourceCode = CachingMethod(
_checkPythonSourceCode,
'_checkPythonSourceCode.{}.{}.{}'.format(
zope_startup_time,
md5_new(source_code_str).hexdigest(),
getSite().getCacheCookie('component_packages')))
# normalize type of portal_type argument to maximize cache hits.
if isinstance(portal_type, unicode):
portal_type = portal_type.encode()
return checkPythonSourceCode(source_code_str, portal_type)
def _checkPythonSourceCode(source_code_str, portal_type):
if not source_code_str:
return []
......
......@@ -108,6 +108,7 @@ def manage_page_footer(self):
textarea_selector=textarea_selector,
portal_url=portal_url,
bound_names=bound_names,
script_name=document.getId(),
mode=mode).encode('utf-8'))
return default
......
......@@ -19,6 +19,7 @@ import sys
import time
import traceback
import urllib
import unittest
import ConfigParser
from contextlib import contextmanager
from cStringIO import StringIO
......@@ -214,7 +215,14 @@ def _parse_args(self, *args, **kw):
_parse_args._original = DateTime._original_parse_args
DateTime._parse_args = _parse_args
class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase):
try:
from erp5.portal_type import ERP5Site as erp5_portal_type_ERP5Site
except ImportError:
pass
class ERP5TypeTestCaseMixin(ProcessingNodeTestCase, PortalTestCase, unittest.TestCase, object):
"""Mixin class for ERP5 based tests.
"""
def __init__(self, *args, **kw):
......@@ -990,6 +998,7 @@ class ERP5TypeCommandLineTestCase(ERP5TypeTestCaseMixin):
return portal_name + '_' + m.hexdigest()
def getPortal(self):
# type: () -> erp5_portal_type_ERP5Site
"""Returns the portal object, i.e. the "fixture root".
It also does some initialization, as if the portal was accessed for the
......
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