I'm a developer on the Django team and we use Trac as our bug tracker. I'm 
interested in moving our infrastructure to Python 3, but need Trac to 
support Python 3 in order to do so. Django supports Python 2 and 3, so I 
have some experience in maintaining a code base that supports both. I 
started working on a patch (attached), but as it will be a non-trivial 
effort, I wanted to get an initial review and ensure this approach is 
agreeable to the Trac team. I also wanted to ensure that when I finish the 
patch, someone will be interested in reviewing and committing it relatively 
quickly so that it doesn't go stale. My strategy is to attempt to run the 
test suite on Python 3 and fix errors, while ensuring the tests pass on 
Python 2.7 as a I go. Thanks! Tim

-- 
You received this message because you are subscribed to the Google Groups "Trac 
Development" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to trac-dev+unsubscr...@googlegroups.com.
To post to this group, send email to trac-dev@googlegroups.com.
Visit this group at http://groups.google.com/group/trac-dev.
For more options, visit https://groups.google.com/d/optout.
Index: setup.py
===================================================================
--- setup.py	(revision 14121)
+++ setup.py	(working copy)
@@ -20,9 +20,6 @@
 if sys.version_info < min_python:
     print("Trac requires Python %d.%d or later" % min_python)
     sys.exit(1)
-if sys.version_info >= (3,):
-    print("Trac doesn't support Python 3 (yet)")
-    sys.exit(1)
 
 extra = {}
 
Index: trac/config.py
===================================================================
--- trac/config.py	(revision 14121)
+++ trac/config.py	(working copy)
@@ -14,10 +14,11 @@
 
 import os.path
 import re
-from ConfigParser import ConfigParser
 from copy import deepcopy
 
+import six
 from genshi.builder import tag
+from six.moves.configparser import ConfigParser
 
 from trac.admin import AdminCommandError, IAdminCommandProvider
 from trac.core import Component, ExtensionPoint, TracError, implements
@@ -694,7 +695,7 @@
             return 'enabled'
         if value is False:
             return 'disabled'
-        if isinstance(value, unicode):
+        if isinstance(value, six.text_type):
             return value
         return to_unicode(value)
 
Index: trac/db/pool.py
===================================================================
--- trac/db/pool.py	(revision 14121)
+++ trac/db/pool.py	(working copy)
@@ -126,7 +126,7 @@
 
         # if we didn't get a cnx after wait(), something's fishy...
         if isinstance(exc_info[1], TracError):
-            raise exc_info[0], exc_info[1], exc_info[2]
+            raise
         timeout = time.time() - start
         errmsg = _("Unable to get database connection within %(time)d seconds.",
                    time=timeout)
Index: trac/env.py
===================================================================
--- trac/env.py	(revision 14121)
+++ trac/env.py	(working copy)
@@ -19,8 +19,9 @@
 import os.path
 import setuptools
 import sys
-from urlparse import urlsplit
 
+from six.moves.urllib.parse import urlsplit
+
 from trac import db_default, log
 from trac.admin import AdminCommandError, IAdminCommandProvider
 from trac.cache import CacheManager, cached
@@ -318,7 +319,7 @@
         info = self.systeminfo[:]
         for provider in self.system_info_providers:
             info.extend(provider.get_system_info() or [])
-        info.sort(key=lambda (name, version): (name != 'Trac', name.lower()))
+        info.sort(key=lambda name_version: (name_version[0] != 'Trac', name_version[0].lower()))
         return info
 
     # ISystemInfoProvider methods
Index: trac/util/__init__.py
===================================================================
--- trac/util/__init__.py	(revision 14121)
+++ trac/util/__init__.py	(working copy)
@@ -17,12 +17,11 @@
 # Author: Jonas Borgström <jo...@edgewall.com>
 #         Matthew Good <t...@matt-good.net>
 
-from cStringIO import StringIO
 import csv
 import errno
 import functools
 import inspect
-from itertools import izip, tee
+from itertools import tee
 import locale
 import os.path
 from pkg_resources import find_distributions
@@ -34,8 +33,11 @@
 import struct
 import tempfile
 import time
-from urllib import quote, unquote, urlencode
 
+import six
+from six.moves import cStringIO as StringIO
+from six.moves.urllib.parse import quote, unquote
+
 from trac.util.compat import any, md5, sha1, sorted
 from trac.util.datefmt import to_datetime, to_timestamp, utc
 from trac.util.text import exception_to_unicode, to_unicode, \
@@ -255,7 +257,7 @@
             flags = os.O_CREAT + os.O_WRONLY + os.O_EXCL
             if hasattr(os, 'O_BINARY'):
                 flags += os.O_BINARY
