<commit_before>#!/usr/bin/env python
"""JSON serializable object and properties.

This module provides an interface of JSON serializable Python objects,
:class:`JSONSerializableObject`, and concise but powerful ways to define
exportable properties.

A short example illustrating what this module does:

>>> class SomeObject(JSONSerializableObject):
...     foo = JSONProperty('foo', default='FOO')
...     bar = ReadonlyJSONProperty('bar', '_bar')
...
...     @jsonproperty
...     def baz(self):
...         return ':'.join([self.foo, self.bar])
...
>>> s = SomeObject(bar='BAR')
>>> s.foo
'FOO'
>>> s.foo = 'FOOFOO'
>>> s.bar = 'BARBAR'
Traceback (most recent call last):
    ...
AttributeError: Not settable
>>> s.baz
'FOOFOO:BAR'
>>> s.json(sort_keys=True)
'{"bar": "BAR", "baz": "FOOFOO:BAR", "foo": "FOOFOO"}'
"""

import json


# TODO: Create JSONSerializableObject from JSON (string) or Python
# representation (a combination of map, list and primitives).
# It's not clear how to handle recursive objects? (Really?)


class JSONSerializableObject(object):
    """Object serializable to JSON.

    :param kwargs: arbitrary key-value mapping to initialize JSON properties

    This class represents an object which is serializable to JSON. Typically
    you subclass this class and define exportable properties using:

    * :class:`JSONProperty`, read/write-able simple data holder
    * :class:`ReadonlyJSONProperty`, readonly version of :class:`JSONProperty`
    * :func:`jsonproperty`, which allows you to customize the property

    See examples in this module for how to use them.
    """

    def __init__(self, **kwargs):
        self_class = self.__class__
        for key, value in kwargs.iteritems():
            if not hasattr(self_class, key):
                raise AttributeError('{}.{} not found'.format(
                    self_class.__name__, key))
            member = getattr(self_class, key)
            member_class = member.__class__
            if not issubclass(member_class, CustomizableJSONProperty):
                raise AttributeError(
                    '{}.{} is {}, not CustomizableJSONProperty'
                    .format(self_class.__name__, key, member_class))
            if issubclass(member_class, ReadonlyJSONProperty):
                setattr(self, member._wrapped_variable, value)
            else:
                setattr(self, key, value)

    def json(self, *args, **kwargs):
        """Serialize this object to JSON.

        :param args: arbitrary positional arguments passed to
                     :func:`json.dumps`
        :param kwargs: arbitrary keyword arguments passed to :func:`json.dumps`
        :returns: JSON representation of this object
        :rtype: string
        :raises TypeError: if this object contains non-serializable object

        Serialize this object to JSON. This is done by creating a map
        constructed by exportable properties. If a property returns a
        non-primitive, such an object will be serialized recursively.
        """
        return json.dumps(self, *args, cls=_JSONSerializableObjectEncoder,
                          **kwargs)

    def _serialize_json(self):
        data = {}
        for key in dir(self):
            attr = None
            # Tries to find a property from the current instance. This is not
            # for an ordinary property, because they are owned by class
            # objects. Properties created dynamically at the instance creation
            # time without creating a dynamic class would fall into this case,
            # but they should be really rare.
            if key in self.__dict__:
                attr = getattr(self, key)
            else:
                # This path is the normal flow.
                # To get a CustomizableJSONProperty instance itself, not a
                # computed value (the result of
                # CustomizableJSONProperty.__get__()), we need to go through
                # MRO to find the class that defines the property.
                for cls in self.__class__.__mro__:
                    if key in cls.__dict__:
                        attr = getattr(cls, key)
                        break
            if attr is None:
                # Special attributes like __doc__ cannot be found in the above
                # way, but can be ignored.
                continue
            if not issubclass(attr.__class__, CustomizableJSONProperty):
                continue
            value = attr.__get__(self)
            if not attr.omittable or value is not None:
                data[attr.name] = value
        return self.postprocess(data)

    def postprocess(self, data):
        """Postprocess the automatically exported data.

        :param map data: map representing this object

        This is a callback method which will be called at the end of export.
        The parameter ``data`` contains all the exported properties' data.
        Usually you don't have to override this method, but if some properties
        need to interact with each other, you can implement such logic here.
        """
        return data


class _JSONSerializableObjectEncoder(json.JSONEncoder):
    """Encoder which tries to encode :class:`JSONSerializableObject` in
    addition to JSON primitives."""

    def default(self, obj):
        try:
            return obj._serialize_json()
        except:
            super(_JSONSerializableObjectEncoder, self).default(obj)


class CustomizableJSONProperty(object):<commit_msg>Add parse_text and parse_json prototypes.<commit_after>#!/usr/bin/env python
"""JSON serializable object and properties.

This module provides an interface of JSON serializable Python objects,
:class:`JSONSerializableObject`, and concise but powerful ways to define
exportable properties.

A short example illustrating what this module does:

>>> class SomeObject(JSONSerializableObject):
...     foo = JSONProperty('foo', default='FOO')
...     bar = ReadonlyJSONProperty('bar', '_bar')
...
...     @jsonproperty
...     def baz(self):
...         return ':'.join([self.foo, self.bar])
...
>>> s = SomeObject(bar='BAR')
>>> s.foo
'FOO'
>>> s.foo = 'FOOFOO'
>>> s.bar = 'BARBAR'
Traceback (most recent call last):
    ...
AttributeError: Not settable
>>> s.baz
'FOOFOO:BAR'
>>> s.json(sort_keys=True)
'{"bar": "BAR", "baz": "FOOFOO:BAR", "foo": "FOOFOO"}'
"""

