Coverage for tw2/core/js.py : 90%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2Python-JS interface to dynamically create JS function calls from your widgets.
4This moudle doesn't aim to serve as a Python-JS "translator". You should code
5your client-side code in JavaScript and make it available in static files which
6you include as JSLinks or inline using JSSources. This module is only intended
7as a "bridge" or interface between Python and JavaScript so JS function
8**calls** can be generated programatically.
9"""
10import re
11import sys
12import six
14import logging
15from six.moves import map
16import json.encoder
18__all__ = ["js_callback", "js_function", "js_symbol", "encoder"]
20log = logging.getLogger(__name__)
23class TWEncoder(json.encoder.JSONEncoder):
24 """A JSON encoder that can encode Widgets, js_calls, js_symbols and
25 js_callbacks.
27 Example::
29 >>> encode = TWEncoder().encode
30 >>> print encode({
31 ... 'onLoad': js_function("do_something")(js_symbol("this"))
32 ... })
33 {"onLoad": do_something(this)}
35 >>> from tw2.core.api import Widget
36 >>> w = Widget("foo")
37 >>> args = {
38 ... 'onLoad': js_callback(
39 ... js_function('jQuery')(w).click(js_symbol('onClick'))
40 ... )
41 ... }
42 >>> print encode(args)
43 {"onLoad": function(){jQuery(\\"foo\\").click(onClick)}}
44 >>> print encode({'args':args})
45 {"args": {"onLoad": function(){jQuery(\\"foo\\").click(onClick)}}}
46 """
48 def __init__(self, *args, **kw):
49 # This makes encoded objects be prettily formatted. It is very nice
50 # for debugging and should be made configurable at some point.
51 # TODO -- make json encoding pretty-printing configurable
52 #kw['indent'] = ' '
54 self.unescape_pattern = re.compile('"TW2Encoder_unescape_([0-9]*)"')
55 self.pass_through = (_js_call, js_callback, js_symbol, js_function)
56 super(TWEncoder, self).__init__(*args, **kw)
58 # This is required to get encoding of _js_call to work
59 self.namedtuple_as_object = False
61 def default(self, obj):
62 if isinstance(obj, self.pass_through):
63 result = self.mark_for_escape(obj)
64 return result
66 if hasattr(obj, '__json__'):
67 return obj.__json__()
69 if hasattr(obj, 'id'):
70 return str(obj.id)
72 return super(TWEncoder, self).default(obj)
74 def encode(self, obj):
75 self.unescape_symbols = {}
76 encoded = super(TWEncoder, self).encode(obj)
77 unescaped = self.unescape_marked(encoded)
78 self.unescape_symbols = {}
79 return unescaped
81 encoded = super(TWEncoder, self).encode(obj)
82 return self.unescape_marked(encoded)
84 def mark_for_escape(self, obj):
85 self.unescape_symbols[id(obj)] = obj
86 return 'TW2Encoder_unescape_' + str(id(obj))
88 def unescape_marked(self, encoded):
89 def unescape(match):
90 obj_id = int(match.group(1))
91 obj = self.unescape_symbols[obj_id]
92 return str(obj)
94 return self.unescape_pattern.sub(unescape, encoded)
97encoder = None # This gets reset at the bottom of the file.
100class js_symbol(object):
101 """ An unquoted js symbol like ``document`` or ``window`` """
103 def __init__(self, name=None, src=None):
104 if name == None and src == None:
105 raise ValueError("js_symbol must be given name or src")
106 if name and src:
107 raise ValueError("js_symbol must not be given name and src")
108 if src != None:
109 self._name = src
110 else:
111 self._name = name
113 def __str__(self):
114 return str(self._name)
117class js_callback(object):
118 """A js function that can be passed as a callback to be called
119 by another JS function
121 Examples:
123 >>> str(js_callback("update_div"))
124 'update_div'
126 >>> str(js_callback("function (event) { .... }"))
127 'function (event) { .... }'
129 Can also create callbacks for deferred js calls
131 >>> str(js_callback(js_function('foo')(1,2,3)))
132 'function(){foo(1, 2, 3)}'
134 Or equivalently
136 >>> str(js_callback(js_function('foo'), 1,2,3))
137 'function(){foo(1, 2, 3)}'
139 A more realistic example
141 >>> jQuery = js_function('jQuery')
142 >>> my_cb = js_callback('function() { alert(this.text)}')
143 >>> on_doc_load = jQuery('#foo').bind('click', my_cb)
144 >>> call = jQuery(js_callback(on_doc_load))
145 >>> print call
146 jQuery(function(){jQuery(\\"#foo\\").bind(
147 \\"click\\", function() { alert(this.text)})})
149 """
150 def __init__(self, cb, *args):
151 if isinstance(cb, six.string_types):
152 self.cb = cb
153 elif isinstance(cb, js_function):
154 self.cb = "function(){%s}" % cb(*args)
155 elif isinstance(cb, _js_call):
156 self.cb = "function(){%s}" % cb
157 else:
158 self.cb = ''
160 def __call__(self, *args):
161 raise TypeError("A js_callback cannot be called from Python")
163 def __str__(self):
164 return self.cb
167class js_function(object):
168 """A JS function that can be "called" from python and added to
169 a widget by widget.add_call() so it get's called every time the widget
170 is rendered.
172 Used to create a callable object that can be called from your widgets to
173 trigger actions in the browser. It's used primarily to initialize JS code
174 programatically. Calls can be chained and parameters are automatically
175 json-encoded into something JavaScript undersrtands. Example::
177 >>> jQuery = js_function('jQuery')
178 >>> call = jQuery('#foo').datePicker({'option1': 'value1'})
179 >>> str(call)
180 'jQuery("#foo").datePicker({"option1": "value1"})'
182 Calls are added to the widget call stack with the ``add_call`` method.
184 If made at Widget initialization those calls will be placed in
185 the template for every request that renders the widget::
187 >>> import tw2.core as twc
188 >>> class SomeWidget(twc.Widget): ...
189 pickerOptions = twc.Param(default={})
190 >>> SomeWidget.add_call( ...
191 jQuery('#%s' % SomeWidget.id).datePicker(SomeWidget.pickerOptions)
192 ... )
194 More likely, we will want to dynamically make calls on every
195 request. Here we will call add_calls inside the ``prepare`` method::
197 >>> class SomeWidget(Widget):
198 ... pickerOptions = twc.Param(default={})
199 ... def prepare(self):
200 ... super(SomeWidget, self).prepare()
201 ... self.add_call(
202 ... jQuery('#%s' % d.id).datePicker(d.pickerOptions)
203 ... )
205 This would allow to pass different options to the datePicker on every
206 display.
208 JS calls are rendered by the same mechanisms that render required css and
209 js for a widget and places those calls at bodybottom so DOM elements which
210 we might target are available.
212 Examples:
214 >>> call = js_function('jQuery')("a .async")
215 >>> str(call)
216 'jQuery("a .async")'
218 js_function calls can be chained:
220 >>> call = js_function('jQuery')("a .async").foo().bar()
221 >>> str(call)
222 'jQuery("a .async").foo().bar()'
224 """
226 def __init__(self, name):
227 self.__name = name
229 def __call__(self, *args):
230 return _js_call(self.__name, [], args, called=True)
232 def __str__(self):
233 return self.__name
236class _js_call(object):
237 __slots__ = ('__name', '__call_list', '__args', '__called')
239 def __init__(self, name, call_list, args=None, called=False):
240 self.__name = name
241 self.__args = args
242 call_list.append(self)
243 self.__call_list = call_list
244 self.__called = called
246 def __getattr__(self, name):
247 return self.__class__(name, self.__call_list)
249 def __call__(self, *args):
250 self.__args = args
251 self.__called = True
252 return self
254 def __get_js_repr(self):
255 if self.__called:
256 args = self.__args
257 rep = '%s(%s)' % (
258 self.__name,
259 ', '.join(map(encoder.encode, args))
260 )
261 return rep\
262 .replace('\\"', '"')\
263 .replace("\\'", "'")\
264 .replace('\\n', '\n')
265 else:
266 return self.__name
268 def __str__(self):
269 if not self.__called:
270 raise TypeError('Last element in the chain has to be called')
271 return '.'.join(c.__get_js_repr() for c in self.__call_list)
273 def __unicode__(self):
274 return str(self).decode(sys.getdefaultencoding())
276encoder = TWEncoder()