Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
wants to merge 10 commits into from
2 changes: 1 addition & 1 deletion django/db/models/fields/__init__.py
Expand Up @@ -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):
Expand Down
306 changes: 154 additions & 152 deletions django/utils/functional.py
@@ -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.
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down Expand Up @@ -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