From b40ccfcba362092a79001aa5c0da77fd904ed692 Mon Sep 17 00:00:00 2001 From: Berker Peksag Date: Tue, 11 Jun 2013 09:15:41 +0300 Subject: [PATCH] Add Python 3 support. --- .travis.yml | 19 +++++++++++------- CHANGELOG | 5 +++++ README.rst | 9 +++++++-- docs/index.rst | 15 +++++++++----- fabfile.py | 40 ------------------------------------- jingo/__init__.py | 3 +++ jingo/helpers.py | 25 ++++++++++++++++------- jingo/monkey.py | 8 ++++++-- jingo/tests/test_basics.py | 7 ++++++- jingo/tests/test_helpers.py | 28 +++++++++++++++----------- jingo/tests/test_loader.py | 19 +++++++++++------- jingo/tests/test_monkey.py | 9 ++++++--- jingo/tests/test_views.py | 7 ++++++- jingo/tests/urls.py | 2 ++ jingo/tests/utils.py | 35 ++++++++++++++++++++++++++++++++ requirements.txt | 5 ++--- run_tests.py | 13 ++++++++++++ setup.py | 5 +++++ 18 files changed, 164 insertions(+), 90 deletions(-) delete mode 100644 fabfile.py create mode 100644 jingo/tests/utils.py create mode 100644 run_tests.py diff --git a/.travis.yml b/.travis.yml index 3637036..d13466a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,16 @@ language: python python: - - "2.6" - - "2.7" + - "2.6" + - "2.7" + - "3.3" env: - - DJANGO_VERSION=1.4.5 - - DJANGO_VERSION=1.5.1 + - DJANGO_VERSION=1.4.6 + - DJANGO_VERSION=1.5.2 install: - - pip install -q Django==${DJANGO_VERSION} --use-mirrors - - pip install -q -r requirements.txt --use-mirrors -script: fab test + - pip install Django==${DJANGO_VERSION} + - pip install -r requirements.txt +matrix: + exclude: + - python: "3.3" + env: DJANGO_VERSION=1.4.6 +script: python run_tests.py diff --git a/CHANGELOG b/CHANGELOG index f86f6aa..72a988b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ CHANGES ======= +v0.x.y +------ + +* Added Python 3 support. + v0.6.2 ------ diff --git a/README.rst b/README.rst index cb008c7..89448ce 100644 --- a/README.rst +++ b/README.rst @@ -190,6 +190,11 @@ Usage:: Testing ------- -Testing is handle via fabric:: +To run the test suite, you need to define ``DJANGO_SETTINGS_MODULE`` first:: - fab test + $ export DJANGO_SETTINGS_MODULE="fake_settings" + $ nosetests + +or simply run:: + + $ python run_tests.py diff --git a/docs/index.rst b/docs/index.rst index 3cbb933..02479b2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -154,9 +154,9 @@ could create a link if I knew how to do that. The other method uses Jinja's ``trans`` tag:: - {% trans user=review.user|user_link, date=review.created|datetime %} - by {{ user }} on {{ date }} - {% endtrans %} + {% trans user=review.user|user_link, date=review.created|datetime %} + by {{ user }} on {{ date }} + {% endtrans %} ``trans`` is nice when you have a lot of text or want to inject some variables directly. Both methods are useful, pick the one that makes you happy. @@ -174,6 +174,11 @@ not recognize. Testing ------- -Testing is handle via fabric:: +To run the test suite, you need to define ``DJANGO_SETTINGS_MODULE`` first:: - fab test + $ export DJANGO_SETTINGS_MODULE="fake_settings" + $ nosetests + +or simply run:: + + $ python run_tests.py diff --git a/fabfile.py b/fabfile.py deleted file mode 100644 index f5c52a0..0000000 --- a/fabfile.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -Creating standalone Django apps is a PITA because you're not in a project, so -you don't have a settings.py file. I can never remember to define -DJANGO_SETTINGS_MODULE, so I run these commands which get the right env -automatically. -""" -import functools -import os - -from fabric.api import local, cd, env -from fabric.contrib.project import rsync_project - -NAME = os.path.basename(os.path.dirname(__file__)) -ROOT = os.path.abspath(os.path.dirname(__file__)) - -os.environ['DJANGO_SETTINGS_MODULE'] = 'fake_settings' -os.environ['PYTHONPATH'] = os.pathsep.join([ROOT, - os.path.join(ROOT, 'examples')]) - -env.hosts = ['jbalogh.me'] - -local = functools.partial(local, capture=False) - - -def doc(kind='html'): - with cd('docs'): - local('make clean %s' % kind) - -def shell(): - local('django-admin.py shell') - -def test(): - local('nosetests') - -def cover(): - local('nosetests --with-coverage') - -def updoc(): - doc('dirhtml') - rsync_project('p/%s' % NAME, 'docs/_build/dirhtml/', delete=True) diff --git a/jingo/__init__.py b/jingo/__init__.py index c483c94..7c6ff40 100644 --- a/jingo/__init__.py +++ b/jingo/__init__.py @@ -1,4 +1,7 @@ """Adapter for using Jinja2 with Django.""" + +from __future__ import unicode_literals + import functools import imp import logging diff --git a/jingo/helpers.py b/jingo/helpers.py index 4df6013..5e404a1 100644 --- a/jingo/helpers.py +++ b/jingo/helpers.py @@ -1,6 +1,14 @@ +# coding: utf-8 + +from __future__ import unicode_literals, print_function + +from django.utils import six from django.utils.translation import ugettext as _ from django.template.defaulttags import CsrfTokenNode -from django.utils.encoding import smart_unicode +try: + from django.utils.encoding import smart_unicode as smart_text +except ImportError: + from django.utils.encoding import smart_text from django.core.urlresolvers import reverse import jinja2 @@ -23,7 +31,6 @@ def f(string, *args, **kwargs): >>> {{ "{0} arguments and {x} arguments"|f('positional', x='keyword') }} "positional arguments and keyword arguments" """ - string = unicode(string) return string.format(*args, **kwargs) @@ -32,12 +39,12 @@ def fe(string, *args, **kwargs): """Format a safe string with potentially unsafe arguments, then return a safe string.""" - string = unicode(string) + string = six.text_type(string) - args = [jinja2.escape(smart_unicode(v)) for v in args] + args = [jinja2.escape(smart_text(v)) for v in args] for k in kwargs: - kwargs[k] = jinja2.escape(smart_unicode(kwargs[k])) + kwargs[k] = jinja2.escape(smart_text(kwargs[k])) return jinja2.Markup(string.format(*args, **kwargs)) @@ -54,8 +61,12 @@ def nl2br(string): def datetime(t, fmt=None): """Call ``datetime.strftime`` with the given format string.""" if fmt is None: - fmt = _('%B %e, %Y') - return smart_unicode(t.strftime(fmt.encode('utf-8'))) if t else u'' + fmt = _(u'%B %e, %Y') + if not six.PY3: + # The datetime.strftime function strictly does not + # support Unicode in Python 2 but is Unicode only in 3.x. + fmt = fmt.encode('utf-8') + return smart_text(t.strftime(fmt)) if t else '' @register.filter diff --git a/jingo/monkey.py b/jingo/monkey.py index 4eabad7..f104071 100644 --- a/jingo/monkey.py +++ b/jingo/monkey.py @@ -21,9 +21,13 @@ from the nuggets project at https://github.com/mozilla/nuggets/blob/master/safe_django_forms.py """ + +from __future__ import unicode_literals + import django.utils.encoding import django.utils.html import django.utils.safestring +from django.utils import six # This function gets directly imported within Django, so this change needs to @@ -47,7 +51,7 @@ def __html__(self): Allows interoperability with other template engines. """ - return unicode(self) + return six.text_type(self) # Django uses StrAndUnicode for classes like Form, BoundField, Widget which @@ -57,7 +61,7 @@ class SafeStrAndUnicode(django.utils.encoding.StrAndUnicode): """A class whose __str__ and __html__ returns __unicode__.""" def __html__(self): - return unicode(self) + return six.text_type(self) def patch(): diff --git a/jingo/tests/test_basics.py b/jingo/tests/test_basics.py index 5693e7c..2d1e113 100644 --- a/jingo/tests/test_basics.py +++ b/jingo/tests/test_basics.py @@ -1,8 +1,13 @@ +from __future__ import unicode_literals + from django.shortcuts import render import jinja2 from nose.tools import eq_ -from mock import Mock, patch, sentinel +try: + from unittest.mock import Mock, patch, sentinel +except ImportError: + from mock import Mock, patch, sentinel import jingo diff --git a/jingo/tests/test_helpers.py b/jingo/tests/test_helpers.py index cd33296..29e17d4 100644 --- a/jingo/tests/test_helpers.py +++ b/jingo/tests/test_helpers.py @@ -1,20 +1,23 @@ # -*- coding: utf-8 -*- """Tests for the jingo's builtin helpers.""" + +from __future__ import unicode_literals + from datetime import datetime from collections import namedtuple +from django.utils import six from jinja2 import Markup -from mock import patch +try: + from unittest.mock import patch +except ImportError: + from mock import patch from nose.tools import eq_ -import jingo from jingo import helpers from jingo import register - -def render(s, context={}): - t = jingo.env.from_string(s) - return t.render(context) +from .utils import htmleq_, render def test_f(): @@ -35,9 +38,9 @@ def test_fe_positional(): def test_fe_unicode(): - context = {'var': u'Français'} + context = {'var': 'Français'} template = '{{ "Speak {0}"|fe(var) }}' - eq_(u'Speak Français', render(template, context)) + eq_('Speak Français', render(template, context)) def test_fe_markup(): @@ -113,7 +116,7 @@ def test_field_attrs(): def __str__(self): attrs = self.field.widget.attrs attr_str = ' '.join('%s="%s"' % (k, v) - for (k, v) in attrs.iteritems()) + for (k, v) in six.iteritems(attrs)) return Markup('' % attr_str) def __html__(self): @@ -122,7 +125,7 @@ def test_field_attrs(): f = field() s = render('{{ field|field_attrs(class="bar",name="baz") }}', {'field': f}) - eq_(s, '') + htmleq_(s, '') def test_url(): @@ -157,14 +160,15 @@ def test_custom_url(s): def test_filter_override(): def f(s): return s.upper() - f.__name__ = 'a' + # See issue 7688: http://bugs.python.org/issue7688 + f.__name__ = 'a' if six.PY3 else b'a' register.filter(f) s = render('{{ s|a }}', {'s': 'Str'}) eq_(s, 'STR') def g(s): return s.lower() - g.__name__ = 'a' + g.__name__ = 'a' if six.PY3 else b'a' register.filter(override=False)(g) s = render('{{ s|a }}', {'s': 'Str'}) eq_(s, 'STR') diff --git a/jingo/tests/test_loader.py b/jingo/tests/test_loader.py index 2bffed2..ad74165 100644 --- a/jingo/tests/test_loader.py +++ b/jingo/tests/test_loader.py @@ -1,34 +1,39 @@ +from __future__ import unicode_literals + from django.shortcuts import render from nose.tools import eq_ -from mock import Mock +try: + from unittest.mock import Mock +except ImportError: + from mock import Mock def test_render(): r = render(Mock(), 'jinja_app/test.html', {}) - eq_(r.content, 'HELLO') + eq_(r.content, b'HELLO') def test_render_no_toplevel_override(): r = render(Mock(), 'jinja_app/test_nonoverride.html', {}) - eq_(r.content, 'HELLO') + eq_(r.content, b'HELLO') def test_render_toplevel_override(): r = render(Mock(), 'jinja_app/test_override.html', {}) - eq_(r.content, 'HELLO') + eq_(r.content, b'HELLO') def test_render_django(): r = render(Mock(), 'django_app/test.html', {}) - eq_(r.content, 'HELLO ...\n') + eq_(r.content, b'HELLO ...\n') def test_render_django_no_toplevel_override(): r = render(Mock(), 'django_app/test_nonoverride.html', {}) - eq_(r.content, 'HELLO ...\n') + eq_(r.content, b'HELLO ...\n') def test_render_django_toplevel_override(): r = render(Mock(), 'django_app/test_override.html', {}) - eq_(r.content, 'HELLO ...\n') + eq_(r.content, b'HELLO ...\n') diff --git a/jingo/tests/test_monkey.py b/jingo/tests/test_monkey.py index dd887d1..c9e4bee 100644 --- a/jingo/tests/test_monkey.py +++ b/jingo/tests/test_monkey.py @@ -1,11 +1,14 @@ +from __future__ import unicode_literals + from django import forms +from django.utils import six from jinja2 import escape from nose.tools import eq_ -import jingo import jingo.monkey -from test_helpers import render + +from .utils import render class MyForm(forms.Form): @@ -23,5 +26,5 @@ def test_monkey_patch(): jingo.monkey.patch() eq_(html, render(t, context)) - s = unicode(form['email']) + s = six.text_type(form['email']) eq_(s, render('{{ form.email }}', {'form': form})) diff --git a/jingo/tests/test_views.py b/jingo/tests/test_views.py index d0abf74..8b28908 100644 --- a/jingo/tests/test_views.py +++ b/jingo/tests/test_views.py @@ -1,6 +1,11 @@ +from __future__ import unicode_literals + from django.utils import translation -from mock import sentinel +try: + from unittest.mock import sentinel +except ImportError: + from mock import sentinel from nose.tools import eq_ from jingo import get_env, render_to_string diff --git a/jingo/tests/urls.py b/jingo/tests/urls.py index 6464b10..f062720 100644 --- a/jingo/tests/urls.py +++ b/jingo/tests/urls.py @@ -1,3 +1,5 @@ +from __future__ import unicode_literals + from django.conf.urls import patterns diff --git a/jingo/tests/utils.py b/jingo/tests/utils.py new file mode 100644 index 0000000..6a52057 --- /dev/null +++ b/jingo/tests/utils.py @@ -0,0 +1,35 @@ +from django.test.html import HTMLParseError, parse_html +from nose.tools import eq_ + +from jingo import env + + +def htmleq_(html1, html2, msg=None): + """ + Asserts that two HTML snippets are semantically the same. + Whitespace in most cases is ignored, and attribute ordering is not + significant. The passed-in arguments must be valid HTML. + + See ticket 16921: https://code.djangoproject.com/ticket/16921 + + """ + dom1 = assert_and_parse_html(html1, msg, + 'First argument is not valid HTML:') + dom2 = assert_and_parse_html(html2, msg, + 'Second argument is not valid HTML:') + + eq_(dom1, dom2) + + +def assert_and_parse_html(html, user_msg, msg): + try: + dom = parse_html(html) + except HTMLParseError as e: + standard_msg = '%s\n%s\n%s' % (user_msg, msg, e.msg) + raise AssertionError(standard_msg) + return dom + + +def render(s, context={}): + t = env.from_string(s) + return t.render(context) diff --git a/requirements.txt b/requirements.txt index 49acbce..64b7be2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ # These are the reqs to build docs and run tests. -Django==1.5.1 +Django==1.5.2 sphinx -jinja2 +jinja2==2.7 nose mock -fabric diff --git a/run_tests.py b/run_tests.py new file mode 100644 index 0000000..9a2a7ec --- /dev/null +++ b/run_tests.py @@ -0,0 +1,13 @@ +import os + +import nose + +NAME = os.path.basename(os.path.dirname(__file__)) +ROOT = os.path.abspath(os.path.dirname(__file__)) + +os.environ['DJANGO_SETTINGS_MODULE'] = 'fake_settings' +os.environ['PYTHONPATH'] = os.pathsep.join([ROOT, + os.path.join(ROOT, 'examples')]) + +if __name__ == '__main__': + nose.main() diff --git a/setup.py b/setup.py index 7d69de9..66f8e54 100644 --- a/setup.py +++ b/setup.py @@ -24,6 +24,11 @@ setup( 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.3', 'Topic :: Software Development :: Libraries :: Python Modules', ] )