You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

292 lines
9.4 KiB

4 years ago
  1. #!/usr/bin/python
  2. # -- Content-Encoding: UTF-8 --
  3. """
  4. The serialization module
  5. :authors: Josh Marshall, Thomas Calmant
  6. :copyright: Copyright 2018, Thomas Calmant
  7. :license: Apache License 2.0
  8. :version: 0.3.2
  9. ..
  10. Copyright 2018 Thomas Calmant
  11. Licensed under the Apache License, Version 2.0 (the "License");
  12. you may not use this file except in compliance with the License.
  13. You may obtain a copy of the License at
  14. http://www.apache.org/licenses/LICENSE-2.0
  15. Unless required by applicable law or agreed to in writing, software
  16. distributed under the License is distributed on an "AS IS" BASIS,
  17. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18. See the License for the specific language governing permissions and
  19. limitations under the License.
  20. """
  21. # Standard library
  22. import inspect
  23. import re
  24. # Local package
  25. import jsonrpclib.config
  26. import jsonrpclib.utils as utils
  27. # ------------------------------------------------------------------------------
  28. # Module version
  29. __version_info__ = (0, 3, 2)
  30. __version__ = ".".join(str(x) for x in __version_info__)
  31. # Documentation strings format
  32. __docformat__ = "restructuredtext en"
  33. # ------------------------------------------------------------------------------
  34. # Supported transmitted code
  35. SUPPORTED_TYPES = (utils.DictType,) + utils.ITERABLE_TYPES \
  36. + utils.PRIMITIVE_TYPES
  37. # Regex of invalid module characters
  38. INVALID_MODULE_CHARS = r'[^a-zA-Z0-9\_\.]'
  39. # ------------------------------------------------------------------------------
  40. class TranslationError(Exception):
  41. """
  42. Unmarshalling exception
  43. """
  44. pass
  45. def _slots_finder(clazz, fields_set):
  46. """
  47. Recursively visits the class hierarchy to find all slots
  48. :param clazz: Class to analyze
  49. :param fields_set: Set where to store __slots___ content
  50. """
  51. # ... class level
  52. try:
  53. fields_set.update(clazz.__slots__)
  54. except AttributeError:
  55. pass
  56. # ... parent classes level
  57. for base_class in clazz.__bases__:
  58. _slots_finder(base_class, fields_set)
  59. def _find_fields(obj):
  60. """
  61. Returns the names of the fields of the given object
  62. :param obj: An object to analyze
  63. :return: A set of field names
  64. """
  65. # Find fields...
  66. fields = set()
  67. # ... using __dict__
  68. try:
  69. fields.update(obj.__dict__)
  70. except AttributeError:
  71. pass
  72. # ... using __slots__
  73. _slots_finder(obj.__class__, fields)
  74. return fields
  75. def dump(obj, serialize_method=None, ignore_attribute=None, ignore=None,
  76. config=jsonrpclib.config.DEFAULT):
  77. """
  78. Transforms the given object into a JSON-RPC compliant form.
  79. Converts beans into dictionaries with a __jsonclass__ entry.
  80. Doesn't change primitive types.
  81. :param obj: An object to convert
  82. :param serialize_method: Custom serialization method
  83. :param ignore_attribute: Name of the object attribute containing the names
  84. of members to ignore
  85. :param ignore: A list of members to ignore
  86. :param config: A JSONRPClib Config instance
  87. :return: A JSON-RPC compliant object
  88. """
  89. # Normalize arguments
  90. serialize_method = serialize_method or config.serialize_method
  91. ignore_attribute = ignore_attribute or config.ignore_attribute
  92. ignore = ignore or []
  93. # Parse / return default "types"...
  94. # Apply additional types, override built-in types
  95. # (reminder: config.serialize_handlers is a dict)
  96. try:
  97. serializer = config.serialize_handlers[type(obj)]
  98. except KeyError:
  99. # Not a serializer
  100. pass
  101. else:
  102. if serializer is not None:
  103. return serializer(obj, serialize_method, ignore_attribute,
  104. ignore, config)
  105. # Primitive
  106. if isinstance(obj, utils.PRIMITIVE_TYPES):
  107. return obj
  108. # Iterative
  109. elif isinstance(obj, utils.ITERABLE_TYPES):
  110. # List, set or tuple
  111. return [dump(item, serialize_method, ignore_attribute, ignore, config)
  112. for item in obj]
  113. elif isinstance(obj, utils.DictType):
  114. # Dictionary
  115. return {key: dump(value, serialize_method, ignore_attribute,
  116. ignore, config)
  117. for key, value in obj.items()}
  118. # It's not a standard type, so it needs __jsonclass__
  119. module_name = inspect.getmodule(type(obj)).__name__
  120. json_class = obj.__class__.__name__
  121. if module_name not in ('', '__main__'):
  122. json_class = '{0}.{1}'.format(module_name, json_class)
  123. # Keep the class name in the returned object
  124. return_obj = {"__jsonclass__": [json_class]}
  125. # If a serialization method is defined..
  126. if hasattr(obj, serialize_method):
  127. # Params can be a dict (keyword) or list (positional)
  128. # Attrs MUST be a dict.
  129. serialize = getattr(obj, serialize_method)
  130. params, attrs = serialize()
  131. return_obj['__jsonclass__'].append(params)
  132. return_obj.update(attrs)
  133. elif utils.is_enum(obj):
  134. # Add parameters for enumerations
  135. return_obj['__jsonclass__'].append([obj.value])
  136. else:
  137. # Otherwise, try to figure it out
  138. # Obviously, we can't assume to know anything about the
  139. # parameters passed to __init__
  140. return_obj['__jsonclass__'].append([])
  141. # Prepare filtering lists
  142. known_types = SUPPORTED_TYPES + tuple(config.serialize_handlers)
  143. ignore_list = getattr(obj, ignore_attribute, []) + ignore
  144. # Find fields and filter them by name
  145. fields = _find_fields(obj)
  146. fields.difference_update(ignore_list)
  147. # Dump field values
  148. attrs = {}
  149. for attr_name in fields:
  150. attr_value = getattr(obj, attr_name)
  151. if isinstance(attr_value, known_types) and \
  152. attr_value not in ignore_list:
  153. attrs[attr_name] = dump(attr_value, serialize_method,
  154. ignore_attribute, ignore, config)
  155. return_obj.update(attrs)
  156. return return_obj
  157. # ------------------------------------------------------------------------------
  158. def load(obj, classes=None):
  159. """
  160. If 'obj' is a dictionary containing a __jsonclass__ entry, converts the
  161. dictionary item into a bean of this class.
  162. :param obj: An object from a JSON-RPC dictionary
  163. :param classes: A custom {name: class} dictionary
  164. :return: The loaded object
  165. """
  166. # Primitive
  167. if isinstance(obj, utils.PRIMITIVE_TYPES):
  168. return obj
  169. # List, set or tuple
  170. elif isinstance(obj, utils.ITERABLE_TYPES):
  171. # This comes from a JSON parser, so it can only be a list...
  172. return [load(entry) for entry in obj]
  173. # Otherwise, it's a dict type
  174. elif '__jsonclass__' not in obj:
  175. return {key: load(value) for key, value in obj.items()}
  176. # It's a dictionary, and it has a __jsonclass__
  177. orig_module_name = obj['__jsonclass__'][0]
  178. params = obj['__jsonclass__'][1]
  179. # Validate the module name
  180. if not orig_module_name:
  181. raise TranslationError('Module name empty.')
  182. json_module_clean = re.sub(INVALID_MODULE_CHARS, '', orig_module_name)
  183. if json_module_clean != orig_module_name:
  184. raise TranslationError('Module name {0} has invalid characters.'
  185. .format(orig_module_name))
  186. # Load the class
  187. json_module_parts = json_module_clean.split('.')
  188. if classes and len(json_module_parts) == 1:
  189. # Local class name -- probably means it won't work
  190. try:
  191. json_class = classes[json_module_parts[0]]
  192. except KeyError:
  193. raise TranslationError('Unknown class or module {0}.'
  194. .format(json_module_parts[0]))
  195. else:
  196. # Module + class
  197. json_class_name = json_module_parts.pop()
  198. json_module_tree = '.'.join(json_module_parts)
  199. try:
  200. # Use fromlist to load the module itself, not the package
  201. temp_module = __import__(json_module_tree,
  202. fromlist=[json_class_name])
  203. except ImportError:
  204. raise TranslationError('Could not import {0} from module {1}.'
  205. .format(json_class_name, json_module_tree))
  206. try:
  207. json_class = getattr(temp_module, json_class_name)
  208. except AttributeError:
  209. raise TranslationError("Unknown class {0}.{1}."
  210. .format(json_module_tree, json_class_name))
  211. # Create the object
  212. if isinstance(params, utils.ListType):
  213. try:
  214. new_obj = json_class(*params)
  215. except TypeError as ex:
  216. raise TranslationError("Error instantiating {0}: {1}"
  217. .format(json_class.__name__, ex))
  218. elif isinstance(params, utils.DictType):
  219. try:
  220. new_obj = json_class(**params)
  221. except TypeError as ex:
  222. raise TranslationError("Error instantiating {0}: {1}"
  223. .format(json_class.__name__, ex))
  224. else:
  225. raise TranslationError("Constructor args must be a dict or a list, "
  226. "not {0}".format(type(params).__name__))
  227. # Remove the class information, as it must be ignored during the
  228. # reconstruction of the object
  229. raw_jsonclass = obj.pop('__jsonclass__')
  230. for key, value in obj.items():
  231. # Recursive loading
  232. setattr(new_obj, key, load(value, classes))
  233. # Restore the class information for further usage
  234. obj['__jsonclass__'] = raw_jsonclass
  235. return new_obj