1 # Copyright (C) 2013 Nippon Telegraph and Telephone Corporation.
2 # Copyright (C) 2013 YAMAMOTO Takashi <yamamoto at valinux co jp>
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
8 # http://www.apache.org/licenses/LICENSE-2.0
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 from __future__ import print_function
24 # Some arguments to __init__ is mangled in order to avoid name conflicts
26 # The standard mangling is to append '_' in order to avoid name clashes
27 # with reserved keywords.
30 # Function and method arguments
31 # If a function argument's name clashes with a reserved keyword,
32 # it is generally better to append a single trailing underscore
33 # rather than use an abbreviation or spelling corruption. Thus
34 # class_ is better than clss. (Perhaps better is to avoid such
35 # clashes by using a synonym.)
37 # grep __init__ *.py | grep '[^_]_\>' showed that
38 # 'len', 'property', 'set', 'type'
39 # A bit more generic way is adopted
41 _RESERVED_KEYWORD = dir(six.moves.builtins)
43 _mapdict = lambda f, d: dict([(k, f(v)) for k, v in d.items()])
44 _mapdict_key = lambda f, d: dict([(f(k), v) for k, v in d.items()])
45 _mapdict_kv = lambda f, d: dict([(k, f(k, v)) for k, v in d.items()])
48 class TypeDescr(object):
52 class AsciiStringType(TypeDescr):
55 # TODO: AsciiStringType data should probably be stored as
56 # text_type in class data. This isinstance() check exists
57 # because OFPDescStats violates this.
58 if six.PY3 and isinstance(v, six.text_type):
60 return six.text_type(v, 'ascii')
66 return v.encode('ascii')
69 class Utf8StringType(TypeDescr):
72 return six.text_type(v, 'utf-8')
76 return v.encode('utf-8')
79 class AsciiStringListType(TypeDescr):
82 return [AsciiStringType.encode(x) for x in v]
86 return [AsciiStringType.decode(x) for x in v]
89 class NXFlowSpecFieldType(TypeDescr):
90 # ("field_name", 0) <-> ["field_name", 0]
94 if not isinstance(v, tuple):
101 if not isinstance(v, list):
108 'ascii': AsciiStringType,
109 'utf-8': Utf8StringType,
110 'asciilist': AsciiStringListType,
111 'nx-flow-spec-field': NXFlowSpecFieldType, # XXX this should not be here
115 class StringifyMixin(object):
118 """_TYPE class attribute is used to annotate types of attributes.
120 This type information is used to find an appropriate conversion for
121 a JSON style dictionary.
123 Currently the following types are implemented.
125 ========= =============
127 ========= =============
130 asciilist list of ascii
131 ========= =============
147 # List of attributes ignored in the str and json representations.
148 _base_attributes = []
150 # Optional attributes included in the str and json representations.
151 # e.g.) In case of attributes are property, the attributes will be
152 # skipped in the str and json representations.
153 # Then, please specify the attributes into this list.
156 def stringify_attrs(self):
157 """an override point for sub classes"""
158 return obj_python_attrs(self)
161 # repr() to escape binaries
162 return self.__class__.__name__ + '(' + \
163 ','.join("%s=%s" % (k, repr(v)) for k, v in
164 self.stringify_attrs()) + ')'
165 __repr__ = __str__ # note: str(list) uses __repr__ for elements
168 def _is_class(cls, dict_):
169 # we distinguish a dict like OFPSwitchFeatures.ports
170 # from OFPxxx classes using heuristics.
171 # Examples of OFP classes:
172 # {"OFPMatch": { ... }}
173 # {"MTIPv6SRC": { ... }}
174 assert isinstance(dict_, dict)
177 k = list(dict_.keys())[0]
178 if not isinstance(k, (bytes, six.text_type)):
180 for p in cls._class_prefixes:
183 for p in cls._class_suffixes:
189 def _get_type(cls, k):
190 if hasattr(cls, '_TYPE'):
191 for t, attrs in cls._TYPE.items():
197 def _get_encoder(cls, k, encode_string):
201 return cls._get_default_encoder(encode_string)
204 def _encode_value(cls, k, v, encode_string=base64.b64encode):
205 return cls._get_encoder(k, encode_string)(v)
208 def _get_default_encoder(cls, encode_string):
210 if isinstance(v, (bytes, six.text_type)):
211 if isinstance(v, six.text_type):
212 v = v.encode('utf-8')
213 json_value = encode_string(v)
215 json_value = json_value.decode('ascii')
216 elif isinstance(v, list):
217 json_value = [_encode(ve) for ve in v]
218 elif isinstance(v, dict):
219 json_value = _mapdict(_encode, v)
220 # while a python dict key can be any hashable object,
221 # a JSON object key should be a string.
222 json_value = _mapdict_key(str, json_value)
223 assert not cls._is_class(json_value)
226 json_value = v.to_jsondict()
232 def to_jsondict(self, encode_string=base64.b64encode):
234 This method returns a JSON style dict to describe this object.
236 The returned dict is compatible with json.dumps() and json.loads().
238 Suppose ClassName object inherits StringifyMixin.
239 For an object like the following::
241 ClassName(Param1=100, Param2=200)
243 this method would produce::
245 { "ClassName": {"Param1": 100, "Param2": 200} }
247 This method takes the following arguments.
249 .. tabularcolumns:: |l|L|
251 ============= =====================================================
253 ============= =====================================================
254 encode_string (Optional) specify how to encode attributes which has
256 The default is base64.
257 This argument is used only for attributes which don't
258 have explicit type annotations in _TYPE class attribute.
259 ============= =====================================================
262 encode = lambda key, val: self._encode_value(key, val, encode_string)
263 for k, v in obj_attrs(self):
264 dict_[k] = encode(k, v)
265 return {self.__class__.__name__: dict_}
268 def cls_from_jsondict_key(cls, k):
269 # find a class with the given name from our class' module.
271 mod = sys.modules[cls.__module__]
272 return getattr(mod, k)
275 def obj_from_jsondict(cls, jsondict, **additional_args):
276 assert len(jsondict) == 1
277 for k, v in jsondict.items():
278 obj_cls = cls.cls_from_jsondict_key(k)
279 return obj_cls.from_jsondict(v, **additional_args)
282 def _get_decoder(cls, k, decode_string):
286 return cls._get_default_decoder(decode_string)
289 def _decode_value(cls, k, json_value, decode_string=base64.b64decode,
291 # Note: To avoid passing redundant arguments (e.g. 'datapath' for
292 # non OFP classes), we omit '**additional_args' here.
293 return cls._get_decoder(k, decode_string)(json_value)
296 def _get_default_decoder(cls, decode_string):
297 def _decode(json_value, **additional_args):
298 if isinstance(json_value, (bytes, six.text_type)):
299 v = decode_string(json_value)
300 elif isinstance(json_value, list):
301 v = [_decode(jv) for jv in json_value]
302 elif isinstance(json_value, dict):
303 if cls._is_class(json_value):
304 v = cls.obj_from_jsondict(json_value, **additional_args)
306 v = _mapdict(_decode, json_value)
308 # try to restore integer keys used by
309 # OFPSwitchFeatures.ports.
311 v = _mapdict_key(int, v)
320 def _restore_args(dict_):
322 if k in _RESERVED_KEYWORD:
325 return _mapdict_key(restore, dict_)
328 def from_jsondict(cls, dict_, decode_string=base64.b64decode,
330 r"""Create an instance from a JSON style dict.
332 Instantiate this class with parameters specified by the dict.
334 This method takes the following arguments.
336 .. tabularcolumns:: |l|L|
338 =============== =====================================================
340 =============== =====================================================
341 dict\_ A dictionary which describes the parameters.
342 For example, {"Param1": 100, "Param2": 200}
343 decode_string (Optional) specify how to decode strings.
344 The default is base64.
345 This argument is used only for attributes which don't
346 have explicit type annotations in _TYPE class
348 additional_args (Optional) Additional kwargs for constructor.
349 =============== =====================================================
351 decode = lambda k, x: cls._decode_value(k, x, decode_string,
353 kwargs = cls._restore_args(_mapdict_kv(decode, dict_))
355 return cls(**dict(kwargs, **additional_args))
358 print("CLS %s" % cls)
359 print("ARG %s" % dict_)
360 print("KWARG %s" % kwargs)
364 def set_classes(cls, registered_dict):
365 cls._class_prefixes.extend([v.__name__ for v in
366 registered_dict.values()])
369 def obj_python_attrs(msg_):
370 """iterate object attributes for stringify purposes
373 # a special case for namedtuple which seems widely used in
374 # ofp parser implementations.
375 if hasattr(msg_, '_fields'):
376 for k in msg_._fields:
377 yield(k, getattr(msg_, k))
379 base = getattr(msg_, '_base_attributes', [])
380 opt = getattr(msg_, '_opt_attributes', [])
381 for k, v in inspect.getmembers(msg_):
384 elif k.startswith('_'):
390 elif hasattr(msg_.__class__, k):
396 """similar to obj_python_attrs() but deals with python reserved keywords
399 if isinstance(msg_, StringifyMixin):
400 itr = msg_.stringify_attrs()
402 # probably called by msg_str_attr
403 itr = obj_python_attrs(msg_)
405 if k.endswith('_') and k[:-1] in _RESERVED_KEYWORD:
406 # XXX currently only StringifyMixin has restoring logic
407 assert isinstance(msg_, StringifyMixin)