Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
base fork: django/django
...
head fork: bmispelon/django
Checking mergeability… Don't worry, you can still create the pull request.
  • 10 commits
  • 5 files changed
  • 0 commit comments
  • 1 contributor
View
2  django/db/models/fields/__init__.py
@@ -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):
View
306 django/utils/functional.py
@@ -1,6 +1,7 @@
+from collections import defaultdict
import copy
-import operator
from functools import wraps
+import operator
import sys
import warnings
@@ -8,6 +9,40 @@
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
View
56 django/utils/safestring.py
@@ -4,7 +4,7 @@
that the producer of the string has already turned characters that should not
be interpreted by the HTML engine (e.g. '<') into the appropriate entities.
"""
-from django.utils.functional import curry, Promise, allow_lazy
+from django.utils.functional import curry, Promise
from django.utils import six
@@ -16,14 +16,14 @@ class EscapeBytes(bytes, EscapeData):
"""
A byte string that should be HTML-escaped when output.
"""
- __new__ = allow_lazy(bytes.__new__, bytes)
+ pass
class EscapeText(six.text_type, EscapeData):
"""
A unicode string object that should be HTML-escaped when output.
"""
- __new__ = allow_lazy(six.text_type.__new__, six.text_type)
+ pass
if six.PY3:
EscapeString = EscapeText
@@ -33,6 +33,16 @@ class EscapeText(six.text_type, EscapeData):
EscapeUnicode = EscapeText
+class BaseEscapeDataPromise(Promise, EscapeData):
+ def _text_cast(self):
+ text = super(BaseEscapeDataPromise, self)._text_cast()
+ return EscapeText(text)
+
+ def _bytes_cast(self):
+ bytes = super(BaseEscapeDataPromise, self)._bytes_cast()
+ return EscapeBytes(bytes)
+
+
class SafeData(object):
def __html__(self):
"""
@@ -48,8 +58,6 @@ class SafeBytes(bytes, SafeData):
A bytes subclass that has been specifically marked as "safe" (requires no
further escaping) for HTML output purposes.
"""
- __new__ = allow_lazy(bytes.__new__, bytes)
-
def __add__(self, rhs):
"""
Concatenating a safe byte string with another safe byte string or safe
@@ -83,8 +91,6 @@ class SafeText(six.text_type, SafeData):
A unicode (Python 2) / str (Python 3) subclass that has been specifically
marked as "safe" for HTML output purposes.
"""
- __new__ = allow_lazy(six.text_type.__new__, six.text_type)
-
def __add__(self, rhs):
"""
Concatenating a safe unicode string with another safe byte string or
@@ -118,6 +124,26 @@ def _proxy_method(self, *args, **kwargs):
SafeUnicode = SafeText
+class BaseSafeDataPromise(Promise, SafeData):
+ def _text_cast(self):
+ text = super(BaseSafeDataPromise, self)._text_cast()
+ return SafeText(text)
+
+ def _bytes_cast(self):
+ bytes = super(BaseSafeDataPromise, self)._bytes_cast()
+ return SafeBytes(bytes)
+
+ def __add__(self, rhs):
+ if isinstance(rhs, Promise):
+ rhs = rhs._cast()
+ t = self._cast() + rhs
+ if not isinstance(rhs, SafeData):
+ return t
+ if self._delegate_bytes:
+ return SafeBytes(t)
+ return SafeText(t)
+
+
def mark_safe(s):
"""
Explicitly mark a string as safe for (HTML) output purposes. The returned
@@ -127,10 +153,14 @@ def mark_safe(s):
"""
if isinstance(s, SafeData):
return s
- if isinstance(s, bytes) or (isinstance(s, Promise) and s._delegate_bytes):
+ if isinstance(s, bytes):
return SafeBytes(s)
- if isinstance(s, (six.text_type, Promise)):
+ if isinstance(s, six.text_type):
return SafeText(s)
+ if isinstance(s, Promise):
+ attrs = {'_func': staticmethod(s._func), '_resultclasses': s._resultclasses}
+ promise_cls = type('SafeDataPromise', (BaseSafeDataPromise,), attrs)
+ return promise_cls(s._args, s._kwargs)
return SafeString(str(s))
@@ -144,8 +174,12 @@ def mark_for_escaping(s):
"""
if isinstance(s, (SafeData, EscapeData)):
return s
- if isinstance(s, bytes) or (isinstance(s, Promise) and s._delegate_bytes):
+ if isinstance(s, bytes):
return EscapeBytes(s)
- if isinstance(s, (six.text_type, Promise)):
+ if isinstance(s, six.text_type):
return EscapeText(s)
+ if isinstance(s, Promise):
+ attrs = {'_func': staticmethod(s._func), '_resultclasses': s._resultclasses}
+ promise_cls = type('EscapeDataPromise', (BaseEscapeDataPromise,), attrs)
+ return promise_cls(s._args, s._kwargs)
return EscapeBytes(bytes(s))
View
12 tests/forms_tests/tests/test_regressions.py
@@ -8,7 +8,9 @@
ModelMultipleChoiceField, MultipleChoiceField, RadioSelect, Select,
TextInput,
)
+from django.template import Context, Template
from django.test import TestCase
+from django.utils.safestring import mark_safe
from django.utils import translation
from django.utils.translation import gettext_lazy, ugettext_lazy
@@ -153,3 +155,13 @@ class Meta:
obj = form.save()
obj.name = 'Camembert'
obj.full_clean()
+
+ def test_regression_21882(self):
+ class FooForm(Form):
+ foo = IntegerField(label=mark_safe(ugettext_lazy("<em>Foo</em>")))
+
+ f = FooForm()
+ self.assertEqual(f.fields['foo'].label, '<em>Foo</em>')
+ tpl = Template('{{ f.foo.label_tag }}')
+ context = Context({'f': f})
+ self.assertEqual(tpl.render(context), '<label for="id_foo"><em>Foo</em>:</label>')
View
63 tests/utils_tests/test_safestring.py
@@ -4,7 +4,7 @@
from django.test import TestCase
from django.utils.encoding import force_text, force_bytes
from django.utils.functional import lazy, Promise
-from django.utils.safestring import mark_safe, mark_for_escaping
+from django.utils.safestring import mark_safe, mark_for_escaping, SafeData, EscapeData
from django.utils import six
from django.utils import translation
@@ -24,25 +24,57 @@ def test_mark_safe(self):
self.assertRenderEqual('{{ s }}', 'a&b', s=s)
self.assertRenderEqual('{{ s|force_escape }}', 'a&amp;b', s=s)
- def test_mark_safe_lazy(self):
+ def test_mark_for_escaping(self):
+ s = mark_for_escaping('a&b')
+ self.assertRenderEqual('{{ s }}', 'a&amp;b', s=s)
+ self.assertRenderEqual('{% autoescape off %}{{ s }}{% endautoescape %}', 'a&amp;b', s=s)
+ self.assertRenderEqual('{{ s }}', 'a&amp;b', s=mark_for_escaping(s))
+
+ def test_html(self):
+ s = '<h1>interop</h1>'
+ self.assertEqual(s, mark_safe(s).__html__())
+
+ def test_concatenation(self):
+ safe = mark_safe('&')
+ unknown = '&'
+ unsafe = mark_for_escaping('&')
+
+ # Adding two safe strings results in another safe string
+ self.assertIsInstance(safe + safe, SafeData)
+ self.assertEqual(safe + safe, '&&')
+
+ # Adding a safe string to an unsafe one results in an unsafe one
+ self.assertNotIsInstance(safe + unknown, SafeData)
+ self.assertNotIsInstance(safe + unsafe, SafeData)
+ self.assertEqual(safe + unknown, '&&')
+ self.assertEqual(safe + unsafe, '&&')
+
+
+class LazySafeStringTests(TestCase):
+ def assertRenderEqual(self, tpl, expected, **context):
+ context = Context(context)
+ tpl = Template(tpl)
+ self.assertEqual(tpl.render(context), expected)
+
+
+ def test_mark_safe(self):
s = lazystr('a&b')
b = lazybytes(b'a&b')
self.assertIsInstance(mark_safe(s), Promise)
self.assertIsInstance(mark_safe(b), Promise)
+ self.assertIsInstance(mark_safe(s), SafeData)
+ self.assertIsInstance(mark_safe(b), SafeData)
self.assertRenderEqual('{{ s }}', 'a&b', s=mark_safe(s))
def test_mark_for_escaping(self):
- s = mark_for_escaping('a&b')
- self.assertRenderEqual('{{ s }}', 'a&amp;b', s=s)
- self.assertRenderEqual('{{ s }}', 'a&amp;b', s=mark_for_escaping(s))
-
- def test_mark_for_escaping_lazy(self):
s = lazystr('a&b')
b = lazybytes(b'a&b')
self.assertIsInstance(mark_for_escaping(s), Promise)
self.assertIsInstance(mark_for_escaping(b), Promise)
+ self.assertIsInstance(mark_for_escaping(s), EscapeData)
+ self.assertIsInstance(mark_for_escaping(b), EscapeData)
self.assertRenderEqual('{% autoescape off %}{{ s }}{% endautoescape %}', 'a&amp;b', s=mark_for_escaping(s))
def test_regression_20296(self):
@@ -50,6 +82,17 @@ def test_regression_20296(self):
with translation.override('fr'):
self.assertRenderEqual('{{ s }}', "nom d'utilisateur", s=s)
- def test_html(self):
- s = '<h1>interop</h1>'
- self.assertEqual(s, mark_safe(s).__html__())
+ def test_concatenation(self):
+ safe = mark_safe(lazystr('&'))
+ unknown = lazystr('&')
+ unsafe = mark_for_escaping(lazystr('&'))
+
+ # Adding two safe strings results in another safe string
+ self.assertIsInstance(safe + safe, SafeData)
+ self.assertEqual(safe + safe, '&&')
+
+ # Adding a safe string to an unsafe one results in an unsafe one
+ self.assertNotIsInstance(safe + unknown, SafeData)
+ self.assertNotIsInstance(safe + unsafe, SafeData)
+ self.assertEqual(safe + unknown, '&&')
+ self.assertEqual(safe + unsafe, '&&')

No commit comments for this range

Something went wrong with that request. Please try again.