changeset 336:03e3e793fa22

Convert project to Python 3.4.
author Ludovic Chabant <ludovic@chabant.com>
date Sun, 19 Apr 2015 20:58:14 -0700
parents cc038c636901
children f1f0192412ea
files requirements.txt tests/__init__.py tests/mock.py wikked/assets/js/wikked.js wikked/auth.py wikked/commands/users.py wikked/db/sql.py wikked/formatter.py wikked/fs.py wikked/indexer/whooshidx.py wikked/page.py wikked/resolver.py wikked/scheduler.py wikked/scm/git.py wikked/scm/mercurial.py wikked/utils.py wikked/views/__init__.py wikked/views/edit.py wikked/views/read.py wikked/views/special.py wikked/wiki.py wikked/witch.py
diffstat 22 files changed, 149 insertions(+), 126 deletions(-) [+]
line wrap: on
line diff
--- a/requirements.txt	Sun Apr 19 20:54:10 2015 -0700
+++ b/requirements.txt	Sun Apr 19 20:58:14 2015 -0700
@@ -1,12 +1,15 @@
+colorama==0.2.7
 Flask==0.10.1
 Flask-Login==0.2.10
-Flask-Script==0.5.1
-Jinja2==2.7.2
+itsdangerous==0.24
+Jinja2==2.7.3
 Markdown==2.2.1
+MarkupSafe==0.23
+py==1.4.26
 Pygments==1.6
+pytest==2.5.2
+python-hglib===unknown
+repoze.lru==0.6
 SQLAlchemy==0.9.3
+Werkzeug==0.10.4
 Whoosh==2.5.5
-colorama==0.2.7
-pytest==2.5.2
-repoze.lru==0.6
-python-hglib
--- a/tests/__init__.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/tests/__init__.py	Sun Apr 19 20:58:14 2015 -0700
@@ -1,11 +1,11 @@
 import os
 import os.path
-import urllib
+import urllib.request, urllib.parse, urllib.error
 import shutil
 import unittest
 from wikked.wiki import Wiki
 from wikked.db.sql import SQLDatabase
-from mock import MockWikiParameters, MockFileSystem
+from .mock import MockWikiParameters, MockFileSystem
 
 
 class MockWikiParametersWithStructure(MockWikiParameters):
@@ -26,7 +26,7 @@
 
     def tearDown(self):
         if hasattr(self, 'wiki') and self.wiki is not None:
-            self.wiki.db.close(False, None)
+            self.wiki.db.close(None)
 
         if os.path.isdir(self.test_data_dir):
             shutil.rmtree(self.test_data_dir)
@@ -68,7 +68,7 @@
     res = '<a class=\"wiki-link'
     if missing:
         res += ' missing'
-    url = urllib.quote(url)
+    url = urllib.parse.quote(url)
     res += '\" data-wiki-url=\"' + url + '\"'
     if mod:
         res += ' data-wiki-mod=\"' + mod + '\"'
--- a/tests/mock.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/tests/mock.py	Sun Apr 19 20:58:14 2015 -0700
@@ -3,10 +3,10 @@
 import types
 import codecs
 import logging
-import StringIO
+import io
 from collections import deque
 from contextlib import closing
-from ConfigParser import SafeConfigParser
+from configparser import SafeConfigParser
 from wikked.fs import FileSystem
 from wikked.db.base import Database
 from wikked.indexer.base import WikiIndex
@@ -63,7 +63,7 @@
         config.readfp(open(default_config_path))
         config.set('wiki', 'root', '/fake/root')
         if self.config_text:
-            with closing(StringIO.StringIO(self.config_text)) as conf:
+            with closing(io.StringIO(self.config_text)) as conf:
                 config.readfp(conf)
 
         return config
@@ -77,7 +77,7 @@
 
         dirnames = []
         filenames = []
-        for name, child in cur_node.iteritems():
+        for name, child in cur_node.items():
             if isinstance(child, dict):
                 dirnames.append(name)
             else:
@@ -147,7 +147,7 @@
     @staticmethod
     def flat_to_nested(flat):
         nested = {}
-        for k, v in flat.iteritems():
+        for k, v in flat.items():
             bits = k.lstrip('/').split('/')
             cur = nested
             for i, b in enumerate(bits):
