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',
]
)