Navigation Menu

Skip to content

Commit

Permalink
Fixed #18363 -- Added Python 3 compatibility layer.
Browse files Browse the repository at this point in the history
Thanks Vinay Sajip for the support of his django3 branch
and Alex Gaynor, kezabelle, YorikSar for the review.
  • Loading branch information
claudep committed Jun 7, 2012
1 parent 87ff89d commit 5e6ded2
Show file tree
Hide file tree
Showing 5 changed files with 365 additions and 1 deletion.
109 changes: 109 additions & 0 deletions django/utils/py3.py
@@ -0,0 +1,109 @@
# Compatibility layer for running Django both in 2.x and 3.x

import sys

if sys.version_info[0] < 3:
PY3 = False
# Changed module locations
from urlparse import (urlparse, urlunparse, urljoin, urlsplit, urlunsplit,
urldefrag, parse_qsl)
from urllib import (quote, unquote, quote_plus, urlopen, urlencode,
url2pathname, urlretrieve, unquote_plus)
from urllib2 import (Request, OpenerDirector, UnknownHandler, HTTPHandler,
HTTPSHandler, HTTPDefaultErrorHandler, FTPHandler,
HTTPError, HTTPErrorProcessor)
import urllib2
import Cookie as cookies
try:
import cPickle as pickle
except ImportError:
import pickle
try:
import thread
except ImportError:
import dummy_thread as thread
from htmlentitydefs import name2codepoint
import HTMLParser
from os import getcwdu
from itertools import izip as zip
unichr = unichr
xrange = xrange
maxsize = sys.maxint

# Type aliases
string_types = basestring,
text_type = unicode
integer_types = int, long
long_type = long

from io import BytesIO as OutputIO

# Glue code for syntax differences
def reraise(tp, value, tb=None):
exec("raise tp, value, tb")

def with_metaclass(meta, base=object):
class _DjangoBase(base):
__metaclass__ = meta
return _DjangoBase

iteritems = lambda o: o.iteritems()
itervalues = lambda o: o.itervalues()
iterkeys = lambda o: o.iterkeys()

# n() is useful when python3 needs a str (unicode), and python2 str (bytes)
n = lambda s: s.encode('utf-8')

else:
PY3 = True
import builtins

# Changed module locations
from urllib.parse import (urlparse, urlunparse, urlencode, urljoin,
urlsplit, urlunsplit, quote, unquote,
quote_plus, unquote_plus, parse_qsl,
urldefrag)
from urllib.request import (urlopen, url2pathname, Request, OpenerDirector,
UnknownHandler, HTTPHandler, HTTPSHandler,
HTTPDefaultErrorHandler, FTPHandler,
HTTPError, HTTPErrorProcessor, urlretrieve)
import urllib.request as urllib2
import http.cookies as cookies
import pickle
try:
import _thread as thread
except ImportError:
import _dummy_thread as thread
from html.entities import name2codepoint
import html.parser as HTMLParser
from os import getcwd as getcwdu
zip = zip
unichr = chr
xrange = range
maxsize = sys.maxsize

# Type aliases
string_types = str,
text_type = str
integer_types = int,
long_type = int

from io import StringIO as OutputIO

# Glue code for syntax differences
def reraise(tp, value, tb=None):
if value.__traceback__ is not tb:
raise value.with_traceback(tb)
raise value

def with_metaclass(meta, base=object):
ns = dict(base=base, meta=meta)
exec("""class _DjangoBase(base, metaclass=meta):
pass""", ns)
return ns["_DjangoBase"]

iteritems = lambda o: o.items()
itervalues = lambda o: o.values()
iterkeys = lambda o: o.keys()

n = lambda s: s
1 change: 1 addition & 0 deletions docs/index.txt
Expand Up @@ -183,6 +183,7 @@ Other batteries included
* :doc:`Logging <topics/logging>`
* :doc:`Messages <ref/contrib/messages>`
* :doc:`Pagination <topics/pagination>`
* :doc:`Python 3 compatibility <topics/python3>`
* :doc:`Redirects <ref/contrib/redirects>`
* :doc:`Security <topics/security>`
* :doc:`Serialization <topics/serialization>`
Expand Down
3 changes: 2 additions & 1 deletion docs/ref/unicode.txt
Expand Up @@ -65,7 +65,8 @@ Python 2 with unicode literals or Python 3::

