1# mako/util.py
2# Copyright (C) 2006-2015 the Mako authors and contributors <see AUTHORS file>
3#
4# This module is part of Mako and is released under
5# the MIT License: http://www.opensource.org/licenses/mit-license.php
6
7import re
8import collections
9import codecs
10import os
11from mako import compat
12import operator
13
14def update_wrapper(decorated, fn):
15    decorated.__wrapped__ = fn
16    decorated.__name__ = fn.__name__
17    return decorated
18
19
20class PluginLoader(object):
21    def __init__(self, group):
22        self.group = group
23        self.impls = {}
24
25    def load(self, name):
26        if name in self.impls:
27            return self.impls[name]()
28        else:
29            import pkg_resources
30            for impl in pkg_resources.iter_entry_points(
31                                self.group,
32                                name):
33                self.impls[name] = impl.load
34                return impl.load()
35            else:
36                from mako import exceptions
37                raise exceptions.RuntimeException(
38                        "Can't load plugin %s %s" %
39                        (self.group, name))
40
41    def register(self, name, modulepath, objname):
42        def load():
43            mod = __import__(modulepath)
44            for token in modulepath.split(".")[1:]:
45                mod = getattr(mod, token)
46            return getattr(mod, objname)
47        self.impls[name] = load
48
49def verify_directory(dir):
50    """create and/or verify a filesystem directory."""
51
52    tries = 0
53
54    while not os.path.exists(dir):
55        try:
56            tries += 1
57            os.makedirs(dir, compat.octal("0775"))
58        except:
59            if tries > 5:
60                raise
61
62def to_list(x, default=None):
63    if x is None:
64        return default
65    if not isinstance(x, (list, tuple)):
66        return [x]
67    else:
68        return x
69
70
71class memoized_property(object):
72    """A read-only @property that is only evaluated once."""
73    def __init__(self, fget, doc=None):
74        self.fget = fget
75        self.__doc__ = doc or fget.__doc__
76        self.__name__ = fget.__name__
77
78    def __get__(self, obj, cls):
79        if obj is None:
80            return self
81        obj.__dict__[self.__name__] = result = self.fget(obj)
82        return result
83
84class memoized_instancemethod(object):
85    """Decorate a method memoize its return value.
86
87    Best applied to no-arg methods: memoization is not sensitive to
88    argument values, and will always return the same value even when
89    called with different arguments.
90
91    """
92    def __init__(self, fget, doc=None):
93        self.fget = fget
94        self.__doc__ = doc or fget.__doc__
95        self.__name__ = fget.__name__
96
97    def __get__(self, obj, cls):
98        if obj is None:
99            return self
100        def oneshot(*args, **kw):
101            result = self.fget(obj, *args, **kw)
102            memo = lambda *a, **kw: result
103            memo.__name__ = self.__name__
104            memo.__doc__ = self.__doc__
105            obj.__dict__[self.__name__] = memo
106            return result
107        oneshot.__name__ = self.__name__
108        oneshot.__doc__ = self.__doc__
109        return oneshot
110
111class SetLikeDict(dict):
112    """a dictionary that has some setlike methods on it"""
113    def union(self, other):
114        """produce a 'union' of this dict and another (at the key level).
115
116        values in the second dict take precedence over that of the first"""
117        x = SetLikeDict(**self)
118        x.update(other)
119        return x
120
121class FastEncodingBuffer(object):
122    """a very rudimentary buffer that is faster than StringIO,
123    but doesn't crash on unicode data like cStringIO."""
124
125    def __init__(self, encoding=None, errors='strict', as_unicode=False):
126        self.data = collections.deque()
127        self.encoding = encoding
128        if as_unicode:
129            self.delim = compat.u('')
130        else:
131            self.delim = ''
132        self.as_unicode = as_unicode
133        self.errors = errors
134        self.write = self.data.append
135
136    def truncate(self):
137        self.data = collections.deque()
138        self.write = self.data.append
139
140    def getvalue(self):
141        if self.encoding:
142            return self.delim.join(self.data).encode(self.encoding,
143                                                     self.errors)
144        else:
145            return self.delim.join(self.data)
146
147class LRUCache(dict):
148    """A dictionary-like object that stores a limited number of items,
149    discarding lesser used items periodically.
150
151    this is a rewrite of LRUCache from Myghty to use a periodic timestamp-based
152    paradigm so that synchronization is not really needed.  the size management
153    is inexact.
154    """
155
156    class _Item(object):
157        def __init__(self, key, value):
158            self.key = key
159            self.value = value
160            self.timestamp = compat.time_func()
161        def __repr__(self):
162            return repr(self.value)
163
164    def __init__(self, capacity, threshold=.5):
165        self.capacity = capacity
166        self.threshold = threshold
167
168    def __getitem__(self, key):
169        item = dict.__getitem__(self, key)
170        item.timestamp = compat.time_func()
171        return item.value
172
173    def values(self):
174        return [i.value for i in dict.values(self)]
175
176    def setdefault(self, key, value):
177        if key in self:
178            return self[key]
179        else:
180            self[key] = value
181            return value
182
183    def __setitem__(self, key, value):
184        item = dict.get(self, key)
185        if item is None:
186            item = self._Item(key, value)
187            dict.__setitem__(self, key, item)
188        else:
189            item.value = value
190        self._manage_size()
191
192    def _manage_size(self):
193        while len(self) > self.capacity + self.capacity * self.threshold:
194            bytime = sorted(dict.values(self),
195                            key=operator.attrgetter('timestamp'), reverse=True)
196            for item in bytime[self.capacity:]:
197                try:
198                    del self[item.key]
199                except KeyError:
200                    # if we couldn't find a key, most likely some other thread
201                    # broke in on us. loop around and try again
202                    break
203
204# Regexp to match python magic encoding line
205_PYTHON_MAGIC_COMMENT_re = re.compile(
206    r'[ \t\f]* \# .* coding[=:][ \t]*([-\w.]+)',
207    re.VERBOSE)
208
209def parse_encoding(fp):
210    """Deduce the encoding of a Python source file (binary mode) from magic
211    comment.
212
213    It does this in the same way as the `Python interpreter`__
214
215    .. __: http://docs.python.org/ref/encodings.html
216
217    The ``fp`` argument should be a seekable file object in binary mode.
218    """
219    pos = fp.tell()
220    fp.seek(0)
221    try:
222        line1 = fp.readline()
223        has_bom = line1.startswith(codecs.BOM_UTF8)
224        if has_bom:
225            line1 = line1[len(codecs.BOM_UTF8):]
226
227        m = _PYTHON_MAGIC_COMMENT_re.match(line1.decode('ascii', 'ignore'))
228        if not m:
229            try:
230                import parser
231                parser.suite(line1.decode('ascii', 'ignore'))
232            except (ImportError, SyntaxError):
233                # Either it's a real syntax error, in which case the source
234                # is not valid python source, or line2 is a continuation of
235                # line1, in which case we don't want to scan line2 for a magic
236                # comment.
237                pass
238            else:
239                line2 = fp.readline()
240                m = _PYTHON_MAGIC_COMMENT_re.match(
241                                               line2.decode('ascii', 'ignore'))
242
243        if has_bom:
244            if m:
245                raise SyntaxError("python refuses to compile code with both a UTF8" \
246                      " byte-order-mark and a magic encoding comment")
247            return 'utf_8'
248        elif m:
249            return m.group(1)
250        else:
251            return None
252    finally:
253        fp.seek(pos)
254
255def sorted_dict_repr(d):
256    """repr() a dictionary with the keys in order.
257
258    Used by the lexer unit test to compare parse trees based on strings.
259
260    """
261    keys = list(d.keys())
262    keys.sort()
263    return "{" + ", ".join(["%r: %r" % (k, d[k]) for k in keys]) + "}"
264
265def restore__ast(_ast):
266    """Attempt to restore the required classes to the _ast module if it
267    appears to be missing them
268    """
269    if hasattr(_ast, 'AST'):
270        return
271    _ast.PyCF_ONLY_AST = 2 << 9
272    m = compile("""\
273def foo(): pass
274class Bar(object): pass
275if False: pass
276baz = 'mako'
2771 + 2 - 3 * 4 / 5
2786 // 7 % 8 << 9 >> 10
27911 & 12 ^ 13 | 14
28015 and 16 or 17
281-baz + (not +18) - ~17
282baz and 'foo' or 'bar'
283(mako is baz == baz) is not baz != mako
284mako > baz < mako >= baz <= mako
285mako in baz not in mako""", '<unknown>', 'exec', _ast.PyCF_ONLY_AST)
286    _ast.Module = type(m)
287
288    for cls in _ast.Module.__mro__:
289        if cls.__name__ == 'mod':
290            _ast.mod = cls
291        elif cls.__name__ == 'AST':
292            _ast.AST = cls
293
294    _ast.FunctionDef = type(m.body[0])
295    _ast.ClassDef = type(m.body[1])
296    _ast.If = type(m.body[2])
297
298    _ast.Name = type(m.body[3].targets[0])
299    _ast.Store = type(m.body[3].targets[0].ctx)
300    _ast.Str = type(m.body[3].value)
301
302    _ast.Sub = type(m.body[4].value.op)
303    _ast.Add = type(m.body[4].value.left.op)
304    _ast.Div = type(m.body[4].value.right.op)
305    _ast.Mult = type(m.body[4].value.right.left.op)
306
307    _ast.RShift = type(m.body[5].value.op)
308    _ast.LShift = type(m.body[5].value.left.op)
309    _ast.Mod = type(m.body[5].value.left.left.op)
310    _ast.FloorDiv = type(m.body[5].value.left.left.left.op)
311
312    _ast.BitOr = type(m.body[6].value.op)
313    _ast.BitXor = type(m.body[6].value.left.op)
314    _ast.BitAnd = type(m.body[6].value.left.left.op)
315
316    _ast.Or = type(m.body[7].value.op)
317    _ast.And = type(m.body[7].value.values[0].op)
318
319    _ast.Invert = type(m.body[8].value.right.op)
320    _ast.Not = type(m.body[8].value.left.right.op)
321    _ast.UAdd = type(m.body[8].value.left.right.operand.op)
322    _ast.USub = type(m.body[8].value.left.left.op)
323
324    _ast.Or = type(m.body[9].value.op)
325    _ast.And = type(m.body[9].value.values[0].op)
326
327    _ast.IsNot = type(m.body[10].value.ops[0])
328    _ast.NotEq = type(m.body[10].value.ops[1])
329    _ast.Is = type(m.body[10].value.left.ops[0])
330    _ast.Eq = type(m.body[10].value.left.ops[1])
331
332    _ast.Gt = type(m.body[11].value.ops[0])
333    _ast.Lt = type(m.body[11].value.ops[1])
334    _ast.GtE = type(m.body[11].value.ops[2])
335    _ast.LtE = type(m.body[11].value.ops[3])
336
337    _ast.In = type(m.body[12].value.ops[0])
338    _ast.NotIn = type(m.body[12].value.ops[1])
339
340
341
342def read_file(path, mode='rb'):
343    fp = open(path, mode)
344    try:
345        data = fp.read()
346        return data
347    finally:
348        fp.close()
349
350def read_python_file(path):
351    fp = open(path, "rb")
352    try:
353        encoding = parse_encoding(fp)
354        data = fp.read()
355        if encoding:
356            data = data.decode(encoding)
357        return data
358    finally:
359        fp.close()
360
361