From 547b181046539f548839e42544521503fbe4d821 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 18 Aug 2012 16:04:06 +0200 Subject: [PATCH] [py3] Ported django.utils.safestring. Backwards compatibility aliases were created under Python 2. --- django/db/backends/mysql/base.py | 8 +- .../db/backends/postgresql_psycopg2/base.py | 6 +- django/db/backends/sqlite3/base.py | 4 +- django/utils/encoding.py | 4 +- django/utils/safestring.py | 74 +++++++++++-------- docs/howto/custom-template-tags.txt | 6 +- docs/ref/utils.txt | 22 +++++- tests/regressiontests/i18n/tests.py | 12 +-- 8 files changed, 82 insertions(+), 54 deletions(-) diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index 2222f89cf0..cec3b04f7e 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -38,7 +38,7 @@ from django.db.backends.mysql.creation import DatabaseCreation from django.db.backends.mysql.introspection import DatabaseIntrospection from django.db.backends.mysql.validation import DatabaseValidation from django.utils.functional import cached_property -from django.utils.safestring import SafeString, SafeUnicode +from django.utils.safestring import SafeBytes, SafeText from django.utils import six from django.utils import timezone @@ -75,7 +75,7 @@ def adapt_datetime_with_timezone_support(value, conv): # MySQLdb-1.2.1 returns TIME columns as timedelta -- they are more like # timedelta in terms of actual behavior as they are signed and include days -- # and Django expects time, so we still need to override that. We also need to -# add special handling for SafeUnicode and SafeString as MySQLdb's type +# add special handling for SafeText and SafeBytes as MySQLdb's type # checking is too tight to catch those (see Django ticket #6052). # Finally, MySQLdb always returns naive datetime objects. However, when # timezone support is active, Django expects timezone-aware datetime objects. @@ -402,8 +402,8 @@ class DatabaseWrapper(BaseDatabaseWrapper): kwargs['client_flag'] = CLIENT.FOUND_ROWS kwargs.update(settings_dict['OPTIONS']) self.connection = Database.connect(**kwargs) - self.connection.encoders[SafeUnicode] = self.connection.encoders[six.text_type] - self.connection.encoders[SafeString] = self.connection.encoders[bytes] + self.connection.encoders[SafeText] = self.connection.encoders[six.text_type] + self.connection.encoders[SafeBytes] = self.connection.encoders[bytes] connection_created.send(sender=self.__class__, connection=self) cursor = self.connection.cursor() if new_connection: diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index d553594ec1..f6f534da8c 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -14,7 +14,7 @@ from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation from django.db.backends.postgresql_psycopg2.version import get_version from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection from django.utils.log import getLogger -from django.utils.safestring import SafeUnicode, SafeString +from django.utils.safestring import SafeText, SafeBytes from django.utils import six from django.utils.timezone import utc @@ -29,8 +29,8 @@ DatabaseError = Database.DatabaseError IntegrityError = Database.IntegrityError psycopg2.extensions.register_type(psycopg2.extensions.UNICODE) -psycopg2.extensions.register_adapter(SafeString, psycopg2.extensions.QuotedString) -psycopg2.extensions.register_adapter(SafeUnicode, psycopg2.extensions.QuotedString) +psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString) +psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString) logger = getLogger('django.db.backends') diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 775ca20e45..31a16b6a2b 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -20,7 +20,7 @@ from django.db.backends.sqlite3.creation import DatabaseCreation from django.db.backends.sqlite3.introspection import DatabaseIntrospection from django.utils.dateparse import parse_date, parse_datetime, parse_time from django.utils.functional import cached_property -from django.utils.safestring import SafeString +from django.utils.safestring import SafeBytes from django.utils import six from django.utils import timezone @@ -80,7 +80,7 @@ if Database.version_info >= (2, 4, 1): # slow-down, this adapter is only registered for sqlite3 versions # needing it (Python 2.6 and up). Database.register_adapter(str, lambda s: s.decode('utf-8')) - Database.register_adapter(SafeString, lambda s: s.decode('utf-8')) + Database.register_adapter(SafeBytes, lambda s: s.decode('utf-8')) class DatabaseFeatures(BaseDatabaseFeatures): # SQLite cannot handle us only partially reading from a cursor's result set diff --git a/django/utils/encoding.py b/django/utils/encoding.py index 7b80f13496..3ac2c508b6 100644 --- a/django/utils/encoding.py +++ b/django/utils/encoding.py @@ -119,8 +119,8 @@ def force_text(s, encoding='utf-8', strings_only=False, errors='strict'): errors) for arg in s]) else: # Note: We use .decode() here, instead of six.text_type(s, encoding, - # errors), so that if s is a SafeString, it ends up being a - # SafeUnicode at the end. + # errors), so that if s is a SafeBytes, it ends up being a + # SafeText at the end. s = s.decode(encoding, errors) except UnicodeDecodeError as e: if not isinstance(s, Exception): diff --git a/django/utils/safestring.py b/django/utils/safestring.py index bfaefd07ee..07e0bf4cea 100644 --- a/django/utils/safestring.py +++ b/django/utils/safestring.py @@ -10,36 +10,43 @@ from django.utils import six class EscapeData(object): pass -class EscapeString(bytes, EscapeData): +class EscapeBytes(bytes, EscapeData): """ - A string that should be HTML-escaped when output. + A byte string that should be HTML-escaped when output. """ pass -class EscapeUnicode(six.text_type, EscapeData): +class EscapeText(six.text_type, EscapeData): """ - A unicode object that should be HTML-escaped when output. + A unicode string object that should be HTML-escaped when output. """ pass +if six.PY3: + EscapeString = EscapeText +else: + EscapeString = EscapeBytes + # backwards compatibility for Python 2 + EscapeUnicode = EscapeText + class SafeData(object): pass -class SafeString(bytes, SafeData): +class SafeBytes(bytes, SafeData): """ - A string subclass that has been specifically marked as "safe" (requires no + A bytes subclass that has been specifically marked as "safe" (requires no further escaping) for HTML output purposes. """ def __add__(self, rhs): """ - Concatenating a safe string with another safe string or safe unicode - object is safe. Otherwise, the result is no longer safe. + Concatenating a safe byte string with another safe byte string or safe + unicode string is safe. Otherwise, the result is no longer safe. """ - t = super(SafeString, self).__add__(rhs) - if isinstance(rhs, SafeUnicode): - return SafeUnicode(t) - elif isinstance(rhs, SafeString): - return SafeString(t) + t = super(SafeBytes, self).__add__(rhs) + if isinstance(rhs, SafeText): + return SafeText(t) + elif isinstance(rhs, SafeBytes): + return SafeBytes(t) return t def _proxy_method(self, *args, **kwargs): @@ -51,25 +58,25 @@ class SafeString(bytes, SafeData): method = kwargs.pop('method') data = method(self, *args, **kwargs) if isinstance(data, bytes): - return SafeString(data) + return SafeBytes(data) else: - return SafeUnicode(data) + return SafeText(data) decode = curry(_proxy_method, method=bytes.decode) -class SafeUnicode(six.text_type, SafeData): +class SafeText(six.text_type, SafeData): """ - A unicode subclass that has been specifically marked as "safe" for HTML - output purposes. + A unicode (Python 2) / str (Python 3) subclass that has been specifically + marked as "safe" for HTML output purposes. """ def __add__(self, rhs): """ - Concatenating a safe unicode object with another safe string or safe - unicode object is safe. Otherwise, the result is no longer safe. + Concatenating a safe unicode string with another safe byte string or + safe unicode string is safe. Otherwise, the result is no longer safe. """ - t = super(SafeUnicode, self).__add__(rhs) + t = super(SafeText, self).__add__(rhs) if isinstance(rhs, SafeData): - return SafeUnicode(t) + return SafeText(t) return t def _proxy_method(self, *args, **kwargs): @@ -81,12 +88,19 @@ class SafeUnicode(six.text_type, SafeData): method = kwargs.pop('method') data = method(self, *args, **kwargs) if isinstance(data, bytes): - return SafeString(data) + return SafeBytes(data) else: - return SafeUnicode(data) + return SafeText(data) encode = curry(_proxy_method, method=six.text_type.encode) +if six.PY3: + SafeString = SafeText +else: + SafeString = SafeBytes + # backwards compatibility for Python 2 + SafeUnicode = SafeText + def mark_safe(s): """ Explicitly mark a string as safe for (HTML) output purposes. The returned @@ -97,10 +111,10 @@ def mark_safe(s): if isinstance(s, SafeData): return s if isinstance(s, bytes) or (isinstance(s, Promise) and s._delegate_bytes): - return SafeString(s) + return SafeBytes(s) if isinstance(s, (six.text_type, Promise)): - return SafeUnicode(s) - return SafeString(bytes(s)) + return SafeText(s) + return SafeString(str(s)) def mark_for_escaping(s): """ @@ -113,8 +127,8 @@ def mark_for_escaping(s): if isinstance(s, (SafeData, EscapeData)): return s if isinstance(s, bytes) or (isinstance(s, Promise) and s._delegate_bytes): - return EscapeString(s) + return EscapeBytes(s) if isinstance(s, (six.text_type, Promise)): - return EscapeUnicode(s) - return EscapeString(bytes(s)) + return EscapeText(s) + return EscapeBytes(bytes(s)) diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt index 054c831fad..5b27af82d6 100644 --- a/docs/howto/custom-template-tags.txt +++ b/docs/howto/custom-template-tags.txt @@ -189,7 +189,7 @@ passed around inside the template code: They're commonly used for output that contains raw HTML that is intended to be interpreted as-is on the client side. - Internally, these strings are of type ``SafeString`` or ``SafeUnicode``. + Internally, these strings are of type ``SafeBytes`` or ``SafeText``. They share a common base class of ``SafeData``, so you can test for them using code like: @@ -204,8 +204,8 @@ passed around inside the template code: not. These strings are only escaped once, however, even if auto-escaping applies. - Internally, these strings are of type ``EscapeString`` or - ``EscapeUnicode``. Generally you don't have to worry about these; they + Internally, these strings are of type ``EscapeBytes`` or + ``EscapeText``. Generally you don't have to worry about these; they exist for the implementation of the :tfilter:`escape` filter. Template filter code falls into one of two situations: diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 97643c29e9..20775fcc81 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -560,15 +560,29 @@ string" means 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. +.. class:: SafeBytes + + .. versionadded:: 1.5 + + A :class:`bytes` subclass that has been specifically marked as "safe" + (requires no further escaping) for HTML output purposes. + .. class:: SafeString - A string subclass that has been specifically marked as "safe" (requires no - further escaping) for HTML output purposes. + A :class:`str` subclass that has been specifically marked as "safe" + (requires no further escaping) for HTML output purposes. This is + :class:`SafeBytes` on Python 2 and :class:`SafeText` on Python 3. + +.. class:: SafeText + + .. versionadded:: 1.5 + + A :class:`str` (in Python 3) or :class:`unicode` (in Python 2) subclass + that has been specifically marked as "safe" for HTML output purposes. .. class:: SafeUnicode - A unicode subclass that has been specifically marked as "safe" for HTML - output purposes. + Historical name of :class:`SafeText`. Only available under Python 2. .. function:: mark_safe(s) diff --git a/tests/regressiontests/i18n/tests.py b/tests/regressiontests/i18n/tests.py index 1a8ff25ff3..c13c1c6f86 100644 --- a/tests/regressiontests/i18n/tests.py +++ b/tests/regressiontests/i18n/tests.py @@ -18,7 +18,7 @@ from django.utils.formats import (get_format, date_format, time_format, number_format) from django.utils.importlib import import_module from django.utils.numberformat import format as nformat -from django.utils.safestring import mark_safe, SafeString, SafeUnicode +from django.utils.safestring import mark_safe, SafeBytes, SafeString, SafeText from django.utils import six from django.utils.six import PY3 from django.utils.translation import (ugettext, ugettext_lazy, activate, @@ -235,9 +235,9 @@ class TranslationTests(TestCase): s = mark_safe(str('Password')) self.assertEqual(SafeString, type(s)) with translation.override('de', deactivate=True): - self.assertEqual(SafeUnicode, type(ugettext(s))) - self.assertEqual('aPassword', SafeString('a') + s) - self.assertEqual('Passworda', s + SafeString('a')) + self.assertEqual(SafeText, type(ugettext(s))) + self.assertEqual('aPassword', SafeText('a') + s) + self.assertEqual('Passworda', s + SafeText('a')) self.assertEqual('Passworda', s + mark_safe('a')) self.assertEqual('aPassword', mark_safe('a') + s) self.assertEqual('as', mark_safe('a') + mark_safe('s')) @@ -897,9 +897,9 @@ class TestModels(TestCase): def test_safestr(self): c = Company(cents_paid=12, products_delivered=1) - c.name = SafeUnicode('Iñtërnâtiônàlizætiøn1') + c.name = SafeText('Iñtërnâtiônàlizætiøn1') c.save() - c.name = SafeString('Iñtërnâtiônàlizætiøn1'.encode('utf-8')) + c.name = SafeBytes('Iñtërnâtiônàlizætiøn1'.encode('utf-8')) c.save()