Browse Source

Fixed #18363 -- Added Python 3 compatibility layer.

Thanks Vinay Sajip for the support of his django3 branch
and Alex Gaynor, kezabelle, YorikSar for the review.
Claude Paroz 12 years ago
parent
commit
5e6ded2e58
5 changed files with 365 additions and 1 deletions
  1. 109 0
      django/utils/py3.py
  2. 1 0
      docs/index.txt
  3. 2 1
      docs/ref/unicode.txt
  4. 1 0
      docs/topics/index.txt
  5. 252 0
      docs/topics/python3.txt

+ 109 - 0
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 - 0
docs/index.txt

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

+ 2 - 1
docs/ref/unicode.txt

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

+ 1 - 0
docs/topics/index.txt

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

+ 252 - 0
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)
+