Note

You are reading an old version of the Evennia documentation. The latest version is here

.

# Source code for evennia.utils.picklefield

    #
    #  Copyright (c) 2009-2010 Gintautas Miliauskas
    #
    #   Permission is hereby granted, free of charge, to any person
    #   obtaining a copy of this software and associated documentation
    #   files (the "Software"), to deal in the Software without
    #   restriction, including without limitation the rights to use,
    #   copy, modify, merge, publish, distribute, sublicense, and/or sell
    #   copies of the Software, and to permit persons to whom the
    #   Software is furnished to do so, subject to the following
    #   conditions:
    #
    #   The above copyright notice and this permission notice shall be
    #   included in all copies or substantial portions of the Software.
    #
    #   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    #   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
    #   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
    #   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
    #   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
    #   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    #   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
    #   OTHER DEALINGS IN THE SOFTWARE.

    """
    Pickle field implementation for Django.

    Modified for Evennia by Griatch and the Evennia community.

    """
    from ast import literal_eval
    from base64 import b64decode, b64encode
    from copy import Error as CopyError
    from copy import deepcopy
    from datetime import datetime
    from pickle import dumps, loads
    from zlib import compress, decompress

    # import six # this is actually a pypy component, not in default syslib
    from django.core.exceptions import ValidationError
    from django.db import models
    from django.forms.fields import CharField
    from django.forms.widgets import Textarea
    from django.utils.encoding import force_str

    from evennia.utils.dbserialize import pack_dbobj

    DEFAULT_PROTOCOL = 4

    class PickledObject(str):
        """
        A subclass of string so it can be told whether a string is a pickled
        object or not (if the object is an instance of this class then it must
        [well, should] be a pickled one).

        Only really useful for passing pre-encoded values to ``default``
        with ``dbsafe_encode``, not that doing so is necessary. If you
        remove PickledObject and its references, you won't be able to pass
        in pre-encoded values anymore, but you can always just pass in the
        python objects themselves.
        """

    class _ObjectWrapper(object):
        """
        A class used to wrap object that have properties that may clash with the
        ORM internals.

        For example, objects with the `prepare_database_save` property such as
        `django.db.Model` subclasses won't work under certain conditions and the
        same apply for trying to retrieve any `callable` object.
        """

        __slots__ = ("_obj",)

        def __init__(self, obj):
            self._obj = obj

    def wrap_conflictual_object(obj):
        if hasattr(obj, "prepare_database_save") or callable(obj):
            obj = _ObjectWrapper(obj)
        return obj

    def dbsafe_encode(value, compress_object=False, pickle_protocol=DEFAULT_PROTOCOL):
        # We use deepcopy() here to avoid a problem with cPickle, where dumps
        # can generate different character streams for same lookup value if
        # they are referenced differently.
        # The reason this is important is because we do all of our lookups as
        # simple string matches, thus the character streams must be the same
        # for the lookups to work properly. See tests.py for more information.
        try:
            value = deepcopy(value)
        except CopyError:
            # this can happen on a manager query where the search query string is a
            # database model.
            value = pack_dbobj(value)

        value = dumps(value, protocol=pickle_protocol)

        if compress_object:
            value = compress(value)
        value = b64encode(value).decode()  # decode bytes to str
        return PickledObject(value)

    def dbsafe_decode(value, compress_object=False):
        value = value.encode()  # encode str to bytes
        value = b64decode(value)
        if compress_object:
            value = decompress(value)
        return loads(value)

    class PickledWidget(Textarea):
        """
        This is responsible for outputting HTML representing a given field.
        """

        def render(self, name, value, attrs=None, renderer=None):
            """Display of the PickledField in django admin"""

            repr_value = repr(value)

            # analyze represented value to see how big the field should be
            if attrs is not None:
                attrs["name"] = name
            else:
                attrs = {"name": name}
            attrs["cols"] = 30
            # adapt number of rows to number of lines in string
            rows = 1
            if isinstance(value, str) and "\n" in repr_value:
                rows = max(1, len(value.split("\n")))
            attrs["rows"] = rows
            attrs = self.build_attrs(attrs)

            try:
                # necessary to convert it back after repr(), otherwise validation errors will mutate it
                value = literal_eval(repr_value)
            except (ValueError, SyntaxError):
                # we could not eval it, just show its prepresentation
                value = repr_value
            return super().render(name, value, attrs=attrs, renderer=renderer)

        def value_from_datadict(self, data, files, name):
            dat = data.get(name)
            # import evennia;evennia.set_trace()
            return dat

    class PickledFormField(CharField):
        """
        This represents one input field for the form.

        """

        widget = PickledWidget
        default_error_messages = dict(CharField.default_error_messages)
        default_error_messages["invalid"] = (
            "This is not a Python Literal. You can store things like strings, "
            "integers, or floats, but you must do it by typing them as you would "
            "type them in the Python Interpreter. For instance, strings must be "
            "surrounded by quote marks. We have converted it to a string for your "
            "convenience. If it is acceptable, please hit save again."
        )

        def __init__(self, *args, **kwargs):
            # This needs to fall through to literal_eval.
            kwargs["required"] = False
            super().__init__(*args, **kwargs)

        def clean(self, value):
            value = super().clean(value)

            # handle empty input
            try:
                if not value.strip():
                    # Field was left blank. Make this None.
                    value = "None"
            except AttributeError:
                pass

            # parse raw Python
            try:
                return literal_eval(value)
            except (ValueError, SyntaxError):
                pass

            # fall through to parsing the repr() of the data
            try:
                value = repr(value)
                return literal_eval(value)
            except (ValueError, SyntaxError):
                raise ValidationError(self.error_messages["invalid"])

    class PickledObjectField(models.Field):
        """
        A field that will accept *any* python object and store it in the
        database. PickledObjectField will optionally compress its values if
        declared with the keyword argument ``compress=True``.

        Does not actually encode and compress ``None`` objects (although you
        can still do lookups using None). This way, it is still possible to
        use the ``isnull`` lookup type correctly.
        """

        def __init__(self, *args, **kwargs):
            self.compress = kwargs.pop("compress", False)
            self.protocol = kwargs.pop("protocol", DEFAULT_PROTOCOL)
            super().__init__(*args, **kwargs)

        def get_default(self):
            """
            Returns the default value for this field.

            The default implementation on models.Field calls force_str
            on the default, which means you can't set arbitrary Python
            objects as the default. To fix this, we just return the value
            without calling force_str on it. Note that if you set a
            callable as a default, the field will still call it. It will
            *not* try to pickle and encode it.

            """
            if self.has_default():
                if callable(self.default):
                    return self.default()
                return self.default
            # If the field doesn't have a default, then we punt to models.Field.
            return super().get_default()

        def from_db_value(self, value, *args):
            """
            B64decode and unpickle the object, optionally decompressing it.

            If an error is raised in de-pickling and we're sure the value is
            a definite pickle, the error is allowed to propagate. If we
            aren't sure if the value is a pickle or not, then we catch the
            error and return the original value instead.

            """
            if value is not None:
                try:
                    value = dbsafe_decode(value, self.compress)
                except Exception:
                    # If the value is a definite pickle; and an error is raised in
                    # de-pickling it should be allowed to propogate.
                    if isinstance(value, PickledObject):
                        raise
                else:
                    if isinstance(value, _ObjectWrapper):
                        return value._obj
            return value

        def formfield(self, **kwargs):
            return PickledFormField(**kwargs)

        def pre_save(self, model_instance, add):
            value = super().pre_save(model_instance, add)
            return wrap_conflictual_object(value)

        def get_db_prep_value(self, value, connection=None, prepared=False):
            """
            Pickle and b64encode the object, optionally compressing it.

            The pickling protocol is specified explicitly (by default 2),
            rather than as -1 or HIGHEST_PROTOCOL, because we don't want the
            protocol to change over time. If it did, ``exact`` and ``in``
            lookups would likely fail, since pickle would now be generating
            a different string.

            """
            if value is not None and not isinstance(value, PickledObject):
                # We call force_str here explicitly, so that the encoded string
                # isn't rejected by the postgresql backend. Alternatively,
                # we could have just registered PickledObject with the psycopg
                # marshaller (telling it to store it like it would a string), but
                # since both of these methods result in the same value being stored,
                # doing things this way is much easier.
                value = force_str(dbsafe_encode(value, self.compress, self.protocol))
            return value

        def value_to_string(self, obj):
            value = self.value_from_object(obj)
            return self.get_db_prep_value(value)

        def get_internal_type(self):
            return "TextField"

        def get_db_prep_lookup(self, lookup_type, value, connection=None, prepared=False):
            if lookup_type not in ["exact", "in", "isnull"]:
                raise TypeError("Lookup type %s is not supported." % lookup_type)
            # The Field model already calls get_db_prep_value before doing the
            # actual lookup, so all we need to do is limit the lookup types.
            return super().get_db_prep_lookup(
                lookup_type, value, connection=connection, prepared=prepared
            )

Note

You are reading an old version of the Evennia documentation. The latest version is here

.