Repository: getnikola/nikola

Issue:
nikola build with error in Python 3.3.2
A while ago works after I've started a new virtualenv and updated the nikola began to occur.

```
nikola build
command: build
args: []
Scanning posts....done!
.  render_listings:output/listings/index.html
.  copy_assets:output/assets/js/bootstrap.js
.  copy_assets:output/assets/js/bootstrap.min.js
.  copy_assets:output/assets/css/bootstrap-theme.min.css
.  copy_assets:output/assets/css/bootstrap-theme.css
.  copy_assets:output/assets/css/bootstrap-glyphicons.css
.  copy_assets:output/assets/css/bootstrap.css
.  copy_assets:output/assets/css/colorbox.css
.  copy_assets:output/assets/css/theme.css
.  copy_assets:output/assets/css/images/ie6/borderTopLeft.png
.  copy_assets:output/assets/css/images/ie6/borderMiddleLeft.png
.  copy_assets:output/assets/css/images/ie6/borderTopCenter.png
.  copy_assets:output/assets/fonts/glyphicons-halflings-regular.eot
.  copy_assets:output/assets/fonts/glyphicons-halflings-regular.woff
.  copy_assets:output/assets/js/jquery.colorbox-min.js
.  copy_assets:output/assets/css/bootstrap-responsive.css
.  copy_assets:output/assets/css/bootstrap-responsive.min.css
.  copy_assets:output/assets/css/images/overlay.png
.  copy_assets:output/assets/css/images/border.png
.  copy_assets:output/assets/css/images/loading.gif
.  copy_assets:output/assets/css/images/loading_background.png
.  copy_assets:output/assets/img/glyphicons-halflings-white.png
.  copy_assets:output/assets/css/code.css
.  render_rss:output/rss.xml
.  sitemap:output/sitemap.xml
Traceback (most recent call last):
  File "/home/neiesc/.virtualenvs/blog_neiesc/lib/python3.3/site-packages/doit/doit_cmd.py", line 106, in run
return self.sub_cmds[command].parse_execute(args)
  File "/home/neiesc/.virtualenvs/blog_neiesc/lib/python3.3/site-packages/doit/cmd_base.py", line 60, in parse_execute
return self.execute(params, args)
  File "/home/neiesc/.virtualenvs/blog_neiesc/lib/python3.3/site-packages/doit/cmd_base.py", line 239, in execute
return self._execute(**exec_params)
  File "/home/neiesc/.virtualenvs/blog_neiesc/lib/python3.3/site-packages/doit/cmd_run.py", line 181, in _execute
return runner.run_all(self.control.task_dispatcher())
  File "/home/neiesc/.virtualenvs/blog_neiesc/lib/python3.3/site-packages/doit/runner.py", line 239, in run_all
self.finish()
  File "/home/neiesc/.virtualenvs/blog_neiesc/lib/python3.3/site-packages/doit/runner.py", line 222, in finish
self.dep_manager.close()
  File "/home/neiesc/.virtualenvs/blog_neiesc/lib/python3.3/site-packages/doit/dependency.py", line 388, in close
self.backend.dump()
  File "/home/neiesc/.virtualenvs/blog_neiesc/lib/python3.3/site-packages/doit/dependency.py", line 204, in dump
self._dbm[task_id] = json.dumps(self._db[task_id])
  File "/usr/lib64/python3.3/json/__init__.py", line 236, in dumps
return _default_encoder.encode(obj)
  File "/usr/lib64/python3.3/json/encoder.py", line 191, in encode
chunks = self.iterencode(o, _one_shot=True)
  File "/usr/lib64/python3.3/json/encoder.py", line 249, in iterencode
return _iterencode(o, 0)
  File "/usr/lib64/python3.3/json/encoder.py", line 173, in default
raise TypeError(repr(o) + " is not JSON serializable")
TypeError: dict_keys(['http://getnikola.com/index.html', 'http://getnikola.com/listings/', 'http://getnikola.com/', 'http://getnikola.com/archive.html', 'http://getnikola.com/listings/index.html', 'http://getnikola.com/categories/index.html', 'http://getnikola.com/galleries/', 'http://getnikola.com/galleries/index.html', 'http://getnikola.com/categories/']) is not JSON serializable
```

Even changing the `SITE_URL` or using `nikola build --strict`

