]>
Commit | Line | Data |
---|---|---|
d8f6e041 | 1 | ## |
50985c20 | 2 | ## This file is part of the libsigrok project. |
d8f6e041 ML |
3 | ## |
4 | ## Copyright (C) 2013 Martin Ling <martin-sigrok@earth.li> | |
5 | ## | |
6 | ## This program is free software: you can redistribute it and/or modify | |
7 | ## it under the terms of the GNU General Public License as published by | |
8 | ## the Free Software Foundation, either version 3 of the License, or | |
9 | ## (at your option) any later version. | |
10 | ## | |
11 | ## This program is distributed in the hope that it will be useful, | |
12 | ## but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | ## GNU General Public License for more details. | |
15 | ## | |
16 | ## You should have received a copy of the GNU General Public License | |
17 | ## along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | ## | |
19 | ||
20 | from functools import partial | |
21 | from fractions import Fraction | |
1cad2115 ML |
22 | from .lowlevel import * |
23 | from . import lowlevel | |
d8f6e041 ML |
24 | import itertools |
25 | ||
9bbd6a6a | 26 | __all__ = ['Error', 'Context', 'Driver', 'Device', 'Session', 'Packet', 'Log', |
417e9f3a | 27 | 'LogLevel', 'PacketType', 'Quantity', 'Unit', 'QuantityFlag', 'ConfigKey', |
f0e764de | 28 | 'ProbeType', 'Probe', 'ProbeGroup', 'InputFormat', 'OutputFormat', |
409d85b3 | 29 | 'InputFile', 'Output'] |
d8f6e041 ML |
30 | |
31 | class Error(Exception): | |
32 | ||
33 | def __str__(self): | |
34 | return sr_strerror(self.args[0]) | |
35 | ||
36 | def check(result): | |
37 | if result != SR_OK: | |
38 | raise Error(result) | |
39 | ||
d8f6e041 ML |
40 | def gvariant_to_python(value): |
41 | type_string = g_variant_get_type_string(value) | |
42 | if type_string == 't': | |
43 | return g_variant_get_uint64(value) | |
44 | if type_string == 'b': | |
45 | return g_variant_get_bool(value) | |
46 | if type_string == 'd': | |
47 | return g_variant_get_double(value) | |
48 | if type_string == 's': | |
49 | return g_variant_get_string(value, None) | |
50 | if type_string == '(tt)': | |
51 | return Fraction( | |
52 | g_variant_get_uint64(g_variant_get_child_value(value, 0)), | |
53 | g_variant_get_uint64(g_variant_get_child_value(value, 1))) | |
1cad2115 | 54 | raise NotImplementedError( |
d8f6e041 ML |
55 | "Can't convert GVariant type '%s' to a Python type." % type_string) |
56 | ||
57 | def python_to_gvariant(value): | |
58 | if isinstance(value, int): | |
59 | return g_variant_new_uint64(value) | |
60 | if isinstance(value, bool): | |
61 | return g_variant_new_boolean(value) | |
62 | if isinstance(value, float): | |
63 | return g_variant_new_double(value) | |
64 | if isinstance(value, str): | |
65 | return g_variant_new_string(value) | |
66 | if isinstance(value, Fraction): | |
67 | array = new_gvariant_ptr_array(2) | |
1e2bd8af ML |
68 | gvariant_ptr_array_setitem(array, 0, |
69 | g_variant_new_uint64(value.numerator)) | |
70 | gvariant_ptr_array_setitem(array, 1, | |
71 | g_variant_new_uint64(value.denominator)) | |
d8f6e041 ML |
72 | result = g_variant_new_tuple(array, 2) |
73 | delete_gvariant_ptr_array(array) | |
74 | return result | |
1cad2115 | 75 | raise NotImplementedError( |
d8f6e041 ML |
76 | "Can't convert Python '%s' to a GVariant." % type(value)) |
77 | ||
78 | def callback_wrapper(session, callback, device_ptr, packet_ptr): | |
79 | device = session.context._devices[int(device_ptr.this)] | |
80 | packet = Packet(session, packet_ptr) | |
81 | callback(device, packet) | |
82 | ||
83 | class Context(object): | |
84 | ||
85 | def __init__(self): | |
86 | context_ptr_ptr = new_sr_context_ptr_ptr() | |
87 | check(sr_init(context_ptr_ptr)) | |
88 | self.struct = sr_context_ptr_ptr_value(context_ptr_ptr) | |
89 | self._drivers = None | |
90 | self._devices = {} | |
a64198c8 ML |
91 | self._input_formats = None |
92 | self._output_formats = None | |
d8f6e041 ML |
93 | self.session = None |
94 | ||
95 | def __del__(self): | |
96 | sr_exit(self.struct) | |
97 | ||
98 | @property | |
99 | def drivers(self): | |
100 | if not self._drivers: | |
101 | self._drivers = {} | |
102 | driver_list = sr_driver_list() | |
103 | for i in itertools.count(): | |
104 | driver_ptr = sr_dev_driver_ptr_array_getitem(driver_list, i) | |
105 | if driver_ptr: | |
106 | self._drivers[driver_ptr.name] = Driver(self, driver_ptr) | |
107 | else: | |
108 | break | |
109 | return self._drivers | |
110 | ||
a64198c8 ML |
111 | @property |
112 | def input_formats(self): | |
113 | if not self._input_formats: | |
114 | self._input_formats = {} | |
115 | input_list = sr_input_list() | |
116 | for i in itertools.count(): | |
117 | input_ptr = sr_input_format_ptr_array_getitem(input_list, i) | |
118 | if input_ptr: | |
119 | self._input_formats[input_ptr.id] = InputFormat(self, input_ptr) | |
120 | else: | |
121 | break | |
122 | return self._input_formats | |
123 | ||
124 | @property | |
125 | def output_formats(self): | |
126 | if not self._output_formats: | |
127 | self._output_formats = {} | |
128 | output_list = sr_output_list() | |
129 | for i in itertools.count(): | |
130 | output_ptr = sr_output_format_ptr_array_getitem(output_list, i) | |
131 | if output_ptr: | |
132 | self._output_formats[output_ptr.id] = OutputFormat(self, output_ptr) | |
133 | else: | |
134 | break | |
135 | return self._output_formats | |
136 | ||
d8f6e041 ML |
137 | class Driver(object): |
138 | ||
139 | def __init__(self, context, struct): | |
140 | self.context = context | |
141 | self.struct = struct | |
142 | self._initialized = False | |
143 | ||
144 | @property | |
145 | def name(self): | |
146 | return self.struct.name | |
147 | ||
3124e80b | 148 | def scan(self, **kwargs): |
d8f6e041 ML |
149 | if not self._initialized: |
150 | check(sr_driver_init(self.context.struct, self.struct)) | |
151 | self._initialized = True | |
3124e80b ML |
152 | options = [] |
153 | for name, value in kwargs.items(): | |
01e9ff61 | 154 | key = getattr(ConfigKey, name) |
3124e80b ML |
155 | src = sr_config() |
156 | src.key = key.id | |
57dd5e63 | 157 | src.data = python_to_gvariant(value) |
3124e80b ML |
158 | options.append(src.this) |
159 | option_list = python_to_gslist(options) | |
160 | device_list = sr_driver_scan(self.struct, option_list) | |
161 | g_slist_free(option_list) | |
05cfe114 ML |
162 | devices = [Device(self, gpointer_to_sr_dev_inst_ptr(ptr)) |
163 | for ptr in gslist_to_python(device_list)] | |
d8f6e041 ML |
164 | g_slist_free(device_list) |
165 | return devices | |
166 | ||
167 | class Device(object): | |
168 | ||
169 | def __new__(cls, driver, struct): | |
170 | address = int(struct.this) | |
171 | if address not in driver.context._devices: | |
172 | device = super(Device, cls).__new__(cls, driver, struct) | |
173 | driver.context._devices[address] = device | |
174 | return driver.context._devices[address] | |
175 | ||
176 | def __init__(self, driver, struct): | |
177 | self.driver = driver | |
178 | self.struct = struct | |
417e9f3a ML |
179 | self._probes = None |
180 | self._probe_groups = None | |
d8f6e041 ML |
181 | |
182 | def __getattr__(self, name): | |
01e9ff61 | 183 | key = getattr(ConfigKey, name) |
d8f6e041 ML |
184 | data = new_gvariant_ptr_ptr() |
185 | try: | |
54e7a3d0 | 186 | check(sr_config_get(self.driver.struct, self.struct, None, |
945e23a5 | 187 | key.id, data)) |
d8f6e041 | 188 | except Error as error: |
0021b077 | 189 | if error.errno == SR_ERR_NA: |
1cad2115 | 190 | raise NotImplementedError( |
d8f6e041 ML |
191 | "Device does not implement %s" % name) |
192 | else: | |
193 | raise AttributeError | |
194 | value = gvariant_ptr_ptr_value(data) | |
195 | return gvariant_to_python(value) | |
196 | ||
197 | def __setattr__(self, name, value): | |
198 | try: | |
01e9ff61 | 199 | key = getattr(ConfigKey, name) |
d8f6e041 ML |
200 | except AttributeError: |
201 | super(Device, self).__setattr__(name, value) | |
202 | return | |
945e23a5 | 203 | check(sr_config_set(self.struct, None, key.id, python_to_gvariant(value))) |
d8f6e041 ML |
204 | |
205 | @property | |
206 | def vendor(self): | |
207 | return self.struct.vendor | |
208 | ||
209 | @property | |
210 | def model(self): | |
211 | return self.struct.model | |
212 | ||
213 | @property | |
214 | def version(self): | |
215 | return self.struct.version | |
216 | ||
417e9f3a ML |
217 | @property |
218 | def probes(self): | |
219 | if self._probes is None: | |
220 | self._probes = {} | |
221 | probe_list = self.struct.probes | |
222 | while (probe_list): | |
223 | probe_ptr = void_ptr_to_sr_probe_ptr(probe_list.data) | |
224 | self._probes[probe_ptr.name] = Probe(self, probe_ptr) | |
225 | probe_list = probe_list.next | |
226 | return self._probes | |
227 | ||
228 | @property | |
229 | def probe_groups(self): | |
230 | if self._probe_groups is None: | |
231 | self._probe_groups = {} | |
232 | probe_group_list = self.struct.probe_groups | |
233 | while (probe_group_list): | |
234 | probe_group_ptr = void_ptr_to_sr_probe_group_ptr( | |
235 | probe_group_list.data) | |
236 | self._probe_groups[probe_group_ptr.name] = ProbeGroup(self, | |
237 | probe_group_ptr) | |
238 | probe_group_list = probe_group_list.next | |
239 | return self._probe_groups | |
240 | ||
241 | class Probe(object): | |
242 | ||
243 | def __init__(self, device, struct): | |
244 | self.device = device | |
245 | self.struct = struct | |
246 | ||
247 | @property | |
248 | def type(self): | |
249 | return ProbeType(self.struct.type) | |
250 | ||
251 | @property | |
252 | def enabled(self): | |
253 | return self.struct.enabled | |
254 | ||
255 | @property | |
256 | def name(self): | |
257 | return self.struct.name | |
258 | ||
259 | class ProbeGroup(object): | |
260 | ||
261 | def __init__(self, device, struct): | |
262 | self.device = device | |
263 | self.struct = struct | |
264 | self._probes = None | |
265 | ||
266 | def __iter__(self): | |
267 | return iter(self.probes) | |
268 | ||
af54bac9 ML |
269 | def __getattr__(self, name): |
270 | key = config_key(name) | |
271 | data = new_gvariant_ptr_ptr() | |
272 | try: | |
273 | check(sr_config_get(self.device.driver.struct, self.device.struct, | |
945e23a5 | 274 | self.struct, key.id, data)) |
af54bac9 ML |
275 | except Error as error: |
276 | if error.errno == SR_ERR_NA: | |
277 | raise NotImplementedError( | |
278 | "Probe group does not implement %s" % name) | |
279 | else: | |
280 | raise AttributeError | |
281 | value = gvariant_ptr_ptr_value(data) | |
282 | return gvariant_to_python(value) | |
283 | ||
284 | def __setattr__(self, name, value): | |
285 | try: | |
286 | key = config_key(name) | |
287 | except AttributeError: | |
288 | super(ProbeGroup, self).__setattr__(name, value) | |
289 | return | |
290 | check(sr_config_set(self.device.struct, self.struct, | |
945e23a5 | 291 | key.id, python_to_gvariant(value))) |
af54bac9 | 292 | |
417e9f3a ML |
293 | @property |
294 | def name(self): | |
295 | return self.struct.name | |
296 | ||
297 | @property | |
298 | def probes(self): | |
299 | if self._probes is None: | |
300 | self._probes = [] | |
301 | probe_list = self.struct.probes | |
302 | while (probe_list): | |
303 | probe_ptr = void_ptr_to_sr_probe_ptr(probe_list.data) | |
304 | self._probes.append(Probe(self, probe_ptr)) | |
305 | probe_list = probe_list.next | |
306 | return self._probes | |
307 | ||
d8f6e041 ML |
308 | class Session(object): |
309 | ||
310 | def __init__(self, context): | |
311 | assert context.session is None | |
312 | self.context = context | |
313 | self.struct = sr_session_new() | |
314 | context.session = self | |
315 | ||
316 | def __del__(self): | |
317 | check(sr_session_destroy()) | |
318 | ||
319 | def add_device(self, device): | |
320 | check(sr_session_dev_add(device.struct)) | |
321 | ||
0e77b7ca UH |
322 | def open_device(self, device): |
323 | check(sr_dev_open(device.struct)) | |
324 | ||
d8f6e041 ML |
325 | def add_callback(self, callback): |
326 | wrapper = partial(callback_wrapper, self, callback) | |
327 | check(sr_session_datafeed_python_callback_add(wrapper)) | |
328 | ||
329 | def start(self): | |
330 | check(sr_session_start()) | |
331 | ||
332 | def run(self): | |
333 | check(sr_session_run()) | |
334 | ||
335 | def stop(self): | |
336 | check(sr_session_stop()) | |
337 | ||
338 | class Packet(object): | |
339 | ||
340 | def __init__(self, session, struct): | |
341 | self.session = session | |
342 | self.struct = struct | |
343 | self._payload = None | |
344 | ||
345 | @property | |
346 | def type(self): | |
9bbd6a6a | 347 | return PacketType(self.struct.type) |
d8f6e041 ML |
348 | |
349 | @property | |
350 | def payload(self): | |
351 | if self._payload is None: | |
352 | pointer = self.struct.payload | |
9bbd6a6a | 353 | if self.type == PacketType.LOGIC: |
d8f6e041 ML |
354 | self._payload = Logic(self, |
355 | void_ptr_to_sr_datafeed_logic_ptr(pointer)) | |
9bbd6a6a | 356 | elif self.type == PacketType.ANALOG: |
15574a3c ML |
357 | self._payload = Analog(self, |
358 | void_ptr_to_sr_datafeed_analog_ptr(pointer)) | |
d8f6e041 | 359 | else: |
1cad2115 | 360 | raise NotImplementedError( |
417e9f3a | 361 | "No Python mapping for packet type %s" % self.struct.type) |
d8f6e041 ML |
362 | return self._payload |
363 | ||
364 | class Logic(object): | |
365 | ||
366 | def __init__(self, packet, struct): | |
367 | self.packet = packet | |
368 | self.struct = struct | |
369 | self._data = None | |
370 | ||
371 | @property | |
372 | def data(self): | |
373 | if self._data is None: | |
374 | self._data = cdata(self.struct.data, self.struct.length) | |
375 | return self._data | |
376 | ||
15574a3c ML |
377 | class Analog(object): |
378 | ||
379 | def __init__(self, packet, struct): | |
380 | self.packet = packet | |
381 | self.struct = struct | |
382 | self._data = None | |
383 | ||
384 | @property | |
385 | def num_samples(self): | |
386 | return self.struct.num_samples | |
387 | ||
c2ec42ce UH |
388 | @property |
389 | def mq(self): | |
9bbd6a6a | 390 | return Quantity(self.struct.mq) |
c2ec42ce UH |
391 | |
392 | @property | |
393 | def unit(self): | |
9bbd6a6a | 394 | return Unit(self.struct.unit) |
c2ec42ce UH |
395 | |
396 | @property | |
397 | def mqflags(self): | |
9bbd6a6a | 398 | return QuantityFlag.set_from_mask(self.struct.mqflags) |
c2ec42ce | 399 | |
15574a3c ML |
400 | @property |
401 | def data(self): | |
402 | if self._data is None: | |
403 | self._data = float_array.frompointer(self.struct.data) | |
404 | return self._data | |
405 | ||
816aed6c UH |
406 | class Log(object): |
407 | ||
816aed6c UH |
408 | @property |
409 | def level(self): | |
9bbd6a6a | 410 | return LogLevel(sr_log_loglevel_get()) |
816aed6c UH |
411 | |
412 | @level.setter | |
413 | def level(self, l): | |
9bbd6a6a | 414 | check(sr_log_loglevel_set(l.id)) |
816aed6c UH |
415 | |
416 | @property | |
417 | def domain(self): | |
418 | return sr_log_logdomain_get() | |
419 | ||
420 | @domain.setter | |
421 | def domain(self, d): | |
422 | check(sr_log_logdomain_set(d)) | |
423 | ||
a64198c8 ML |
424 | class InputFormat(object): |
425 | ||
426 | def __init__(self, context, struct): | |
427 | self.context = context | |
428 | self.struct = struct | |
429 | ||
430 | @property | |
431 | def id(self): | |
432 | return self.struct.id | |
433 | ||
434 | @property | |
435 | def description(self): | |
436 | return self.struct.description | |
437 | ||
f0e764de ML |
438 | def format_match(self, filename): |
439 | return bool(self.struct.call_format_match(filename)) | |
440 | ||
441 | class InputFile(object): | |
442 | ||
443 | def __init__(self, format, filename, **kwargs): | |
444 | self.format = format | |
445 | self.filename = filename | |
446 | self.struct = sr_input() | |
447 | self.struct.format = self.format.struct | |
448 | self.struct.param = g_hash_table_new_full( | |
409d85b3 | 449 | g_str_hash_ptr, g_str_equal_ptr, g_free_ptr, g_free_ptr) |
f0e764de ML |
450 | for key, value in kwargs.items(): |
451 | g_hash_table_insert(self.struct.param, g_strdup(key), g_strdup(str(value))) | |
452 | check(self.format.struct.call_init(self.struct, self.filename)) | |
453 | ||
454 | def load(self): | |
455 | check(self.format.struct.call_loadfile(self.struct, self.filename)) | |
456 | ||
457 | def __del__(self): | |
458 | g_hash_table_destroy(self.struct.param) | |
459 | ||
a64198c8 ML |
460 | class OutputFormat(object): |
461 | ||
462 | def __init__(self, context, struct): | |
463 | self.context = context | |
464 | self.struct = struct | |
465 | ||
466 | @property | |
467 | def id(self): | |
468 | return self.struct.id | |
469 | ||
470 | @property | |
471 | def description(self): | |
472 | return self.struct.description | |
473 | ||
409d85b3 ML |
474 | class Output(object): |
475 | ||
476 | def __init__(self, format, device, param=None): | |
477 | self.format = format | |
478 | self.device = device | |
479 | self.param = param | |
480 | self.struct = sr_output() | |
481 | self.struct.format = self.format.struct | |
482 | self.struct.sdi = self.device.struct | |
483 | self.struct.param = param | |
484 | check(self.format.struct.call_init(self.struct)) | |
485 | ||
486 | def receive(self, packet): | |
487 | ||
488 | output_buf_ptr = new_uint8_ptr_ptr() | |
489 | output_len_ptr = new_uint64_ptr() | |
490 | using_obsolete_api = False | |
491 | ||
492 | if self.format.struct.event and packet.type in ( | |
493 | PacketType.TRIGGER, PacketType.FRAME_BEGIN, | |
494 | PacketType.FRAME_END, PacketType.END): | |
495 | check(self.format.struct.call_event(self.struct, packet.type.id, | |
496 | output_buf_ptr, output_len_ptr)) | |
497 | using_obsolete_api = True | |
498 | elif self.format.struct.data and packet.type.id == self.format.struct.df_type: | |
499 | check(self.format.struct.call_data(self.struct, | |
500 | packet.payload.struct.data, packet.payload.struct.length, | |
501 | output_buf_ptr, output_len_ptr)) | |
502 | using_obsolete_api = True | |
503 | ||
504 | if using_obsolete_api: | |
505 | output_buf = uint8_ptr_ptr_value(output_buf_ptr) | |
506 | output_len = uint64_ptr_value(output_len_ptr) | |
507 | result = cdata(output_buf, output_len) | |
508 | g_free(output_buf) | |
509 | return result | |
510 | ||
511 | if self.format.struct.receive: | |
512 | out_ptr = new_gstring_ptr_ptr() | |
513 | check(self.format.struct.call_receive(self.struct, self.device.struct, | |
514 | packet.struct, out_ptr)) | |
515 | out = gstring_ptr_ptr_value(out_ptr) | |
516 | if out: | |
517 | result = out.str | |
518 | g_string_free(out, True) | |
519 | return result | |
520 | ||
521 | return None | |
522 | ||
523 | def __del__(self): | |
524 | check(self.format.struct.call_cleanup(self.struct)) | |
525 | ||
8593c8e3 ML |
526 | class ConfigInfo(object): |
527 | ||
14e8eb33 ML |
528 | def __new__(cls, key): |
529 | struct = sr_config_info_get(key.id) | |
530 | if not struct: | |
531 | return None | |
532 | obj = super(ConfigInfo, cls).__new__(cls) | |
533 | obj.key = key | |
534 | obj.struct = struct | |
535 | return obj | |
8593c8e3 ML |
536 | |
537 | @property | |
538 | def datatype(self): | |
539 | return DataType(self.struct.datatype) | |
540 | ||
541 | @property | |
542 | def id(self): | |
543 | return self.struct.id | |
544 | ||
545 | @property | |
546 | def name(self): | |
547 | return self.struct.name | |
548 | ||
549 | @property | |
550 | def description(self): | |
551 | return self.struct.description | |
552 | ||
9bbd6a6a ML |
553 | class EnumValue(object): |
554 | ||
555 | _enum_values = {} | |
556 | ||
557 | def __new__(cls, id): | |
558 | if cls not in cls._enum_values: | |
559 | cls._enum_values[cls] = {} | |
560 | if id not in cls._enum_values[cls]: | |
561 | value = super(EnumValue, cls).__new__(cls) | |
562 | value.id = id | |
563 | cls._enum_values[cls][id] = value | |
564 | return cls._enum_values[cls][id] | |
565 | ||
566 | class LogLevel(EnumValue): | |
567 | pass | |
568 | ||
569 | class PacketType(EnumValue): | |
570 | pass | |
571 | ||
572 | class Quantity(EnumValue): | |
573 | pass | |
574 | ||
575 | class Unit(EnumValue): | |
576 | pass | |
577 | ||
578 | class QuantityFlag(EnumValue): | |
579 | ||
580 | @classmethod | |
581 | def set_from_mask(cls, mask): | |
582 | result = set() | |
583 | while mask: | |
584 | new_mask = mask & (mask - 1) | |
585 | result.add(cls(mask ^ new_mask)) | |
586 | mask = new_mask | |
587 | return result | |
588 | ||
f245b766 ML |
589 | class ConfigKey(EnumValue): |
590 | pass | |
591 | ||
8593c8e3 ML |
592 | class DataType(EnumValue): |
593 | pass | |
594 | ||
417e9f3a ML |
595 | class ProbeType(EnumValue): |
596 | pass | |
597 | ||
d8f6e041 | 598 | for symbol_name in dir(lowlevel): |
9bbd6a6a ML |
599 | for prefix, cls in [ |
600 | ('SR_LOG_', LogLevel), | |
601 | ('SR_DF_', PacketType), | |
602 | ('SR_MQ_', Quantity), | |
603 | ('SR_UNIT_', Unit), | |
f245b766 | 604 | ('SR_MQFLAG_', QuantityFlag), |
417e9f3a | 605 | ('SR_CONF_', ConfigKey), |
8593c8e3 | 606 | ('SR_T_', DataType), |
417e9f3a | 607 | ('SR_PROBE_', ProbeType)]: |
9bbd6a6a ML |
608 | if symbol_name.startswith(prefix): |
609 | name = symbol_name[len(prefix):] | |
610 | value = getattr(lowlevel, symbol_name) | |
cad0acef ML |
611 | obj = cls(value) |
612 | setattr(cls, name, obj) | |
613 | if cls is ConfigKey: | |
614 | obj.info = ConfigInfo(obj) | |
615 | if obj.info: | |
616 | setattr(cls, obj.info.id, obj) | |
01e9ff61 ML |
617 | else: |
618 | setattr(cls, name.lower(), obj) |