Commit 3425583d authored by Jérome Perrin's avatar Jérome Perrin

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

parent de2a0603
<?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
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