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


[docs]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
[docs]def wrap_conflictual_object(obj): if hasattr(obj, "prepare_database_save") or callable(obj): obj = _ObjectWrapper(obj) return obj
[docs]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)
[docs]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)
[docs]class PickledWidget(Textarea): """ This is responsible for outputting HTML representing a given field. """
[docs] 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)
[docs] def value_from_datadict(self, data, files, name): dat = data.get(name) # import evennia;evennia.set_trace() return dat
[docs]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." )
[docs] def __init__(self, *args, **kwargs): # This needs to fall through to literal_eval. kwargs["required"] = False super().__init__(*args, **kwargs)
[docs] 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"])
[docs]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. """
[docs] def __init__(self, *args, **kwargs): self.compress = kwargs.pop("compress", False) self.protocol = kwargs.pop("protocol", DEFAULT_PROTOCOL) super().__init__(*args, **kwargs)
[docs] 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()
[docs] 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
[docs] def formfield(self, **kwargs): return PickledFormField(**kwargs)
[docs] def pre_save(self, model_instance, add): value = super().pre_save(model_instance, add) return wrap_conflictual_object(value)
[docs] 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
[docs] def value_to_string(self, obj): value = self.value_from_object(obj) return self.get_db_prep_value(value)
[docs] def get_internal_type(self): return "TextField"
[docs] 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 )