@@ -166,7 +166,7 @@
             os.makedirs(path)
         for node in structure:
             node_path = os.path.join(path, node)
-            if isinstance(structure[node], types.StringTypes):
+            if isinstance(structure[node], str):
                 with codecs.open(node_path, 'w', encoding='utf-8') as f:
                     f.write(structure[node])
             else:
--- a/wikked/assets/js/wikked.js	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/assets/js/wikked.js	Sun Apr 19 20:58:14 2015 -0700
@@ -4,7 +4,7 @@
  * We need to alias/shim some of the libraries.
  */
 require.config({
-    urlArgs: "bust=" + (new Date()).getTime(),
+    //urlArgs: "bust=" + (new Date()).getTime(),
     paths: {
         jquery: 'js/jquery-1.8.3.min',
         jquery_validate: 'js/jquery/jquery.validate.min',
--- a/wikked/auth.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/auth.py	Sun Apr 19 20:58:14 2015 -0700
@@ -23,7 +23,7 @@
         return False
 
     def get_id(self):
-        return unicode(self.username)
+        return str(self.username)
 
     def is_admin(self):
         return 'administrators' in self.groups
--- a/wikked/commands/users.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/commands/users.py	Sun Apr 19 20:58:14 2015 -0700
@@ -1,5 +1,5 @@
 import logging
-from flask.ext.script import prompt_pass
+import getpass
 from wikked.bcryptfallback import generate_password_hash
 from wikked.commands.base import WikkedCommand, register_command
 
@@ -37,6 +37,6 @@
 
     def run(self, ctx):
         username = ctx.args.username
-        password = ctx.args.password or prompt_pass('Password: ')
+        password = ctx.args.password or getpass.getpass('Password: ')
         password = generate_password_hash(password)
         logger.info("%s = %s" % (username[0], password))
--- a/wikked/db/sql.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/db/sql.py	Sun Apr 19 20:58:14 2015 -0700
@@ -422,7 +422,7 @@
         q = self.session.query(SQLPage)
         if meta_query:
             q = q.join(SQLReadyMeta)
-            for name, values in meta_query.iteritems():
+            for name, values in meta_query.items():
                 for v in values:
                     q = q.filter(and_(SQLReadyMeta.name == name,
                         SQLReadyMeta.value == v))
@@ -462,10 +462,10 @@
         db_obj.needs_invalidate = False
 
         del db_obj.ready_meta[:]
-        for name, value in page._data.ext_meta.iteritems():
+        for name, value in page._data.ext_meta.items():
             if isinstance(value, bool):
                 value = ""
-            if isinstance(value, types.StringTypes):
+            if isinstance(value, str):
                 db_obj.ready_meta.append(SQLReadyMeta(name, value))
             else:
                 for v in value:
@@ -579,10 +579,10 @@
         po.ready_text = None
         po.is_ready = False
 
-        for name, value in page.getLocalMeta().iteritems():
+        for name, value in page.getLocalMeta().items():
             if isinstance(value, bool):
                 value = ""
-            if isinstance(value, types.StringTypes):
+            if isinstance(value, str):
                 po.meta.append(SQLMeta(name, value))
             else:
                 for v in value:
--- a/wikked/formatter.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/formatter.py	Sun Apr 19 20:58:14 2015 -0700
@@ -3,8 +3,8 @@
 import re
 import logging
 import jinja2
-from StringIO import StringIO
-from utils import get_meta_name_and_modifiers, html_escape
+from io import StringIO
+from .utils import get_meta_name_and_modifiers, html_escape
 
 
 FILE_FORMAT_REGEX = re.compile(r'\r\n?', re.MULTILINE)
@@ -61,8 +61,8 @@
 
     def _processWikiMeta(self, ctx, text):
         def repl(m):
-            meta_name = unicode(m.group('name')).lower()
-            meta_value = unicode(m.group('value'))
+            meta_name = str(m.group('name')).lower()
+            meta_value = str(m.group('value'))
 
             if meta_value is None or meta_value == '':
                 # No value provided: this is a "flag" meta.
@@ -163,8 +163,8 @@
             value = p
             m = re.match('\s*(?P<name>\w[\w\d]*)\s*=(?P<value>.*)', value)
             if m:
-                name = unicode(m.group('name'))
-                value = unicode(m.group('value'))
+                name = str(m.group('name'))
+                value = str(m.group('value'))
             value = html_escape(value.strip())
             parameters += '<div class="wiki-param" data-name="%s">%s</div>' % (name, value)
 
@@ -182,8 +182,8 @@
         arg_pattern = r"(\A|\|)\s*(?P<name>(__)?[a-zA-Z][a-zA-Z0-9_\-]+)\s*="\
             r"(?P<value>[^\|]+)"
         for m in re.finditer(arg_pattern, query, re.MULTILINE):
-            name = unicode(m.group('name')).strip()
-            value = unicode(m.group('value')).strip()
+            name = str(m.group('name')).strip()
+            value = str(m.group('value')).strip()
             processed_args.append('%s=%s' % (name, value))
 
         mod_attr = ''
@@ -230,7 +230,7 @@
         urls = []
         pattern = r"<a class=\"[^\"]*\" data-wiki-url=\"(?P<url>[^\"]+)\">"
         for m in re.finditer(pattern, text):
-            urls.append(unicode(m.group('url')))
+            urls.append(str(m.group('url')))
         return urls
 
     LEXER_STATE_NORMAL = 0
--- a/wikked/fs.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/fs.py	Sun Apr 19 20:58:14 2015 -0700
@@ -1,12 +1,11 @@
 import os
 import os.path
 import re
-import string
 import codecs
 import fnmatch
 import logging
 import itertools
-from utils import (PageNotFoundError, NamespaceNotFoundError,
+from .utils import (PageNotFoundError, NamespaceNotFoundError,
         split_page_url)
 
 
@@ -39,7 +38,7 @@
         to list existing pages.
     """
     def __init__(self, root, config):
-        self.root = unicode(root)
+        self.root = root
 
         self.excluded = None
         self.page_extensions = None
@@ -47,7 +46,7 @@
 
     def start(self, wiki):
         self.page_extensions = list(set(
-            itertools.chain(*wiki.formatters.itervalues())))
+            itertools.chain(*wiki.formatters.values())))
 
         excluded = []
         excluded += wiki.getSpecialFilenames()
@@ -84,8 +83,6 @@
 
     def getPageInfo(self, path):
         logger.debug("Reading page info from: %s" % path)
-        if not isinstance(path, unicode):
-            path = unicode(path)
         for e in self.excluded:
             if fnmatch.fnmatch(path, e):
                 return None
@@ -101,7 +98,7 @@
         logger.debug("Saving page '%s' to: %s" % (url, path))
         dirname = os.path.dirname(path)
         if not os.path.isdir(dirname):
-            os.makedirs(dirname, 0775)
+            os.makedirs(dirname, 0o775)
         with codecs.open(path, 'w', encoding='utf-8') as f:
             f.write(content)
         return PageInfo(url, path)
@@ -137,14 +134,14 @@
 
         url = '/' + name
         if meta:
-            url = u"%s:/%s" % (meta.lower(), name)
+            url = "%s:/%s" % (meta.lower(), name)
         return PageInfo(url, abs_path)
 
     def _getPhysicalPath(self, url, is_file=True, make_new=False):
         endpoint, url = split_page_url(url)
         if url[0] != '/':
             raise ValueError("Page URLs need to be absolute: " + url)
-        if string.find(url, '..') >= 0:
+        if '..' in url:
             raise ValueError("Page URLs can't contain '..': " + url)
 
         # Find the root directory in which we'll be searching for the
--- a/wikked/indexer/whooshidx.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/indexer/whooshidx.py	Sun Apr 19 20:58:14 2015 -0700
@@ -1,7 +1,7 @@
 import os
 import os.path
 import logging
-from base import WikiIndex, HitResult
+from .base import WikiIndex, HitResult
 from whoosh.analysis import (StandardAnalyzer, StemmingAnalyzer,
         CharsetFilter, NgramFilter)
 from whoosh.fields import Schema, ID, TEXT, STORED
@@ -119,10 +119,10 @@
     def _indexPage(self, writer, page):
         logger.debug("Indexing '%s'." % page.url)
         writer.add_document(
-            url=unicode(page.url),
-            title_preview=unicode(page.title),
-            title=unicode(page.title),
-            text=unicode(page.text),
+            url=page.url,
+            title_preview=page.title,
+            title=page.title,
+            text=page.text,
             path=page.path,
             time=os.path.getmtime(page.path)
             )
--- a/wikked/page.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/page.py	Sun Apr 19 20:58:14 2015 -0700
@@ -2,7 +2,7 @@
 import os.path
 import re
 import logging
-from formatter import PageFormatter, FormattingContext
+from .formatter import PageFormatter, FormattingContext
 
 
 logger = logging.getLogger(__name__)
--- a/wikked/resolver.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/resolver.py	Sun Apr 19 20:58:14 2015 -0700
@@ -1,5 +1,5 @@
 import re
-import urllib
+import urllib.request, urllib.parse, urllib.error
 import os.path
 import logging
 import jinja2
@@ -83,7 +83,7 @@
             self.meta = dict(page.getLocalMeta())
 
     def add(self, other):
-        for original_key, val in other.meta.iteritems():
+        for original_key, val in other.meta.items():
             # Ignore internal properties. Strip include-only properties
             # from their prefix.
             key, mod = get_meta_name_and_modifiers(original_key)
@@ -135,9 +135,9 @@
             return self._unsafeRun()
         except Exception as e:
             logger.error("Error resolving page '%s':" % self.page.url)
-            logger.exception(unicode(e.message))
+            logger.exception(e.message)
             self.output = ResolveOutput(self.page)
-            self.output.text = u'<div class="error">%s</div>' % e
+            self.output.text = '<div class="error">%s</div>' % e
             return self.output
 
     def _getPage(self, url):
@@ -173,15 +173,15 @@
 
         # Resolve queries, includes, etc.
         def repl2(m):
-            meta_name = unicode(m.group('name'))
-            meta_value = unicode(m.group('value'))
+            meta_name = m.group('name')
+            meta_value = m.group('value')
             meta_opts = {}
             if m.group('opts'):
                 for c in re.finditer(
                         r'data-wiki-(?P<name>[a-z]+)="(?P<value>[^"]+)"',
-                        unicode(m.group('opts'))):
-                    opt_name = unicode(c.group('name'))
-                    opt_value = unicode(c.group('value'))
+                        m.group('opts')):
+                    opt_name = c.group('name')
+                    opt_value = c.group('value')
                     meta_opts[opt_name] = opt_value
 
             resolver = self.resolvers.get(meta_name)
@@ -207,10 +207,10 @@
 
             # Resolve link states.
             def repl1(m):
-                raw_url = unicode(m.group('url'))
+                raw_url = m.group('url')
                 url = self.ctx.getAbsoluteUrl(raw_url)
                 self.output.out_links.append(url)
-                quoted_url = urllib.quote(url.encode('utf-8'))
+                quoted_url = urllib.parse.quote(url.encode('utf-8'))
                 if self.wiki.pageExists(url):
                     return '<a class="wiki-link" data-wiki-url="%s">' % quoted_url
                 return '<a class="wiki-link missing" data-wiki-url="%s">' % quoted_url
@@ -261,12 +261,12 @@
             # root page.
             arg_pattern = r'<div class="wiki-param" data-name="(?P<name>\w[\w\d]*)?">(?P<value>.*?)</div>'
             for i, m in enumerate(re.finditer(arg_pattern, args)):
-                value = unicode(m.group('value')).strip()
+                value = m.group('value').strip()
                 value = html_unescape(value)
                 value = self._renderTemplate(value, self.parameters,
                                              error_url=self.page.url)
                 if m.group('name'):
-                    key = unicode(m.group('name')).lower()
+                    key = m.group('name').lower()
                     parameters[key] = value
                 else:
                     parameters['__xargs'].append(value)
@@ -306,9 +306,9 @@
         for m in re.finditer(arg_pattern, query):
             key = m.group('name').lower()
             if key in parameters:
-                parameters[key] = unicode(m.group('value'))
+                parameters[key] = m.group('value')
             else:
-                meta_query[key] = unicode(m.group('value'))
+                meta_query[key] = m.group('value')
 
         # Find pages that match the query, excluding any page
         # that is in the URL trail.
@@ -317,13 +317,13 @@
         for p in self.pages_meta_getter():
             if p.url in self.ctx.url_trail:
                 continue
-            for key, value in meta_query.iteritems():
+            for key, value in meta_query.items():
                 try:
                     if self._isPageMatch(p, key, value):
                         matched_pages.append(p)
                 except Exception as e:
                     logger.error("Can't query page '%s' for '%s':" % (p.url, self.page.url))
-                    logger.exception(unicode(e.message))
+                    logger.exception(e.message)
 
         # We'll have to format things...
         fmt_ctx = FormattingContext(self.page.url)
@@ -440,7 +440,7 @@
 
     def _getFormatter(self, extension):
         known_exts = []
-        for k, v in self.page.wiki.formatters.iteritems():
+        for k, v in self.page.wiki.formatters.items():
             if extension in v:
                 return k
             known_exts += v
--- a/wikked/scheduler.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/scheduler.py	Sun Apr 19 20:58:14 2015 -0700
@@ -3,7 +3,7 @@
 import datetime
 import threading
 import jinja2
-from Queue import Queue, Empty
+from queue import Queue, Empty
 from repoze.lru import LRUCache
 from wikked.resolver import PageResolver, ResolveOutput, CircularIncludeError
 
--- a/wikked/scm/git.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/scm/git.py	Sun Apr 19 20:58:14 2015 -0700
@@ -2,7 +2,7 @@
 import os.path
 import logging
 import subprocess
-from base import (
+from .base import (
         SourceControl,
         STATE_NEW, STATE_MODIFIED, STATE_COMMITTED)
 
--- a/wikked/scm/mercurial.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/scm/mercurial.py	Sun Apr 19 20:58:14 2015 -0700
@@ -8,7 +8,7 @@
 import subprocess
 from hglib.error import CommandError
 from hglib.util import cmdbuilder
-from base import (
+from .base import (
         SourceControl, Author, Revision, SourceControlError,
         ACTION_ADD, ACTION_EDIT, ACTION_DELETE,
         STATE_NEW, STATE_MODIFIED, STATE_COMMITTED)
@@ -61,7 +61,8 @@
         MercurialBaseSourceControl.__init__(self, root)
 
         self.hg = 'hg'
-        self.log_style = os.path.join(os.path.dirname(__file__), 'resources', 'hg_log.style')
+        self.log_style = os.path.join(os.path.dirname(__file__),
+                                      'resources', 'hg_log.style')
 
     def getHistory(self, path=None, limit=10, after_rev=None):
         if path is not None:
@@ -148,7 +149,8 @@
 
         m = re.match(r'(\d+) ([0-9a-f]+) \[([^\]]+)\] ([^ ]+)', lines[0])
         if m is None:
-            raise Exception('Error parsing history from Mercurial, got: ' + lines[0])
+            raise Exception('Error parsing history from Mercurial, got: ' +
+                            lines[0])
 
         rev = Revision()
         rev.rev_id = int(m.group(1))
@@ -190,6 +192,24 @@
 cl_lock = threading.Lock()
 
 
+def _b(strs):
+    """ Convert a list of strings to binary UTF8 arrays. """
+    if strs is None:
+        return None
+    if isinstance(strs, str):
+        return strs.encode('utf8')
+    return list([s.encode('utf8') if s is not None else None for s in strs])
+
+
+def _s(strs):
+    """ Convert a byte array to string using UTF8 encoding. """
+    if strs is None:
+        return None
+    if isinstance(strs, bytes):
+        return strs.decode('utf8')
+    return list([s.decode('utf8') if s is not None else None for s in strs])
+
+
 def create_hg_client(root):
     logger.debug("Spawning Mercurial command server at: %s" % root)
     import hglib
@@ -241,7 +261,7 @@
 
     def getHistory(self, path=None, limit=10, after_rev=None):
         if path is not None:
-            status = self.client.status(include=[path])
+            status = _s(self.client.status(include=_b([path])))
             if len(status) > 0 and status[0] == '?':
                 return []
 
@@ -253,34 +273,34 @@
 
         needs_files = False
         if path is not None:
-            repo_revs = self.client.log(files=[path], follow=True,
-                                        limit=limit, revrange=rev)
+            repo_revs = self.client.log(files=_b([path]), follow=True,
+                                        limit=limit, revrange=_b(rev))
         else:
             needs_files = True
             repo_revs = self.client.log(follow=True, limit=limit,
-                                        revrange=rev)
+                                        revrange=_b(rev))
         revisions = []
         for rev in repo_revs:
-            r = Revision(rev.node)
-            r.rev_name = rev.node[:12]
-            r.author = Author(rev.author)
+            r = Revision(_s(rev.node))
+            r.rev_name = _s(rev.node[:12])
+            r.author = Author(_s(rev.author))
             r.timestamp = time.mktime(rev.date.timetuple())
-            r.description = unicode(rev.desc)
+            r.description = _s(rev.desc)
             if needs_files:
                 rev_statuses = self.client.status(change=rev.node)
                 for rev_status in rev_statuses:
                     r.files.append({
-                        'path': rev_status[1].decode('utf-8', 'replace'),
-                        'action': self.actions[rev_status[0]]
+                        'path': _s(rev_status[1]),
+                        'action': self.actions[_s(rev_status[0])]
                         })
             revisions.append(r)
         return revisions
 
     def getState(self, path):
-        statuses = self.client.status(include=[path])
+        statuses = self.client.status(include=_b([path]))
         if len(statuses) == 0:
             return STATE_COMMITTED
-        status = statuses[0]
+        status = _s(statuses[0])
         if status[0] == '?' or status[0] == 'A':
             return STATE_NEW
         if status[0] == 'M':
@@ -288,12 +308,14 @@
         raise Exception("Unsupported status: %s" % status)
 
     def getRevision(self, path, rev):
-        return self.client.cat([path], rev=rev)
+        return _s(self.client.cat(_b([path]), rev=_b(rev)))
 
     def diff(self, path, rev1, rev2):
         if rev2 is None:
-            return self.client.diff(files=[path], change=rev1, git=True)
-        return self.client.diff(files=[path], revs=[rev1, rev2], git=True)
+            return _s(self.client.diff(files=_b([path]), change=_b(rev1),
+                                       git=True))
+        return _s(self.client.diff(files=_b([path]), revs=_b([rev1, rev2]),
+                                   git=True))
 
     def commit(self, paths, op_meta):
         if 'message' not in op_meta or not op_meta['message']:
@@ -301,13 +323,14 @@
 
         kwargs = {}
         if 'author' in op_meta:
-            kwargs['u'] = op_meta['author']
+            kwargs['u'] = _b(op_meta['author'])
         try:
             # We need to write our own command because somehow the `commit`
             # method in `hglib` doesn't support specifying the file(s)
             # directly -- only with `--include`. Weird.
-            args = cmdbuilder('commit', *paths,
-                    debug=True, m=op_meta['message'], A=True,
+            args = cmdbuilder(
+                    b'commit', *_b(paths),
+                    debug=True, m=_b(op_meta['message']), A=True,
                     **kwargs)
             self.client.rawcommand(args)
         except CommandError as e:
@@ -315,7 +338,7 @@
 
     def revert(self, paths=None):
         if paths is not None:
-            self.client.revert(files=paths, nobackup=True)
+            self.client.revert(files=_b(paths), nobackup=True)
         else:
             self.client.revert(all=True, nobackup=True)
 
--- a/wikked/utils.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/utils.py	Sun Apr 19 20:58:14 2015 -0700
@@ -1,7 +1,7 @@
 import re
 import os
 import os.path
-import urllib
+import urllib.request, urllib.parse, urllib.error
 from xml.sax.saxutils import escape, unescape
 
 
@@ -68,7 +68,7 @@
         raw_abs_url = os.path.join(urldir, url)
         abs_url = os.path.normpath(raw_abs_url).replace('\\', '/')
     if quote:
-        abs_url = urllib.quote(abs_url.encode('utf-8'))
+        abs_url = urllib.parse.quote(abs_url.encode('utf-8'))
     if endpoint:
         return '%s:%s' % (endpoint, abs_url)
     return abs_url
@@ -78,8 +78,8 @@
     m = endpoint_regex.match(url)
     if m is None:
         return (None, url)
-    endpoint = unicode(m.group(1))
-    path = unicode(m.group(2))
+    endpoint = m.group(1)
+    path = m.group(2)
     return (endpoint, path)
 
 
@@ -100,7 +100,7 @@
 
 
 def flatten_single_metas(meta):
-    items = list(meta.iteritems())
+    items = list(meta.items())
     for k, v in items:
         if isinstance(v, list):
             l = len(v)
@@ -112,7 +112,7 @@
 
 
 html_escape_table = {'"': "&quot;", "'": "&apos;"}
-html_unescape_table = {v: k for k, v in html_escape_table.items()}
+html_unescape_table = {v: k for k, v in list(html_escape_table.items())}
 
 def html_escape(text):
     return escape(text, html_escape_table)
--- a/wikked/views/__init__.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/views/__init__.py	Sun Apr 19 20:58:14 2015 -0700
@@ -1,5 +1,5 @@
 import os.path
-import urllib
+import urllib.request, urllib.parse, urllib.error
 import string
 import datetime
 from flask import g, abort, jsonify
@@ -17,16 +17,16 @@
 def url_from_viewarg(url):
     endpoint, path = split_url_from_viewarg(url)
     if endpoint:
-        return u'%s:%s' % (endpoint, path)
+        return '%s:%s' % (endpoint, path)
     return path
 
 
 def split_url_from_viewarg(url):
-    url = urllib.unquote(url)
+    url = urllib.parse.unquote(url)
     endpoint, path = split_page_url(url)
     if endpoint:
         return (endpoint, path)
-    return (None, u'/' + path)
+    return (None, '/' + path)
 
 
 def make_page_title(url):
@@ -99,7 +99,7 @@
     else:
         meta = dict(page.getMeta() or {})
     meta['title'] = page.title
-    meta['url'] = urllib.quote(page.url.encode('utf-8'))
+    meta['url'] = urllib.parse.quote(page.url.encode('utf-8'))
     for name in COERCE_META:
         if name in meta:
             meta[name] = COERCE_META[name](meta[name])
@@ -110,7 +110,7 @@
     result = []
     for item in category:
         result.append({
-            'url': u'category:/' + urllib.quote(item.encode('utf-8')),
+            'url': 'category:/' + urllib.parse.quote(item.encode('utf-8')),
             'name': item
             })
     return result
--- a/wikked/views/edit.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/views/edit.py	Sun Apr 19 20:58:14 2015 -0700
@@ -1,4 +1,4 @@
-import urllib
+import urllib.request, urllib.parse, urllib.error
 from flask import g, abort, request, jsonify
 from flask.ext.login import current_user
 from wikked.page import Page, PageData
@@ -43,7 +43,7 @@
     if page is None:
         result = {
                 'meta': {
-                    'url': urllib.quote(url.encode('utf-8')),
+                    'url': urllib.parse.quote(url.encode('utf-8')),
                     'title': default_title or make_page_title(url)
                     },
                 'text': ''
@@ -108,8 +108,8 @@
         default_title = None
         custom_data = None
         if endpoint is not None:
-            url = u'%s:%s' % (endpoint, path)
-            default_title = u'%s: %s' % (endpoint, path)
+            url = '%s:%s' % (endpoint, path)
+            default_title = '%s: %s' % (endpoint, path)
             custom_data = {
                     'meta_query': endpoint,
                     'meta_value': path.lstrip('/')
@@ -121,10 +121,10 @@
                 custom_data=custom_data)
 
     url = path
-    default_message = u'Edited ' + url
+    default_message = 'Edited ' + url
     if endpoint is not None:
-        url = u'%s:%s' % (endpoint, path)
-        default_message = u'Edited %s %s' % (endpoint, path.lstrip('/'))
+        url = '%s:%s' % (endpoint, path)
+        default_message = 'Edited %s %s' % (endpoint, path.lstrip('/'))
     return do_edit_page(url, default_message)
 
 
--- a/wikked/views/read.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/views/read.py	Sun Apr 19 20:58:14 2015 -0700
@@ -1,5 +1,5 @@
 import time
-import urllib
+import urllib.request, urllib.parse, urllib.error
 from flask import (render_template, request, g, jsonify, make_response,
                    abort)
 from flask.ext.login import current_user
@@ -17,15 +17,15 @@
     tpl_name = 'index.html'
     if app.config['WIKI_DEV_ASSETS']:
         tpl_name = 'index-dev.html'
-    return render_template(tpl_name, cache_bust=('?%d' % time.time()));
+    return render_template(tpl_name, cache_bust=('?%d' % time.time()))
 
 
 @app.route('/read/<path:url>')
-def read():
+def read(url):
     tpl_name = 'index.html'
     if app.config['WIKI_DEV_ASSETS']:
         tpl_name = 'index-dev.html'
-    return render_template(tpl_name, cache_bust=('?%d' % time.time()));
+    return render_template(tpl_name, cache_bust=('?%d' % time.time()))
 
 
 @app.route('/search')
@@ -33,7 +33,7 @@
     tpl_name = 'index.html'
     if app.config['WIKI_DEV_ASSETS']:
         tpl_name = 'index-dev.html'
-    return render_template(tpl_name, cache_bust=('?%d' % time.time()));
+    return render_template(tpl_name, cache_bust=('?%d' % time.time()))
 
 
 @app.route('/api/list')
@@ -44,7 +44,7 @@
 @app.route('/api/list/<path:url>')
 def api_list_pages(url):
     wiki = get_wiki()
-    pages = filter(is_page_readable, wiki.getPages(url_from_viewarg(url)))
+    pages = list(filter(is_page_readable, wiki.getPages(url_from_viewarg(url))))
     page_metas = [get_page_meta(page) for page in pages]
     result = {'path': url, 'pages': list(page_metas)}
     return jsonify(result)
@@ -140,7 +140,7 @@
             'name': endpoint,
             'url': meta_page_url,
             'value': value,
-            'safe_value': urllib.quote(value.encode('utf-8')),
+            'safe_value': urllib.parse.quote(value.encode('utf-8')),
             'pages': [get_page_meta(p) for p in pages]
             # TODO: skip pages that are forbidden for the current user
         }
@@ -156,7 +156,7 @@
             'meta_value': value,
             'query': query,
             'meta': {
-                    'url': urllib.quote(meta_page_url.encode('utf-8')),
+                    'url': urllib.parse.quote(meta_page_url.encode('utf-8')),
                     'title': value
                 },
             'text': text
--- a/wikked/views/special.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/views/special.py	Sun Apr 19 20:58:14 2015 -0700
@@ -43,7 +43,7 @@
                 rev_links[abs_l] = cnt + 1
 
         or_pages = []
-        for tgt, cnt in rev_links.iteritems():
+        for tgt, cnt in rev_links.items():
             if cnt == 0:
                 or_pages.append(pages[tgt])
         return or_pages
@@ -93,7 +93,7 @@
                 redirs[p.url] = target
 
         dr_pages = []
-        for src, tgt in redirs.iteritems():
+        for src, tgt in redirs.items():
             if tgt in redirs:
                 dr_pages.append(pages[src])
         return dr_pages
--- a/wikked/wiki.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/wiki.py	Sun Apr 19 20:58:14 2015 -0700
@@ -3,7 +3,7 @@
 import time
 import logging
 import importlib
-from ConfigParser import SafeConfigParser, NoOptionError
+from configparser import SafeConfigParser, NoOptionError
 from wikked.page import FileSystemPage
 from wikked.fs import FileSystem
 from wikked.auth import UserManager
@@ -426,5 +426,5 @@
                 mtimes[path] = mtime
                 continue
             elif mtime > old_time:
-                print "Change detected in '%s'." % path
+                print("Change detected in '%s'." % path)
         time.sleep(interval)
--- a/wikked/witch.py	Sun Apr 19 20:54:10 2015 -0700
+++ b/wikked/witch.py	Sun Apr 19 20:58:14 2015 -0700
@@ -109,7 +109,7 @@
 
     # Setup the command parsers.
     subparsers = parser.add_subparsers()
-    commands = map(lambda cls: cls(), command_classes)
+    commands = [cls() for cls in command_classes]
     logger.debug("Got %d commands." % len(commands))
     for c in commands:
         cp = subparsers.add_parser(c.name, help=c.description)
@@ -152,14 +152,14 @@
 
 def print_version():
     if os.path.isdir(os.path.join(os.path.dirname(__file__), '..', '.hg')):
-        print "Wikked (development version)"
+        print("Wikked (development version)")
         return 0
     try:
         from wikked.__version__ import version
     except ImportError:
-        print "Can't find version information."
+        print("Can't find version information.")
         return 1
-    print "Wikked %s" % version
+    print("Wikked %s" % version)
     return 0