aboutsummaryrefslogtreecommitdiff
path: root/diplomacy/utils/jsonable.py
diff options
context:
space:
mode:
authorPhilip Paquette <pcpaquette@gmail.com>2018-09-26 07:48:55 -0400
committerPhilip Paquette <pcpaquette@gmail.com>2019-04-18 11:14:24 -0400
commit6187faf20384b0c5a4966343b2d4ca47f8b11e45 (patch)
tree151ccd21aea20180432c13fe4b58240d3d9e98b6 /diplomacy/utils/jsonable.py
parent96b7e2c03ed98705754f13ae8efa808b948ee3a8 (diff)
Release v1.0.0 - Diplomacy Game Engine - AGPL v3+ License
Diffstat (limited to 'diplomacy/utils/jsonable.py')
-rw-r--r--diplomacy/utils/jsonable.py141
1 files changed, 141 insertions, 0 deletions
diff --git a/diplomacy/utils/jsonable.py b/diplomacy/utils/jsonable.py
new file mode 100644
index 0000000..5e558ee
--- /dev/null
+++ b/diplomacy/utils/jsonable.py
@@ -0,0 +1,141 @@
+# ==============================================================================
+# Copyright (C) 2019 - Philip Paquette, Steven Bocco
+#
+# This program is free software: you can redistribute it and/or modify it under
+# the terms of the GNU Affero General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option) any
+# later version.
+#
+# This program is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+# details.
+#
+# You should have received a copy of the GNU Affero General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+# ==============================================================================
+""" Abstract Jsonable class with automatic attributes checking and conversion to/from JSON dict.
+ To write a Jsonable sub-class:
+ - Define a model with expected attribute names and types. Use module `parsing` to describe expected types.
+ - Override initializer __init__(**kwargs):
+ - **first**: initialize each attribute defined in model with value None.
+ - **then** : call parent __init__() method. Attributes will be checked and filled by
+ Jsonable's __init__() method.
+ - If needed, add further initialization code after call to parent __init__() method. At this point,
+ attributes were correctly set based on defined model, and you can now work with them.
+
+ Example:
+ ```
+ class MyClass(Jsonable):
+ model = {
+ 'my_attribute': parsing.Sequence(int),
+ }
+ def __init__(**kwargs):
+ self.my_attribute = None
+ super(MyClass, self).__init__(**kwargs)
+ # my_attribute is now initialized based on model. You can then do any further initialization if needed.
+ ```
+"""
+import logging
+import ujson as json
+
+from diplomacy.utils import exceptions, parsing
+
+LOGGER = logging.getLogger(__name__)
+
+class Jsonable():
+ """ Abstract class to ease conversion from/to JSON dict. """
+ __slots__ = []
+ __cached__models__ = {}
+ model = {}
+
+ def __init__(self, **kwargs):
+ """ Validates given arguments, update them if necessary (e.g. to add default values),
+ and fill instance attributes with updated argument.
+ If a derived class adds new attributes, it must override __init__() method and
+ initialize new attributes (e.g. `self.attribute = None`)
+ **BEFORE** calling parent __init__() method.
+
+ :param kwargs: arguments to build class. Must match keys and values types defined in model.
+ """
+ model = self.get_model()
+
+ # Adding default value
+ updated_kwargs = {model_key: None for model_key in model}
+ updated_kwargs.update(kwargs)
+
+ # Validating and updating
+ try:
+ parsing.validate_data(updated_kwargs, model)
+ except exceptions.TypeException as exception:
+ LOGGER.error('Error occurred while building class %s', self.__class__)
+ raise exception
+ updated_kwargs = parsing.update_data(updated_kwargs, model)
+
+ # Building.
+ for model_key in model:
+ setattr(self, model_key, updated_kwargs[model_key])
+
+ def json(self):
+ """ Convert this object to a JSON string ready to be sent/saved.
+ :return: string
+ """
+ return json.dumps(self.to_dict())
+
+ def to_dict(self):
+ """ Convert this object to a python dictionary ready for any JSON work.
+ :return: dict
+ """
+ model = self.get_model()
+ return {key: parsing.to_json(getattr(self, key), key_type) for key, key_type in model.items()}
+
+ @classmethod
+ def update_json_dict(cls, json_dict):
+ """ Update a JSON dictionary before being parsed with class model.
+ JSON dictionary is passed by class method from_dict() (see below), and is guaranteed to contain
+ at least all expected model keys. Some keys may be associated to None if initial JSON dictionary
+ did not provide values for them.
+ :param json_dict: a JSON dictionary to be parsed.
+ :type json_dict: dict
+ """
+ pass
+
+ @classmethod
+ def from_dict(cls, json_dict):
+ """ Convert a JSON dictionary to an instance of this class.
+ :param json_dict: a JSON dictionary to parse. Dictionary with basic types (int, bool, dict, str, None, etc.)
+ :return: an instance from this class or from a derived one from which it's called.
+ :rtype: cls
+ """
+ model = cls.get_model()
+
+ # json_dict must be a a dictionary
+ if not isinstance(json_dict, dict):
+ raise exceptions.TypeException(dict, type(json_dict))
+
+ # By default, we set None for all expected keys
+ default_json_dict = {key: None for key in model}
+ default_json_dict.update(json_dict)
+ cls.update_json_dict(json_dict)
+
+ # Building this object
+ # NB: We don't care about extra keys in provided dict, we just focus on expected keys, nothing more.
+ kwargs = {key: parsing.to_type(default_json_dict[key], key_type) for key, key_type in model.items()}
+ return cls(**kwargs)
+
+ @classmethod
+ def build_model(cls):
+ """ Return model associated to current class. You can either define model class field
+ or override this function.
+ """
+ return cls.model
+
+ @classmethod
+ def get_model(cls):
+ """ Return model associated to current class, and cache it for future uses, to avoid
+ multiple rendering of model for each class derived from Jsonable. Private method.
+ :return: dict: model associated to current class.
+ """
+ if cls not in cls.__cached__models__:
+ cls.__cached__models__[cls] = cls.build_model()
+ return cls.__cached__models__[cls]