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