```
pip list
bbcode (1.0.12)
docopt (0.6.1)
docutils (0.11)
doit (0.22.1)
Jinja2 (2.7.1)
livereload (1.0.1)
Logbook (0.5.0)
lxml (3.2.3)
Mako (0.9.0)
Markdown (2.3.1)
MarkupSafe (0.18)
micawber (0.3.0)
mock (1.0.1)
Nikola (6.0.3, /home/neiesc/Devel/nikola)
phpserialize (1.3)
Pillow (2.1.0)
pip (1.4.1)
pygal (1.1.0)
Pygments (1.6)
pyinotify (0.9.4)
Pyphen (0.8)
PyRSS2Gen (1.1)
python-dateutil (2.1)
pytz (2013d)
requests (1.2.3)
setuptools (0.9.8)
six (1.4.1)
smartypants (1.8.2)
tornado (3.1.1)
typogrify (2.0.0)
Unidecode (0.04.13)
webassets (0.8)
Yapsy (1.10.2-pythons2n3)
```



Discussion:

Resolving patch:
diff --git a/nikola/plugins/compile/rest/doc.py b/nikola/plugins/compile/rest/doc.py
new file mode 100644
--- /dev/null
+++ b/nikola/plugins/compile/rest/doc.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+
+# Copyright © 2012-2013 Roberto Alsina and others.
+
+# Permission is hereby granted, free of charge, to any
+# person obtaining a copy of this software and associated
+# documentation files (the "Software"), to deal in the
+# Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the
+# Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice
+# shall be included in all copies or substantial portions of
+# the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
+# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+from docutils import nodes
+from docutils.parsers.rst import roles
+
+from nikola.utils import split_explicit_title
+from nikola.plugin_categories import RestExtension
+
+
+class Plugin(RestExtension):
+
+    name = 'rest_doc'
+
+    def set_site(self, site):
+        self.site = site
+        roles.register_canonical_role('doc', doc_role)
+        doc_role.site = site
+        return super(Plugin, self).set_site(site)
+
+
+def doc_role(name, rawtext, text, lineno, inliner,
+             options={}, content=[]):
+
+    # split link's text and post's slug in role content
+    has_explicit_title, title, slug = split_explicit_title(text)
+
+    # check if the slug given is part of our blog posts/pages
+    twin_slugs = False
+    post = None
+    for p in doc_role.site.timeline:
+        if p.meta('slug') == slug:
+            if post is None:
+                post = p
+            else:
+                twin_slugs = True
+                break
+
+    try:
+        if post is None:
+            raise ValueError
+    except ValueError:
+        msg = inliner.reporter.error(
+            '"{}" slug doesn\'t exist.'.format(slug),
+            line=lineno)
+        prb = inliner.problematic(rawtext, rawtext, msg)
+        return [prb], [msg]
+
+    if not has_explicit_title:
+        # use post's title as link's text
+        title = post.title()
+
+    permalink = post.permalink()
+    if twin_slugs:
+        msg = inliner.reporter.warning(
+            'More than one post with the same slug. Using "{}"'.format(permalink))
+
+    node = make_link_node(rawtext, title, permalink, options)
+    return [node], []
+
+
+def make_link_node(rawtext, text, url, options):
+    node = nodes.reference(rawtext, text, refuri=url, *options)
+    return node
diff --git a/nikola/utils.py b/nikola/utils.py
--- a/nikola/utils.py
+++ b/nikola/utils.py
@@ -613,3 +613,16 @@ def publish_extensions(self, handler):
                 'type': "application/rss+xml"
             })
             handler.endElement("atom:link")
+
+
+# \x00 means the "<" was backslash-escaped
+explicit_title_re = re.compile(r'^(.+?)\s*(?<!\x00)<(.*?)>$', re.DOTALL)
+def split_explicit_title(text):
+    """Split role content into title and target, if given.
+
+       From Sphinx's "sphinx/util/nodes.py"
+    """
+    match = explicit_title_re.match(text)
+    if match:
+        return True, match.group(1), match.group(2)
+    return False, text, text


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
diff --git a/tests/test_integration.py b/tests/test_integration.py
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -19,6 +19,8 @@
 import nikola.plugins.command
 import nikola.plugins.command.init
 
+from .base import BaseTestCase
+
 
 @contextmanager
 def cd(path):
@@ -28,20 +30,22 @@ def cd(path):
     os.chdir(old_dir)
 
 
-class EmptyBuildTest(unittest.TestCase):
+class EmptyBuildTest(BaseTestCase):
     """Basic integration testcase."""
 
     dataname = None
 
-    def setUp(self):
+    @classmethod
+    def setUpClass(cls):
         """Setup a demo site."""
