Fixed #20296 -- allow lazy strings to be used with mark_safe(). #2234

Closed
wants to merge 10 commits into
from
@@ -582,7 +582,7 @@ def get_prep_value(self, value):
Perform preliminary non-db specific value checks and conversions.
"""
if isinstance(value, Promise):
- value = value._proxy____cast()
+ value = value._cast()
return value
def get_db_prep_value(self, value, connection, prepared=False):
@@ -1,13 +1,48 @@
+from collections import defaultdict
import copy
-import operator
from functools import wraps
+import operator
import sys
import warnings
from django.utils import six
from django.utils.six.moves import copyreg
+if sys.version_info >= (2, 7, 2):
+ from functools import total_ordering
+else:
+ # For Python < 2.7.2. total_ordering in versions prior to 2.7.2 is buggy.
+ # See http://bugs.python.org/issue10042 for details. For these versions use
+ # code borrowed from Python 2.7.3.
+ def total_ordering(cls):
+ """Class decorator that fills in missing ordering methods"""
+ convert = {
+ '__lt__': [('__gt__', lambda self, other: not (self < other or self == other)),
+ ('__le__', lambda self, other: self < other or self == other),
+ ('__ge__', lambda self, other: not self < other)],
+ '__le__': [('__ge__', lambda self, other: not self <= other or self == other),
+ ('__lt__', lambda self, other: self <= other and not self == other),
+ ('__gt__', lambda self, other: not self <= other)],
+ '__gt__': [('__lt__', lambda self, other: not (self > other or self == other)),
+ ('__ge__', lambda self, other: self > other or self == other),
+ ('__le__', lambda self, other: not self > other)],
+ '__ge__': [('__le__', lambda self, other: (not self >= other) or self == other),
+ ('__gt__', lambda self, other: self >= other and not self == other),
+ ('__lt__', lambda self, other: not self >= other)]
+ }
+ roots = set(dir(cls)) & set(convert)
+ if not roots:
+ raise ValueError('must define at least one ordering operation: < > <= >=')
+ root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__
+ for opname, opfunc in convert[root]:
+ if opname not in roots:
+ opfunc.__name__ = opname
+ opfunc.__doc__ = getattr(int, opname).__doc__
+ setattr(cls, opname, opfunc)
+ return cls
+
+
# You can't trivially replace this with `functools.partial` because this binds
# to classes and returns bound instances, whereas functools.partial (on
# CPython) is a type and its instances don't bind.
@@ -55,13 +90,122 @@ def __get__(self, instance, type=None):
return res
+@total_ordering
class Promise(object):
"""
- This is just a base class for the proxy class created in
- the closure of the lazy function. It can be used to recognize
- promises in code.
+ A base class for the proxy class created in the closure of the lazy
+ function. It can be used to recognize promises in code.
+
+ It encapsulates a function call and acts as a proxy for methods that are
+ called on the result of that function. The function is not evaluated
+ until one of the methods on the result is called.
"""
- pass
+ _dispatch = None
+ _func = None
+ _resultclasses = None
+
+ def __init__(self, args=(), kwargs=None):
+ if kwargs is None:
+ kwargs = {}
+ self._args = args
+ self._kwargs = kwargs
+ if self._dispatch is None:
+ self._prepare_class()
+
+ def __reduce__(self):
+ return (
+ _lazy_proxy_unpickle,
+ (self._func, self._args, self._kwargs) + self._resultclasses
+ )
+
+ @classmethod
+ def _prepare_class(cls):
+ cls._dispatch = defaultdict(dict)
+ for resultclass in cls._resultclasses:
+ for type_ in reversed(resultclass.mro()):
+ for (k, v) in type_.__dict__.items():
+ # Register the method for the given type
+ cls._dispatch[resultclass][k] = v
+ if hasattr(cls, k):
+ continue
+ setattr(cls, k, cls._method_wrapper(resultclass, k, v))
+ cls._delegate_bytes = bytes in cls._resultclasses
+ cls._delegate_text = six.text_type in cls._resultclasses
+ assert not (cls._delegate_bytes and cls._delegate_text), "Cannot call lazy() with both bytes and text return types."
+ if cls._delegate_text:
+ if six.PY3:
+ cls.__str__ = cls._text_cast
+ else:
+ cls.__unicode__ = cls._text_cast
+ elif cls._delegate_bytes:
+ if six.PY3:
+ cls.__bytes__ = cls._bytes_cast
+ else:
+ cls.__str__ = cls._bytes_cast
+
+ @classmethod
+ def _method_wrapper(cls, klass, funcname, method):
+ """Builds a wrapper around some magic method."""
+ def wrapper(self, *args, **kw):
+ # Automatically triggers the evaluation of a lazy value and
+ # applies the given magic method of the result type.
+ res = self._eval()
+ for t in type(res).mro():
+ if t in self._dispatch:
+ return self._dispatch[t][funcname](res, *args, **kw)
+ raise TypeError("Lazy object returned unexpected type.")
+
+ return wrapper
+
+ def _eval(self):
+ """Evaluate the wrapped function."""
+ return self._func(*self._args, **self._kwargs)
+
+ def _text_cast(self):
+ return self._eval()
+
+ def _bytes_cast(self):
+ return bytes(self._eval())
+
+ def _cast(self):
+ if self._delegate_bytes:
+ return self._bytes_cast()
+ elif self._delegate_text:
+ return self._text_cast()
+ else:
+ return self._eval()
+
+ def __ne__(self, other):
+ if isinstance(other, Promise):
+ other = other._cast()
+ return self._cast() != other
+
+ def __eq__(self, other):
+ if isinstance(other, Promise):
+ other = other._cast()
+ return self._cast() == other
+
+ def __lt__(self, other):
+ if isinstance(other, Promise):
+ other = other._cast()
+ return self._cast() < other
+
+ def __hash__(self):
+ return hash(self._cast())
+
+ def __mod__(self, rhs):
+ if self._delegate_bytes and six.PY2:
+ return bytes(self) % rhs
+ elif self._delegate_text:
+ return six.text_type(self) % rhs
+ return self._cast() % rhs
+
+ def __deepcopy__(self, memo):
+ # Instances of this class are effectively immutable. It's just a
+ # collection of functions. So we don't need to do anything
+ # complicated for copying.
+ memo[id(self)] = self
+ return self
def lazy(func, *resultclasses):
@@ -72,125 +216,16 @@ def lazy(func, *resultclasses):
function is evaluated on every access.
"""
- @total_ordering
class __proxy__(Promise):
- """
- Encapsulate a function call and act as a proxy for methods that are
- called on the result of that function. The function is not evaluated
- until one of the methods on the result is called.
- """
- __dispatch = None
-
- def __init__(self, args, kw):
- self.__args = args
- self.__kw = kw
- if self.__dispatch is None:
- self.__prepare_class__()
-
- def __reduce__(self):
- return (
- _lazy_proxy_unpickle,
- (func, self.__args, self.__kw) + resultclasses
- )
-
- @classmethod
- def __prepare_class__(cls):
- cls.__dispatch = {}
- for resultclass in resultclasses:
- cls.__dispatch[resultclass] = {}
- for type_ in reversed(resultclass.mro()):
- for (k, v) in type_.__dict__.items():
- # All __promise__ return the same wrapper method, but
- # they also do setup, inserting the method into the
- # dispatch dict.
- meth = cls.__promise__(resultclass, k, v)
- if hasattr(cls, k):
- continue
- setattr(cls, k, meth)
- cls._delegate_bytes = bytes in resultclasses
- cls._delegate_text = six.text_type in resultclasses
- assert not (cls._delegate_bytes and cls._delegate_text), "Cannot call lazy() with both bytes and text return types."
- if cls._delegate_text:
- if six.PY3:
- cls.__str__ = cls.__text_cast
- else:
- cls.__unicode__ = cls.__text_cast
- elif cls._delegate_bytes:
- if six.PY3:
- cls.__bytes__ = cls.__bytes_cast
- else:
- cls.__str__ = cls.__bytes_cast
-
- @classmethod
- def __promise__(cls, klass, funcname, method):
- # Builds a wrapper around some magic method and registers that
- # magic method for the given type and method name.
- def __wrapper__(self, *args, **kw):
- # Automatically triggers the evaluation of a lazy value and
- # applies the given magic method of the result type.
- res = func(*self.__args, **self.__kw)
- for t in type(res).mro():
- if t in self.__dispatch:
- return self.__dispatch[t][funcname](res, *args, **kw)
- raise TypeError("Lazy object returned unexpected type.")
-
- if klass not in cls.__dispatch:
- cls.__dispatch[klass] = {}
- cls.__dispatch[klass][funcname] = method
- return __wrapper__
-
- def __text_cast(self):
- return func(*self.__args, **self.__kw)
-
- def __bytes_cast(self):
- return bytes(func(*self.__args, **self.__kw))
-
- def __cast(self):
- if self._delegate_bytes:
- return self.__bytes_cast()
- elif self._delegate_text:
- return self.__text_cast()
- else:
- return func(*self.__args, **self.__kw)
-
- def __ne__(self, other):
- if isinstance(other, Promise):
- other = other.__cast()
- return self.__cast() != other
-
- def __eq__(self, other):
- if isinstance(other, Promise):
- other = other.__cast()
- return self.__cast() == other
-
- def __lt__(self, other):
- if isinstance(other, Promise):
- other = other.__cast()
- return self.__cast() < other
-
- def __hash__(self):
- return hash(self.__cast())
-
- def __mod__(self, rhs):
- if self._delegate_bytes and six.PY2:
- return bytes(self) % rhs
- elif self._delegate_text:
- return six.text_type(self) % rhs
- return self.__cast() % rhs
-
- def __deepcopy__(self, memo):
- # Instances of this class are effectively immutable. It's just a
- # collection of functions. So we don't need to do anything
- # complicated for copying.
- memo[id(self)] = self
- return self
+ _func = staticmethod(func) # to avoid automatic binding
+ _resultclasses = resultclasses
@wraps(func)
- def __wrapper__(*args, **kw):
+ def wrapper(*args, **kwargs):
# Creates the proxy object, instead of the actual value.
- return __proxy__(args, kw)
+ return __proxy__(args, kwargs)
- return __wrapper__
+ return wrapper
def _lazy_proxy_unpickle(func, args, kwargs, *resultclasses):
@@ -399,36 +434,3 @@ def partition(predicate, values):
for item in values:
results[predicate(item)].append(item)
return results
-
-if sys.version_info >= (2, 7, 2):
- from functools import total_ordering
-else:
- # For Python < 2.7.2. total_ordering in versions prior to 2.7.2 is buggy.
- # See http://bugs.python.org/issue10042 for details. For these versions use
- # code borrowed from Python 2.7.3.
- def total_ordering(cls):
- """Class decorator that fills in missing ordering methods"""
- convert = {
- '__lt__': [('__gt__', lambda self, other: not (self < other or self == other)),
- ('__le__', lambda self, other: self < other or self == other),
- ('__ge__', lambda self, other: not self < other)],
- '__le__': [('__ge__', lambda self, other: not self <= other or self == other),
- ('__lt__', lambda self, other: self <= other and not self == other),
- ('__gt__', lambda self, other: not self <= other)],
- '__gt__': [('__lt__', lambda self, other: not (self > other or self == other)),
- ('__ge__', lambda self, other: self > other or self == other),
- ('__le__', lambda self, other: not self > other)],
- '__ge__': [('__le__', lambda self, other: (not self >= other) or self == other),
- ('__gt__', lambda self, other: self >= other and not self == other),
- ('__lt__', lambda self, other: not self >= other)]
- }
- roots = set(dir(cls)) & set(convert)
- if not roots:
- raise ValueError('must define at least one ordering operation: < > <= >=')
- root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__
- for opname, opfunc in convert[root]:
- if opname not in roots:
- opfunc.__name__ = opname
- opfunc.__doc__ = getattr(int, opname).__doc__
- setattr(cls, opname, opfunc)
- return cls
Oops, something went wrong. Retry.