my_string = b"This is a bytestring"
my_unicode = "This is an Unicode string"


See also :doc:`Python 3 compatibility </topics/python3>`.

.. admonition:: Warning

Expand Down
1 change: 1 addition & 0 deletions docs/topics/index.txt
Expand Up @@ -22,6 +22,7 @@ Introductions to all the key parts of Django you'll need to know:
i18n/index
logging
pagination
python3
security
serialization
settings
Expand Down
252 changes: 252 additions & 0 deletions docs/topics/python3.txt
@@ -0,0 +1,252 @@
======================
Python 3 compatibility
======================

Django 1.5 introduces a compatibility layer that allows the code to be run both
in Python 2 (2.6/2.7) and Python 3 (>= 3.2) (*work in progress*).

This document is not meant as a complete Python 2 to Python 3 migration guide.
There are many existing resources you can read. But we describe some utilities
and guidelines that we recommend you should use when you want to ensure your
code can be run with both Python 2 and 3.

* http://docs.python.org/py3k/howto/pyporting.html
* http://python3porting.com/

django.utils.py3
================

Whenever a symbol or module has different semantics or different locations on
Python 2 and Python 3, you can import it from ``django.utils.py3`` where it
will be automatically converted depending on your current Python version.

PY3
---

If you need to know anywhere in your code if you are running Python 3 or a
previous Python 2 version, you can check the ``PY3`` boolean variable::

from django.utils.py3 import PY3

if PY3:
# Do stuff Python 3-wise
else:
# Do stuff Python 2-wise

This should be considered as a last resort solution when it is not possible
to import a compatible name from django.utils.py3, as described in the sections
below.

String handling
===============

In Python 3, all strings are considered Unicode strings by default. Byte strings
have to be prefixed with the letter 'b'. To mimic the same behaviour in Python 2,
we recommend you import ``unicode_literals`` from the ``__future__`` library::

from __future__ import unicode_literals

my_string = "This is an unicode literal"
my_bytestring = b"This is a bytestring"

Be cautious if you have to slice bytestrings.
See http://docs.python.org/py3k/howto/pyporting.html#bytes-literals

Different expected strings
--------------------------

Some method parameters have changed the expected string type of a parameter.
For example, ``strftime`` format parameter expects a bytestring on Python 2 but
a normal (Unicode) string on Python 3. For these cases, ``django.utils.py3``
provides a ``n()`` function which encodes the string parameter only with
Python 2.

>>> from __future__ import unicode_literals
>>> from datetime import datetime

>>> print(datetime.date(2012, 5, 21).strftime(n("%m → %Y")))
05 → 2012

Renamed types
=============

Several types are named differently in Python 2 and Python 3. In order to keep
compatibility while using those types, import their corresponding aliases from
``django.utils.py3``.

=========== ========= =====================
Python 2 Python 3 django.utils.py3
=========== ========= =====================
basestring, str, string_types (tuple)
unicode str text_type
int, long int, integer_types (tuple)
long int long_type
=========== ========= =====================

String aliases
--------------

Code sample::

if isinstance(foo, basestring):
print("foo is a string")

# I want to convert a number to a Unicode string
bar = 45
bar_string = unicode(bar)

Should be replaced by::

from django.utils.py3 import string_types, text_type

if isinstance(foo, string_types):
print("foo is a string")

# I want to convert a number to a Unicode string
bar = 45
bar_string = text_type(bar)

No more long type
-----------------

``long`` and ``int`` types have been unified in Python 3, meaning that ``long``
is no longer available. ``django.utils.py3`` provides both ``long_type`` and
``integer_types`` aliases. For example:

.. code-block:: python

# Old Python 2 code
my_var = long(333463247234623)
if isinstance(my_var, (int, long)):
# ...

Should be replaced by:

.. code-block:: python

from django.utils.py3 import long_type, integer_types

my_var = long_type(333463247234623)
if isinstance(my_var, integer_types):
# ...


Changed module locations
========================

The following modules have changed their location in Python 3. Therefore, it is
recommended to import them from the ``django.utils.py3`` compatibility layer:

=============================== ====================================== ======================
Python 2 Python3 django.utils.py3
=============================== ====================================== ======================
Cookie http.cookies cookies

