Hide keyboard shortcuts

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. 

3 

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 

13 

14import logging 

15from six.moves import map 

16import json.encoder 

17 

18__all__ = ["js_callback", "js_function", "js_symbol", "encoder"] 

19 

20log = logging.getLogger(__name__) 

21 

22 

23class TWEncoder(json.encoder.JSONEncoder): 

24 """A JSON encoder that can encode Widgets, js_calls, js_symbols and 

25 js_callbacks. 

26 

27 Example:: 

28 

29 >>> encode = TWEncoder().encode 

30 >>> print encode({ 

31 ... 'onLoad': js_function("do_something")(js_symbol("this")) 

32 ... }) 

33 {"onLoad": do_something(this)} 

34 

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 """ 

47 

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'] = ' ' 

53 

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) 

57 

58 # This is required to get encoding of _js_call to work 

59 self.namedtuple_as_object = False 

60 

61 def default(self, obj): 

62 if isinstance(obj, self.pass_through): 

63 result = self.mark_for_escape(obj) 

64 return result 

65 

66 if hasattr(obj, '__json__'): 

67 return obj.__json__() 

68 

69 if hasattr(obj, 'id'): 

70 return str(obj.id) 

71 

72 return super(TWEncoder, self).default(obj) 

73 

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 

80 

81 encoded = super(TWEncoder, self).encode(obj) 

82 return self.unescape_marked(encoded) 

83 

84 def mark_for_escape(self, obj): 

85 self.unescape_symbols[id(obj)] = obj 

86 return 'TW2Encoder_unescape_' + str(id(obj)) 

87 

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) 

93 

94 return self.unescape_pattern.sub(unescape, encoded) 

95 

96 

97encoder = None # This gets reset at the bottom of the file. 

98 

99 

100class js_symbol(object): 

101 """ An unquoted js symbol like ``document`` or ``window`` """ 

102 

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 

112 

113 def __str__(self): 

114 return str(self._name) 

115 

116 

117class js_callback(object): 

118 """A js function that can be passed as a callback to be called 

119 by another JS function 

120 

121 Examples: 

122 

123 >>> str(js_callback("update_div")) 

124 'update_div' 

125 

126 >>> str(js_callback("function (event) { .... }")) 

127 'function (event) { .... }' 

128 

129 Can also create callbacks for deferred js calls 

130 

131 >>> str(js_callback(js_function('foo')(1,2,3))) 

132 'function(){foo(1, 2, 3)}' 

133 

134 Or equivalently 

135 

136 >>> str(js_callback(js_function('foo'), 1,2,3)) 

137 'function(){foo(1, 2, 3)}' 

138 

139 A more realistic example 

140 

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)})}) 

148 

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 = '' 

159 

160 def __call__(self, *args): 

161 raise TypeError("A js_callback cannot be called from Python") 

162 

163 def __str__(self): 

164 return self.cb 

165 

166 

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. 

171 

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:: 

176 

177 >>> jQuery = js_function('jQuery') 

178 >>> call = jQuery('#foo').datePicker({'option1': 'value1'}) 

179 >>> str(call) 

180 'jQuery("#foo").datePicker({"option1": "value1"})' 

181 

182 Calls are added to the widget call stack with the ``add_call`` method. 

183 

184 If made at Widget initialization those calls will be placed in 

185 the template for every request that renders the widget:: 

186 

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 ... ) 

193 

194 More likely, we will want to dynamically make calls on every 

195 request. Here we will call add_calls inside the ``prepare`` method:: 

196 

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 ... ) 

204 

205 This would allow to pass different options to the datePicker on every 

206 display. 

207 

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. 

211 

212 Examples: 

213 

214 >>> call = js_function('jQuery')("a .async") 

215 >>> str(call) 

216 'jQuery("a .async")' 

217 

218 js_function calls can be chained: 

219 

220 >>> call = js_function('jQuery')("a .async").foo().bar() 

221 >>> str(call) 

222 'jQuery("a .async").foo().bar()' 

223 

224 """ 

225 

226 def __init__(self, name): 

227 self.__name = name 

228 

229 def __call__(self, *args): 

230 return _js_call(self.__name, [], args, called=True) 

231 

232 def __str__(self): 

233 return self.__name 

234 

235 

236class _js_call(object): 

237 __slots__ = ('__name', '__call_list', '__args', '__called') 

238 

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 

245 

246 def __getattr__(self, name): 

247 return self.__class__(name, self.__call_list) 

248 

249 def __call__(self, *args): 

250 self.__args = args 

251 self.__called = True 

252 return self 

253 

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 

267 

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) 

272 

273 def __unicode__(self): 

274 return str(self).decode(sys.getdefaultencoding()) 

275 

276encoder = TWEncoder()