OLD | NEW |
---|---|
(Empty) | |
1 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 # Use of this source code is governed by a BSD-style license that can be | |
3 # found in the LICENSE file. | |
4 """ | |
5 Generator language component for compiler.py that adds Dart language support. | |
6 | |
7 Pass 'dart' with the -l flag to compiler.py to activate the use of this library. | |
8 """ | |
9 | |
10 from code import Code | |
11 from model import * | |
12 from schema_util import * | |
13 | |
14 import os | |
15 from datetime import datetime | |
16 | |
17 LICENSE = (""" | |
18 // Copyright (c) %s, the Dart project authors. Please see the AUTHORS file | |
19 // for details. All rights reserved. Use of this source code is governed by a | |
20 // BSD-style license that can be found in the LICENSE file.""" % | |
21 datetime.now().year) | |
22 | |
23 class DartGenerator(object): | |
24 """A .dart generator for a namespace. | |
25 """ | |
26 | |
27 def __init__(self, namespace, dart_overrides_dir=None): | |
28 self._namespace = namespace | |
29 self._types = namespace.types | |
30 | |
31 # Build a dictionary of Type Name --> Custom Dart code. | |
32 self._custom_dart = {} | |
not at google - send to devlin
2013/01/31 02:08:48
_type_overrides?
sashab
2013/01/31 04:41:40
Done.
| |
33 if dart_overrides_dir is not None: | |
34 for filename in os.listdir(dart_overrides_dir): | |
35 if filename.startswith(namespace.unix_name): | |
36 with open(os.path.join(dart_overrides_dir, filename)) as f: | |
37 # Split off the namespace and file extension, leaving just the type. | |
38 type_path = '.'.join(filename.split('.')[1:-1]) | |
39 self._custom_dart[type_path] = f.read() | |
40 | |
41 def Generate(self): | |
42 """Generates a Code object with the .dart for the entire namespace. | |
43 """ | |
44 c = Code() | |
45 (c.Append(LICENSE) | |
46 .Append() | |
47 .Append('part of chrome;')) | |
48 | |
49 if self._types: | |
50 (c.Append() | |
51 .Append('/**') | |
52 .Append(' * Types') | |
53 .Append(' */') | |
54 ) | |
55 for type_name in self._types: | |
56 c.Concat(self._GenerateType(self._types[type_name])) | |
57 | |
58 if self._namespace.events: | |
59 (c.Append() | |
60 .Append('/**') | |
61 .Append(' * Events') | |
62 .Append(' */') | |
63 ) | |
64 for event_name in self._namespace.events: | |
65 c.Concat(self._GenerateEvent(self._namespace.events[event_name])) | |
66 | |
67 (c.Append() | |
68 .Append('/**') | |
69 .Append(' * Functions') | |
70 .Append(' */') | |
71 ) | |
72 c.Concat(self._GenerateMainClass()) | |
73 | |
74 return c | |
75 | |
76 def _GenerateType(self, type_): | |
77 """Given a Type object, returns the Code with the .dart for this | |
78 type's definition. | |
79 | |
80 Assumes this type is a Parameter Type (creatable by user), and creates an | |
81 object that extends ChromeObject. All parameters are specifiable as named | |
82 arguments in the constructor, and all methods are wrapped with getters and | |
83 setters that hide the JS() implementation. | |
84 """ | |
85 c = Code() | |
86 (c.Append() | |
87 .Concat(self._GenerateDocumentation(type_)) | |
88 .Sblock('class %(type_name)s extends ChromeObject {') | |
89 ) | |
90 | |
91 # Check whether this type has function members. If it does, don't allow | |
92 # public construction. | |
93 for p in type_.properties.values(): | |
94 print p.name, self._IsFunction(p) | |
95 add_public_constructor = all(not self._IsFunction(p) | |
96 for p in type_.properties.values()) | |
97 constructor_fields = [self._GeneratePropertySignature(p) | |
98 for p in type_.properties.values()] | |
99 | |
100 if add_public_constructor: | |
101 (c.Append('/*') | |
102 .Append(' * Public constructor') | |
103 .Append(' */') | |
104 .Sblock('%(type_name)s({%(constructor_fields)s}) {') | |
105 ) | |
106 | |
107 for prop_name in type_.properties: | |
108 c.Append('this.%s = %s;' % (prop_name, prop_name)) | |
109 (c.Eblock('}') | |
110 .Append() | |
111 ) | |
112 | |
113 (c.Append('/*') | |
114 .Append(' * Private constructor') | |
115 .Append(' */') | |
116 .Append('%(type_name)s._proxy(_jsObject) : super._proxy(_jsObject);') | |
117 ) | |
118 | |
119 # Add an accessor (getter & setter) for each property. | |
120 properties = [p for p in type_.properties.values() | |
121 if not self._IsFunction(p)] | |
122 if properties: | |
123 (c.Append() | |
124 .Append('/*') | |
125 .Append(' * Public accessors') | |
126 .Append(' */') | |
127 ) | |
128 for prop in properties: | |
129 type_name = self._GetPropertyType(prop) | |
130 prop_is_base_type = self._IsBaseType(prop) | |
131 | |
132 # Check for custom dart for this whole property. | |
133 if not self._TryAppendOverride(c, type_, prop, add_doc=True): | |
134 # Add the getter. | |
135 if not self._TryAppendOverride(c, type_, prop, '.get', add_doc=True): | |
not at google - send to devlin
2013/01/31 02:08:48
key_suffix='.get'
sashab
2013/01/31 04:41:40
Done.
| |
136 # Add the documentation for this property. | |
137 (c.Append() | |
138 .Concat(self._GenerateDocumentation(prop)) | |
139 ) | |
140 | |
141 # TODO(sashab): Serialize generic Dart objects differently. | |
142 if prop_is_base_type or self._IsObjectType(prop): | |
143 c.Append("%s get %s => JS('%s', '#.%s', this._jsObject);" % | |
144 (type_name, prop.name, type_name, prop.name)) | |
145 elif prop.type_.property_type == PropertyType.REF: | |
146 c.Append("%s get %s => new %s._proxy(JS('', '#.%s', " | |
147 "this._jsObject));" | |
148 % (type_name, prop.name, type_name, prop.name)) | |
149 else: | |
150 raise Exception( | |
151 "Could not generate wrapper for %s.%s: unserializable type %s" % | |
152 (type_.name, prop.name, type_name) | |
153 ) | |
154 | |
155 # Add the setter. | |
156 if not self._TryAppendOverride(c, type_, prop, '.set'): | |
not at google - send to devlin
2013/01/31 02:08:48
key_suffix='.set'
sashab
2013/01/31 04:41:40
Done.
| |
157 wrapped_name = prop.name | |
158 if not prop_is_base_type: | |
159 wrapped_name = 'convertArgument(%s)' % prop.name | |
160 | |
161 (c.Append() | |
162 .Sblock("void set %s(%s %s) {" % (prop.name, type_name, prop.name)) | |
163 .Append("JS('void', '#.%s = #', this._jsObject, %s);" % | |
164 (prop.name, wrapped_name)) | |
165 .Eblock("}") | |
166 ) | |
167 | |
168 # Now add all the methods. | |
169 methods = [t for t in type_.properties.values() | |
170 if self._IsFunction(t)] | |
not at google - send to devlin
2013/01/31 02:08:48
might fit on 1 line?
sashab
2013/01/31 04:41:40
Done.
| |
171 if methods: | |
172 (c.Append() | |
173 .Append('/*') | |
174 .Append(' * Methods') | |
175 .Append(' */') | |
176 ) | |
177 for prop in methods: | |
178 c.Concat(self._GenerateFunction(prop.type_.function)) | |
179 | |
180 (c.Eblock('}') | |
181 .Substitute({ | |
182 'type_name': type_.simple_name, | |
183 'constructor_fields': ', '.join(constructor_fields) | |
184 }) | |
185 ) | |
186 | |
187 return c | |
188 | |
189 def _GenerateDocumentation(self, prop): | |
190 """Given an object, generates the documentation for this object (as a | |
191 code string) and returns the Code object. | |
192 | |
193 Returns an empty code object if the object has no documentation. | |
194 | |
195 Uses triple-quotes for the string. | |
196 """ | |
197 c = Code() | |
198 if prop.description is not None: | |
199 for line in prop.description.split('\n'): | |
200 c.Comment(line, comment_prefix='/// ') | |
201 return c | |
202 | |
203 | |
204 def _GenerateFunction(self, f): | |
205 """Returns the Code object for the given function. | |
206 """ | |
207 c = Code() | |
208 (c.Append() | |
209 .Concat(self._GenerateDocumentation(f)) | |
210 ) | |
211 | |
212 if not self._NeedsProxiedCallback(f): | |
213 c.Append("%s => %s;" % (self._GenerateFunctionSignature(f), | |
214 self._GenerateProxyCall(f))) | |
215 return c | |
216 | |
217 c.Sblock("%s {" % self._GenerateFunctionSignature(f)) | |
218 | |
219 # Define the proxied callback. | |
220 proxied_params = [] | |
221 for p in f.callback.params: | |
222 if self._IsBaseType(p): | |
223 proxied_params.append(p.name) | |
224 elif self._IsObjectType(p): | |
225 proxied_params.append("new %s._proxy(%s)" % ( | |
226 self._GetPropertyType(p), p.name)) | |
227 else: | |
228 raise Exception( | |
229 "Cannot automatically create proxy; can't wrap %s.%s, type %s" % ( | |
230 f.name, p.name, p.type_.name)) | |
231 | |
232 (c.Sblock("void __proxy_callback(%s) {" % ', '.join(p.name for p in | |
233 f.callback.params)) | |
234 .Sblock("if (?%s)" % f.callback.name) | |
235 .Append("%s(%s);" % (f.callback.name, ', '.join(proxied_params))) | |
236 .Eblock(None) | |
not at google - send to devlin
2013/01/31 02:08:48
like I said earlier, just make None the default? T
sashab
2013/01/31 04:41:40
Done.
| |
237 .Eblock("}") | |
238 .Append("%s;" % self._GenerateProxyCall(f)) | |
239 .Eblock("}") | |
240 ) | |
241 return c | |
242 | |
243 def _NeedsProxiedCallback(self, f): | |
244 """Given a function, returns True if this function's callback needs to be | |
245 proxied, False if not. | |
246 | |
247 Function callbacks need to be proxied if they have at least one | |
248 non-base-type parameter. | |
249 """ | |
250 return f.callback and any( | |
251 not self._IsBaseType(p) for p in f.callback.params) | |
252 | |
253 def _GenerateProxyCall(self, function, call_target='this._jsObject'): | |
254 """Given a function, generates the code to call that function via JS(). | |
255 Returns a string. | |
256 | |
257 |call_target| is the name of the object to call the function on. The default | |
258 is this._jsObject. | |
259 | |
260 e.g. | |
261 JS('void', '#.resizeTo(#, #)', this._jsObject, width, height) | |
262 JS('void', '#.setBounds(#)', this._jsObject, convertArgument(bounds)) | |
263 """ | |
264 n_params = len(function.params) | |
265 if function.callback: | |
266 n_params += 1 | |
267 | |
268 params = ["'%s'" % self._GetPropertyType(function.returns), | |
269 "'#.%s(%s)'" % (function.name, | |
270 ', '.join(['#'] * n_params)), | |
271 call_target] | |
272 | |
273 for param in function.params: | |
274 if not self._IsBaseType(param): | |
275 params.append('convertArgument(%s)' % param.name) | |
276 else: | |
277 params.append(param.name) | |
278 if function.callback: | |
279 # If this isn't a base type, we need a proxied callback. | |
280 callback_name = function.callback.name | |
281 if self._NeedsProxiedCallback(function): | |
282 callback_name = "__proxy_callback" | |
283 params.append('convertDartClosureToJS(%s, %s)' % (callback_name, | |
284 len(function.callback.params))) | |
285 | |
286 return 'JS(%s)' % ', '.join(params) | |
287 | |
288 def _GenerateEvent(self, event): | |
289 """Given a Function object, returns the Code with the .dart for this event, | |
290 represented by the function. | |
291 | |
292 All events extend the Event base type. | |
293 """ | |
294 c = Code() | |
295 | |
296 # Add documentation for this event. | |
297 (c.Append() | |
298 .Concat(self._GenerateDocumentation(event)) | |
299 .Sblock('class Event_%(event_name)s extends Event {') | |
300 ) | |
301 | |
302 # Override Event callback type definitions. | |
303 for ret_type, event_func in (('void', 'addListener'), | |
304 ('void', 'removeListener'), | |
305 ('bool', 'hasListener')): | |
306 | |
307 param_list = self._GenerateParameterList(event.params, event.callback, | |
308 allow_optional = False) | |
309 | |
310 c.Append('%s %s(void callback(%s)) => super.%s(callback);' % | |
311 (ret_type, event_func, param_list, event_func)) | |
312 | |
313 # Generate the constructor. | |
314 (c.Append() | |
315 .Append('Event_%(event_name)s(jsObject) : ' | |
316 'super(jsObject, %(param_num)d);') | |
317 ) | |
318 | |
319 (c.Eblock('}') | |
320 .Substitute({ | |
321 'event_name': self._namespace.unix_name + '_' + event.name, | |
322 'param_num': len(event.params) | |
323 }) | |
324 ) | |
325 | |
326 return c | |
327 | |
328 def _GenerateMainClass(self): | |
329 """Generates the main class for this file, which links to all functions | |
330 and events. | |
331 | |
332 Returns a code object. | |
333 """ | |
334 c = Code() | |
335 (c.Append() | |
336 .Sblock('class API_%s {' % self._namespace.unix_name) | |
337 .Append('/*') | |
338 .Append(' * API connection') | |
339 .Append(' */') | |
340 .Append('Object _jsObject;') | |
341 ) | |
342 | |
343 # Add events. | |
344 if self._namespace.events: | |
345 (c.Append() | |
346 .Append('/*') | |
347 .Append(' * Events') | |
348 .Append(' */') | |
349 ) | |
350 for event_name in self._namespace.events: | |
351 c.Append('Event_%s_%s %s;' % (self._namespace.unix_name, event_name, | |
352 event_name)) | |
353 | |
354 # Add functions. | |
355 if self._namespace.functions: | |
356 (c.Append() | |
357 .Append('/*') | |
358 .Append(' * Functions') | |
359 .Append(' */') | |
360 ) | |
361 for function in self._namespace.functions.values(): | |
362 c.Concat(self._GenerateFunction(function)) | |
363 | |
364 # Add the constructor. | |
365 (c.Append() | |
366 .Sblock('API_%s(this._jsObject) {' % self._namespace.unix_name) | |
367 ) | |
368 | |
369 # Add events to constructor. | |
370 for event_name in self._namespace.events: | |
371 c.Append("%s = new Event_%s_%s(JS('', '#.%s', this._jsObject));" % | |
372 (event_name, self._namespace.unix_name, event_name, event_name)) | |
373 | |
374 (c.Eblock('}') | |
375 .Eblock('}') | |
376 ) | |
377 return c | |
378 | |
379 def _GeneratePropertySignature(self, prop): | |
380 """Given a property, returns a signature for that property. | |
381 Recursively generates the signature for callbacks. | |
382 Returns a String for the given property. | |
383 | |
384 e.g. | |
385 bool x | |
386 void onClosed() | |
387 void doSomething(bool x, void callback([String x])) | |
388 """ | |
389 if self._IsFunction(prop): | |
390 return self._GenerateFunctionSignature(prop.type_.function) | |
391 return '%(type)s %(name)s' % { | |
392 'type': self._GetPropertyType(prop), | |
393 'name': prop.simple_name | |
394 } | |
395 | |
396 def _GenerateFunctionSignature(self, function): | |
397 """Given a function object, returns the signature for that function. | |
398 Recursively generates the signature for callbacks. | |
399 Returns a String for the given function. | |
400 | |
401 If prepend_this is True, adds "this." to the function's name. | |
402 | |
403 e.g. | |
404 void onClosed() | |
405 bool isOpen([String type]) | |
406 void doSomething(bool x, void callback([String x])) | |
407 | |
408 e.g. If prepend_this is True: | |
409 void this.onClosed() | |
410 bool this.isOpen([String type]) | |
411 void this.doSomething(bool x, void callback([String x])) | |
412 """ | |
413 sig = '%(return_type)s %(name)s(%(params)s)' | |
414 | |
415 if function.returns: | |
416 return_type = self._GetPropertyType(function.returns) | |
417 else: | |
418 return_type = 'void' | |
419 | |
420 return sig % { | |
421 'return_type': return_type, | |
422 'name': function.simple_name, | |
423 'params': self._GenerateParameterList(function.params, | |
424 function.callback) | |
425 } | |
426 | |
427 def _GenerateParameterList(self, params, callback=None, | |
428 allow_optional=True): | |
not at google - send to devlin
2013/01/31 02:08:48
if it can't fit on 1 line then align parameters ve
sashab
2013/01/31 04:41:40
Done.
| |
429 """Given a list of function parameters, generates their signature (as a | |
430 string). | |
431 | |
432 e.g. | |
433 [String type] | |
434 bool x, void callback([String x]) | |
435 | |
436 If allow_optional is False, ignores optional parameters. Useful for | |
437 callbacks, where optional parameters are not used. | |
438 """ | |
439 # Params lists (required & optional), to be joined with ,'s. | |
not at google - send to devlin
2013/01/31 02:08:48
commas?
sashab
2013/01/31 04:41:40
Done.
| |
440 # FIXME(sashab): assuming all optional params come after required ones | |
not at google - send to devlin
2013/01/31 02:08:48
I can't parse this sentence. Should it be like "Do
sashab
2013/01/31 04:41:40
Haha, yes, you're right. Fixed
| |
441 params_req = [] | |
442 params_opt = [] | |
443 for param in params: | |
444 p_sig = self._GeneratePropertySignature(param) | |
445 if allow_optional and param.optional: | |
446 params_opt.append(p_sig) | |
447 else: | |
448 params_req.append(p_sig) | |
not at google - send to devlin
2013/01/31 02:08:48
this isn't ignoring optional parameters, it's maki
sashab
2013/01/31 04:41:40
Sorry, comment changed. And I also changed the nam
| |
449 | |
450 # Add the callback, if it exists. | |
451 if callback: | |
452 c_sig = self._GenerateFunctionSignature(callback) | |
453 if callback.optional: | |
454 params_opt.append(c_sig) | |
455 else: | |
456 params_req.append(c_sig) | |
457 | |
458 # Join the parameters, wrapping the optional params in square brackets. | |
459 if params_opt: | |
460 params_opt[0] = '[%s' % params_opt[0] | |
461 params_opt[-1] = '%s]' % params_opt[-1] | |
not at google - send to devlin
2013/01/31 02:08:48
a bit bizarre but much better, and I can't think o
sashab
2013/01/31 04:41:40
Done. Or should this rather be in the function sig
not at google - send to devlin
2013/02/02 00:45:47
What you have is good.
| |
462 param_sets = [', '.join(params_req), | |
463 ', '.join(params_opt)] | |
not at google - send to devlin
2013/01/31 02:08:48
single line
sashab
2013/01/31 04:41:40
Done.
| |
464 | |
465 return ', '.join(p for p in param_sets if p) | |
not at google - send to devlin
2013/01/31 02:08:48
why the "if p"? I.e. why can't it be ', '.join(par
sashab
2013/01/31 04:41:40
This is part of the bizarre logic. If there are no
| |
466 | |
467 def _TryAppendOverride(self, c, type_, prop, key_suffix='', add_doc=False): | |
not at google - send to devlin
2013/01/31 02:08:48
TryAppendOverride is a bit of an awkward name. Per
sashab
2013/01/31 04:41:40
It returns True if it appended the override, False
| |
468 """Given a particular type and property to find in the custom dart | |
469 overrides, checks whether there is an override for that key. | |
470 If there is, adds a newline, appends the override code, and returns True. | |
not at google - send to devlin
2013/01/31 02:08:48
"adds a newline" -> doesn't seem like this should
sashab
2013/01/31 04:41:40
Ok, the reason this is needed is in case the overr
| |
471 If not, returns False. | |
472 | |
473 |key_suffix| will be added to the end of the key before searching, e.g. | |
474 '.set' or '.get' can be used for setters and getters respectively. | |
475 | |
476 If add_doc is given, adds the documentation for this property before the | |
477 override code. | |
478 If the override code is empty, does nothing (and does not add | |
479 documentation), but returns True (treats it as a valid override). | |
480 """ | |
481 contents = self._custom_dart.get('%s.%s%s' % (type_.name, prop.name, | |
482 key_suffix)) | |
483 if contents is None: | |
484 return False | |
485 | |
not at google - send to devlin
2013/01/31 02:08:48
maybe early-exit here too, if it's empty:
if cont
sashab
2013/01/31 04:41:40
Good idea! :D
| |
486 if contents.strip(): | |
487 if prop is not None: | |
not at google - send to devlin
2013/01/31 02:08:48
although TBH I find this idiom a bit strange. Peop
sashab
2013/01/31 04:41:40
It's a little tough, because its unclear: does put
| |
488 (c.Append() | |
489 .Concat(self._GenerateDocumentation(prop)) | |
490 ) | |
491 else: | |
492 c.Append() | |
not at google - send to devlin
2013/01/31 02:08:48
seem to be always appending here, so pull out abov
sashab
2013/01/31 04:41:40
Thanks, fixed this one; see comment above about ca
| |
493 for line in contents.split('\n'): | |
494 c.Append(line) | |
495 return True | |
496 | |
497 def _IsFunction(self, prop): | |
498 """Given a model.Property, returns whether this type is a function. | |
499 """ | |
500 return prop.type_.property_type == PropertyType.FUNCTION | |
501 | |
502 def _IsObjectType(self, prop): | |
503 """Given a model.Property, returns whether this type is an object. | |
504 """ | |
505 return prop.type_.property_type in [PropertyType.OBJECT, PropertyType.ANY] | |
506 | |
507 def _IsBaseType(self, prop): | |
508 """Given a model.Property, returns whether this type is a base type | |
509 (string, number or boolean). | |
510 """ | |
511 return (self._GetPropertyType(prop) in | |
512 ['bool', 'num', 'int', 'double', 'String']) | |
513 | |
514 def _GetPropertyType(self, prop, container_type=False): | |
not at google - send to devlin
2013/01/31 02:08:48
call this _GetDartType and pass in the type rather
sashab
2013/01/31 04:41:40
Yup, was going to do this before. Made a wrapper _
| |
515 """Given a model.Property object, returns its type as a Dart string. | |
516 | |
517 If container_type is True, returns the type of the objects prop is | |
518 containing, rather than the type of prop itself. | |
519 """ | |
520 if prop is None: | |
521 return 'void' | |
522 | |
523 if container_type: | |
524 type_ = prop.type_.item_type | |
525 else: | |
526 type_ = prop.type_ | |
527 prop_type = type_.property_type | |
528 | |
529 if prop_type is None: | |
530 return 'void' | |
531 elif prop_type is PropertyType.REF: | |
532 if GetNamespace(type_.ref_type) == self._namespace.name: | |
533 # This type is in this namespace; just use its base name. | |
534 return GetBaseNamespace(type_.ref_type) | |
535 else: | |
536 # TODO(sashab): Work out how to import this foreign type. | |
537 return type_.ref_type | |
538 elif prop_type is PropertyType.BOOLEAN: | |
539 return 'bool' | |
540 elif prop_type is PropertyType.INTEGER: | |
541 return 'int' | |
542 elif prop_type is PropertyType.INT64: | |
543 return 'num' | |
544 elif prop_type is PropertyType.DOUBLE: | |
545 return 'double' | |
546 elif prop_type is PropertyType.STRING: | |
547 return 'String' | |
548 elif prop_type is PropertyType.ENUM: | |
549 return 'String' | |
550 elif prop_type is PropertyType.CHOICES: | |
551 # TODO: What is a Choices type? Is it closer to a Map Dart object? | |
552 return 'Object' | |
not at google - send to devlin
2013/01/31 02:08:48
I wrote a long thing in response to what you said,
sashab
2013/01/31 04:41:40
'Object' is the equivalent of untyped. See other c
| |
553 elif prop_type is PropertyType.ANY: | |
554 return 'Object' | |
555 elif prop_type is PropertyType.OBJECT: | |
556 # TODO(sashab): Work out a mapped type name. | |
557 return type_.instance_of or 'Object' | |
558 elif prop_type is PropertyType.FUNCTION: | |
559 return 'Function' | |
560 elif prop_type is PropertyType.ARRAY: | |
561 if type_.item_type: | |
not at google - send to devlin
2013/01/31 02:08:48
there should always be a list type, this check sho
sashab
2013/01/31 04:41:40
After investigating, you're right - its invalid ID
| |
562 return 'List<%s>' % self._GetPropertyType(prop, container_type=True) | |
563 else: | |
564 return 'List' | |
565 elif prop_type is PropertyType.BINARY: | |
566 return 'String' | |
567 else: | |
568 raise NotImplementedError(prop_type) | |
569 | |
OLD | NEW |