-        self.tmpdir = tempfile.mkdtemp()
-        self.target_dir = os.path.join(self.tmpdir, "target")
-        self.init_command = nikola.plugins.command.init.CommandInit()
-        self.fill_site()
-        self.patch_site()
-        self.build()
-
+        cls.tmpdir = tempfile.mkdtemp()
+        cls.target_dir = os.path.join(cls.tmpdir, "target")
+        cls.init_command = nikola.plugins.command.init.CommandInit()
+        cls.fill_site()
+        cls.patch_site()
+        cls.build()
+
+    @classmethod
     def fill_site(self):
         """Add any needed initial content."""
         self.init_command.create_empty_site(self.target_dir)
@@ -57,15 +61,18 @@ def fill_site(self):
                     src_file = os.path.join(root, src_name)
                     shutil.copy2(src_file, dst_file)
 
+    @classmethod
     def patch_site(self):
         """Make any modifications you need to the site."""
 
+    @classmethod
     def build(self):
         """Build the site."""
         with cd(self.target_dir):
             main.main(["build"])
 
-    def tearDown(self):
+    @classmethod
+    def tearDownClass(self):
         """Remove the demo site."""
         shutil.rmtree(self.tmpdir)
         # Fixes Issue #438
@@ -84,6 +91,7 @@ def test_build(self):
 class DemoBuildTest(EmptyBuildTest):
     """Test that a default build of --demo works."""
 
+    @classmethod
     def fill_site(self):
         """Fill the site with demo content."""
         self.init_command.copy_sample_site(self.target_dir)
@@ -109,6 +117,7 @@ def test_avoid_double_slash_in_rss(self):
 
 class RepeatedPostsSetting(DemoBuildTest):
     """Duplicate POSTS, should not read each post twice, which causes conflicts."""
+    @classmethod
     def patch_site(self):
         """Set the SITE_URL to have a path"""
         conf_path = os.path.join(self.target_dir, "conf.py")
@@ -119,6 +128,7 @@ def patch_site(self):
 class FuturePostTest(EmptyBuildTest):
     """Test a site with future posts."""
 
+    @classmethod
     def fill_site(self):
         import datetime
         from nikola.utils import current_time
@@ -199,6 +209,7 @@ def test_translated_titles(self):
 class RelativeLinkTest(DemoBuildTest):
     """Check that SITE_URL with a path doesn't break links."""
 
+    @classmethod
     def patch_site(self):
         """Set the SITE_URL to have a path"""
         conf_path = os.path.join(self.target_dir, "conf.py")
@@ -265,6 +276,7 @@ def test_check_files_fail(self):
 class RelativeLinkTest2(DemoBuildTest):
     """Check that dropping stories to the root doesn't break links."""
 
+    @classmethod
     def patch_site(self):
         """Set the SITE_URL to have a path"""
         conf_path = os.path.join(self.target_dir, "conf.py")
diff --git a/tests/test_plugin_importing.py b/tests/test_plugin_importing.py
--- a/tests/test_plugin_importing.py
+++ b/tests/test_plugin_importing.py
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals, absolute_import
 
-from context import nikola  # NOQA
+from .context import nikola  # NOQA
 import unittest
 
 
diff --git a/tests/test_rst_compiler.py b/tests/test_rst_compiler.py
--- a/tests/test_rst_compiler.py
+++ b/tests/test_rst_compiler.py
@@ -36,6 +36,7 @@
 import sys
 import tempfile
 
+import docutils
 from lxml import html
 from nose.plugins.skip import SkipTest
 import unittest
@@ -58,6 +59,22 @@
 )
 from base import BaseTestCase
 
+class FakePost(object):
+
+    def __init__(self, title, slug):
+        self._title = title
+        self._slug = slug
+        self._meta = {'slug': slug}
+
+    def title(self):
+        return self._title
+
+    def meta(self, key):
+        return self._meta[key]
+
+    def permalink(self):
+        return '/posts/' + self._slug
+
 
 class FakeSite(object):
     def __init__(self):
