Commit 9af873cb authored by Jérome Perrin's avatar Jérome Perrin

monaco_editor: jedi WIP

parent fb688735
# coding: utf-8
# TODO: drop this ? it confuse type checking this file
from __future__ import unicode_literals from __future__ import unicode_literals
import json import json
import sys import sys
import inspect
# pylint: disable=unused-import
from typing import List, Type, Optional, Dict, Tuple, Sequence, TYPE_CHECKING
import typing import typing
import logging import logging
from threading import RLock from threading import RLock
from Products.ERP5Type.Cache import transactional_cached
logger = logging.getLogger("erp5.extension.Jedi") logger = logging.getLogger("erp5.extension.Jedi")
logger.setLevel(logging.DEBUG)
import os import os
import jedi import jedi
import time import time
import erp5.portal_type
last_reload_time = time.time() last_reload_time = time.time()
# increase default cache duration # increase default cache duration
...@@ -42,9 +52,6 @@ from jedi.evaluate.gradual.typing import InstanceWrapper ...@@ -42,9 +52,6 @@ from jedi.evaluate.gradual.typing import InstanceWrapper
from jedi.evaluate.lazy_context import LazyKnownContexts from jedi.evaluate.lazy_context import LazyKnownContexts
from jedi.evaluate.base_context import ContextSet, NO_CONTEXTS from jedi.evaluate.base_context import ContextSet, NO_CONTEXTS
if typing.TYPE_CHECKING:
import erp5.portal_type.ERP5Site # pylint: disable=unused-import,no-name-in-module,import-error
def executeJediXXX(callback, context, arguments): def executeJediXXX(callback, context, arguments):
# XXX function for relaodability # XXX function for relaodability
...@@ -55,8 +62,7 @@ def executeJediXXX(callback, context, arguments): ...@@ -55,8 +62,7 @@ def executeJediXXX(callback, context, arguments):
def filter_func(val): def filter_func(val):
if isinstance(val, TreeInstance) and val.tree_node.type == 'classdef': if isinstance(val, TreeInstance) and val.tree_node.type == 'classdef':
logger.info( logger.info(
"classdef cool => %s == %s", "classdef cool => %s == %s", val.tree_node.name.value,
val.tree_node.name.value,
class_from_portal_type) class_from_portal_type)
return 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()): if isinstance(val, LazyKnownContexts) and filter_func(val.infer()):
...@@ -68,9 +74,9 @@ def executeJediXXX(callback, context, arguments): ...@@ -68,9 +74,9 @@ def executeJediXXX(callback, context, arguments):
if filter_func(wrapped): if filter_func(wrapped):
return True return True
return False return False
annotation_classes = val.gather_annotation_classes() ## annotation_classes = val.gather_annotation_classes()
#import pdb; pdb.set_trace() ## import pdb; pdb.set_trace()
return val.gather_annotation_classes().filter(filter_func) ## return val.gather_annotation_classes().filter(filter_func)
logger.info("not found in %s", val) logger.info("not found in %s", val)
return False return False
...@@ -108,8 +114,7 @@ def executeJediXXX(callback, context, arguments): ...@@ -108,8 +114,7 @@ def executeJediXXX(callback, context, arguments):
# {x for x in original._set if class_from_portal_type in str(x)}) # {x for x in original._set if class_from_portal_type in str(x)})
logger.info( logger.info(
'portal_type based method, returning\n %s instead of\n %s', 'portal_type based method, returning\n %s instead of\n %s',
filtered, filtered, original)
original)
return filtered return filtered
# methods returning List of portal types # methods returning List of portal types
...@@ -190,20 +195,23 @@ _TYPE_MAP = { ...@@ -190,20 +195,23 @@ _TYPE_MAP = {
def _label(definition): def _label(definition):
# type: (jedi.api.classes.Completion,) -> str # type: (jedi.api.classes.Completion,) -> str
if definition.type == 'param': #if definition.type == 'param':
return '{}='.format(definition.name) # return '{}='.format(definition.name)
if definition.type in ('function', 'method') and hasattr(definition, if definition.type in ('function', 'method') and hasattr(definition,
'params'): 'params'):
params = ', '.join([param.name for param in definition.params]) params = ', '.join([param.name for param in definition.params])
return '{}({})'.format(definition.name, params) return '{}({})'.format(definition.name, params)
return definition.name return definition.name
def _insertText(definition): def _insertText(definition):
# type: (jedi.api.classes.Completion,) -> str # type: (jedi.api.classes.Completion,) -> str
if definition.type == 'param': # XXX
return '{}='.format(definition.name) #if definition.type == 'param':
# return '{}='.format(definition.name)
return definition.name return definition.name
def _detail(definition): def _detail(definition):
try: try:
return definition.parent().full_name or '' return definition.parent().full_name or ''
...@@ -220,17 +228,21 @@ def _sort_text(definition): ...@@ -220,17 +228,21 @@ def _sort_text(definition):
return prefix.format(definition.name) return prefix.format(definition.name)
def _format_docstring(docstring): def _format_docstring(d):
return docstring try:
return d.docstring()
except Exception as e:
logger.exception('error getting completions from %s', d)
return "```{}```".format(repr(e))
def _format_completion(d): def _format_completion(d):
# type: (jedi.api.classes.Completion,) -> typing.Dict[str, str] # type: (jedi.api.classes.Completion,) -> Dict[str, Optional[str]]
completion = { completion = {
'label': _label(d), 'label': _label(d),
'_kind': _TYPE_MAP.get(d.type), '_kind': _TYPE_MAP.get(d.type),
'detail': _detail(d), 'detail': _detail(d),
'documentation': _format_docstring(d.docstring()), 'documentation': _format_docstring(d),
'sortText': _sort_text(d), 'sortText': _sort_text(d),
'insertText': _insertText(d), 'insertText': _insertText(d),
} }
...@@ -247,7 +259,8 @@ def _guessType(name, context_type=None): ...@@ -247,7 +259,8 @@ def _guessType(name, context_type=None):
return context_type return context_type
if name in ( if name in (
'context', 'context',
'container',): 'container',
):
return 'erp5.portal_type.ERP5Site' return 'erp5.portal_type.ERP5Site'
if name == 'script': if name == 'script':
return 'Products.PythonScripts.PythonScript' return 'Products.PythonScripts.PythonScript'
...@@ -260,7 +273,7 @@ def _guessType(name, context_type=None): ...@@ -260,7 +273,7 @@ def _guessType(name, context_type=None):
# Jedi is not thread safe # Jedi is not thread safe
import Products.ERP5Type.Utils import Products.ERP5Type.Utils
jedi_lock = getattr(Products.ERP5Type.Utils, 'jedi_lock', None) # type: RLock jedi_lock = getattr(Products.ERP5Type.Utils, 'jedi_lock', None) # type: RLock
if jedi_lock is None: if jedi_lock is None:
logger.critical("There was no lock, making a new one") logger.critical("There was no lock, making a new one")
jedi_lock = Products.ERP5Type.Utils.jedi_lock = RLock() jedi_lock = Products.ERP5Type.Utils.jedi_lock = RLock()
...@@ -272,66 +285,70 @@ def ERP5Site_getPythonSourceCodeCompletionList(self, data, REQUEST=None): ...@@ -272,66 +285,70 @@ def ERP5Site_getPythonSourceCodeCompletionList(self, data, REQUEST=None):
""" """
portal = self.getPortalObject() portal = self.getPortalObject()
logger.debug('jedi get lock %s (%s)', jedi_lock, id(jedi_lock)) logger.debug('jedi get lock %s (%s)', jedi_lock, id(jedi_lock))
if not jedi_lock.acquire(False): 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') raise RuntimeError('jedi is locked')
with jedi_lock:
with jedi_lock:
# register our erp5 plugin # register our erp5 plugin
from jedi.plugins import plugin_manager from jedi.plugins import plugin_manager
if not getattr(plugin_manager, '_erp5_plugin_registered', None): if not getattr(plugin_manager, '_erp5_plugin_registered', None):
plugin_manager.register(makeERP5Plugin()) plugin_manager.register(makeERP5Plugin())
plugin_manager._erp5_plugin_registered = True plugin_manager._erp5_plugin_registered = True
if isinstance(data, basestring): if isinstance(data, basestring):
data = json.loads(data) data = json.loads(data)
# data contains the code, the bound names and the script params. From this # data contains the code, the bound names and the script params. From this
# we reconstruct a function that can be checked # we reconstruct a function that can be checked
def indent(text): def indent(text):
return ''.join((" " + line) for line in text.splitlines(True)) return ''.join((" " + line) for line in text.splitlines(True))
script_name = data.get('script_name', 'unknown.py') # TODO name script_name = data.get('script_name', 'unknown.py') # TODO name
is_python_script = 'bound_names' in data 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']
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) logger.debug("jedi getting completions for %s ...", script_name)
start = time.time() start = time.time()
script = jedi.Script( script = jedi.Script(
...@@ -342,18 +359,80 @@ def ERP5Site_getPythonSourceCodeCompletionList(self, data, REQUEST=None): ...@@ -342,18 +359,80 @@ def ERP5Site_getPythonSourceCodeCompletionList(self, data, REQUEST=None):
sys_path=['/tmp/ahaha/'] + list(sys.path), sys_path=['/tmp/ahaha/'] + list(sys.path),
) )
completions = [_format_completion(c) for c in script.completions()] def _get_param_name(p):
logger.info( if (p.name.startswith('param ')):
"jedi got %d completions in %.2fs", return p.name[6:] # drop leading 'param '
len(completions), (time.time() - start)) 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_hover'): if data.get('xxx_hover'):
completions = '' # XXX this is not "completions" ... completions = '' # XXX this is not "completions" ...
for definition in script.goto_definitions(): for definition in script.goto_definitions():
completions = definition.docstring() documentation_lines = definition.docstring().splitlines()
if REQUEST is not None: # reformat this in nicer markdown
REQUEST.RESPONSE.setHeader('content-type', 'application/json') completions = textwrap.dedent(
return json.dumps(completions) '''\
`{}`
---
{}
''').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 import textwrap
...@@ -374,7 +453,7 @@ def safe_docstring(docstring): ...@@ -374,7 +453,7 @@ def safe_docstring(docstring):
""" """
if not docstring: if not docstring:
return '...' return '...'
return "'''{}'''".format(docstring.replace("'''", r"\'\'\'")) return "'''{}\n'''".format(docstring.replace("'''", r"\'\'\'"))
from Products.ERP5Type.Accessor import Constant from Products.ERP5Type.Accessor import Constant
...@@ -413,11 +492,18 @@ def SkinsTool_getStubForClass(self, class_name): ...@@ -413,11 +492,18 @@ def SkinsTool_getStubForClass(self, class_name):
# collect skins by type # collect skins by type
skin_by_type = defaultdict(list) skin_by_type = defaultdict(list)
# TODO: sort by default skin selection and use only the ones registered in skin selections # 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 skin_folder in portal.portal_skins.objectValues():
for script in skin_folder.objectValues(spec=('Script (Python)', for script in skin_folder.objectValues(spec=('Script (Python)',
'External Method')): 'External Method')):
if not '_' in script.getId(): if not '_' in script.getId():
logger.debug('Skipping wrongly named script %s', 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 continue
type_ = script.getId().split('_')[0] type_ = script.getId().split('_')[0]
if type_ != class_name: if type_ != class_name:
...@@ -446,6 +532,7 @@ def SkinsTool_getStubForClass(self, class_name): ...@@ -446,6 +532,7 @@ def SkinsTool_getStubForClass(self, class_name):
if next(iter(grammar.iter_errors(module)), None) is not None: if next(iter(grammar.iter_errors(module)), None) is not None:
first_leaf = module.get_first_leaf() first_leaf = module.get_first_leaf()
type_comment = first_leaf.prefix.strip() type_comment = first_leaf.prefix.strip()
# TODO: adjust type comment ?
if not type_coment_re.match(type_comment): if not type_coment_re.match(type_comment):
type_comment = '' type_comment = ''
else: else:
...@@ -469,33 +556,40 @@ def SkinsTool_getStubForClass(self, class_name): ...@@ -469,33 +556,40 @@ def SkinsTool_getStubForClass(self, class_name):
skin_by_type[type_].append( skin_by_type[type_].append(
SkinDefinition( SkinDefinition(
script.getId(), script.getId(), docstring, type_comment, skin_folder.getId(),
docstring,
type_comment,
skin_folder.getId(),
params)) params))
# TODO: this loop is nonsense.
for type_, skins in skin_by_type.items(): for type_, skins in skin_by_type.items():
line_list.append( line_list.append(
textwrap.dedent( textwrap.dedent(
"""\ """\
# coding: utf-8
import erp5.portal_type import erp5.portal_type
from erp5 import portal_type import typing
class {class_name}: class {class_name}:
{docstring} {docstring}
""").format( """).format(
class_name=safe_python_identifier(type_), class_name=safe_python_identifier(type_),
docstring=safe_docstring("Skins for {}".format(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: for skin in skins:
skin = skin # type: SkinDefinition 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( line_list.append(
# the comment is also here so that dedent keep indentation, because this method block needs # the comment is also here so that dedent keep indentation, because this method block needs
# more indentation than class block # more indentation than class block
textwrap.dedent( textwrap.dedent(
"""\ """\
# {skin_id} in {skin_folder} # {skin_id} in {skin_folder}
def {skin_id}(self{params}): def {skin_id}(self{params}):
{type_comment}{docstring} {type_comment}{docstring}
""").format( """).format(
...@@ -507,6 +601,115 @@ def SkinsTool_getStubForClass(self, class_name): ...@@ -507,6 +601,115 @@ def SkinsTool_getStubForClass(self, class_name):
return "\n".join(line_list) 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(
"/tmp/ahaha/erp5/skins_tool/{name}.pyi".format(name=name))
def TypeInformation_getStub(self): def TypeInformation_getStub(self):
# type: (ERP5TypeInformation) -> str # type: (ERP5TypeInformation) -> str
"""returns a .pyi stub file for this portal type """returns a .pyi stub file for this portal type
...@@ -514,18 +717,11 @@ def TypeInformation_getStub(self): ...@@ -514,18 +717,11 @@ def TypeInformation_getStub(self):
https://www.python.org/dev/peps/pep-0484/ https://www.python.org/dev/peps/pep-0484/
""" """
portal = self.getPortalObject() portal = self.getPortalObject()
portal_url = portal.absolute_url()
# TODO: getParentValue # TODO: getParentValue
# TODO: a class for magic things like getPortalObject ?
@WorkflowMethod.disable temp_class = makeTempClass(portal, self.getId())
def makeTempClass():
# everything is allowed in portal trash so we create our
# temp object there.
return portal.portal_trash.newContent(
portal_type=self.getId(), temp_object=True, id='?', title='?').__class__
temp_class = makeTempClass()
# mro() of temp objects is like : # mro() of temp objects is like :
# (<class 'erp5.temp_portal_type.Temporary Person Module'>, # (<class 'erp5.temp_portal_type.Temporary Person Module'>,
...@@ -538,13 +734,18 @@ def TypeInformation_getStub(self): ...@@ -538,13 +734,18 @@ def TypeInformation_getStub(self):
parent_class = temp_class.mro()[1] parent_class = temp_class.mro()[1]
parent_class_module = parent_class.__module__ parent_class_module = parent_class.__module__
imports = set([ imports = set(
'from erp5.portal_type import Type_CatalogBrain', [
'from erp5.portal_type import Type_AnyPortalTypeList', 'from Products.ERP5Type.Base import Base as Products_ERP5Type_Base_Base',
'from erp5.portal_type import Type_AnyPortalTypeCatalogBrainList', 'import erp5.portal_type',
'from typing import Union, List, Optional, Any, overload, Literal, TypeVar, Generic', # TODO use "" style type definition without importing
'from DateTime import DateTime.DateTime as DateTime # XXX help jedi', # '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 = "" header = ""
methods = [] methods = []
debug = "" debug = ""
...@@ -555,30 +756,18 @@ def TypeInformation_getStub(self): ...@@ -555,30 +756,18 @@ def TypeInformation_getStub(self):
decorator='', decorator='',
method_name='getPortalType', method_name='getPortalType',
method_args="self", method_args="self",
return_type='Literal["{}"]'.format(self.getId()), return_type='Literal[b"{}"]'.format(self.getId()),
# We want to be able to infer based on the portal type named returned by x.getPortalType() # 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. # jedi does not support Literal in this context, so add a method implementation.
# This is not really valid for a .pyi, but jedi does not care. docstring="{}\n return b'{}'".format(
docstring="{}\n return '{}'".format(
safe_docstring(self.getId()), self.getId()))) safe_docstring(self.getId()), self.getId())))
# XXX debug
methods.append(
method_template_template.format(
decorator='',
method_name='reveal_portal_tye_{}'.format(
safe_python_identifier(self.getId())),
method_args='self',
return_type='',
docstring=safe_docstring("ahaha cool :)")))
imports.add('from erp5.portal_type import ERP5Site')
methods.append( methods.append(
method_template_template.format( method_template_template.format(
decorator='', decorator='',
method_name='getPortalObject', method_name='getPortalObject',
method_args="self", method_args="self",
return_type='ERP5Site', return_type='"ERP5Site"',
docstring=safe_docstring( docstring=safe_docstring(
getattr(temp_class.getPortalObject, '__doc__', None) or '...'))) getattr(temp_class.getPortalObject, '__doc__', None) or '...')))
...@@ -588,38 +777,92 @@ def TypeInformation_getStub(self): ...@@ -588,38 +777,92 @@ def TypeInformation_getStub(self):
continue continue
property_value = getattr(temp_class, property_name) property_value = getattr(temp_class, property_name)
if isinstance(property_value, Constant.Getter): 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 # TODO: add an implementation returning the value so that jedi can infer
methods.append( if 0:
method_template_template.format( methods.append(
decorator='', method_template_template.format(
method_name=safe_python_identifier(property_name), decorator='',
method_args="self", method_name=safe_python_identifier(property_name),
return_type=type(property_value.value).__name__, method_args="self",
docstring=safe_docstring('TODO %s' % property_value))) return_type=type(property_value.value).__name__,
elif isinstance(property_value, docstring=safe_docstring('TODO %s' % property_value)))
(WorkflowState.TitleGetter, elif isinstance(
WorkflowState.TranslatedGetter, property_value,
WorkflowState.TranslatedTitleGetter, (
WorkflowState.Getter)): # we don't generate for TitleGetter and TranslatedGetter because they are useless
# TODO: docstring (with link to workflow) 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( methods.append(
method_template_template.format( method_template_template.format(
decorator='', decorator='',
method_name=safe_python_identifier(property_name), method_name=safe_python_identifier(property_name),
method_args="self", method_args="self",
return_type="str", return_type="str",
docstring=safe_docstring('TODO %s' % property_value))) docstring=safe_docstring(docstring)))
elif isinstance(property_value, WorkflowMethod): elif isinstance(property_value, WorkflowMethod):
# TODO: docstring (with link to workflow) 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 # 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). # 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( methods.append(
method_template_template.format( method_template_template.format(
decorator='', decorator='',
method_name=safe_python_identifier(property_name), method_name=safe_python_identifier(property_name),
method_args="self", method_args=method_args,
return_type='None', return_type=return_type,
docstring=safe_docstring('TODO %s' % property_value))) docstring=safe_docstring(docstring)))
elif property_name.startswith( elif property_name.startswith(
'serialize' 'serialize'
): # isinstance(property_value, WorkflowState.SerializeGetter): XXX not a class.. ): # isinstance(property_value, WorkflowState.SerializeGetter): XXX not a class..
...@@ -634,81 +877,120 @@ def TypeInformation_getStub(self): ...@@ -634,81 +877,120 @@ def TypeInformation_getStub(self):
# TODO: generated methods for categories. # TODO: generated methods for categories.
else: else:
debug += "\n # not handled property: {} -> {} {}".format( debug += "\n # not handled property: {} -> {} {}".format(
property_name, property_name, property_value,
property_value,
getattr(property_value, '__dict__', '')) getattr(property_value, '__dict__', ''))
# for folderish contents, generate typed contentValues # for folderish contents, generate typed contentValues and other folderish methods
allowed_content_types = self.getTypeAllowedContentTypeList() allowed_content_types = self.getTypeAllowedContentTypeList()
allowed_content_types_classes = [ multiple_allowed_content_types = len(allowed_content_types) > 1
safe_python_identifier(t) for t in allowed_content_types # TODO generate contentValues() without portal_type argument
] for allowed_content_type in allowed_content_types:
if allowed_content_types and hasattr(temp_class, 'contentValues'): if multiple_allowed_content_types:
for allowed in allowed_content_types_classes: new_content_portal_type_type_annotation = 'Literal[b"{allowed_content_type}"]'.format(
imports.add('from erp5.portal_type import {}'.format(allowed)) allowed_content_type=allowed_content_type)
if len(allowed_content_types) == 1:
subdocument_type = '{}'.format(allowed_content_types_classes[0])
else: else:
subdocument_type = 'Union[{}]'.format( new_content_portal_type_type_annotation = 'str="{allowed_content_type}"'.format(
', '.join(allowed_content_types_classes)) allowed_content_type=allowed_content_type)
subdocument_type = '"{}"'.format(
safe_python_identifier(allowed_content_type))
# TODO: getParentValue 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'): for method_name in ('contentValues', 'objectValues', 'searchFolder'):
return_type = 'List[{}]'.format(subdocument_type) return_type = 'Sequence[{}]'.format(subdocument_type)
if method_name == 'searchFolder': if 0 and method_name == 'searchFolder': # TODO searchFolder is different, it returns brain and accepts **kw
return_type = 'List[Type_CatalogBrain[{}]]'.format(subdocument_type) return_type = 'Sequence[Type_CatalogBrain[{}]]'.format(subdocument_type)
if len(allowed_content_types) > 1: if multiple_allowed_content_types:
# not correct but it makes jedi complete well when portal_type='one' # not correct but it makes jedi complete well when portal_type='one'
return_type = 'Union[{}]'.format( return_type = 'Union[{}]'.format(
', '.join(( ', '.join(
'List[Type_CatalogBrain[{}]]'.format(t) (
for t in allowed_content_types_classes))) 'Sequence[Type_CatalogBrain["erp5.portal_type.{}"]]'
# TODO
.format(t) for t in 'allowed_content_types_classes')))
methods.append( methods.append(
method_template_template.format( method_template_template.format(
decorator='', decorator='@overload' if multiple_allowed_content_types else '',
method_name=method_name, method_name=method_name,
method_args="self", method_args=method_args,
return_type=return_type, return_type=return_type,
docstring=safe_docstring( docstring=safe_docstring(
getattr(getattr(temp_class, method_name), '__doc__', None)))) 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( methods.append(
method_template_template.format( method_template_template.format(
decorator='', decorator='',
method_name='newContent', method_name=method_name,
method_args="self", # TODO method_args="self, attribute:str, default:Any=None",
return_type=subdocument_type, return_type=subdocument_type,
docstring=safe_docstring( docstring='...'))
getattr(temp_class.newContent, '__doc__', None))))
# getattr, getitem and other Zope.OFS alais returns an instance of allowed content types.
# so that portal.person_module['1'] is a person
for method_name in (
'__getattr__',
'__getitem__',
'_getOb',
'get',):
methods.append(
method_template_template.format(
decorator='',
method_name=method_name,
method_args="self, attribute:str",
return_type=subdocument_type,
docstring='...'))
# TODO not true for __of__(context) and asContent(**kw)
for identity_method in ( for identity_method in (
'getObject', 'getObject',
'asContext', 'asContext',
'__of__',): '__of__',
):
method = getattr(temp_class, identity_method, None) method = getattr(temp_class, identity_method, None)
if method is not None: if method is not None:
methods.append( methods.append(
method_template_template.format( method_template_template.format(
decorator='', decorator='',
method_name=identity_method, method_name=identity_method,
method_args="self", # TODO method_args="self",
return_type=safe_python_identifier(temp_class.__name__), return_type='"{}"'.format(
safe_python_identifier(temp_class.__name__)),
docstring=safe_docstring(getattr(method, '__doc__', None)))) docstring=safe_docstring(getattr(method, '__doc__', None))))
# the parent class is imported in a name that should not clash # the parent class is imported in a name that should not clash
...@@ -727,24 +1009,30 @@ def TypeInformation_getStub(self): ...@@ -727,24 +1009,30 @@ def TypeInformation_getStub(self):
base_classes.append(prefixed_class_name) base_classes.append(prefixed_class_name)
# Fake name for skins # Fake name for skins
prefixed_class_name = 'skins_tool_{}'.format( if XXX_skins_class_exists(pc.__name__):
safe_python_identifier(pc.__name__)) class_name = safe_python_identifier(pc.__name__)
imports.add( prefixed_class_name = 'skins_tool_{class_name}'.format(
'from erp5.skins_tool import {} as {}'.format( class_name=class_name)
safe_python_identifier(pc.__name__), prefixed_class_name)) imports.add(
base_classes.append(prefixed_class_name) '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 # everything can use ERP5Site_ skins
imports.add('from erp5.skins_tool import ERP5Site as skins_tool_ERP5Site') if 'skins_tool_ERP5Site' not in base_classes:
base_classes.append('skins_tool_ERP5Site') imports.add(
base_classes.append(prefixed_class_name) 'from erp5.skins_tool.ERP5Site import ERP5Site as skins_tool_ERP5Site')
base_classes.append('skins_tool_ERP5Site')
class_template = textwrap.dedent( class_template = textwrap.dedent(
"""\ """\
{header} {header}
{imports} {imports}
from {parent_class_module} import {parent_class} as {parent_class_alias} from {parent_class_module} import {parent_class} as {parent_class_alias}
class {class_name}({base_classes}): class {class_name}(
{base_classes}):
{docstring} {docstring}
{methods} {methods}
{debug} {debug}
...@@ -752,11 +1040,12 @@ def TypeInformation_getStub(self): ...@@ -752,11 +1040,12 @@ def TypeInformation_getStub(self):
docstring = textwrap.dedent( docstring = textwrap.dedent(
''' '''
# {type_title_or_id} ## [{type_title_or_id}](type_url)
--- ---
{type_description} {type_description}
{type_url}
''').format( ''').format(
type_title_or_id=self.getTitleOrId(), type_title_or_id=self.getTitleOrId(),
type_description=self.getDescription(), type_description=self.getDescription(),
...@@ -767,7 +1056,7 @@ def TypeInformation_getStub(self): ...@@ -767,7 +1056,7 @@ def TypeInformation_getStub(self):
header=header, header=header,
docstring=safe_docstring(docstring), docstring=safe_docstring(docstring),
class_name=safe_python_identifier(temp_class.__name__), class_name=safe_python_identifier(temp_class.__name__),
base_classes=', '.join(base_classes), base_classes=',\n '.join(base_classes),
parent_class=safe_python_identifier(parent_class.__name__), parent_class=safe_python_identifier(parent_class.__name__),
parent_class_alias=parent_class_alias, parent_class_alias=parent_class_alias,
parent_class_module=safe_python_identifier(parent_class_module), parent_class_module=safe_python_identifier(parent_class_module),
...@@ -800,7 +1089,6 @@ def PropertySheet_getStub(self): ...@@ -800,7 +1089,6 @@ def PropertySheet_getStub(self):
class_template = textwrap.dedent( class_template = textwrap.dedent(
"""\ """\
{imports}
class {class_name}: class {class_name}:
'''{property_sheet_id} '''{property_sheet_id}
...@@ -811,56 +1099,8 @@ def PropertySheet_getStub(self): ...@@ -811,56 +1099,8 @@ def PropertySheet_getStub(self):
""") """)
debug = '' debug = ''
methods = [] methods = []
imports = [
'from typing import Optional, List, Any',
'from DateTime import DateTime',
'from erp5.portal_type import Type_CatalogBrain',
'from erp5.portal_type import Type_AnyPortalType',
'from erp5.portal_type import Type_AnyPortalTypeList'
]
method_template_template = """ def {method_name}({method_args}) -> {return_type}:\n {docstring}"""
# debug method_template_template = """ def {method_name}({method_args}) -> {return_type}:\n {docstring}"""
methods.append(
method_template_template.format(
method_name='reveal_property_sheet_{}'.format(
safe_python_identifier(self.getId())),
method_args='self',
return_type='str',
docstring=safe_docstring("ahaha cool :)")))
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': 'List[str]',
'tokens': 'List[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 'List[{}]'.format(mapped_type)
return mapped_type
def _isMultiValuedProperty(prop):
# type: (erp5.portal_type.StandardProperty,) -> str
"""If this is a multi valued property, we have to generate list accessor.
"""
if prop.isMultivalued():
return True
return prop.getElementaryType() in ('lines', 'tokens')
from Products.ERP5Type.Utils import convertToUpperCase from Products.ERP5Type.Utils import convertToUpperCase
from Products.ERP5Type.Utils import evaluateExpressionFromString from Products.ERP5Type.Utils import evaluateExpressionFromString
...@@ -868,15 +1108,22 @@ def PropertySheet_getStub(self): ...@@ -868,15 +1108,22 @@ def PropertySheet_getStub(self):
expression_context = createExpressionContext(self) expression_context = createExpressionContext(self)
for prop in self.contentValues(): 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'): if prop.getPortalType() in ('Standard Property', 'Acquired Property'):
docstring = safe_docstring( docstring = safe_docstring(
textwrap.dedent( textwrap.dedent(
"""\ """\
[{property_sheet_title} {property_reference}]({property_url}) [{property_sheet_title} {property_reference}]({property_url})
{property_description} {property_description}
""").format( """).format(
property_description=prop.getDescription(), property_description=prop.getDescription(),
property_sheet_title=self.getTitle(), property_sheet_title=self.getTitle(),
property_reference=prop.getReference(), property_reference=prop.getReference(),
...@@ -920,6 +1167,9 @@ def PropertySheet_getStub(self): ...@@ -920,6 +1167,9 @@ def PropertySheet_getStub(self):
category_value = portal_categories._getOb(category, None) category_value = portal_categories._getOb(category, None)
if category_value is None: if category_value is None:
continue continue
# XXX size category clashes with size accessor from Data propertysheet
if category in ('size',):
continue
docstring = safe_docstring( docstring = safe_docstring(
textwrap.dedent( textwrap.dedent(
...@@ -966,14 +1216,14 @@ def PropertySheet_getStub(self): ...@@ -966,14 +1216,14 @@ def PropertySheet_getStub(self):
method_name='get{}Value'.format( method_name='get{}Value'.format(
convertToUpperCase(category_value.getId())), convertToUpperCase(category_value.getId())),
method_args='self', method_args='self',
return_type='Type_AnyPortalType', return_type='"erp5.portal_type.Type_AnyPortalType"',
docstring=docstring)) docstring=docstring))
methods.append( methods.append(
method_template_template.format( method_template_template.format(
method_name='get{}ValueList'.format( method_name='get{}ValueList'.format(
convertToUpperCase(category_value.getId())), convertToUpperCase(category_value.getId())),
method_args='self', method_args='self',
return_type='Type_AnyPortalTypeList', return_type='"erp5.portal_type.Type_AnyPortalTypeList"',
docstring=docstring)) docstring=docstring))
methods.append( methods.append(
method_template_template.format( method_template_template.format(
...@@ -986,19 +1236,18 @@ def PropertySheet_getStub(self): ...@@ -986,19 +1236,18 @@ def PropertySheet_getStub(self):
method_template_template.format( method_template_template.format(
method_name='set{}Value'.format( method_name='set{}Value'.format(
convertToUpperCase(category_value.getId())), convertToUpperCase(category_value.getId())),
method_args='self, value: Base', method_args='self, value: "erp5.portal_type.Type_AnyPortalType"',
return_type='None', return_type='None',
docstring=docstring)) docstring=docstring))
methods.append( methods.append(
method_template_template.format( method_template_template.format(
method_name='set{}ValueList'.format( method_name='set{}ValueList'.format(
convertToUpperCase(category_value.getId())), convertToUpperCase(category_value.getId())),
method_args='self, value_list: List[Base]', method_args='self, value_list: "erp5.portal_type.Type_AnyPortalTypeList"',
return_type='None', return_type='None',
docstring=docstring)) docstring=docstring))
return class_template.format( return class_template.format(
imports='\n'.join(imports),
class_name=safe_python_identifier(self.getId()), class_name=safe_python_identifier(self.getId()),
property_sheet_id=self.getId(), property_sheet_id=self.getId(),
property_sheet_description=self.getDescription().replace( property_sheet_description=self.getDescription().replace(
...@@ -1014,20 +1263,23 @@ def ERP5Site_getPortalStub(self): ...@@ -1014,20 +1263,23 @@ def ERP5Site_getPortalStub(self):
module_stub_template = textwrap.dedent( module_stub_template = textwrap.dedent(
''' '''
@property @property
def {module_id}(self): def {module_id}(self) -> '{module_class_name}':
from erp5.portal_type import {module_class_name} ...
return {module_class_name}() #return {module_class_name}()
''') ''')
tool_stub_template = textwrap.dedent( tool_stub_template = textwrap.dedent(
''' '''
@property @property
def {tool_id}(self): def {tool_id}(self) -> 'tool_{tool_id}_{tool_class}':
{tool_import} ...
return {tool_class}() #return tool_{tool_id}_{tool_class}()
''') ''')
source = [] source = []
imports = []
from Acquisition import aq_base
for m in self.objectValues(): for m in self.objectValues():
if m.getPortalType().endswith('Module'): if hasattr(aq_base(m), 'getPortalType'):
source.extend( source.extend(
module_stub_template.format( module_stub_template.format(
module_id=m.getId(), module_id=m.getId(),
...@@ -1036,33 +1288,40 @@ def ERP5Site_getPortalStub(self): ...@@ -1036,33 +1288,40 @@ def ERP5Site_getPortalStub(self):
else: else:
tool_class = safe_python_identifier(m.__class__.__name__) tool_class = safe_python_identifier(m.__class__.__name__)
tool_import = 'from {} import {}'.format( tool_import = 'from {tool_module} import {tool_class} as tool_{tool_id}_{tool_class}'.format(
m.__class__.__module__, tool_class) tool_module=m.__class__.__module__,
if m.getId() == 'portal_catalog': tool_class=tool_class,
tool_class = 'ICatalogTool' # XXX these I-prefix are stupid tool_id=m.getId(),
tool_import = 'from erp5.portal_type import ICatalogTool' )
elif m.getId() == 'portal_simulation': if 0:
tool_class = 'ISimulationTool' # XXX these I-prefix are stupid 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' tool_import = 'from erp5.portal_type import ISimulationTool'
imports.append(tool_import)
source.extend( source.extend(
tool_stub_template.format( tool_stub_template.format(
tool_id=m.getId(), tool_class=tool_class, tool_id=m.getId(),
tool_import=tool_import).splitlines()) tool_class=tool_class,
).splitlines())
# TODO: tools with at least base categories for CategoryTool # TODO: tools with at least base categories for CategoryTool
return textwrap.dedent( return textwrap.dedent(
''' '''
from Products.ERP5.ERP5Site import ERP5Site as ERP5Site_parent_ERP5Site from Products.ERP5.ERP5Site import ERP5Site as ERP5Site_parent_ERP5Site
from erp5.skins_tool import ERP5Site as skins_tool_ERP5Site from erp5.skins_tool.ERP5Site import ERP5Site as skins_tool_ERP5Site
from erp5.skins_tool import Base as skins_tool_Base from erp5.skins_tool.Base import Base as skins_tool_Base
{imports}
class ERP5Site(ERP5Site_parent_ERP5Site, skins_tool_ERP5Site, skins_tool_Base): class ERP5Site(ERP5Site_parent_ERP5Site, skins_tool_ERP5Site, skins_tool_Base):
{} {source}
def getPortalObject(self): def getPortalObject(self) -> 'ERP5Site':
return self return self
''').format('\n '.join(source))
''').format(
imports='\n'.join(imports), source='\n '.join(source))
def ERP5Site_dumpModuleCode(self, component_or_script=None): def ERP5Site_dumpModuleCode(self, component_or_script=None):
...@@ -1073,22 +1332,41 @@ def ERP5Site_dumpModuleCode(self, component_or_script=None): ...@@ -1073,22 +1332,41 @@ def ERP5Site_dumpModuleCode(self, component_or_script=None):
to files. to files.
""" """
def mkdir_p(path): def mkdir_p(path):
# type: (str) -> None
if not os.path.exists(path): if not os.path.exists(path):
os.mkdir(path, 0o700) 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() portal = self.getPortalObject()
module_dir = '/tmp/ahaha/erp5/' # TODO module_dir = '/tmp/ahaha/erp5/' # TODO
mkdir_p(module_dir) mkdir_p(module_dir)
# generate erp5/__init__.py # generate erp5/__init__.py
with open( # mypy wants __init__.pyi jedi wants __init__.py so we generate both
writeFile(
os.path.join(module_dir, '__init__.py'), os.path.join(module_dir, '__init__.py'),
'w',) as erp5__init__f: "# 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 ( for module in (
'portal_type', 'portal_type',
'accessor_holder', 'accessor_holder',
'skins_tool', 'skins_tool',
'component',): 'component',
):
erp5__init__f.write('from . import {module}\n'.format(module=module)) erp5__init__f.write('from . import {module}\n'.format(module=module))
mkdir_p(os.path.join(module_dir, module)) mkdir_p(os.path.join(module_dir, module))
if module == 'portal_type': if module == 'portal_type':
...@@ -1098,104 +1376,147 @@ def ERP5Site_dumpModuleCode(self, component_or_script=None): ...@@ -1098,104 +1376,147 @@ def ERP5Site_dumpModuleCode(self, component_or_script=None):
os.path.join( os.path.join(
module_dir, module_dir,
module, module,
'__init__.py',), '__init__.pyi',
'w',) as module_f: ),
'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(): for ti in portal.portal_types.contentValues():
class_name = safe_python_identifier(ti.getId()) class_name = safe_python_identifier(ti.getId())
all_portal_type_class_names.append(class_name) all_portal_type_class_names.append(class_name)
module_f.write( module_f.write(
'from .{class_name} import {class_name}\n'.format( '# from {class_name} import {class_name}\n'.format(
class_name=class_name)) class_name=class_name))
with open( try:
os.path.join( stub_code = ti.TypeInformation_getStub().encode('utf-8')
module_dir, except Exception as e:
module, logger.exception("Could not generate code for %s", ti.getId())
'{class_name}.pyi'.format(class_name=class_name), stub_code = """class {class_name}:\n {error}""".format(
), class_name=class_name,
'w', error=safe_docstring(
) as type_information_f: "Error trying to create {}: {} {}".format(
try: ti.getId(), e.__class__, e)))
stub_code = ti.TypeInformation_getStub().encode('utf-8') module_f.write(stub_code)
except Exception as e: if 0:
logger.exception("Could not generate code for %s", ti.getId()) with open(
stub_code = """class {class_name}:\n {error}""".format( os.path.join(
class_name=class_name, module_dir,
error=safe_docstring("Error trying to create {}: {} {}".format( module,
ti.getId(), '{class_name}.pyi'.format(class_name=class_name),
e.__class__, ),
e 'w',
)) ) as type_information_f:
) try:
type_information_f.write(stub_code) 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 ) # portal type groups ( useful ? used in Simulation Tool only )
portal_types_by_group = defaultdict(list) if 0:
for ti_for_group in portal.portal_types.contentValues(): portal_types_by_group = defaultdict(list)
for group in ti_for_group.getTypeGroupList(): for ti_for_group in portal.portal_types.contentValues():
portal_types_by_group[group].append( for group in ti_for_group.getTypeGroupList():
safe_python_identifier(ti_for_group.getId())) 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) for group, portal_type_class_list in portal_types_by_group.items():
module_f.write( group_class = 'Group_{}'.format(group)
'from .{} import {}\n'.format(group_class, group_class)) module_f.write(
with open( 'from {} import {}\n'.format(group_class, group_class))
os.path.join( with open(
module_dir, os.path.join(
module, module_dir,
'{}.pyi'.format(group_class),), module,
'w', '{}.pyi'.format(group_class),
) as group_f: ),
group_f.write( 'w',
textwrap.dedent( ) as group_f:
''' group_f.write(
{imports} textwrap.dedent(
class {group_class}({bases}): '''
"""All portal types of group {group}. import erp5.portal_type
""" class {group_class}({bases}):
''').format( """All portal types of group {group}.
imports='\n'.join( """
'from erp5.portal_type import {}'.format( ''').format(
portal_type_class) group_class=group_class,
for portal_type_class in portal_type_class_list), bases=', \n'.join(
group_class=group_class, 'erp5.portal_type.{}'.format(c)
bases=', '.join(portal_type_class_list), for c in portal_type_class_list),
group=group)) group=group))
# tools with extra type annotations # tools with extra type annotations
module_f.write('from .ICatalogTool import ICatalogTool\n') module_f.write('from ICatalogTool import ICatalogTool\n')
with open( with open(
os.path.join( os.path.join(
module_dir, module_dir,
module, module,
'ICatalogTool.pyi',), 'ICatalogTool.pyi',
'w',) as portal_f: ),
'w',
) as portal_f:
portal_f.write( portal_f.write(
textwrap.dedent( textwrap.dedent(
''' '''
from Products.ERP5Catalog.Tool.ERP5CatalogTool import ERP5CatalogTool from Products.ERP5Catalog.Tool.ERP5CatalogTool import ERP5CatalogTool
# XXX CatalogTool itself has a portal type # XXX CatalogTool itself has a portal type
from erp5.portal_type import Type_AnyPortalTypeCatalogBrainList # from erp5.portal_type import Type_AnyPortalTypeCatalogBrainList
from typing import Any
class ICatalogTool(ERP5CatalogTool): class ICatalogTool(ERP5CatalogTool):
def searchResults(self) -> Type_AnyPortalTypeCatalogBrainList: def searchResults(self) -> Any: #Type_AnyPortalTypeCatalogBrainList:
"""Search Catalog""" """Search Catalog"""
def __call__(self) -> Type_AnyPortalTypeCatalogBrainList: def __call__(self) -> Any: #Type_AnyPortalTypeCatalogBrainList:
"""Search Catalog""" """Search Catalog"""
''')) '''))
module_f.write('from .ISimulationTool import ISimulationTool\n') if 0: # TODO
with open( module_f.write('from ISimulationTool import ISimulationTool\n')
os.path.join( with open(
module_dir, os.path.join(
module, module_dir,
'ISimulationTool.pyi',), module,
'w',) as portal_f: 'ISimulationTool.pyi',
portal_f.write( ),
textwrap.dedent( 'w',
''' ) as portal_f:
portal_f.write(
textwrap.dedent(
'''
from erp5.portal_type import SimulationTool from erp5.portal_type import SimulationTool
from erp5.portal_type import Type_AnyPortalTypeInventoryListBrainList from erp5.portal_type import Type_AnyPortalTypeInventoryListBrainList
...@@ -1205,158 +1526,172 @@ def ERP5Site_dumpModuleCode(self, component_or_script=None): ...@@ -1205,158 +1526,172 @@ def ERP5Site_dumpModuleCode(self, component_or_script=None):
''')) '''))
# portal object # portal object
module_f.write('from .ERP5Site import ERP5Site\n') module_f.write('from ERP5Site import ERP5Site\n')
with open( with open(
os.path.join( os.path.join(
module_dir, module_dir,
module, module,
'ERP5Site.pyi',), 'ERP5Site.pyi',
'w',) as portal_f: ),
portal_f.write(ERP5Site_getPortalStub(self.getPortalObject())) 'w',
) as portal_f:
portal_f.write(ERP5Site_getPortalStub(self.getPortalObject()))
# some type helpers # some type helpers
module_f.write('from .Type_CatalogBrain import Type_CatalogBrain\n') if 0:
with open( module_f.write('from Type_CatalogBrain import Type_CatalogBrain\n')
os.path.join( with open(
module_dir, os.path.join(
module, module_dir,
'Type_CatalogBrain.pyi',), module,
'w',) as catalog_brain_f: 'Type_CatalogBrain.pyi',
catalog_brain_f.write( ),
textwrap.dedent( 'w',
''' ) as catalog_brain_f:
from typing import TypeVar, Generic catalog_brain_f.write(
textwrap.dedent(
T = TypeVar('T') '''
class Type_CatalogBrain(Generic[T]): from typing import TypeVar, Generic
id: str
path: str T = TypeVar('T')
def getObject(self) -> T: class Type_CatalogBrain(Generic[T]):
... id: str
''')) path: str
module_f.write( def getObject(self) -> T:
'from .Type_InventoryListBrain import Type_InventoryListBrain\n') ...
with open( '''))
os.path.join( module_f.write(
module_dir, 'from Type_InventoryListBrain import Type_InventoryListBrain\n')
module, with open(
'Type_InventoryListBrain.pyi',), os.path.join(
'w', module_dir,
) as catalog_brain_f: module,
catalog_brain_f.write( 'Type_InventoryListBrain.pyi',
textwrap.dedent( ),
''' 'w',
from typing import TypeVar, Generic ) as catalog_brain_f:
from erp5.component.extension.InventoryBrain import InventoryListBrain catalog_brain_f.write(
from DateTime import DateTime.DateTime as DateTime textwrap.dedent(
from erp5.portal_type import Group_node '''
from erp5.portal_type import Group_resource from typing import TypeVar, Generic
from erp5.component.extension.InventoryBrain import InventoryListBrain
T = TypeVar('T') from DateTime.DateTime import DateTime as DateTime
class Type_InventoryListBrain(Generic[T], InventoryListBrain): import erp5.portal_type
node_uid: int
mirror_node_uid: int T = TypeVar('T')
section_uid: int class Type_InventoryListBrain(Generic[T], InventoryListBrain):
mirror_section_uid: int node_uid: int
function_uid: int mirror_node_uid: int
project_uid: int section_uid: int
function_uid: int mirror_section_uid: int
funding_uid: int function_uid: int
ledger_uid: int project_uid: int
payment_request_uid: int funding_uid: int
ledger_uid: int
node_value: Group_node payment_request_uid: int
mirror_node_value: Group_node
section_value: Group_node node_value: 'erp5.portal_type.Organisation' # TODO
mirror_section_value: Group_node mirror_node_value: 'erp5.portal_type.Organisation'
resource_value: Group_resource section_value: 'erp5.portal_type.Organisation'
mirror_section_value: 'erp5.portal_type.Organisation'
date: DateTime resource_value: 'erp5.portal_type.Product' # TODO
mirror_date: DateTime
date: DateTime
variation_text: str mirror_date: DateTime
sub_variation_text: str
simulation_state: str variation_text: str
sub_variation_text: str
inventory: float simulation_state: str
total_price: float
inventory: float
path: str total_price: float
stock_uid: uid
def getObject(self) -> T: path: str
... stock_uid: int
def getObject(self) -> T:
''')) ...
module_f.write('from typing import List, Union\n') '''))
module_f.write('from typing import Sequence, Union\n')
module_f.write( module_f.write(
'Type_AnyPortalType = Union[\n {}]\n'.format( 'Type_AnyPortalType = Union[\n {}]\n'.format(
',\n '.join( ',\n '.join(
'{}'.format(portal_type_class) '{}'.format(portal_type_class)
for portal_type_class in all_portal_type_class_names), for portal_type_class in all_portal_type_class_names),))
)) # TODO: Union[Sequence] or Sequence[Union] ?
module_f.write( module_f.write(
'Type_AnyPortalTypeList = Union[\n {}]\n'.format( 'Type_AnyPortalTypeList = Union[\n {}]\n'.format(
',\n '.join( ',\n '.join(
'List[{}]'.format(portal_type_class) 'Sequence[{}]'.format(portal_type_class)
for portal_type_class in all_portal_type_class_names))) for portal_type_class in all_portal_type_class_names)))
module_f.write( if 0:
'Type_AnyPortalTypeCatalogBrainList = Union[\n {}]\n'.format( module_f.write(
',\n '.join( 'Type_AnyPortalTypeCatalogBrainList = Union[\n {}]\n'.format(
'List[Type_CatalogBrain[{}]]'.format(portal_type_class) ',\n '.join(
for portal_type_class in all_portal_type_class_names), 'List[Type_CatalogBrain[{}]]'.format(portal_type_class)
)) for portal_type_class in all_portal_type_class_names),))
module_f.write( module_f.write(
'Type_AnyPortalTypeInventoryListBrainList = Union[\n {}]\n' 'Type_AnyPortalTypeInventoryListBrainList = Union[\n {}]\n'
.format( .format(
',\n '.join( ',\n '.join(
'List[Type_InventoryListBrain[{}]]'.format( 'List[Type_InventoryListBrain[{}]]'.format(
portal_type_class) portal_type_class)
for portal_type_class in all_portal_type_class_names), for portal_type_class in all_portal_type_class_names),))
))
elif module == 'accessor_holder': elif module == 'accessor_holder':
# TODO: real path is accessor_holder.something !? # TODO: real path is accessor_holder.something !?
with open( with open(
os.path.join(module_dir, module, '__init__.py'), os.path.join(module_dir, module, '__init__.pyi'),
'w',) as accessor_holder_f: '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(): for ps in portal.portal_property_sheets.contentValues():
class_name = safe_python_identifier(ps.getId()) class_name = safe_python_identifier(ps.getId())
accessor_holder_f.write( accessor_holder_f.write(ps.PropertySheet_getStub().encode('utf-8'))
'from .{class_name} import {class_name}\n'.format( if 0:
class_name=class_name)) with open(
with open( os.path.join(
os.path.join( module_dir,
module_dir, module,
module, '{class_name}.pyi'.format(class_name=class_name),
'{class_name}.pyi'.format(class_name=class_name), ),
), 'w',
'w', ) as property_sheet_f:
) as property_sheet_f: property_sheet_f.write(
property_sheet_f.write(ps.PropertySheet_getStub().encode('utf-8')) ps.PropertySheet_getStub().encode('utf-8'))
elif module == 'skins_tool': elif module == 'skins_tool':
skins_tool = portal.portal_skins skins_tool = portal.portal_skins
with open( with open(
os.path.join(module_dir, module, '__init__.py'), os.path.join(module_dir, module, '__init__.pyi'),
'w',) as skins_tool_f: 'w',
) as skins_tool_f:
skins_tool_f.write("# coding: utf-8\n")
for class_name in SkinsTool_getClassSet(skins_tool): for class_name in SkinsTool_getClassSet(skins_tool):
skins_tool_f.write( skins_tool_f.write(
'from {class_name} import {class_name}\n'.format( 'from {class_name} import {class_name}\n'.format(
class_name=class_name)) class_name=class_name))
with open( writeFile(
os.path.join( os.path.join(
module_dir, module_dir,
module, module,
'{}.pyi'.format(class_name),), '{}.pyi'.format(class_name),
'w', ),
) as skin_f: SkinsTool_getStubForClass(
skin_f.write( skins_tool,
SkinsTool_getStubForClass( class_name,
skins_tool, ).encode('utf-8'))
class_name,).encode('utf-8'))
elif module == 'component': elif module == 'component':
# TODO: component versions ?
module_to_component_portal_type_mapping = { module_to_component_portal_type_mapping = {
'test': 'Test Component', 'test': 'Test Component',
'document': 'Document Component', 'document': 'Document Component',
...@@ -1367,11 +1702,21 @@ def ERP5Site_dumpModuleCode(self, component_or_script=None): ...@@ -1367,11 +1702,21 @@ def ERP5Site_dumpModuleCode(self, component_or_script=None):
} }
with open( with open(
os.path.join(module_dir, module, '__init__.py'), os.path.join(module_dir, module, '__init__.py'),
'w',) as component_module__init__f: 'w',
for sub_module, portal_type in module_to_component_portal_type_mapping.items(): ) as component_module__init__f:
for sub_module, portal_type in module_to_component_portal_type_mapping.items(
):
component_module__init__f.write( component_module__init__f.write(
'from . import {}\n'.format(sub_module)) 'from . import {}\n'.format(sub_module))
mkdir_p(os.path.join(module_dir, module, 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( with open(
os.path.join(module_dir, module, sub_module, '__init__.py'), os.path.join(module_dir, module, sub_module, '__init__.py'),
'w', 'w',
...@@ -1379,17 +1724,32 @@ def ERP5Site_dumpModuleCode(self, component_or_script=None): ...@@ -1379,17 +1724,32 @@ def ERP5Site_dumpModuleCode(self, component_or_script=None):
for brain in portal.portal_catalog( for brain in portal.portal_catalog(
portal_type=portal_type, validation_state=('validated',)): portal_type=portal_type, validation_state=('validated',)):
component = brain.getObject() component = brain.getObject()
# TODO write __init__ for erp5_version as well
component_sub_module_init_f.write( component_sub_module_init_f.write(
"from {component_reference} import {component_reference}\n" "from {component_reference} import {component_reference}\n"
.format(component_reference=component.getReference())) .format(component_reference=component.getReference()))
with open( writeFile(
os.path.join( os.path.join(
module_dir, module_dir,
module, module,
sub_module, sub_module,
'{}.py'.format(component.getReference()), '{}.py'.format(component.getReference()),
), ), component.getTextContent())
'w', writeFile(
) as component_f: os.path.join(
component_f.write(component.getTextContent()) #.encode('utf-8')) 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' return 'done'
...@@ -100,24 +100,28 @@ ...@@ -100,24 +100,28 @@
</record> </record>
<record id="4" aka="AAAAAAAAAAQ="> <record id="4" aka="AAAAAAAAAAQ=">
<pickle> <pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/> <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle> </pickle>
<pickle> <pickle>
<tuple> <dictionary>
<none/> <item>
<list> <key> <string>_log</string> </key>
<dictionary> <value>
<item> <list>
<key> <string>action</string> </key> <dictionary>
<value> <string>validate</string> </value> <item>
</item> <key> <string>action</string> </key>
<item> <value> <string>validate</string> </value>
<key> <string>validation_state</string> </key> </item>
<value> <string>validated</string> </value> <item>
</item> <key> <string>validation_state</string> </key>
</dictionary> <value> <string>validated</string> </value>
</list> </item>
</tuple> </dictionary>
</list>
</value>
</item>
</dictionary>
</pickle> </pickle>
</record> </record>
</ZopeData> </ZopeData>
...@@ -2,6 +2,9 @@ from yapf.yapflib import yapf_api ...@@ -2,6 +2,9 @@ from yapf.yapflib import yapf_api
import json import json
import tempfile import tempfile
import textwrap import textwrap
import logging
logger = logging.getLogger(__name__)
def ERP5Site_formatPythonSourceCode(self, data, REQUEST=None): def ERP5Site_formatPythonSourceCode(self, data, REQUEST=None):
...@@ -31,6 +34,7 @@ def ERP5Site_formatPythonSourceCode(self, data, REQUEST=None): ...@@ -31,6 +34,7 @@ def ERP5Site_formatPythonSourceCode(self, data, REQUEST=None):
formatted_code, changed = yapf_api.FormatCode( formatted_code, changed = yapf_api.FormatCode(
data['code'], style_config=f.name, **extra) data['code'], style_config=f.name, **extra)
except SyntaxError as e: except SyntaxError as e:
logger.exception("Error in source code")
return json.dumps(dict(error=True, error_line=e.lineno)) return json.dumps(dict(error=True, error_line=e.lineno))
if REQUEST is not None: if REQUEST is not None:
......
...@@ -100,24 +100,28 @@ ...@@ -100,24 +100,28 @@
</record> </record>
<record id="4" aka="AAAAAAAAAAQ="> <record id="4" aka="AAAAAAAAAAQ=">
<pickle> <pickle>
<global name="WorkflowHistoryList" module="Products.ERP5Type.patches.WorkflowTool"/> <global name="WorkflowHistoryList" module="Products.ERP5Type.Workflow"/>
</pickle> </pickle>
<pickle> <pickle>
<tuple> <dictionary>
<none/> <item>
<list> <key> <string>_log</string> </key>
<dictionary> <value>
<item> <list>
<key> <string>action</string> </key> <dictionary>
<value> <string>validate</string> </value> <item>
</item> <key> <string>action</string> </key>
<item> <value> <string>validate</string> </value>
<key> <string>validation_state</string> </key> </item>
<value> <string>validated</string> </value> <item>
</item> <key> <string>validation_state</string> </key>
</dictionary> <value> <string>validated</string> </value>
</list> </item>
</tuple> </dictionary>
</list>
</value>
</item>
</dictionary>
</pickle> </pickle>
</record> </record>
</ZopeData> </ZopeData>
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