Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(331)

Side by Side Diff: third_party/pystache/src/context.py

Issue 2962783004: Adding pystache to third_party (Closed)
Patch Set: Merge branch 'master' into mustache Created 3 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « third_party/pystache/src/common.py ('k') | third_party/pystache/src/defaults.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # coding: utf-8
2
3 """
4 Exposes a ContextStack class.
5
6 The Mustache spec makes a special distinction between two types of context
7 stack elements: hashes and objects. For the purposes of interpreting the
8 spec, we define these categories mutually exclusively as follows:
9
10 (1) Hash: an item whose type is a subclass of dict.
11
12 (2) Object: an item that is neither a hash nor an instance of a
13 built-in type.
14
15 """
16
17 from pystache.common import PystacheError
18
19
20 # This equals '__builtin__' in Python 2 and 'builtins' in Python 3.
21 _BUILTIN_MODULE = type(0).__module__
22
23
24 # We use this private global variable as a return value to represent a key
25 # not being found on lookup. This lets us distinguish between the case
26 # of a key's value being None with the case of a key not being found --
27 # without having to rely on exceptions (e.g. KeyError) for flow control.
28 #
29 # TODO: eliminate the need for a private global variable, e.g. by using the
30 # preferred Python approach of "easier to ask for forgiveness than permission" :
31 # http://docs.python.org/glossary.html#term-eafp
32 class NotFound(object):
33 pass
34 _NOT_FOUND = NotFound()
35
36
37 def _get_value(context, key):
38 """
39 Retrieve a key's value from a context item.
40
41 Returns _NOT_FOUND if the key does not exist.
42
43 The ContextStack.get() docstring documents this function's intended behavior .
44
45 """
46 if isinstance(context, dict):
47 # Then we consider the argument a "hash" for the purposes of the spec.
48 #
49 # We do a membership test to avoid using exceptions for flow control
50 # (e.g. catching KeyError).
51 if key in context:
52 return context[key]
53 elif type(context).__module__ != _BUILTIN_MODULE:
54 # Then we consider the argument an "object" for the purposes of
55 # the spec.
56 #
57 # The elif test above lets us avoid treating instances of built-in
58 # types like integers and strings as objects (cf. issue #81).
59 # Instances of user-defined classes on the other hand, for example,
60 # are considered objects by the test above.
61 try:
62 attr = getattr(context, key)
63 except AttributeError:
64 # TODO: distinguish the case of the attribute not existing from
65 # an AttributeError being raised by the call to the attribute.
66 # See the following issue for implementation ideas:
67 # http://bugs.python.org/issue7559
68 pass
69 else:
70 # TODO: consider using EAFP here instead.
71 # http://docs.python.org/glossary.html#term-eafp
72 if callable(attr):
73 return attr()
74 return attr
75
76 return _NOT_FOUND
77
78
79 class KeyNotFoundError(PystacheError):
80
81 """
82 An exception raised when a key is not found in a context stack.
83
84 """
85
86 def __init__(self, key, details):
87 self.key = key
88 self.details = details
89
90 def __str__(self):
91 return "Key %s not found: %s" % (repr(self.key), self.details)
92
93
94 class ContextStack(object):
95
96 """
97 Provides dictionary-like access to a stack of zero or more items.
98
99 Instances of this class are meant to act as the rendering context
100 when rendering Mustache templates in accordance with mustache(5)
101 and the Mustache spec.
102
103 Instances encapsulate a private stack of hashes, objects, and built-in
104 type instances. Querying the stack for the value of a key queries
105 the items in the stack in order from last-added objects to first
106 (last in, first out).
107
108 Caution: this class does not currently support recursive nesting in
109 that items in the stack cannot themselves be ContextStack instances.
110
111 See the docstrings of the methods of this class for more details.
112
113 """
114
115 # We reserve keyword arguments for future options (e.g. a "strict=True"
116 # option for enabling a strict mode).
117 def __init__(self, *items):
118 """
119 Construct an instance, and initialize the private stack.
120
121 The *items arguments are the items with which to populate the
122 initial stack. Items in the argument list are added to the
123 stack in order so that, in particular, items at the end of
124 the argument list are queried first when querying the stack.
125
126 Caution: items should not themselves be ContextStack instances, as
127 recursive nesting does not behave as one might expect.
128
129 """
130 self._stack = list(items)
131
132 def __repr__(self):
133 """
134 Return a string representation of the instance.
135
136 For example--
137
138 >>> context = ContextStack({'alpha': 'abc'}, {'numeric': 123})
139 >>> repr(context)
140 "ContextStack({'alpha': 'abc'}, {'numeric': 123})"
141
142 """
143 return "%s%s" % (self.__class__.__name__, tuple(self._stack))
144
145 @staticmethod
146 def create(*context, **kwargs):
147 """
148 Build a ContextStack instance from a sequence of context-like items.
149
150 This factory-style method is more general than the ContextStack class's
151 constructor in that, unlike the constructor, the argument list
152 can itself contain ContextStack instances.
153
154 Here is an example illustrating various aspects of this method:
155
156 >>> obj1 = {'animal': 'cat', 'vegetable': 'carrot', 'mineral': 'copper'}
157 >>> obj2 = ContextStack({'vegetable': 'spinach', 'mineral': 'silver'})
158 >>>
159 >>> context = ContextStack.create(obj1, None, obj2, mineral='gold')
160 >>>
161 >>> context.get('animal')
162 'cat'
163 >>> context.get('vegetable')
164 'spinach'
165 >>> context.get('mineral')
166 'gold'
167
168 Arguments:
169
170 *context: zero or more dictionaries, ContextStack instances, or object s
171 with which to populate the initial context stack. None
172 arguments will be skipped. Items in the *context list are
173 added to the stack in order so that later items in the argument
174 list take precedence over earlier items. This behavior is the
175 same as the constructor's.
176
177 **kwargs: additional key-value data to add to the context stack.
178 As these arguments appear after all items in the *context list,
179 in the case of key conflicts these values take precedence over
180 all items in the *context list. This behavior is the same as
181 the constructor's.
182
183 """
184 items = context
185
186 context = ContextStack()
187
188 for item in items:
189 if item is None:
190 continue
191 if isinstance(item, ContextStack):
192 context._stack.extend(item._stack)
193 else:
194 context.push(item)
195
196 if kwargs:
197 context.push(kwargs)
198
199 return context
200
201 # TODO: add more unit tests for this.
202 # TODO: update the docstring for dotted names.
203 def get(self, name):
204 """
205 Resolve a dotted name against the current context stack.
206
207 This function follows the rules outlined in the section of the
208 spec regarding tag interpolation. This function returns the value
209 as is and does not coerce the return value to a string.
210
211 Arguments:
212
213 name: a dotted or non-dotted name.
214
215 default: the value to return if name resolution fails at any point.
216 Defaults to the empty string per the Mustache spec.
217
218 This method queries items in the stack in order from last-added
219 objects to first (last in, first out). The value returned is
220 the value of the key in the first item that contains the key.
221 If the key is not found in any item in the stack, then the default
222 value is returned. The default value defaults to None.
223
224 In accordance with the spec, this method queries items in the
225 stack for a key differently depending on whether the item is a
226 hash, object, or neither (as defined in the module docstring):
227
228 (1) Hash: if the item is a hash, then the key's value is the
229 dictionary value of the key. If the dictionary doesn't contain
230 the key, then the key is considered not found.
231
232 (2) Object: if the item is an an object, then the method looks for
233 an attribute with the same name as the key. If an attribute
234 with that name exists, the value of the attribute is returned.
235 If the attribute is callable, however (i.e. if the attribute
236 is a method), then the attribute is called with no arguments
237 and that value is returned. If there is no attribute with
238 the same name as the key, then the key is considered not found.
239
240 (3) Neither: if the item is neither a hash nor an object, then
241 the key is considered not found.
242
243 *Caution*:
244
245 Callables are handled differently depending on whether they are
246 dictionary values, as in (1) above, or attributes, as in (2).
247 The former are returned as-is, while the latter are first
248 called and that value returned.
249
250 Here is an example to illustrate:
251
252 >>> def greet():
253 ... return "Hi Bob!"
254 >>>
255 >>> class Greeter(object):
256 ... greet = None
257 >>>
258 >>> dct = {'greet': greet}
259 >>> obj = Greeter()
260 >>> obj.greet = greet
261 >>>
262 >>> dct['greet'] is obj.greet
263 True
264 >>> ContextStack(dct).get('greet') #doctest: +ELLIPSIS
265 <function greet at 0x...>
266 >>> ContextStack(obj).get('greet')
267 'Hi Bob!'
268
269 TODO: explain the rationale for this difference in treatment.
270
271 """
272 if name == '.':
273 try:
274 return self.top()
275 except IndexError:
276 raise KeyNotFoundError(".", "empty context stack")
277
278 parts = name.split('.')
279
280 try:
281 result = self._get_simple(parts[0])
282 except KeyNotFoundError:
283 raise KeyNotFoundError(name, "first part")
284
285 for part in parts[1:]:
286 # The full context stack is not used to resolve the remaining parts.
287 # From the spec--
288 #
289 # 5) If any name parts were retained in step 1, each should be
290 # resolved against a context stack containing only the result
291 # from the former resolution. If any part fails resolution, the
292 # result should be considered falsey, and should interpolate as
293 # the empty string.
294 #
295 # TODO: make sure we have a test case for the above point.
296 result = _get_value(result, part)
297 # TODO: consider using EAFP here instead.
298 # http://docs.python.org/glossary.html#term-eafp
299 if result is _NOT_FOUND:
300 raise KeyNotFoundError(name, "missing %s" % repr(part))
301
302 return result
303
304 def _get_simple(self, name):
305 """
306 Query the stack for a non-dotted name.
307
308 """
309 for item in reversed(self._stack):
310 result = _get_value(item, name)
311 if result is not _NOT_FOUND:
312 return result
313
314 raise KeyNotFoundError(name, "part missing")
315
316 def push(self, item):
317 """
318 Push an item onto the stack.
319
320 """
321 self._stack.append(item)
322
323 def pop(self):
324 """
325 Pop an item off of the stack, and return it.
326
327 """
328 return self._stack.pop()
329
330 def top(self):
331 """
332 Return the item last added to the stack.
333
334 """
335 return self._stack[-1]
336
337 def copy(self):
338 """
339 Return a copy of this instance.
340
341 """
342 return ContextStack(*self._stack)
OLDNEW
« no previous file with comments | « third_party/pystache/src/common.py ('k') | third_party/pystache/src/defaults.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698