Add Python 3 support.

This commit is contained in:
Berker Peksag 2013-06-11 09:15:41 +03:00
parent 95b30e6207
commit b40ccfcba3
18 changed files with 164 additions and 90 deletions

View File

@ -1,11 +1,16 @@
language: python language: python
python: python:
- "2.6" - "2.6"
- "2.7" - "2.7"
- "3.3"
env: env:
- DJANGO_VERSION=1.4.5 - DJANGO_VERSION=1.4.6
- DJANGO_VERSION=1.5.1 - DJANGO_VERSION=1.5.2
install: install:
- pip install -q Django==${DJANGO_VERSION} --use-mirrors - pip install Django==${DJANGO_VERSION}
- pip install -q -r requirements.txt --use-mirrors - pip install -r requirements.txt
script: fab test matrix:
exclude:
- python: "3.3"
env: DJANGO_VERSION=1.4.6
script: python run_tests.py

View File

@ -1,6 +1,11 @@
CHANGES CHANGES
======= =======
v0.x.y
------
* Added Python 3 support.
v0.6.2 v0.6.2
------ ------

View File

@ -190,6 +190,11 @@ Usage::
Testing 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

View File

@ -154,9 +154,9 @@ could create a link if I knew how to do that.
The other method uses Jinja's ``trans`` tag:: The other method uses Jinja's ``trans`` tag::
{% trans user=review.user|user_link, date=review.created|datetime %} {% trans user=review.user|user_link, date=review.created|datetime %}
by {{ user }} on {{ date }} by {{ user }} on {{ date }}
{% endtrans %} {% endtrans %}
``trans`` is nice when you have a lot of text or want to inject some variables ``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. directly. Both methods are useful, pick the one that makes you happy.
@ -174,6 +174,11 @@ not recognize.
Testing 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

40
fabfile.py vendored
View File

@ -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)

View File

@ -1,4 +1,7 @@
"""Adapter for using Jinja2 with Django.""" """Adapter for using Jinja2 with Django."""
from __future__ import unicode_literals
import functools import functools
import imp import imp
import logging import logging

View File

@ -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.utils.translation import ugettext as _
from django.template.defaulttags import CsrfTokenNode 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 from django.core.urlresolvers import reverse
import jinja2 import jinja2
@ -23,7 +31,6 @@ def f(string, *args, **kwargs):
>>> {{ "{0} arguments and {x} arguments"|f('positional', x='keyword') }} >>> {{ "{0} arguments and {x} arguments"|f('positional', x='keyword') }}
"positional arguments and keyword arguments" "positional arguments and keyword arguments"
""" """
string = unicode(string)
return string.format(*args, **kwargs) 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 """Format a safe string with potentially unsafe arguments, then return a
safe string.""" 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: 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)) return jinja2.Markup(string.format(*args, **kwargs))
@ -54,8 +61,12 @@ def nl2br(string):
def datetime(t, fmt=None): def datetime(t, fmt=None):
"""Call ``datetime.strftime`` with the given format string.""" """Call ``datetime.strftime`` with the given format string."""
if fmt is None: if fmt is None:
fmt = _('%B %e, %Y') fmt = _(u'%B %e, %Y')
return smart_unicode(t.strftime(fmt.encode('utf-8'))) if t else u'' 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 @register.filter

View File

@ -21,9 +21,13 @@ from the nuggets project at
https://github.com/mozilla/nuggets/blob/master/safe_django_forms.py https://github.com/mozilla/nuggets/blob/master/safe_django_forms.py
""" """
from __future__ import unicode_literals
import django.utils.encoding import django.utils.encoding
import django.utils.html import django.utils.html
import django.utils.safestring import django.utils.safestring
from django.utils import six
# This function gets directly imported within Django, so this change needs to # 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. Allows interoperability with other template engines.
""" """
return unicode(self) return six.text_type(self)
# Django uses StrAndUnicode for classes like Form, BoundField, Widget which # 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__.""" """A class whose __str__ and __html__ returns __unicode__."""
def __html__(self): def __html__(self):
return unicode(self) return six.text_type(self)
def patch(): def patch():

View File

@ -1,8 +1,13 @@
from __future__ import unicode_literals
from django.shortcuts import render from django.shortcuts import render
import jinja2 import jinja2
from nose.tools import eq_ 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 import jingo

View File

