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

@ -2,10 +2,15 @@ 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

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

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

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

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

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

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

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

@ -1,7 +1,12 @@
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_
try:
from unittest.mock import Mock, patch, sentinel
except ImportError:
from mock import Mock, patch, sentinel from mock import Mock, patch, sentinel
import jingo import jingo

@ -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
try:
from unittest.mock import patch
except ImportError:
from mock import patch 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')

@ -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_
try:
from unittest.mock import Mock
except ImportError:
from mock import Mock 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')

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

@ -1,5 +1,10 @@
from __future__ import unicode_literals
from django.utils import translation from django.utils import translation
try:
from unittest.mock import sentinel
except ImportError:
from mock import sentinel from mock import sentinel
from nose.tools import eq_ from nose.tools import eq_

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

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

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

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

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