Source code for pyams_zodbbrowser.value

#
# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#

__docformat__ = 'restructuredtext'


# import standard library
import logging
import itertools
import collections
import re
from html import escape

# import interfaces
from pyams_zodbbrowser.interfaces import IValueRenderer, IObjectHistory

# import packages
from persistent import Persistent
from persistent.dict import PersistentDict
from persistent.list import PersistentList
from persistent.mapping import PersistentMapping
from pyams_utils.adapter import adapter_config
from ZODB.utils import u64, oid_repr
from zope.interface import implementer, Interface
from zope.interface.declarations import ProvidesClass


log = logging.getLogger(__name__)


MAX_CACHE_SIZE = 1000
TRUNCATIONS = {}
TRUNCATIONS_IN_ORDER = collections.deque()
next_id = itertools.count(1).__next__


[docs]def resetTruncations(): # for tests only! global next_id next_id = itertools.count(1).__next__ TRUNCATIONS.clear() TRUNCATIONS_IN_ORDER.clear()
[docs]def pruneTruncations(): while len(TRUNCATIONS_IN_ORDER) > MAX_CACHE_SIZE: del TRUNCATIONS[TRUNCATIONS_IN_ORDER.popleft()]
[docs]def truncate(text): id = 'tr%d' % next_id() TRUNCATIONS[id] = text TRUNCATIONS_IN_ORDER.append(id) return id
[docs]@adapter_config(context=Interface, provides=IValueRenderer) @implementer(IValueRenderer) class GenericValue(object): """Default value renderer. Uses the object's __repr__, truncating if too long. """ def __init__(self, context): self.context = context def _repr(self): # hook for subclasses if getattr(self.context.__class__, '__repr__', None) is object.__repr__: # Special-case objects with the default __repr__ (LP#1087138) if isinstance(self.context, Persistent): return '<%s.%s with oid %s>' % ( self.context.__class__.__module__, self.context.__class__.__name__, oid_repr(self.context._p_oid)) try: return repr(self.context) except Exception: try: return '<unrepresentable %s>' % self.context.__class__.__name__ except Exception: return '<unrepresentable>'
[docs] def render(self, tid=None, can_link=True, limit=200): text = self._repr() if len(text) > limit: id = truncate(text[limit:]) text = '%s<span id="%s" class="truncated">...</span>' % ( escape(text[:limit]), id) else: text = escape(text) if not isinstance(self.context, str): try: n = len(self.context) except Exception: pass else: if n == 1: # this is a crime against i18n, but oh well text += ' (%d item)' % n else: text += ' (%d items)' % n return text
[docs]def join_with_commas(html, open, close): """Helper to join multiple html snippets into a struct.""" prefix = open + '<span class="struct">' suffix = '</span>' for n, item in enumerate(html): if n == len(html) - 1: trailer = close else: trailer = ',' if item.endswith(suffix): item = item[:-len(suffix)] + trailer + suffix else: item += trailer html[n] = item return prefix + '<br />'.join(html) + suffix
[docs]@adapter_config(context=str, provides=IValueRenderer) class StringValue(GenericValue): """String renderer.""" def __init__(self, context): self.context = context
[docs] def render(self, tid=None, can_link=True, limit=200, threshold=4): if self.context.count('\n') <= threshold: return GenericValue.render(self, tid, can_link=can_link, limit=limit) else: if isinstance(self.context, str): prefix = 'u' context = self.context else: prefix = '' context = self.context.decode('latin-1').encode('ascii', 'backslashreplace') lines = [re.sub(r'^[ \t]+', lambda m: '&nbsp;' * len(m.group(0).expandtabs()), escape(line)) for line in context.splitlines()] nl = '<br />' # hm, maybe '\\n<br />'? if sum(map(len, lines)) > limit: head = nl.join(lines[:5]) tail = nl.join(lines[5:]) id = truncate(tail) return (prefix + "'<span class=\"struct\">" + head + nl + '<span id="%s" class="truncated">...</span>' % id + "'</span>") else: return (prefix + "'<span class=\"struct\">" + nl.join(lines) + "'</span>")
[docs]@adapter_config(context=tuple, provides=IValueRenderer) @implementer(IValueRenderer) class TupleValue(object): """Tuple renderer.""" def __init__(self, context): self.context = context
[docs] def render(self, tid=None, can_link=True, threshold=100): html = [] for item in self.context: html.append(IValueRenderer(item).render(tid, can_link)) if len(html) == 1: html.append('') # (item) -> (item, ) result = '(%s)' % ', '.join(html) if len(result) > threshold or '<span class="struct">' in result: if len(html) == 2 and html[1] == '': return join_with_commas(html[:1], '(', ', )') else: return join_with_commas(html, '(', ')') return result
[docs]@adapter_config(context=list, provides=IValueRenderer) @implementer(IValueRenderer) class ListValue(object): """List renderer.""" def __init__(self, context): self.context = context
[docs] def render(self, tid=None, can_link=True, threshold=100): html = [] for item in self.context: html.append(IValueRenderer(item).render(tid, can_link)) result = '[%s]' % ', '.join(html) if len(result) > threshold or '<span class="struct">' in result: return join_with_commas(html, '[', ']') return result
[docs]@adapter_config(context=dict, provides=IValueRenderer) @implementer(IValueRenderer) class DictValue(object): """Dict renderer.""" def __init__(self, context): self.context = context
[docs] def render(self, tid=None, can_link=True, threshold=100): html = [] for key, value in sorted(self.context.items()): html.append(IValueRenderer(key).render(tid, can_link) + ': ' + IValueRenderer(value).render(tid, can_link)) if (sum(map(len, html)) < threshold and '<span class="struct">' not in ''.join(html)): return '{%s}' % ', '.join(html) else: return join_with_commas(html, '{', '}')
[docs]@adapter_config(context=Persistent, provides=IValueRenderer) @implementer(IValueRenderer) class PersistentValue(object): """Persistent object renderer. Uses __repr__ and makes it a hyperlink to the actual object. """ view_name = '#zodbbrowser' delegate_to = GenericValue def __init__(self, context): self.context = context
[docs] def render(self, tid=None, can_link=True): obj = self.context url = '%s?oid=0x%x' % (self.view_name, u64(self.context._p_oid)) if tid is not None: url += "&tid=%d" % u64(tid) try: oldstate = IObjectHistory(self.context).loadState(tid) clone = self.context.__class__.__new__(self.context.__class__) clone.__setstate__(oldstate) clone._p_oid = self.context._p_oid obj = clone except Exception: log.debug('Could not load old state for %s 0x%x', self.context.__class__, u64(self.context._p_oid)) value = self.delegate_to(obj).render(tid, can_link=False) if can_link: return '<a class="objlink" href="%s">%s</a>' % (escape(url), value) else: return value
[docs]@adapter_config(context=PersistentMapping, provides=IValueRenderer) class PersistentMappingValue(PersistentValue): delegate_to = DictValue
[docs]@adapter_config(context=PersistentList, provides=IValueRenderer) class PersistentListValue(PersistentValue): delegate_to = ListValue
if PersistentMapping is PersistentDict: # ZODB 3.9 deprecated PersistentDict and made it an alias for # PersistentMapping. I don't know a clean way to conditionally disable the # <adapter> directive in ZCML to avoid conflicting configuration actions, # therefore I'll register a decoy adapter registered for a decoy class. # This adapter will never get used.
[docs] class DecoyPersistentDict(PersistentMapping): """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9."""
@adapter_config(context=DecoyPersistentDict, provides=IValueRenderer) class PersistentDictValue(PersistentValue): """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9.""" delegate_to = DictValue else:
[docs] @adapter_config(context=PersistentDict, provides=IValueRenderer) class PersistentDictValue(PersistentValue): delegate_to = DictValue
[docs]@adapter_config(context=ProvidesClass, provides=IValueRenderer) class ProvidesValue(GenericValue): """zope.interface.Provides object renderer. The __repr__ of zope.interface.Provides is decidedly unhelpful. """ def _repr(self): return '<Provides: %s>' % ', '.join(i.__identifier__ for i in self.context._Provides__args[1:])