@@ -65,6 +82,7 @@ def __init__(self):
         self.config = {
             'DISABLED_PLUGINS': [],
             'EXTRA_PLUGINS': [],
+            'DEFAULT_LANG': 'en',
         }
         self.EXTRA_PLUGINS = self.config['EXTRA_PLUGINS']
         self.plugin_manager = PluginManager(categories_filter={
@@ -88,6 +106,12 @@ def __init__(self):
         self.plugin_manager.setPluginPlaces(places)
         self.plugin_manager.collectPlugins()
 
+        self.timeline = [
+            FakePost(title='Fake post',
+                     slug='fake-post')
+        ]
+
+
     def render_template(self, name, _, context):
         return('<img src="IMG.jpg">')
 
@@ -99,10 +123,12 @@ class ReSTExtensionTestCase(BaseTestCase):
     deps = None
 
     def setUp(self):
-        """ Parse cls.sample into a HTML document tree """
-        super(ReSTExtensionTestCase, self).setUp()
         self.compiler = nikola.plugins.compile.rest.CompileRest()
         self.compiler.set_site(FakeSite())
+        return super(ReSTExtensionTestCase, self).setUp()
+
+    def basic_test(self):
+        """ Parse cls.sample into a HTML document tree """
         self.setHtmlFromRst(self.sample)
 
     def setHtmlFromRst(self, rst):
@@ -151,6 +177,7 @@ class ReSTExtensionTestCaseTestCase(ReSTExtensionTestCase):
     sample = '.. raw:: html\n\n   <iframe src="foo" height="bar">spam</iframe>'
 
     def test_test(self):
+        self.basic_test()
         self.assertHTMLContains("iframe", attributes={"src": "foo"},
                                 text="spam")
         self.assertRaises(Exception, self.assertHTMLContains, "eggs", {})
@@ -161,6 +188,7 @@ class MathTestCase(ReSTExtensionTestCase):
 
     def test_mathjax(self):
         """ Test that math is outputting MathJax."""
+        self.basic_test()
         self.assertHTMLContains("span", attributes={"class": "math"},
                                 text="\(e^{ix} = \cos x + i\sin x\)")
 
@@ -177,6 +205,7 @@ class GistTestCase(ReSTExtensionTestCase):
 
     def setUp(self):
         """ Patch GitHubGist for avoiding network dependency """
+        super(GistTestCase, self).setUp()
         self.gist_type.get_raw_gist_with_filename = lambda *_: 'raw_gist_file'
         self.gist_type.get_raw_gist = lambda *_: "raw_gist"
         _reload(nikola.plugins.compile.rest)
@@ -207,6 +236,7 @@ class GistIntegrationTestCase(ReSTExtensionTestCase):
 
     def test_gist_integration(self):
         """ Fetch contents of the gist from GH and render in a noscript tag """
+        self.basic_test()
         text = ('Be alone, that is the secret of invention: be alone, that is'
                 ' when ideas are born. -- Nikola Tesla')
         self.assertHTMLContains('pre', text=text)
@@ -219,6 +249,7 @@ class SlidesTestCase(ReSTExtensionTestCase):
 
     def test_slides(self):
         """ Test the slides js generation and img tag creation """
+        self.basic_test()
         self.assertHTMLContains("img", attributes={"src": "IMG.jpg"})
 
 
@@ -229,6 +260,7 @@ class SoundCloudTestCase(ReSTExtensionTestCase):
 
     def test_soundcloud(self):
         """ Test SoundCloud iframe tag generation """
+        self.basic_test()
         self.assertHTMLContains("iframe",
                                 attributes={"src": ("https://w.soundcloud.com"
                                                     "/player/?url=http://"
@@ -253,6 +285,7 @@ def setUp(self):
 
     def test_vimeo(self):
         """ Test Vimeo iframe tag generation """
+        self.basic_test()
         self.assertHTMLContains("iframe",
                                 attributes={"src": ("http://player.vimeo.com/"
                                                     "video/VID"),
@@ -266,6 +299,7 @@ class YoutubeTestCase(ReSTExtensionTestCase):
 
     def test_youtube(self):
         """ Test Youtube iframe tag generation """
+        self.basic_test()
         self.assertHTMLContains("iframe",
                                 attributes={"src": ("http://www.youtube.com/"
                                                     "embed/YID?rel=0&hd=1&"
@@ -305,5 +339,33 @@ def test_codeblock_alias(self):
         self.setHtmlFromRst(self.sample3)
 
 
+class RefTestCase(ReSTExtensionTestCase):
+    """ Ref role test case """
+
+    sample = 'Sample for testing my :doc:`doesnt-exist-post`'
+    sample1 = 'Sample for testing my :doc:`fake-post`'
+    sample2 = 'Sample for testing my :doc:`titled post <fake-post>`'
+
+    def setUp(self):
+        f =  docutils.parsers.rst.roles.role('doc', None, None, None)[0]
+        f.site = FakeSite()
+        return super(RefTestCase, self).setUp()
+
+    def test_doc_doesnt_exist(self):
+        self.assertRaises(Exception, self.assertHTMLContains, 'anything', {})
+
+    def test_doc(self):
+        self.setHtmlFromRst(self.sample1)
+        self.assertHTMLContains('a',
+                                text='Fake post',
+                                attributes={'href': '/posts/fake-post'})
+
+    def test_doc_titled(self):
+        self.setHtmlFromRst(self.sample2)
+        self.assertHTMLContains('a',
+                                text='titled post',
+                                attributes={'href': '/posts/fake-post'})
+
+
 if __name__ == "__main__":
     unittest.main()