From 4359316b10e4d0c382ac74c5ffba850a854f2e4a Mon Sep 17 00:00:00 2001 From: Yingtong Li Date: Mon, 25 Sep 2017 17:18:02 +1000 Subject: [PATCH] Combine JS and Python eos.core.objects implementations --- build_js.sh | 3 + eos/core/objects/__init__.py | 285 ++++++++++++++++++++++++++++++++++- eos/core/objects/js.py | 233 ---------------------------- eos/core/objects/python.py | 222 --------------------------- 4 files changed, 286 insertions(+), 457 deletions(-) delete mode 100644 eos/core/objects/js.py delete mode 100644 eos/core/objects/python.py diff --git a/build_js.sh b/build_js.sh index ce435f8..faa1676 100755 --- a/build_js.sh +++ b/build_js.sh @@ -32,4 +32,7 @@ for f in eos.js_tests; do # Disable handling of special attributes perl -0777 -pi -e "s/var __specialattrib__ = function \(attrib\) \{/var __specialattrib__ = function (attrib) { return false;/g" eos/__javascript__/$f.js + + # Transcrypt bug + perl -0777 -pi -e "s/EosObject.EosObject/EosObject/g" eos/__javascript__/$f.js done diff --git a/eos/core/objects/__init__.py b/eos/core/objects/__init__.py index 7e5e9bb..f0f070b 100644 --- a/eos/core/objects/__init__.py +++ b/eos/core/objects/__init__.py @@ -22,9 +22,290 @@ except: def __pragma__(*args): pass +# Libraries +# ========= + if is_python: __pragma__('skip') - from eos.core.objects.python import * + import pymongo + from bson.binary import UUIDLegacy + + import base64 + import hashlib + import json + import uuid __pragma__('noskip') else: - from eos.core.objects.js import * + # Load json.js, jssha-sha256.js + lib = __pragma__('js', ''' + (function() {{ + {} + {} + var exports = {{}}; + exports.stringify = stringify_main; + exports.jsSHA = window.jsSHA; + return exports; + }})()''', __include__('eos/core/objects/json.js'), __include__('eos/core/objects/jssha-sha256.js')) + +# Database +# ======== + +if is_python: + client = pymongo.MongoClient() + db = client['test'] + +# Fields +# ====== + +class Field: + def __init__(self, *args, **kwargs): + self.default = kwargs.get('default', kwargs.get('py_default', None)) + self.hashed = kwargs.get('hashed', True) + +class PrimitiveField(Field): + def serialise(self, value): + return value + + def deserialise(self, value): + return value + +DictField = PrimitiveField +IntField = PrimitiveField +ListField = PrimitiveField +StringField = PrimitiveField + +class EmbeddedObjectField(Field): + def __init__(self, object_type=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.object_type = object_type + + def serialise(self, value): + return EosObject.serialise_and_wrap(value, self.object_type) + + def deserialise(self, value): + return EosObject.deserialise_and_unwrap(value, self.object_type) + +class ListField(Field): + def __init__(self, element_field=None, *args, **kwargs): + super().__init__(default=EosList, *args, **kwargs) + self.element_field = element_field + + def serialise(self, value): + return [self.element_field.serialise(x) for x in value] + + def deserialise(self, value): + return [self.element_field.deserialise(x) for x in value] + +class EmbeddedObjectListField(Field): + def __init__(self, object_type=None, *args, **kwargs): + super().__init__(default=EosList, *args, **kwargs) + self.object_type = object_type + + def serialise(self, value): + return [EosObject.serialise_and_wrap(x, self.object_type) for x in value] + + def deserialise(self, value): + return [EosObject.deserialise_and_unwrap(x, self.object_type) for x in value] + +if is_python: + class UUIDField(Field): + def __init__(self, *args, **kwargs): + super().__init__(default=uuid.uuid4, *args, **kwargs) + + def serialise(self, value): + return str(value) + + def deserialise(self, value): + return uuid.UUID(value) +else: + UUIDField = PrimitiveField + +# Objects +# ======= + +class EosObjectType(type): + def __new__(meta, name, bases, attrs): + cls = type.__new__(meta, name, bases, attrs) + cls._name = ((cls.__module__ if is_python else meta.__next_class_module__) + '.' + cls.__name__).replace('.js.', '.').replace('.python.', '.') #TNYI: module and qualname + if name != 'EosObject': + EosObject.objects[cls._name] = cls + return cls + +class EosObject(metaclass=EosObjectType): + objects = {} + + def __init__(self): + self._instance = (None, None) + self._inited = False + + def post_init(self): + self._inited = True + + def recurse_parents(self, cls): + if isinstance(self, cls): + return self + if self._instance[0]: + return self._instance[0].recurse_parents(cls) + return None + + @property + def hash(self): + return EosObject.to_sha256(EosObject.serialise_and_wrap(self)) + + @staticmethod + def serialise_and_wrap(value, object_type=None): + if object_type: + return value.serialise() + return {'type': value._name, 'value': value.serialise()} + + @staticmethod + def deserialise_and_unwrap(value, object_type=None): + if object_type: + return object_type.deserialise(value) + return EosObject.objects[value['type']].deserialise(value['value']) + + @staticmethod + def to_json(value): + if is_python: + return json.dumps(value, sort_keys=True) + else: + return lib.stringify(value) + + @staticmethod + def from_json(value): + if is_python: + return json.loads(value) + else: + return JSON.parse(value) + + @staticmethod + def to_sha256(value): + if is_python: + sha_obj = hashlib.sha256() + sha_obj.update(value.encode('utf-8')) + return base64.b64encode(sha_obj.digest()).decode('utf-8') + else: + # TNYI: This is completely borked + sha_obj = __pragma__('js', '{}', 'new lib.jsSHA("SHA-256", "TEXT")') + sha_obj.js_update(value) + return sha_obj.getHash('B64') + +class EosList(EosObject): + def __init__(self, *args): + self.impl = list(*args) + + # Lists in JS are implemented as native Arrays, so no cheating here :( + def __len__(self): + return len(self.impl) + def __getitem__(self, idx): + return self.impl[idx] + def __setitem__(self, idx, val): + self.impl[idx] = val + def __contains__(self, val): + return val in self.impl + + def append(self, value): + if isinstance(value, EosObject): + value._instance = (self, len(self)) + if not value._inited: + value.post_init() + return self.impl.append(value) + +class DocumentObjectType(EosObjectType): + def __new__(meta, name, bases, attrs): + cls = EosObjectType.__new__(meta, name, bases, attrs) + + # Process fields + fields = {} + if hasattr(cls, '_fields'): + fields = cls._fields.copy() if is_python else Object.create(cls._fields) + for attr in list(dir(cls)): + if not is_python: + # We must skip things with getters or else they will be called here (too soon) + if Object.getOwnPropertyDescriptor(cls, attr).js_get: + continue + + val = getattr(cls, attr) + if isinstance(val, Field): + val._instance = (cls, name) + fields[attr] = val + delattr(cls, attr) + cls._fields = fields + + # Make properties + if is_python: + def make_property(name, field): + def field_getter(self): + return self._field_values[name] + def field_setter(self, value): + if isinstance(value, EosObject): + value._instance = (self, name) + if not value._inited: + value.post_init() + + self._field_values[name] = value + return property(field_getter, field_setter) + + for attr, val in fields.items(): + setattr(cls, attr, make_property(attr, val)) + else: + # Handled at instance level + pass + + return cls + +class DocumentObject(EosObject, metaclass=DocumentObjectType): + _ver = StringField(default='0.1') + + def __init__(self, *args, **kwargs): + super().__init__() + + self._field_values = {} + + # Different to Python + for attr, val in self._fields.items(): + if is_python: + # Properties handled above + pass + else: + def make_property(name, field): + def field_getter(): + return self._field_values[name] + def field_setter(value): + if isinstance(value, EosObject): + value._instance = (self, name) + if not value._inited: + value.post_init() + + self._field_values[name] = value + return (field_getter, field_setter) + prop = make_property(attr, val) + # TNYI: No support for property() + Object.defineProperty(self, attr, { + 'get': prop[0], + 'set': prop[1] + }) + + if attr in kwargs: + setattr(self, attr, kwargs[attr]) + else: + default = val.default + if default is not None and callable(default): + default = default() + setattr(self, attr, default) + + # TNYI: Strange things happen with py_ attributes + def serialise(self): + return {(attr[3:] if attr.startswith('py_') else attr): val.serialise(getattr(self, attr)) for attr, val in self._fields.items()} + + @classmethod + def deserialise(cls, value): + return cls(**{attr: val.deserialise(value[attr[3:] if attr.startswith('py_') else attr]) for attr, val in cls._fields.items()}) + +class TopLevelObject(DocumentObject): + def save(self): + #res = db[self._name].replace_one({'_id': self.serialise()['_id']}, self.serialise(), upsert=True) + res = db[self._name].replace_one({'_id': self._fields['_id'].serialise(self._id)}, EosObject.serialise_and_wrap(self), upsert=True) + +class EmbeddedObject(DocumentObject): + pass diff --git a/eos/core/objects/js.py b/eos/core/objects/js.py deleted file mode 100644 index 4562597..0000000 --- a/eos/core/objects/js.py +++ /dev/null @@ -1,233 +0,0 @@ -# Eos - Verifiable elections -# Copyright © 2017 RunasSudo (Yingtong Li) -# -# 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 . - -# Load json.js, jssha-sha256.js -lib = __pragma__('js', ''' -(function() {{ - {} - {} - var exports = {{}}; - exports.stringify = stringify_main; - exports.jsSHA = window.jsSHA; - return exports; -}})()''', __include__('eos/core/objects/json.js'), __include__('eos/core/objects/jssha-sha256.js')) - -# Fields -# ====== - -class Field: - def __init__(self, *args, **kwargs): - #console.log(kwargs.get('hashed', None)) - self.default = kwargs['py_default'] if kwargs.hasOwnProperty('py_default') else None - self.hashed = kwargs.get('hashed', True) - -class PrimitiveField(Field): - def serialise(self, value): - return value - - def deserialise(self, value): - return value - -DictField = PrimitiveField -IntField = PrimitiveField -ListField = PrimitiveField -StringField = PrimitiveField -UUIDField = PrimitiveField # Different to Python - -class EmbeddedObjectField(Field): - def __init__(self, object_type=None, *args, **kwargs): - super().__init__(*args, **kwargs) - self.object_type = object_type - - def serialise(self, value): - return EosObject.serialise_and_wrap(value, self.object_type) - - def deserialise(self, value): - return EosObject.deserialise_and_unwrap(value, self.object_type) - -class ListField(Field): - def __init__(self, element_field=None, *args, **kwargs): - super().__init__(default=EosList, *args, **kwargs) - self.element_field = element_field - - def serialise(self, value): - return [self.element_field.serialise(x) for x in value] - - def deserialise(self, value): - return [self.element_field.deserialise(x) for x in value] - -class EmbeddedObjectListField(Field): - def __init__(self, object_type=None, *args, **kwargs): - super().__init__(default=EosList, *args, **kwargs) - self.object_type = object_type - - def serialise(self, value): - return [EosObject.serialise_and_wrap(x, self.object_type) for x in value] - - def deserialise(self, value): - return [EosObject.deserialise_and_unwrap(x, self.object_type) for x in value] - -# Objects -# ======= - -class EosObjectType(type): - def __new__(meta, name, bases, attrs): - cls = type.__new__(meta, name, bases, attrs) - cls._name = (meta.__next_class_module__ + '.' + cls.__name__).replace('.js.', '.') #TNYI: module and qualname - if name != 'EosObject': - EosObject.objects[cls._name] = cls - return cls - -class EosObject(metaclass=EosObjectType): - objects = {} - - def __init__(self): - self._instance = (None, None) - self._inited = False - - def post_init(self): - self._inited = True - - def recurse_parents(self, cls): - if isinstance(self, cls): - return self - if self._instance[0]: - return self._instance[0].recurse_parents(cls) - return None - - #@property - def hash(self): - return EosObject.to_sha256(EosObject.serialise_and_wrap(self)) - - @staticmethod - def serialise_and_wrap(value, object_type=None): - if object_type: - return value.serialise() - return {'type': value._name, 'value': value.serialise()} - - @staticmethod - def deserialise_and_unwrap(value, object_type=None): - if object_type: - return object_type.deserialise(value) - print(value['type']) - return EosObject.objects[value['type']].deserialise(value['value']) - - # Different to Python - @staticmethod - def to_json(value): - return lib.stringify(value) - - @staticmethod - def from_json(value): - return JSON.parse(value) - - @staticmethod - def to_sha256(value): - # TNYI: This is completely borked - sha_obj = __pragma__('js', '{}', 'new lib.jsSHA("SHA-256", "TEXT")') - sha_obj.js_update(value) - return sha_obj.getHash('B64') - -class EosList(EosObject): - def __init__(self, *args): - self.impl = list(*args) - - # Diferent to Python - # Lists are implemented as native JS Arrays, so no cheating here :( - def __len__(self): - return len(self.impl) - def __getitem__(self, idx): - return self.impl[idx] - def __setitem__(self, idx, val): - self.impl[idx] = val - def __contains__(self, val): - return val in self.impl - - def append(self, value): - if isinstance(value, EosObject): - value._instance = (self, len(self)) - if not value._inited: - value.post_init() - return self.impl.append(value) - -class DocumentObjectType(EosObjectType): - def __new__(meta, name, bases, attrs): - cls = EosObjectType.__new__(meta, name, bases, attrs) - - # Process fields - fields = Object.create(cls._fields) if hasattr(cls, '_fields') else {} # Different to Python: TNYI dict.copy not implemented - for attr in list(dir(cls)): - val = getattr(cls, attr) - if isinstance(val, Field): - val._instance = (cls, name) - fields[attr] = val - delattr(cls, attr) - cls._fields = fields - - # Make properties - # Different to Python: This is handled at the instance level - - return cls - -class DocumentObject(EosObject, metaclass=DocumentObjectType): - _ver = StringField(default='0.1') - - def __init__(self, *args, **kwargs): - super().__init__() - - self._field_values = {} - - # Different to Python - for attr, val in self._fields.items(): - def make_property(name, field): - def field_getter(): - return self._field_values[name] - def field_setter(value): - if isinstance(value, EosObject): - value._instance = (self, name) - if not value._inited: - value.post_init() - - self._field_values[name] = value - return (field_getter, field_setter) - prop = make_property(attr, val) - Object.defineProperty(self, attr, { - 'get': prop[0], - 'set': prop[1] - }) - - if attr in kwargs: - setattr(self, attr, kwargs[attr]) - else: - default = val.default - if default is not None and callable(default): - default = default() - setattr(self, attr, default) - - # Different to Python - # TNYI: Strange things happen with py_ attributes - def serialise(self): - return {(attr[3:] if attr.startswith('py_') else attr): val.serialise(getattr(self, attr)) for attr, val in self._fields.items()} - - @classmethod - def deserialise(cls, value): - return cls(**{attr: val.deserialise(value[attr[3:] if attr.startswith('py_') else attr]) for attr, val in cls._fields.items()}) - -class TopLevelObject(DocumentObject): - pass - -class EmbeddedObject(DocumentObject): - pass diff --git a/eos/core/objects/python.py b/eos/core/objects/python.py deleted file mode 100644 index f8b0378..0000000 --- a/eos/core/objects/python.py +++ /dev/null @@ -1,222 +0,0 @@ -# Eos - Verifiable elections -# Copyright © 2017 RunasSudo (Yingtong Li) -# -# 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 . - -import pymongo -from bson.binary import UUIDLegacy - -import base64 -import hashlib -import json -import uuid - -# Database -# ======== - -client = pymongo.MongoClient() -db = client['test'] - -# Fields -# ====== - -class Field: - def __init__(self, *args, **kwargs): - self.default = kwargs.get('default', None) - self.hashed = kwargs.get('hashed', True) - -class PrimitiveField(Field): - def serialise(self, value): - return value - - def deserialise(self, value): - return value - -DictField = PrimitiveField -IntField = PrimitiveField -ListField = PrimitiveField -StringField = PrimitiveField - -class EmbeddedObjectField(Field): - def __init__(self, object_type=None, *args, **kwargs): - super().__init__(*args, **kwargs) - self.object_type = object_type - - def serialise(self, value): - return EosObject.serialise_and_wrap(value, self.object_type) - - def deserialise(self, value): - return EosObject.deserialise_and_unwrap(value, self.object_type) - -class ListField(Field): - def __init__(self, element_field=None, *args, **kwargs): - super().__init__(default=EosList, *args, **kwargs) - self.element_field = element_field - - def serialise(self, value): - return [self.element_field.serialise(x) for x in value] - - def deserialise(self, value): - return [self.element_field.deserialise(x) for x in value] - -class EmbeddedObjectListField(Field): - def __init__(self, object_type=None, *args, **kwargs): - super().__init__(default=EosList, *args, **kwargs) - self.object_type = object_type - - def serialise(self, value): - return [EosObject.serialise_and_wrap(x, self.object_type) for x in value] - - def deserialise(self, value): - return [EosObject.deserialise_and_unwrap(x, self.object_type) for x in value] - -class UUIDField(Field): - def __init__(self, *args, **kwargs): - super().__init__(default=uuid.uuid4, *args, **kwargs) - - def serialise(self, value): - return str(value) - - def deserialise(self, value): - return uuid.UUID(value) - -# Objects -# ======= - -class EosObjectType(type): - def __new__(meta, name, bases, attrs): - cls = type.__new__(meta, name, bases, attrs) - cls._name = (cls.__module__ + '.' + cls.__name__).replace('.js.', '.') #TNYI: qualname - if name != 'EosObject': - EosObject.objects[cls._name] = cls - return cls - -class EosObject(metaclass=EosObjectType): - objects = {} - - def __init__(self): - self._instance = (None, None) - self._inited = False - - def post_init(self): - self._inited = True - - def recurse_parents(self, cls): - if isinstance(self, cls): - return self - if self._instance[0]: - return self._instance[0].recurse_parents(cls) - return None - - #@property - def hash(self): - return EosObject.to_sha256(EosObject.serialise_and_wrap(self)) - - @staticmethod - def serialise_and_wrap(value, object_type=None): - if object_type: - return value.serialise() - return {'type': value._name, 'value': value.serialise()} - - @staticmethod - def deserialise_and_unwrap(value, object_type=None): - if object_type: - return object_type.deserialise(value) - return EosObject.objects[value['type']].deserialise(value['value']) - - @staticmethod - def to_json(value): - return json.dumps(value, sort_keys=True) - - @staticmethod - def from_json(value): - return json.loads(value) - - @staticmethod - def to_sha256(value): - sha_obj = hashlib.sha256() - sha_obj.update(value.encode('utf-8')) - return base64.b64encode(sha_obj.digest()).decode('utf-8') - -class EosList(EosObject, list): - def append(self, value): - if isinstance(value, EosObject): - value._instance = (self, len(self)) - if not value._inited: - value.post_init() - return super().append(value) - -class DocumentObjectType(EosObjectType): - def __new__(meta, name, bases, attrs): - cls = EosObjectType.__new__(meta, name, bases, attrs) - - # Process fields - fields = cls._fields.copy() if hasattr(cls, '_fields') else {} # remember to .copy() XD - for attr in list(dir(cls)): - val = getattr(cls, attr) - if isinstance(val, Field): - val._instance = (cls, name) - fields[attr] = val - delattr(cls, attr) - cls._fields = fields - - # Make properties - def make_property(name, field): - def field_getter(self): - return self._field_values[name] - def field_setter(self, value): - if isinstance(value, EosObject): - value._instance = (self, name) - if not value._inited: - value.post_init() - - self._field_values[name] = value - return property(field_getter, field_setter) - - for attr, val in fields.items(): - setattr(cls, attr, make_property(attr, val)) - - return cls - -class DocumentObject(EosObject, metaclass=DocumentObjectType): - _ver = StringField(default='0.1') - - def __init__(self, *args, **kwargs): - super().__init__() - - self._field_values = {} - - for attr, val in self._fields.items(): - if attr in kwargs: - setattr(self, attr, kwargs[attr]) - else: - default = val.default - if default is not None and callable(default): - default = default() - setattr(self, attr, default) - - def serialise(self): - return {attr: val.serialise(getattr(self, attr)) for attr, val in self._fields.items()} - - @classmethod - def deserialise(cls, value): - return cls(**{attr: val.deserialise(value[attr]) for attr, val in cls._fields.items()}) - -class TopLevelObject(DocumentObject): - def save(self): - #res = db[self._name].replace_one({'_id': self.serialise()['_id']}, self.serialise(), upsert=True) - res = db[self._name].replace_one({'_id': self._fields['_id'].serialise(self._id)}, EosObject.serialise_and_wrap(self), upsert=True) - -class EmbeddedObject(DocumentObject): - pass