-            return path, os.fdopen(os.open(path, flags, 0666), 'w')
+            return path, os.fdopen(os.open(path, flags, 0x666), 'w')
         except OSError as e:
             if e.errno != errno.EEXIST:
                 raise
@@ -332,16 +334,16 @@
         if not zipinfo.filename.endswith('/'):
             zipinfo.filename += '/'
         zipinfo.compress_type = ZIP_STORED
-        zipinfo.external_attr = 040755 << 16L        # permissions drwxr-xr-x
+        zipinfo.external_attr = 0x40755 << 16        # permissions drwxr-xr-x
         zipinfo.external_attr |= 0x10                # MS-DOS directory flag
     else:
         zipinfo.compress_type = ZIP_DEFLATED
-        zipinfo.external_attr = 0644 << 16L          # permissions -r-wr--r--
+        zipinfo.external_attr = 0x644 << 16          # permissions -r-wr--r--
         if executable:
-            zipinfo.external_attr |= 0755 << 16L     # -rwxr-xr-x
+            zipinfo.external_attr |= 0x755 << 16     # -rwxr-xr-x
         if symlink:
             zipinfo.compress_type = ZIP_STORED
-            zipinfo.external_attr |= 0120000 << 16L  # symlink file type
+            zipinfo.external_attr |= 0x120000 << 16  # symlink file type
 
     if comment:
         zipinfo.comment = comment.encode('utf-8')
@@ -989,7 +991,7 @@
 
     """
 
-    RE_STR = ur'[0-9]+(?:[-:][0-9]+)?(?:,\u200b?[0-9]+(?:[-:][0-9]+)?)*'
+    RE_STR = six.u(r'[0-9]+(?:[-:][0-9]+)?(?:,\u200b?[0-9]+(?:[-:][0-9]+)?)*')
 
     def __init__(self, r=None, reorder=False):
         self.pairs = []
Index: trac/util/datefmt.py
===================================================================
--- trac/util/datefmt.py	(revision 14121)
+++ trac/util/datefmt.py	(working copy)
@@ -110,7 +110,7 @@
     if not dt:
         return 0
     diff = dt - _epoc
-    return (diff.days * 86400000000L + diff.seconds * 1000000
+    return (diff.days * 86400000000 + diff.seconds * 1000000
             + diff.microseconds)
 
 def from_utimestamp(ts):
@@ -494,7 +494,7 @@
             'date': get_date_format_hint,
             'relative': get_datetime_format_hint,
             'iso8601': lambda l: get_datetime_format_hint('iso8601'),
-        }.get(hint, lambda(l): hint)(locale)
+        }.get(hint, lambda l: hint)(locale)
         if hint != 'iso8601':
             msg = _('"%(date)s" is an invalid date, or the date format '
                     'is not known. Try "%(hint)s" or "%(isohint)s" instead.',
Index: trac/util/html.py
===================================================================
--- trac/util/html.py	(revision 14121)
+++ trac/util/html.py	(working copy)
@@ -11,7 +11,6 @@
 # individuals. For the exact contribution history, see the revision
 # history and logs, available at http://trac.edgewall.org/log/.
 
-from HTMLParser import HTMLParser
 import re
 
 from genshi import Markup, HTML, escape, unescape
@@ -24,6 +23,8 @@
 except ImportError:
     LazyProxy = None
 
+from six.moves.html_parser import HTMLParser
+
 from trac.core import TracError
 from trac.util.text import to_unicode
 
Index: trac/util/text.py
===================================================================
--- trac/util/text.py	(revision 14121)
+++ trac/util/text.py	(working copy)
@@ -18,18 +18,19 @@
 #         Matthew Good <t...@matt-good.net>
 #         Christian Boos <cb...@edgewall.org>
 
-import __builtin__
 import locale
 import os
 import re
 import sys
 import textwrap
-from urllib import quote, quote_plus, unquote
 from unicodedata import east_asian_width
 
+import six
+from six.moves.urllib.parse import quote, quote_plus, unquote
+
 CRLF = '\r\n'
 
-class Empty(unicode):
+class Empty(six.text_type):
     """A special tag object evaluating to the empty string"""
     __slots__ = []
 
@@ -54,9 +55,9 @@
     """
     if isinstance(text, str):
         try:
-            return unicode(text, charset or 'utf-8')
+            return six.text_type(text, charset or 'utf-8')
         except UnicodeDecodeError:
-            return unicode(text, 'latin1')
+            return six.text_type(text, 'latin1')
     elif isinstance(text, Exception):
         if os.name == 'nt' and isinstance(text, (OSError, IOError)):
             # the exception might have a localized error string encoded with
@@ -68,11 +69,11 @@
         # two possibilities for storing unicode strings in exception data:
         try:
             # custom __str__ method on the exception (e.g. PermissionError)
-            return unicode(text)
+            return six.text_type(text)
         except UnicodeError:
             # unicode arguments given to the exception (e.g. parse_date)
             return ' '.join([to_unicode(arg) for arg in text.args])
-    return unicode(text)
+    return six.text_type(text)
 
 
 def exception_to_unicode(e, traceback=False):
@@ -99,8 +100,8 @@
     return unicode(path)
 
 
-_ws_leading_re = re.compile(ur'\A[\s\u200b]+', re.UNICODE)
-_ws_trailing_re = re.compile(ur'[\s\u200b]+\Z', re.UNICODE)
+_ws_leading_re = re.compile(six.u(r'\A[\s\u200b]+'), re.UNICODE)
+_ws_trailing_re = re.compile(six.u(r'[\s\u200b]+\Z'), re.UNICODE)
 
 def stripws(text, leading=True, trailing=True):
     """Strips unicode white-spaces and ZWSPs from ``text``.
@@ -135,10 +136,11 @@
 
 _js_quote = {'\\': '\\\\', '"': '\\"', '\b': '\\b', '\f': '\\f',
              '\n': '\\n', '\r': '\\r', '\t': '\\t', "'": "\\'"}
-for i in range(0x20) + [ord(c) for c in u'&<>\u2028\u2029']:
-    _js_quote.setdefault(unichr(i), '\\u%04x' % i)
-_js_quote_re = re.compile(ur'[\x00-\x1f\\"\b\f\n\r\t\'&<>\u2028\u2029]')
-_js_string_re = re.compile(ur'[\x00-\x1f\\"\b\f\n\r\t&<>\u2028\u2029]')
+_js_quote_chars = [i for i in range(0x20)] + [ord(c) for c in u'&<>\u2028\u2029']
+for i in _js_quote_chars:
+    _js_quote.setdefault(six.unichr(i), '\\u%04x' % i)
+_js_quote_re = re.compile(six.u(r'[\x00-\x1f\\"\b\f\n\r\t\'&<>\u2028\u2029]'))
+_js_string_re = re.compile(six.u(r'[\x00-\x1f\\"\b\f\n\r\t&<>\u2028\u2029]'))
 
 
 def javascript_quote(text):
@@ -216,7 +218,7 @@
     return '&'.join(l)
 
 
-_qs_quote_safe = ''.join(chr(c) for c in xrange(0x21, 0x7f))
+_qs_quote_safe = ''.join(chr(c) for c in range(0x21, 0x7f))
 
 def quote_query_string(text):
     """Quote strings for query string
@@ -249,7 +251,7 @@
     return u.encode('utf-8')
 
 
-class unicode_passwd(unicode):
+class unicode_passwd(six.text_type):
     """Conceal the actual content of the string when `repr` is called."""
     def __repr__(self):
         return '*******'
@@ -452,8 +454,8 @@
         surrogate_pairs = []
         for val in cls.breakable_char_ranges:
             try:
-                high = unichr(val[0])
-                low = unichr(val[1])
+                high = six.unichr(val[0])
+                low = six.unichr(val[1])
                 char_ranges.append(u'%s-%s' % (high, low))
             except ValueError:
                 # Narrow build, `re` cannot use characters >= 0x10000
@@ -466,12 +468,12 @@
             pattern = u'[%s]+' % char_ranges
 
         cls.split_re = re.compile(
-            ur'(\s+|' +                                 # any whitespace
+            r'(\s+|' +                                 # any whitespace
             pattern + u'|' +                            # breakable text
-            ur'[^\s\w]*\w+[^0-9\W]-(?=\w+[^0-9\W])|' +  # hyphenated words
-            ur'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))',    # em-dash
+            r'[^\s\w]*\w+[^0-9\W]-(?=\w+[^0-9\W])|' +  # hyphenated words
+            r'(?<=[\w\!\"\'\&\.\,\?])-{2,}(?=\w))',    # em-dash
             re.UNICODE)
-        cls.breakable_re = re.compile(ur'\A' + pattern, re.UNICODE)
+        cls.breakable_re = re.compile(r'\A' + pattern, re.UNICODE)
 
     def __init__(self, cols, replace_whitespace=0, break_long_words=0,
                  initial_indent='', subsequent_indent='', ambiwidth=1):

Reply via email to