urlparse.urlparse urllib.parse.urlparse urlparse
urlparse.urlunparse urllib.parse.urlunparse urlunparse
urlparse.urljoin urllib.parse.urljoin urljoin
urlparse.urlsplit urllib.parse.urlsplit urlsplit
urlparse.urlunsplit urllib.parse.urlunsplit urlunsplit
urlparse.urldefrag urllib.parse.urldefrag urldefrag
urlparse.parse_qsl urllib.parse.parse_qsl parse_qsl
urllib.quote urllib.parse.quote quote
urllib.unquote urllib.parse.unquote unquote
urllib.quote_plus urllib.parse.quote_plus quote_plus
urllib.unquote_plus urllib.parse.unquote_plus unquote_plus
urllib.urlencode urllib.parse.urlencode urlencode
urllib.urlopen urllib.request.urlopen urlopen
urllib.url2pathname urllib.request.url2pathname url2pathname
urllib.urlretrieve urllib.request.urlretrieve urlretrieve
urllib2 urllib.request urllib2
urllib2.Request urllib.request.Request Request
urllib2.OpenerDirector urllib.request.OpenerDirector OpenerDirector
urllib2.UnknownHandler urllib.request.UnknownHandler UnknownHandler
urllib2.HTTPHandler urllib.request.HTTPHandler HTTPHandler
urllib2.HTTPSHandler urllib.request.HTTPSHandler HTTPSHandler
urllib2.HTTPDefaultErrorHandler urllib.request.HTTPDefaultErrorHandler HTTPDefaultErrorHandler
urllib2.FTPHandler urllib.request.FTPHandler FTPHandler
urllib2.HTTPError urllib.request.HTTPError HTTPError
urllib2.HTTPErrorProcessor urllib.request.HTTPErrorProcessor HTTPErrorProcessor

htmlentitydefs.name2codepoint html.entities.name2codepoint name2codepoint
HTMLParser html.parser HTMLParser
cPickle/pickle pickle pickle
thread/dummy_thread _thread/_dummy_thread thread

os.getcwdu os.getcwd getcwdu
itertools.izip zip zip
sys.maxint sys.maxsize maxsize
unichr chr unichr
xrange range xrange
=============================== ====================================== ======================


Ouptut encoding now Unicode
===========================

If you want to catch stdout/stderr output, the output content is UTF-8 encoded
in Python 2, while it is Unicode strings in Python 3. You can use the OutputIO
stream to capture this output::

from django.utils.py3 import OutputIO

try:
old_stdout = sys.stdout
out = OutputIO()
sys.stdout = out
# Do stuff which produces standard output
result = out.getvalue()
finally:
sys.stdout = old_stdout

Dict iteritems/itervalues/iterkeys
==================================

The iteritems(), itervalues() and iterkeys() methods of dictionaries do not
exist any more in Python 3, simply because they represent the default items()
values() and keys() behavior in Python 3. Therefore, to keep compatibility,
use similar functions from ``django.utils.py3``::

from django.utils.py3 import iteritems, itervalues, iterkeys

my_dict = {'a': 21, 'b': 42}
for key, value in iteritems(my_dict):
# ...
for value in itervalues(my_dict):
# ...
for key in iterkeys(my_dict):
# ...

Note that in Python 3, dict.keys(), dict.items() and dict.values() return
"views" instead of lists. Wrap them into list() if you really need their return
values to be in a list.

http://docs.python.org/release/3.0.1/whatsnew/3.0.html#views-and-iterators-instead-of-lists

Metaclass
=========

The syntax for declaring metaclasses has changed in Python 3.
``django.utils.py3`` offers a compatible way to declare metaclasses::

from django.utils.py3 import with_metaclass

class MyClass(with_metaclass(SubClass1, SubClass2,...)):
# ...

Re-raising exceptions
=====================

One of the syntaxes to raise exceptions (raise E, V, T) is gone in Python 3.
This is especially used in very specific cases where you want to re-raise a
different exception that the initial one, while keeping the original traceback.
So, instead of::

raise Exception, Exception(msg), traceback

Use::

from django.utils.py3 import reraise

reraise(Exception, Exception(msg), traceback)

0 comments on commit 5e6ded2

Please sign in to comment.