diff options
author | Daniel Neuhäuser <ich@danielneuhaeuser.de> | 2010-08-10 23:07:03 +0200 |
---|---|---|
committer | Daniel Neuhäuser <ich@danielneuhaeuser.de> | 2010-08-10 23:07:03 +0200 |
commit | 4f7c5ec6d30c2e443133392da42c81daf88cd978 (patch) | |
tree | ad0df36ddb526342a347e36b63affd2bb5d29108 | |
parent | 1068ece635731ca80dbcdb777600d60513170d63 (diff) | |
parent | 096b7e2cd4b83edeeae68c6478f2a4db2fe4a0a8 (diff) | |
download | sphinx-git-4f7c5ec6d30c2e443133392da42c81daf88cd978.tar.gz |
Automated merge with ssh://bitbucket.org/jacobmason/sphinx-web-support
-rw-r--r-- | sphinx/builders/websupport.py | 32 | ||||
-rw-r--r-- | sphinx/util/__init__.py | 36 | ||||
-rw-r--r-- | sphinx/versioning.py | 144 | ||||
-rw-r--r-- | tests/root/contents.txt | 1 | ||||
-rw-r--r-- | tests/root/versioning/added.txt | 20 | ||||
-rw-r--r-- | tests/root/versioning/deleted.txt | 12 | ||||
-rw-r--r-- | tests/root/versioning/deleted_end.txt | 11 | ||||
-rw-r--r-- | tests/root/versioning/index.txt | 11 | ||||
-rw-r--r-- | tests/root/versioning/insert.txt | 18 | ||||
-rw-r--r-- | tests/root/versioning/modified.txt | 17 | ||||
-rw-r--r-- | tests/root/versioning/original.txt | 15 | ||||
-rw-r--r-- | tests/test_config.py | 3 | ||||
-rw-r--r-- | tests/test_versioning.py | 87 |
13 files changed, 389 insertions, 18 deletions
diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index 30cf28314..8cc70ea0c 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -17,6 +17,7 @@ import shutil from docutils.io import StringOutput from sphinx.util.osutil import os_path, relative_uri, ensuredir, copyfile +from sphinx.util.jsonimpl import dumps as dump_json from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.writers.websupport import WebSupportTranslator @@ -131,20 +132,17 @@ class WebSupportBuilder(StandaloneHTMLBuilder): path = ctx['pathto'](file, 1) return '<script type="text/javascript" src="%s"></script>' % path - opts = """ -<script type="text/javascript"> - var DOCUMENTATION_OPTIONS = { - URL_ROOT: '%s', - VERSION: '%s', - COLLAPSE_INDEX: false, - FILE_SUFFIX: '', - HAS_SOURCE: '%s' - }; -</script>""" - opts = opts % (ctx.get('url_root', ''), escape(ctx['release']), - str(ctx['has_source']).lower()) - scripts = [] - for file in ctx['script_files']: - scripts.append(make_script(file)) - scripts.append(make_script('_static/websupport.js')) - return opts + '\n' + '\n'.join(scripts) + opts = { + 'URL_ROOT': ctx.get('url_root', ''), + 'VERSION': ctx['release'], + 'COLLAPSE_INDEX': False, + 'FILE_SUFFIX': '', + 'HAS_SOURCE': ctx['has_source'] + } + scripts = [make_script('_static/websupport.js')] + scripts += [make_script(file) for file in ctx['script_files']] + return '\n'.join([ + '<script type="text/javascript">' + 'var DOCUMENTATION_OPTIONS = %s;' % dump_json(opts), + '</script>' + ] + scripts) diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index ec48009f4..a434f3a82 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -19,6 +19,7 @@ import posixpath import traceback from os import path from codecs import open +from collections import deque import docutils from docutils.utils import relative_path @@ -297,3 +298,38 @@ def format_exception_cut_frames(x=1): res += tbres[-x:] res += traceback.format_exception_only(typ, val) return ''.join(res) + +class PeekableIterator(object): + """ + An iterator which wraps any iterable and makes it possible to peek to see + what's the next item. + """ + def __init__(self, iterable): + self.remaining = deque() + self._iterator = iter(iterable) + + def __iter__(self): + return self + + def next(self): + """ + Returns the next item from the iterator. + """ + if self.remaining: + return self.remaining.popleft() + return self._iterator.next() + + def push(self, item): + """ + Pushes the `item` on the internal stack, it will be returned on the + next :meth:`next` call. + """ + self.remaining.append(item) + + def peek(self): + """ + Returns the next item without changing the state of the iterator. + """ + item = self.next() + self.push(item) + return item diff --git a/sphinx/versioning.py b/sphinx/versioning.py new file mode 100644 index 000000000..42b8bd515 --- /dev/null +++ b/sphinx/versioning.py @@ -0,0 +1,144 @@ +# -*- coding: utf-8 -*- +""" + sphinx.versioning + ~~~~~~~~~~~~~~~~~ + + Implements the low-level algorithms Sphinx uses for the versioning of + doctrees. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +from uuid import uuid4 +from itertools import izip_longest, product +from difflib import SequenceMatcher + +from sphinx.util import PeekableIterator + +def add_uids(doctree, condition): + """ + Adds a unique id to every node in the `doctree` which matches the condition + and yields it. + + :param doctree: + A :class:`docutils.nodes.document` instance. + + :param condition: + A callable which returns either ``True`` or ``False`` for a given node. + """ + for node in doctree.traverse(condition): + node.uid = uuid4().hex + yield node + +def merge_node(old, new): + """ + Merges the `old` node with the `new` one, if it's successful the `new` node + get's the unique identifier of the `new` one and ``True`` is returned. If + the merge is unsuccesful ``False`` is returned. + """ + equals, changed, replaced = make_diff(old.rawsource, + new.rawsource) + if equals or changed: + new.uid = old.uid + return True + return False + +def merge_doctrees(old, new, condition): + """ + Merges the `old` doctree with the `new` one while looking at nodes matching + the `condition`. + + Each node which replaces another one or has been added to the `new` doctree + will be yielded. + + :param condition: + A callable which returns either ``True`` or ``False`` for a given node. + """ + old_iter = PeekableIterator(old.traverse(condition)) + new_iter = PeekableIterator(new.traverse(condition)) + old_nodes = [] + new_nodes = [] + for old_node, new_node in izip_longest(old_iter, new_iter): + if old_node is None: + new_nodes.append(new_node) + continue + if new_node is None: + old_nodes.append(old_node) + continue + if not merge_node(old_node, new_node): + if old_nodes: + for i, very_old_node in enumerate(old_nodes): + if merge_node(very_old_node, new_node): + del old_nodes[i] + # If the last identified node which has not matched the + # unidentified node matches the current one, we have to + # assume that the last unidentified one has been + # inserted. + # + # As the required time multiplies with each insert, we + # want to avoid that by checking if the next + # unidentified node matches the current identified one + # and if so we make a shift. + if i == len(old_nodes): + next_new_node = new_iter.next() + if not merge_node(old_node, next_new_node): + new_iter.push(next_new_node) + break + else: + old_nodes.append(old_node) + new_nodes.append(new_node) + for (i, new_node), (j, old_node) in product(enumerate(new_nodes), + enumerate(old_nodes)): + if merge_node(old_node, new_node): + del new_nodes[i] + del old_nodes[j] + for node in new_nodes: + node.uid = uuid4().hex + # Yielding the new nodes here makes it possible to use this generator + # like add_uids + yield node + +def make_diff(old, new): + """ + Takes two strings `old` and `new` and returns a :class:`tuple` of boolean + values ``(equals, changed, replaced)``. + + equals + + ``True`` if the `old` string and the `new` one are equal. + + changed + + ``True`` if the `new` string is a changed version of the `old` one. + + replaced + + ``True`` if the `new` string and the `old` string are totally + different. + + .. note:: This assumes the two strings are human readable text or at least + something very similar to that, otherwise it can not detect if + the string has been changed or replaced. In any case the + detection should not be considered reliable. + """ + if old == new: + return True, False, False + if new in old or levenshtein_distance(old, new) / (len(old) / 100.0) < 70: + return False, True, False + return False, False, True + +def levenshtein_distance(a, b): + if len(a) < len(b): + a, b = b, a + if not a: + return len(b) + previous_row = xrange(len(b) + 1) + for i, column1 in enumerate(a): + current_row = [i + 1] + for j, column2 in enumerate(b): + insertions = previous_row[j + 1] + 1 + deletions = current_row[j] + 1 + substitutions = previous_row[j] + (column1 != column2) + current_row.append(min(insertions, deletions, substitutions)) + previous_row = current_row + return previous_row[-1] diff --git a/tests/root/contents.txt b/tests/root/contents.txt index e052e04b2..280953b46 100644 --- a/tests/root/contents.txt +++ b/tests/root/contents.txt @@ -26,6 +26,7 @@ Contents: extensions doctest extensions + versioning/index Python <http://python.org/> diff --git a/tests/root/versioning/added.txt b/tests/root/versioning/added.txt new file mode 100644 index 000000000..22a70739c --- /dev/null +++ b/tests/root/versioning/added.txt @@ -0,0 +1,20 @@ +Versioning test text +==================== + +So the thing is I need some kind of text - not the lorem ipsum stuff, that +doesn't work out that well - to test :mod:`sphinx.versioning`. I couldn't find +a good text for that under public domain so I thought the easiest solution is +to write one by myself. It's not really interesting, in fact it is *really* +boring. + +Anyway I need more than one paragraph, at least three for the original +document, I think, and another one for two different ones. + +So the previous paragraph was a bit short because I don't want to test this +only on long paragraphs, I hope it was short enough to cover most stuff. +Anyway I see this lacks ``some markup`` so I have to add a **little** bit. + +Woho another paragraph, if this test fails we really have a problem because +this means the algorithm itself fails and not the diffing algorithm which is +pretty much doomed anyway as it probably fails for some kind of language +respecting certain nodes anyway but we can't work around that anyway. diff --git a/tests/root/versioning/deleted.txt b/tests/root/versioning/deleted.txt new file mode 100644 index 000000000..a1a9c4c91 --- /dev/null +++ b/tests/root/versioning/deleted.txt @@ -0,0 +1,12 @@ +Versioning test text +==================== + +So the thing is I need some kind of text - not the lorem ipsum stuff, that +doesn't work out that well - to test :mod:`sphinx.versioning`. I couldn't find +a good text for that under public domain so I thought the easiest solution is +to write one by myself. It's not really interesting, in fact it is *really* +boring. + +So the previous paragraph was a bit short because I don't want to test this +only on long paragraphs, I hope it was short enough to cover most stuff. +Anyway I see this lacks ``some markup`` so I have to add a **little** bit. diff --git a/tests/root/versioning/deleted_end.txt b/tests/root/versioning/deleted_end.txt new file mode 100644 index 000000000..f30e63007 --- /dev/null +++ b/tests/root/versioning/deleted_end.txt @@ -0,0 +1,11 @@ +Versioning test text +==================== + +So the thing is I need some kind of text - not the lorem ipsum stuff, that +doesn't work out that well - to test :mod:`sphinx.versioning`. I couldn't find +a good text for that under public domain so I thought the easiest solution is +to write one by myself. It's not really interesting, in fact it is *really* +boring. + +Anyway I need more than one paragraph, at least three for the original +document, I think, and another one for two different ones. diff --git a/tests/root/versioning/index.txt b/tests/root/versioning/index.txt new file mode 100644 index 000000000..234e223f1 --- /dev/null +++ b/tests/root/versioning/index.txt @@ -0,0 +1,11 @@ +Versioning Stuff +================ + +.. toctree:: + + original + added + insert + deleted + deleted_end + modified diff --git a/tests/root/versioning/insert.txt b/tests/root/versioning/insert.txt new file mode 100644 index 000000000..1c157cc90 --- /dev/null +++ b/tests/root/versioning/insert.txt @@ -0,0 +1,18 @@ +Versioning test text +==================== + +So the thing is I need some kind of text - not the lorem ipsum stuff, that +doesn't work out that well - to test :mod:`sphinx.versioning`. I couldn't find +a good text for that under public domain so I thought the easiest solution is +to write one by myself. It's not really interesting, in fact it is *really* +boring. + +So this paragraph is just something I inserted in this document to test if our +algorithm notices that this paragraph is not just a changed version. + +Anyway I need more than one paragraph, at least three for the original +document, I think, and another one for two different ones. + +So the previous paragraph was a bit short because I don't want to test this +only on long paragraphs, I hope it was short enough to cover most stuff. +Anyway I see this lacks ``some markup`` so I have to add a **little** bit. diff --git a/tests/root/versioning/modified.txt b/tests/root/versioning/modified.txt new file mode 100644 index 000000000..49cdad935 --- /dev/null +++ b/tests/root/versioning/modified.txt @@ -0,0 +1,17 @@ +Versioning test text +==================== + +So the thing is I need some kind of text - not the lorem ipsum stuff, that +doesn't work out that well - to test :mod:`sphinx.versioning`. I couldn't find +a good text for that under public domain so I thought the easiest solution is +to write one by myself. Inserting something silly as a modification, btw. have +you seen the typo below?. It's not really interesting, in fact it is *really* +boring. + +Anyway I need more than one paragraph, at least three for the original +document, I think, and another one for two different ones. So this is a small +modification by adding something to this paragraph. + +So the previous paragraph was a bit short because I don't want to test this +only on long paragraphs, I hoep it was short enough to cover most stuff. +Anyway I see this lacks ``some markup`` so I have to add a **little** bit. diff --git a/tests/root/versioning/original.txt b/tests/root/versioning/original.txt new file mode 100644 index 000000000..b3fe06094 --- /dev/null +++ b/tests/root/versioning/original.txt @@ -0,0 +1,15 @@ +Versioning test text +==================== + +So the thing is I need some kind of text - not the lorem ipsum stuff, that +doesn't work out that well - to test :mod:`sphinx.versioning`. I couldn't find +a good text for that under public domain so I thought the easiest solution is +to write one by myself. It's not really interesting, in fact it is *really* +boring. + +Anyway I need more than one paragraph, at least three for the original +document, I think, and another one for two different ones. + +So the previous paragraph was a bit short because I don't want to test this +only on long paragraphs, I hope it was short enough to cover most stuff. +Anyway I see this lacks ``some markup`` so I have to add a **little** bit. diff --git a/tests/test_config.py b/tests/test_config.py index 7fce4495b..b5f88a6f5 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -89,7 +89,8 @@ def test_errors_warnings(dir): raises_msg(ConfigError, 'conf.py', Config, dir, 'conf.py', {}, None) # test the automatic conversion of 2.x only code in configs - write_file(dir / 'conf.py', u'\n\nproject = u"Jägermeister"\n', 'utf-8') + write_file(dir / 'conf.py', u'# -*- coding: utf-8\n\n' + u'project = u"Jägermeister"\n', 'utf-8') cfg = Config(dir, 'conf.py', {}, None) cfg.init_values() assert cfg.project == u'Jägermeister' diff --git a/tests/test_versioning.py b/tests/test_versioning.py new file mode 100644 index 000000000..77306580c --- /dev/null +++ b/tests/test_versioning.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +""" + test_versioning + ~~~~~~~~~~~~~~~ + + Test the versioning implementation. + + :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +from util import * + +from docutils.statemachine import ViewList + +from sphinx.versioning import make_diff, add_uids, merge_doctrees + +def setup_module(): + global app, original, original_uids + app = TestApp() + app.builder.env.app = app + app.connect('doctree-resolved', on_doctree_resolved) + app.build() + original = doctrees['versioning/original'] + original_uids = [n.uid for n in add_uids(original, is_paragraph)] + +def teardown_module(): + app.cleanup() + (test_root / '_build').rmtree(True) + +doctrees = {} + +def on_doctree_resolved(app, doctree, docname): + doctrees[docname] = doctree + +def test_make_diff(): + tests = [ + (('aaa', 'aaa'), (True, False, False)), + (('aaa', 'aab'), (False, True, False)), + (('aaa', 'abb'), (False, True, False)), + (('aaa', 'aba'), (False, True, False)), + (('aaa', 'baa'), (False, True, False)), + (('aaa', 'bbb'), (False, False, True)) + ] + for args, result in tests: + assert make_diff(*args) == result + +def is_paragraph(node): + return node.__class__.__name__ == 'paragraph' + +def test_add_uids(): + assert len(original_uids) == 3 + +def test_modified(): + modified = doctrees['versioning/modified'] + new_nodes = list(merge_doctrees(original, modified, is_paragraph)) + uids = [n.uid for n in modified.traverse(is_paragraph)] + assert not new_nodes + assert original_uids == uids + +def test_added(): + added = doctrees['versioning/added'] + new_nodes = list(merge_doctrees(original, added, is_paragraph)) + uids = [n.uid for n in added.traverse(is_paragraph)] + assert len(new_nodes) == 1 + assert original_uids == uids[:-1] + +def test_deleted(): + deleted = doctrees['versioning/deleted'] + new_nodes = list(merge_doctrees(original, deleted, is_paragraph)) + uids = [n.uid for n in deleted.traverse(is_paragraph)] + assert not new_nodes + assert original_uids[::2] == uids + +def test_deleted_end(): + deleted_end = doctrees['versioning/deleted_end'] + new_nodes = list(merge_doctrees(original, deleted_end, is_paragraph)) + uids = [n.uid for n in deleted_end.traverse(is_paragraph)] + assert not new_nodes + assert original_uids[:-1] == uids + +def test_insert(): + insert = doctrees['versioning/insert'] + new_nodes = list(merge_doctrees(original, insert, is_paragraph)) + uids = [n.uid for n in insert.traverse(is_paragraph)] + assert len(new_nodes) == 1 + assert original_uids[0] == uids[0] + assert original_uids[1:] == uids[2:] |