src/pyams_zodbbrowser/value.py
changeset 0 a02202f95e2c
equal deleted inserted replaced
-1:000000000000 0:a02202f95e2c
       
     1 #
       
     2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
       
     3 # All Rights Reserved.
       
     4 #
       
     5 # This software is subject to the provisions of the Zope Public License,
       
     6 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
       
     7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
       
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
       
    10 # FOR A PARTICULAR PURPOSE.
       
    11 #
       
    12 
       
    13 __docformat__ = 'restructuredtext'
       
    14 
       
    15 
       
    16 # import standard library
       
    17 import logging
       
    18 import itertools
       
    19 import collections
       
    20 import re
       
    21 from html import escape
       
    22 
       
    23 # import interfaces
       
    24 from pyams_zodbbrowser.interfaces import IValueRenderer, IObjectHistory
       
    25 
       
    26 # import packages
       
    27 from persistent import Persistent
       
    28 from persistent.dict import PersistentDict
       
    29 from persistent.list import PersistentList
       
    30 from persistent.mapping import PersistentMapping
       
    31 from pyams_utils.adapter import adapter_config
       
    32 from ZODB.utils import u64, oid_repr
       
    33 from zope.interface import implementer, Interface
       
    34 from zope.interface.declarations import ProvidesClass
       
    35 
       
    36 
       
    37 log = logging.getLogger(__name__)
       
    38 
       
    39 
       
    40 MAX_CACHE_SIZE = 1000
       
    41 TRUNCATIONS = {}
       
    42 TRUNCATIONS_IN_ORDER = collections.deque()
       
    43 next_id = itertools.count(1).__next__
       
    44 
       
    45 
       
    46 def resetTruncations():  # for tests only!
       
    47     global next_id
       
    48     next_id = itertools.count(1).__next__
       
    49     TRUNCATIONS.clear()
       
    50     TRUNCATIONS_IN_ORDER.clear()
       
    51 
       
    52 
       
    53 def pruneTruncations():
       
    54     while len(TRUNCATIONS_IN_ORDER) > MAX_CACHE_SIZE:
       
    55         del TRUNCATIONS[TRUNCATIONS_IN_ORDER.popleft()]
       
    56 
       
    57 
       
    58 def truncate(text):
       
    59     id = 'tr%d' % next_id()
       
    60     TRUNCATIONS[id] = text
       
    61     TRUNCATIONS_IN_ORDER.append(id)
       
    62     return id
       
    63 
       
    64 
       
    65 @adapter_config(context=Interface, provides=IValueRenderer)
       
    66 @implementer(IValueRenderer)
       
    67 class GenericValue(object):
       
    68     """Default value renderer.
       
    69 
       
    70     Uses the object's __repr__, truncating if too long.
       
    71     """
       
    72 
       
    73     def __init__(self, context):
       
    74         self.context = context
       
    75 
       
    76     def _repr(self):
       
    77         # hook for subclasses
       
    78         if getattr(self.context.__class__, '__repr__', None) is object.__repr__:
       
    79             # Special-case objects with the default __repr__ (LP#1087138)
       
    80             if isinstance(self.context, Persistent):
       
    81                 return '<%s.%s with oid %s>' % (
       
    82                     self.context.__class__.__module__,
       
    83                     self.context.__class__.__name__,
       
    84                     oid_repr(self.context._p_oid))
       
    85         try:
       
    86             return repr(self.context)
       
    87         except Exception:
       
    88             try:
       
    89                 return '<unrepresentable %s>' % self.context.__class__.__name__
       
    90             except Exception:
       
    91                 return '<unrepresentable>'
       
    92 
       
    93     def render(self, tid=None, can_link=True, limit=200):
       
    94         text = self._repr()
       
    95         if len(text) > limit:
       
    96             id = truncate(text[limit:])
       
    97             text = '%s<span id="%s" class="truncated">...</span>' % (
       
    98                 escape(text[:limit]), id)
       
    99         else:
       
   100             text = escape(text)
       
   101         if not isinstance(self.context, str):
       
   102             try:
       
   103                 n = len(self.context)
       
   104             except Exception:
       
   105                 pass
       
   106             else:
       
   107                 if n == 1: # this is a crime against i18n, but oh well
       
   108                     text += ' (%d item)' % n
       
   109                 else:
       
   110                     text += ' (%d items)' % n
       
   111         return text
       
   112 
       
   113 
       
   114 def join_with_commas(html, open, close):
       
   115     """Helper to join multiple html snippets into a struct."""
       
   116     prefix = open + '<span class="struct">'
       
   117     suffix = '</span>'
       
   118     for n, item in enumerate(html):
       
   119         if n == len(html) - 1:
       
   120             trailer = close
       
   121         else:
       
   122             trailer = ','
       
   123         if item.endswith(suffix):
       
   124             item = item[:-len(suffix)] + trailer + suffix
       
   125         else:
       
   126             item += trailer
       
   127         html[n] = item
       
   128     return prefix + '<br />'.join(html) + suffix
       
   129 
       
   130 
       
   131 @adapter_config(context=str, provides=IValueRenderer)
       
   132 class StringValue(GenericValue):
       
   133     """String renderer."""
       
   134 
       
   135     def __init__(self, context):
       
   136         self.context = context
       
   137 
       
   138     def render(self, tid=None, can_link=True, limit=200, threshold=4):
       
   139         if self.context.count('\n') <= threshold:
       
   140             return GenericValue.render(self, tid, can_link=can_link,
       
   141                                        limit=limit)
       
   142         else:
       
   143             if isinstance(self.context, str):
       
   144                 prefix = 'u'
       
   145                 context = self.context
       
   146             else:
       
   147                 prefix = ''
       
   148                 context = self.context.decode('latin-1').encode('ascii',
       
   149                                                             'backslashreplace')
       
   150             lines = [re.sub(r'^[ \t]+',
       
   151                             lambda m: '&nbsp;' * len(m.group(0).expandtabs()),
       
   152                             escape(line))
       
   153                      for line in context.splitlines()]
       
   154             nl = '<br />' # hm, maybe '\\n<br />'?
       
   155             if sum(map(len, lines)) > limit:
       
   156                 head = nl.join(lines[:5])
       
   157                 tail = nl.join(lines[5:])
       
   158                 id = truncate(tail)
       
   159                 return (prefix + "'<span class=\"struct\">" + head + nl
       
   160                         + '<span id="%s" class="truncated">...</span>' % id
       
   161                         + "'</span>")
       
   162             else:
       
   163                 return (prefix + "'<span class=\"struct\">" + nl.join(lines)
       
   164                         + "'</span>")
       
   165 
       
   166 
       
   167 @adapter_config(context=tuple, provides=IValueRenderer)
       
   168 @implementer(IValueRenderer)
       
   169 class TupleValue(object):
       
   170     """Tuple renderer."""
       
   171 
       
   172     def __init__(self, context):
       
   173         self.context = context
       
   174 
       
   175     def render(self, tid=None, can_link=True, threshold=100):
       
   176         html = []
       
   177         for item in self.context:
       
   178             html.append(IValueRenderer(item).render(tid, can_link))
       
   179         if len(html) == 1:
       
   180             html.append('')  # (item) -> (item, )
       
   181         result = '(%s)' % ', '.join(html)
       
   182         if len(result) > threshold or '<span class="struct">' in result:
       
   183             if len(html) == 2 and html[1] == '':
       
   184                 return join_with_commas(html[:1], '(', ', )')
       
   185             else:
       
   186                 return join_with_commas(html, '(', ')')
       
   187         return result
       
   188 
       
   189 
       
   190 @adapter_config(context=list, provides=IValueRenderer)
       
   191 @implementer(IValueRenderer)
       
   192 class ListValue(object):
       
   193     """List renderer."""
       
   194 
       
   195     def __init__(self, context):
       
   196         self.context = context
       
   197 
       
   198     def render(self, tid=None, can_link=True, threshold=100):
       
   199         html = []
       
   200         for item in self.context:
       
   201             html.append(IValueRenderer(item).render(tid, can_link))
       
   202         result = '[%s]' % ', '.join(html)
       
   203         if len(result) > threshold or '<span class="struct">' in result:
       
   204             return join_with_commas(html, '[', ']')
       
   205         return result
       
   206 
       
   207 
       
   208 @adapter_config(context=dict, provides=IValueRenderer)
       
   209 @implementer(IValueRenderer)
       
   210 class DictValue(object):
       
   211     """Dict renderer."""
       
   212 
       
   213     def __init__(self, context):
       
   214         self.context = context
       
   215 
       
   216     def render(self, tid=None, can_link=True, threshold=100):
       
   217         html = []
       
   218         for key, value in sorted(self.context.items()):
       
   219             html.append(IValueRenderer(key).render(tid, can_link) + ': ' +
       
   220                         IValueRenderer(value).render(tid, can_link))
       
   221         if (sum(map(len, html)) < threshold and
       
   222                 '<span class="struct">' not in ''.join(html)):
       
   223             return '{%s}' % ', '.join(html)
       
   224         else:
       
   225             return join_with_commas(html, '{', '}')
       
   226 
       
   227 
       
   228 @adapter_config(context=Persistent, provides=IValueRenderer)
       
   229 @implementer(IValueRenderer)
       
   230 class PersistentValue(object):
       
   231     """Persistent object renderer.
       
   232 
       
   233     Uses __repr__ and makes it a hyperlink to the actual object.
       
   234     """
       
   235 
       
   236     view_name = '#zodbbrowser'
       
   237     delegate_to = GenericValue
       
   238 
       
   239     def __init__(self, context):
       
   240         self.context = context
       
   241 
       
   242     def render(self, tid=None, can_link=True):
       
   243         obj = self.context
       
   244         url = '%s?oid=0x%x' % (self.view_name, u64(self.context._p_oid))
       
   245         if tid is not None:
       
   246             url += "&tid=%d" % u64(tid)
       
   247             try:
       
   248                 oldstate = IObjectHistory(self.context).loadState(tid)
       
   249                 clone = self.context.__class__.__new__(self.context.__class__)
       
   250                 clone.__setstate__(oldstate)
       
   251                 clone._p_oid = self.context._p_oid
       
   252                 obj = clone
       
   253             except Exception:
       
   254                 log.debug('Could not load old state for %s 0x%x',
       
   255                           self.context.__class__, u64(self.context._p_oid))
       
   256         value = self.delegate_to(obj).render(tid, can_link=False)
       
   257         if can_link:
       
   258             return '<a class="objlink" href="%s">%s</a>' % (escape(url), value)
       
   259         else:
       
   260             return value
       
   261 
       
   262 
       
   263 @adapter_config(context=PersistentMapping, provides=IValueRenderer)
       
   264 class PersistentMappingValue(PersistentValue):
       
   265     delegate_to = DictValue
       
   266 
       
   267 
       
   268 @adapter_config(context=PersistentList, provides=IValueRenderer)
       
   269 class PersistentListValue(PersistentValue):
       
   270     delegate_to = ListValue
       
   271 
       
   272 
       
   273 if PersistentMapping is PersistentDict:
       
   274     # ZODB 3.9 deprecated PersistentDict and made it an alias for
       
   275     # PersistentMapping.  I don't know a clean way to conditionally disable the
       
   276     # <adapter> directive in ZCML to avoid conflicting configuration actions,
       
   277     # therefore I'll register a decoy adapter registered for a decoy class.
       
   278     # This adapter will never get used.
       
   279 
       
   280     class DecoyPersistentDict(PersistentMapping):
       
   281         """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9."""
       
   282 
       
   283     @adapter_config(context=DecoyPersistentDict, provides=IValueRenderer)
       
   284     class PersistentDictValue(PersistentValue):
       
   285         """Decoy to avoid ZCML errors while supporting both ZODB 3.8 and 3.9."""
       
   286         delegate_to = DictValue
       
   287 
       
   288 else:
       
   289     @adapter_config(context=PersistentDict, provides=IValueRenderer)
       
   290     class PersistentDictValue(PersistentValue):
       
   291         delegate_to = DictValue
       
   292 
       
   293 
       
   294 @adapter_config(context=ProvidesClass, provides=IValueRenderer)
       
   295 class ProvidesValue(GenericValue):
       
   296     """zope.interface.Provides object renderer.
       
   297 
       
   298     The __repr__ of zope.interface.Provides is decidedly unhelpful.
       
   299     """
       
   300 
       
   301     def _repr(self):
       
   302         return '<Provides: %s>' % ', '.join(i.__identifier__
       
   303                                             for i in self.context._Provides__args[1:])
       
   304