import json


class JSONSerializableObject(object):
    """Object serializable to JSON.

    :param kwargs: arbitrary key-value mapping to initialize JSON properties

    This class represents an object which is serializable to JSON. Typically
    you subclass this class and define exportable properties using:

    * :class:`JSONProperty`, read/write-able simple data holder
    * :class:`ReadonlyJSONProperty`, readonly version of :class:`JSONProperty`
    * :func:`jsonproperty`, which allows you to customize the property

    See examples in this module for how to use them.
    """

    def __init__(self, **kwargs):
        self_class = self.__class__
        for key, value in kwargs.iteritems():
            if not hasattr(self_class, key):
                raise AttributeError('{}.{} not found'.format(
                    self_class.__name__, key))
            member = getattr(self_class, key)
            member_class = member.__class__
            if not issubclass(member_class, CustomizableJSONProperty):
                raise AttributeError(
                    '{}.{} is {}, not CustomizableJSONProperty'
                    .format(self_class.__name__, key, member_class))
            if issubclass(member_class, ReadonlyJSONProperty):
                setattr(self, member._wrapped_variable, value)
            else:
                setattr(self, key, value)

    def json(self, *args, **kwargs):
        """Serialize this object to JSON.

        :param args: arbitrary positional arguments passed to
                     :func:`json.dumps`
        :param kwargs: arbitrary keyword arguments passed to :func:`json.dumps`
        :returns: JSON representation of this object
        :rtype: string
        :raises TypeError: if this object contains non-serializable object

        Serialize this object to JSON. This is done by creating a map
        constructed by exportable properties. If a property returns a
        non-primitive, such an object will be serialized recursively.
        """
        return json.dumps(self, *args, cls=_JSONSerializableObjectEncoder,
                          **kwargs)

    def _serialize_json(self):
        data = {}
        for key in dir(self):
            attr = None
            # Tries to find a property from the current instance. This is not
            # for an ordinary property, because they are owned by class
            # objects. Properties created dynamically at the instance creation
            # time without creating a dynamic class would fall into this case,
            # but they should be really rare.
            if key in self.__dict__:
                attr = getattr(self, key)
            else:
                # This path is the normal flow.
                # To get a CustomizableJSONProperty instance itself, not a
                # computed value (the result of
                # CustomizableJSONProperty.__get__()), we need to go through
                # MRO to find the class that defines the property.
                for cls in self.__class__.__mro__:
                    if key in cls.__dict__:
                        attr = getattr(cls, key)
                        break
            if attr is None:
                # Special attributes like __doc__ cannot be found in the above
                # way, but can be ignored.
                continue
            if not issubclass(attr.__class__, CustomizableJSONProperty):
                continue
            value = attr.__get__(self)
            if not attr.omittable or value is not None:
                data[attr.name] = value
        return self.postprocess(data)

    def postprocess(self, data):
        """Postprocess the automatically exported data.

        :param map data: map representing this object

        This is a callback method which will be called at the end of export.
        The parameter ``data`` contains all the exported properties' data.
        Usually you don't have to override this method, but if some properties
        need to interact with each other, you can implement such logic here.
        """
        return data


# TODO: Create JSONSerializableObject from JSON (string) or Python
# representation (a combination of map, list and primitives).
# It's not clear how to handle recursive objects? (Really?)


def parse_text(text, *args, **kwargs):
    """Parse JSON text and creates a dynamic JSONSerializaObject.

    :param text: text representing JSON object
    :type text: str or unicode
    :param args: arbitrary positional arguments passed to :func:`json.dumps`
    :param kwargs: arbitrary keyword arguments passed to :func:`json.dumps`
    :returns: Object parsed from the text
    :rtype: :class:`JSONSerializableObject`
    """
    return parse_json(json.dumps(text, *args, **kwargs))


def parse_json(obj, *args, **kwargs):
    """Parse JSON Python object and creates a dynamic JSONSerializaObject.

    :param obj: Python object representing JSON object
    :param args: arbitrary positional arguments passed to :func:`json.dumps`
    :param kwargs: arbitrary keyword arguments passed to :func:`json.dumps`
    :returns: Object parsed from the text
    :rtype: :class:`JSONSerializableObject`
    """
    pass


class _JSONSerializableObjectEncoder(json.JSONEncoder):
    """Encoder which tries to encode :class:`JSONSerializableObject` in
    addition to JSON primitives."""

    def default(self, obj):
        try:
            return obj._serialize_json()
        except:
            super(_JSONSerializableObjectEncoder, self).default(obj)


class CustomizableJSONProperty(object):
{'fraction_non_alphanumeric': 0.0850974813823111, 'fraction_numerical': 0.0, 'mean_word_length': 2.4119326291749927, 'pattern_counts': {'":': 6, '<': 3, '<?xml version=': 0, '>': 45, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6179657', 'n_tokens_mistral': 3050, 'n_tokens_neox': 2935, 'n_words': 1147}