@ -1,20 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""Tests for the jingo's builtin helpers.""" """Tests for the jingo's builtin helpers."""
from __future__ import unicode_literals
from datetime import datetime from datetime import datetime
from collections import namedtuple from collections import namedtuple
from django.utils import six
from jinja2 import Markup 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_ from nose.tools import eq_
import jingo
from jingo import helpers from jingo import helpers
from jingo import register from jingo import register
from .utils import htmleq_, render
def render(s, context={}):
t = jingo.env.from_string(s)
return t.render(context)
def test_f(): def test_f():
@ -35,9 +38,9 @@ def test_fe_positional():
def test_fe_unicode(): def test_fe_unicode():
context = {'var': u'Français'} context = {'var': 'Français'}
template = '{{ "Speak {0}"|fe(var) }}' template = '{{ "Speak {0}"|fe(var) }}'
eq_(u'Speak Français', render(template, context)) eq_('Speak Français', render(template, context))
def test_fe_markup(): def test_fe_markup():
@ -113,7 +116,7 @@ def test_field_attrs():
def __str__(self): def __str__(self):
attrs = self.field.widget.attrs attrs = self.field.widget.attrs
attr_str = ' '.join('%s="%s"' % (k, v) attr_str = ' '.join('%s="%s"' % (k, v)
for (k, v) in attrs.iteritems()) for (k, v) in six.iteritems(attrs))
return Markup('<input %s />' % attr_str) return Markup('<input %s />' % attr_str)
def __html__(self): def __html__(self):
@ -122,7 +125,7 @@ def test_field_attrs():
f = field() f = field()
s = render('{{ field|field_attrs(class="bar",name="baz") }}', s = render('{{ field|field_attrs(class="bar",name="baz") }}',
{'field': f}) {'field': f})
eq_(s, '<input class="bar" name="baz" />') htmleq_(s, '<input class="bar" name="baz" />')
def test_url(): def test_url():
@ -157,14 +160,15 @@ def test_custom_url(s):
def test_filter_override(): def test_filter_override():
def f(s): def f(s):
return s.upper() 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) register.filter(f)
s = render('{{ s|a }}', {'s': 'Str'}) s = render('{{ s|a }}', {'s': 'Str'})
eq_(s, 'STR') eq_(s, 'STR')
def g(s): def g(s):
return s.lower() return s.lower()
g.__name__ = 'a' g.__name__ = 'a' if six.PY3 else b'a'
register.filter(override=False)(g) register.filter(override=False)(g)
s = render('{{ s|a }}', {'s': 'Str'}) s = render('{{ s|a }}', {'s': 'Str'})
eq_(s, 'STR') eq_(s, 'STR')

View File

@ -1,34 +1,39 @@
from __future__ import unicode_literals
from django.shortcuts import render from django.shortcuts import render
from nose.tools import eq_ from nose.tools import eq_
from mock import Mock try:
from unittest.mock import Mock
except ImportError:
from mock import Mock
def test_render(): def test_render():
r = render(Mock(), 'jinja_app/test.html', {}) r = render(Mock(), 'jinja_app/test.html', {})
eq_(r.content, 'HELLO') eq_(r.content, b'HELLO')
def test_render_no_toplevel_override(): def test_render_no_toplevel_override():
r = render(Mock(), 'jinja_app/test_nonoverride.html', {}) r = render(Mock(), 'jinja_app/test_nonoverride.html', {})
eq_(r.content, 'HELLO') eq_(r.content, b'HELLO')
def test_render_toplevel_override(): def test_render_toplevel_override():
r = render(Mock(), 'jinja_app/test_override.html', {}) r = render(Mock(), 'jinja_app/test_override.html', {})
eq_(r.content, 'HELLO') eq_(r.content, b'HELLO')
def test_render_django(): def test_render_django():
r = render(Mock(), 'django_app/test.html', {}) 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(): def test_render_django_no_toplevel_override():
r = render(Mock(), 'django_app/test_nonoverride.html', {}) 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(): def test_render_django_toplevel_override():
r = render(Mock(), 'django_app/test_override.html', {}) r = render(Mock(), 'django_app/test_override.html', {})
eq_(r.content, 'HELLO ...\n') eq_(r.content, b'HELLO ...\n')

View File

@ -1,11 +1,14 @@
from __future__ import unicode_literals
from django import forms from django import forms
from django.utils import six
from jinja2 import escape from jinja2 import escape
from nose.tools import eq_ from nose.tools import eq_
import jingo
import jingo.monkey import jingo.monkey
from test_helpers import render
from .utils import render
class MyForm(forms.Form): class MyForm(forms.Form):
@ -23,5 +26,5 @@ def test_monkey_patch():
jingo.monkey.patch() jingo.monkey.patch()
eq_(html, render(t, context)) eq_(html, render(t, context))
s = unicode(form['email']) s = six.text_type(form['email'])
eq_(s, render('{{ form.email }}', {'form': form})) eq_(s, render('{{ form.email }}', {'form': form}))

View File

@ -1,6 +1,11 @@
from __future__ import unicode_literals
from django.utils import translation 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 nose.tools import eq_
from jingo import get_env, render_to_string from jingo import get_env, render_to_string

View File

@ -1,3 +1,5 @@
from __future__ import unicode_literals
from django.conf.urls import patterns from django.conf.urls import patterns

35
jingo/tests/utils.py Normal file
View File

@ -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)

View File

@ -1,7 +1,6 @@
# These are the reqs to build docs and run tests. # These are the reqs to build docs and run tests.
Django==1.5.1 Django==1.5.2
sphinx sphinx
jinja2 jinja2==2.7
nose nose
mock mock
fabric

13
run_tests.py Normal file
View File

@ -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()

View File

@ -24,6 +24,11 @@ setup(
'License :: OSI Approved :: BSD License', 'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Programming Language :: Python', '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', 'Topic :: Software Development :: Libraries :: Python Modules',
] ]
) )